aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/Buttons.hx64
-rw-r--r--src/client/Main.hx88
-rw-r--r--src/client/Player.hx17
-rw-r--r--src/client/Utils.hx22
-rw-r--r--src/client/players/Raw.hx2
-rw-r--r--src/client/players/RawSubs.hx2
-rw-r--r--src/client/players/Youtube.hx2
7 files changed, 123 insertions, 74 deletions
diff --git a/src/client/Buttons.hx b/src/client/Buttons.hx
index dbcf759..36f5729 100644
--- a/src/client/Buttons.hx
+++ b/src/client/Buttons.hx
@@ -20,7 +20,7 @@ class Buttons {
if (settings.isSwapped) swapPlayerAndChat();
initSplit();
setSplitSize(settings.chatSize);
- initChatInput(main);
+ initChatInputs(main);
for (item in settings.checkboxes) {
if (item.checked == null) continue;
@@ -49,7 +49,7 @@ class Buttons {
if (list.children.length == 0) return;
final isActive = smilesBtn.classList.toggle("active");
if (isActive) {
- wrap.style.display = "block";
+ wrap.style.display = "";
wrap.style.height = outerHeight(list) + "px";
} else {
wrap.style.height = "0";
@@ -65,22 +65,15 @@ class Buttons {
}
final scrollToChatEndBtn = ge("#scroll-to-chat-end");
- function scrollToChatEndBtnAnim():Void {
- if (scrollToChatEndBtn.style.opacity == "0") return;
- scrollToChatEndBtn.style.opacity = "0";
- scrollToChatEndBtn.addEventListener("transitionend", e -> {
- scrollToChatEndBtn.style.display = "none";
- }, {once: true});
- }
scrollToChatEndBtn.onclick = e -> {
main.scrollChatToEnd();
- scrollToChatEndBtnAnim();
+ main.hideScrollToChatEndBtn();
}
// hide scroll button when chat is scrolled to the end
final msgBuf = ge("#messagebuffer");
msgBuf.onscroll = e -> {
- if (msgBuf.offsetHeight + msgBuf.scrollTop < msgBuf.scrollHeight - 1) return;
- scrollToChatEndBtnAnim();
+ if (!main.isInChatEnd(1)) return;
+ main.hideScrollToChatEndBtn();
}
ge("#clearchatbtn").onclick = e -> {
@@ -114,7 +107,7 @@ class Buttons {
final style = ge("#userlist").style;
if (isHidden) {
icon.setAttribute("name", "chevron-down");
- style.display = "block";
+ style.display = "";
final list = wrap.firstElementChild;
wrap.style.height = "15vh";
wrap.style.marginBottom = "1rem";
@@ -161,6 +154,7 @@ class Buttons {
final fullscreenBtn = ge("#fullscreenbtn");
fullscreenBtn.onclick = e -> {
if ((Utils.isTouch() || main.isVerbose()) && !Utils.hasFullscreen()) {
+ window.scrollTo(0, 0);
Utils.requestFullscreen(document.documentElement);
} else {
Utils.requestFullscreen(ge("#ytapiplayer"));
@@ -254,9 +248,9 @@ class Buttons {
final exitBtn = ge("#exitBtn");
exitBtn.onclick = e -> {
+ showOptions.onclick();
if (main.isUser()) main.send({type: Logout});
else ge("#guestname").focus();
- toggleGroup(showOptions);
}
final swapLayoutBtn = ge("#swapLayoutBtn");
@@ -429,7 +423,7 @@ class Buttons {
ge("#hotkeysBtn").innerText = '$text: $state';
}
- static function initChatInput(main:Main):Void {
+ static function initChatInputs(main:Main):Void {
final guestName:InputElement = cast ge("#guestname");
guestName.onkeydown = e -> {
if (e.keyCode == KeyCode.Return) {
@@ -447,38 +441,25 @@ class Buttons {
}
}
- if (Utils.isIOS()) {
- document.ontouchmove = e -> {
- e.preventDefault();
- }
- document.body.style.height = "-webkit-fill-available";
- ge("#chat").style.height = "-webkit-fill-available";
- }
final chatline:InputElement = cast ge("#chatline");
chatline.onfocus = e -> {
if (Utils.isIOS()) {
- final startY = window.scrollY;
+ // final startY = window.scrollY;
+ final startY = 0;
Timer.delay(() -> {
window.scrollBy(0, -(window.scrollY - startY));
ge("#video").scrollTop = 0;
main.scrollChatToEnd();
- if (getVisualViewport() == null) { // ios < 13
- ge("#chat").style.height = '${window.innerHeight}px';
- }
}, 100);
- } else if (Utils.isTouch()) main.scrollChatToEnd();
- }
- if (Utils.isIOS() && getVisualViewport() != null) {
- final viewport = getVisualViewport();
- viewport.addEventListener("resize", e -> {
- ge("#chat").style.height = '${window.innerHeight}px';
- });
- }
- chatline.onblur = e -> {
- if (Utils.isIOS() && getVisualViewport() == null) { // ios < 13
- ge("#chat").style.height = "-webkit-fill-available";
+ } else if (Utils.isTouch()) {
+ main.scrollChatToEnd();
}
}
+ final viewport = getVisualViewport();
+ if (viewport != null) {
+ viewport.addEventListener("resize", e -> onViewportResize());
+ onViewportResize();
+ }
new InputWithHistory(chatline, 50, value -> {
if (main.handleCommands(value)) return true;
main.send({
@@ -506,6 +487,15 @@ class Buttons {
}
}
+ public static function onViewportResize():Void {
+ final viewport = getVisualViewport() ?? return;
+ final isPortrait = window.innerHeight > window.innerWidth;
+ final playerH = ge("#ytapiplayer").offsetHeight;
+ var h = viewport.height - playerH;
+ if (!isPortrait) h = viewport.height;
+ ge("#chat").style.height = '${h}px';
+ }
+
static inline function getVisualViewport():Null<VisualViewport> {
return (window : Dynamic).visualViewport;
}
diff --git a/src/client/Main.hx b/src/client/Main.hx
index 72dbf8e..9a4b426 100644
--- a/src/client/Main.hx
+++ b/src/client/Main.hx
@@ -26,6 +26,7 @@ import js.html.WebSocket;
using ClientTools;
class Main {
+ public static var instance(default, null):Main;
static inline var SETTINGS_VERSION = 5;
public final settings:ClientSettings;
@@ -51,9 +52,10 @@ class Main {
var onTimeGet:Timer;
var onBlinkTab:Null<Timer>;
var gotFirstPageInteraction = false;
+ var msgBuf = ge("#messagebuffer");
static function main():Void {
- new Main();
+ instance = new Main();
}
function new() {
@@ -723,7 +725,7 @@ class Main {
button.disabled = true;
}
final adminMenu = ge("#adminMenu");
- if (isAdmin()) adminMenu.style.display = "block";
+ if (isAdmin()) adminMenu.style.display = "";
else adminMenu.style.display = "none";
}
@@ -815,7 +817,7 @@ class Main {
}
function showGuestLoginPanel():Void {
- ge("#guestlogin").style.display = "flex";
+ ge("#guestlogin").style.display = "";
ge("#guestpassword").style.display = "none";
ge("#chatbox").style.display = "none";
ge("#exitBtn").textContent = Lang.get("login");
@@ -824,14 +826,14 @@ class Main {
function hideGuestLoginPanel():Void {
ge("#guestlogin").style.display = "none";
ge("#guestpassword").style.display = "none";
- ge("#chatbox").style.display = "flex";
+ ge("#chatbox").style.display = "";
ge("#exitBtn").textContent = Lang.get("exit");
}
function showGuestPasswordPanel():Void {
ge("#guestlogin").style.display = "none";
ge("#chatbox").style.display = "none";
- ge("#guestpassword").style.display = "flex";
+ ge("#guestpassword").style.display = "";
(cast ge("#guestpass") : InputElement).type = "password";
ge("#guestpass_icon").setAttribute("name", "eye");
}
@@ -850,35 +852,32 @@ class Main {
}
function chatMessageConnected():Void {
- final msgBuf = ge("#messagebuffer");
if (isLastMessageConnectionStatus()) {
msgBuf.removeChild(msgBuf.lastChild);
}
final div = document.createDivElement();
div.className = "server-msg-reconnect";
div.textContent = Lang.get("msgConnected");
- msgBuf.appendChild(div);
+ addMessageDiv(div);
scrollChatToEnd();
}
function chatMessageDisconnected():Void {
- final msgBuf = ge("#messagebuffer");
if (isLastMessageConnectionStatus()) {
msgBuf.removeChild(msgBuf.lastChild);
}
final div = document.createDivElement();
div.className = "server-msg-disconnect";
div.textContent = Lang.get("msgDisconnected");
- msgBuf.appendChild(div);
+ addMessageDiv(div);
scrollChatToEnd();
}
function isLastMessageConnectionStatus():Bool {
- final msgBuf = ge("#messagebuffer");
return msgBuf.lastElementChild?.className.startsWith("server-msg");
}
- public static function serverMessage(text:String, isText = true, withTimestamp = true):Void {
+ public function serverMessage(text:String, isText = true, withTimestamp = true):Void {
final div = document.createDivElement();
final time = Date.now().toString().split(" ")[1];
div.className = "server-whisper";
@@ -889,12 +888,11 @@ class Main {
final textDiv = div.querySelector(".server-whisper");
if (isText) textDiv.textContent = text;
else textDiv.innerHTML = text;
- final msgBuf = ge("#messagebuffer");
- msgBuf.appendChild(div);
- msgBuf.scrollTop = msgBuf.scrollHeight;
+ addMessageDiv(div);
+ scrollChatToEnd();
}
- public static function serverHtmlMessage(el:Element):Void {
+ public function serverHtmlMessage(el:Element):Void {
final div = document.createDivElement();
final time = Date.now().toString().split(" ")[1];
div.className = "server-whisper";
@@ -903,9 +901,8 @@ class Main {
<span class="timestamp">$time</span>
</div>';
div.querySelector(".server-whisper").appendChild(el);
- final msgBuf = ge("#messagebuffer");
- msgBuf.appendChild(div);
- msgBuf.scrollTop = msgBuf.scrollHeight;
+ addMessageDiv(div);
+ scrollChatToEnd();
}
function updateUserList():Void {
@@ -931,7 +928,7 @@ class Main {
}
function clearChat():Void {
- ge("#messagebuffer").textContent = "";
+ msgBuf.textContent = "";
}
function getLocalDateFromUtc(utcDate:String):String {
@@ -941,7 +938,6 @@ class Main {
}
function addMessage(name:String, text:String, ?date:String):Void {
- final msgBuf = ge("#messagebuffer");
final userDiv = document.createDivElement();
userDiv.className = 'chat-msg-$name';
@@ -968,10 +964,9 @@ class Main {
text = filter.regex.replace(text, filter.replace);
}
textDiv.innerHTML = text;
- final isInChatEnd = msgBuf.scrollTop
- + msgBuf.clientHeight >= msgBuf.scrollHeight - 50;
+ final inChatEnd = isInChatEnd();
- if (isInChatEnd) { // scroll chat to end after images loaded
+ if (inChatEnd) { // scroll chat to end after images loaded
for (img in textDiv.getElementsByTagName("img")) {
img.onload = onChatImageLoaded;
}
@@ -984,13 +979,14 @@ class Main {
headDiv.appendChild(nameDiv);
headDiv.appendChild(tstamp);
userDiv.appendChild(textDiv);
- msgBuf.appendChild(userDiv);
- if (isInChatEnd) {
+ addMessageDiv(userDiv);
+
+ if (inChatEnd) {
while (msgBuf.children.length > 200) {
msgBuf.removeChild(msgBuf.firstChild);
}
}
- if (isInChatEnd || name == personal.name) {
+ if (inChatEnd || name == personal.name) {
scrollChatToEnd();
} else {
showScrollToChatEndBtn();
@@ -998,12 +994,26 @@ class Main {
if (onBlinkTab == null) blinkTabWithTitle('*${Lang.get("chat")}*');
}
- function showScrollToChatEndBtn() {
+ function addMessageDiv(userDiv:Element):Void {
+ if (isMessageBufferReversed()) msgBuf.prepend(userDiv);
+ else msgBuf.appendChild(userDiv);
+ }
+
+ public function showScrollToChatEndBtn():Void {
final btn = ge("#scroll-to-chat-end");
- btn.style.display = "block";
+ btn.style.display = "";
Timer.delay(() -> btn.style.opacity = "1", 0);
}
+ public function hideScrollToChatEndBtn():Void {
+ final btn = ge("#scroll-to-chat-end");
+ if (btn.style.opacity == "0") return;
+ btn.style.opacity = "0";
+ btn.addEventListener("transitionend", e -> {
+ btn.style.display = "none";
+ }, {once: true});
+ }
+
function onChatImageLoaded(e:Event):Void {
scrollChatToEnd();
(cast e.target : Element).onload = null;
@@ -1028,9 +1038,27 @@ class Main {
el.onloadedmetadata = null;
}
+ public function isMessageBufferReversed():Bool {
+ return msgBuf.style.flexDirection == "column-reverse";
+ }
+
+ public function isInChatEnd(ignoreOffset = 50):Bool {
+ final isReverse = isMessageBufferReversed();
+ var scrollTop = msgBuf.scrollTop;
+ // zero to negative in column-reverse
+ if (isReverse) scrollTop = -scrollTop;
+ if (isReverse) return scrollTop <= ignoreOffset;
+ return scrollTop + msgBuf.clientHeight >= msgBuf.scrollHeight - ignoreOffset;
+ }
+
public function scrollChatToEnd():Void {
- final msgBuf = ge("#messagebuffer");
- msgBuf.scrollTop = msgBuf.scrollHeight;
+ final isReverse = isMessageBufferReversed();
+ if (isReverse) {
+ if (Utils.isMacSafari) msgBuf.scrollTop = -1;
+ msgBuf.scrollTop = 0;
+ } else {
+ msgBuf.scrollTop = msgBuf.scrollHeight;
+ }
}
/* Returns `true` if text should not be sent to chat */
diff --git a/src/client/Player.hx b/src/client/Player.hx
index f51d017..0efa2e3 100644
--- a/src/client/Player.hx
+++ b/src/client/Player.hx
@@ -24,7 +24,7 @@ class Player {
final rawPlayer:IPlayer;
final videoList = new VideoList();
final videoItemsEl = ge("#queue");
- final playerEl:Element = ge("#ytapiplayer");
+ final playerEl = ge("#ytapiplayer");
var player:Null<IPlayer>;
var isLoaded = false;
var skipSetTime = false;
@@ -49,6 +49,20 @@ class Player {
iframePlayer = new Iframe(main, this);
rawPlayer = new Raw(main, this);
initItemButtons();
+
+ final resizeObserver = Utils.createResizeObserver(entries -> {
+ if (isLoaded) return;
+ Buttons.onViewportResize();
+ });
+ if (resizeObserver != null) {
+ resizeObserver.observe(playerEl);
+ } else {
+ final timer = new haxe.Timer(50);
+ timer.run = () -> {
+ if (isLoaded) return;
+ Buttons.onViewportResize();
+ }
+ }
}
function initItemButtons():Void {
@@ -239,6 +253,7 @@ class Player {
public function onCanBePlayed():Void {
if (!isLoaded) main.send({type: VideoLoaded});
isLoaded = true;
+ Buttons.onViewportResize();
}
public function onPlay():Void {
diff --git a/src/client/Utils.hx b/src/client/Utils.hx
index a0e19f7..155292f 100644
--- a/src/client/Utils.hx
+++ b/src/client/Utils.hx
@@ -26,6 +26,16 @@ class Utils {
|| (~/^Mac/.match(navigator.platform) && navigator.maxTouchPoints > 4);
}
+ public static var isMacSafari = _isMacSafari();
+
+ static function _isMacSafari():Bool {
+ final isMac = navigator.userAgent.contains("Macintosh");
+ final isSafari = navigator.userAgent.contains("Safari")
+ && !navigator.userAgent.contains("Chrom")
+ && !navigator.userAgent.contains("Edg");
+ return isMac && isSafari;
+ }
+
public static function isAndroid():Bool {
final ua = navigator.userAgent.toLowerCase();
return ua.indexOf("android") > -1;
@@ -66,8 +76,6 @@ class Utils {
final el2:Dynamic = el;
if (el.requestFullscreen != null) {
el.requestFullscreen();
- } else if (el2.mozRequestFullScreen != null) {
- el2.mozRequestFullScreen();
} else if (el2.webkitRequestFullscreen != null) {
el2.webkitRequestFullscreen(untyped Element.ALLOW_KEYBOARD_INPUT);
} else return false;
@@ -77,7 +85,6 @@ class Utils {
public static function cancelFullscreen(el:Element):Void {
final doc:Dynamic = document;
if (doc.cancelFullScreen != null) doc.cancelFullScreen();
- else if (doc.mozCancelFullScreen != null) doc.mozCancelFullScreen();
else if (doc.webkitCancelFullScreen != null) doc.webkitCancelFullScreen();
}
@@ -159,4 +166,13 @@ class Utils {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
+
+ public static function createResizeObserver(callback:(entries:Array<Dynamic>) -> Void):Null<{
+ observe:(el:Element) -> Void
+ }> {
+ return null;
+ final window = js.Browser.window;
+ final observer = (window : Dynamic).ResizeObserver ?? return null;
+ return js.Syntax.code("new ResizeObserver({0})", callback);
+ }
}
diff --git a/src/client/players/Raw.hx b/src/client/players/Raw.hx
index eb93c84..01752e7 100644
--- a/src/client/players/Raw.hx
+++ b/src/client/players/Raw.hx
@@ -140,7 +140,7 @@ class Raw implements IPlayer {
final subsUri = try {
new URL(subsUrl);
} catch (e) {
- Main.serverMessage('Failed to add subs: bad url ($subsUrl)');
+ Main.instance.serverMessage('Failed to add subs: bad url ($subsUrl)');
return;
}
// make local url as relative path to skip proxy
diff --git a/src/client/players/RawSubs.hx b/src/client/players/RawSubs.hx
index a4404b0..e9b2203 100644
--- a/src/client/players/RawSubs.hx
+++ b/src/client/players/RawSubs.hx
@@ -215,7 +215,7 @@ class RawSubs {
static function isProxyError(text:String):Bool {
if (text.startsWith("Proxy error:")) {
- Main.serverMessage("Failed to add subs: proxy error");
+ Main.instance.serverMessage("Failed to add subs: proxy error");
trace('Failed to add subs: $text');
return true;
}
diff --git a/src/client/players/Youtube.hx b/src/client/players/Youtube.hx
index 5a8921a..b30dfb3 100644
--- a/src/client/players/Youtube.hx
+++ b/src/client/players/Youtube.hx
@@ -165,7 +165,7 @@ class Youtube implements IPlayer {
function youtubeApiError(error:Dynamic):Void {
final code:Int = error.code;
final msg:String = error.message;
- Main.serverMessage('Error $code: $msg', false);
+ Main.instance.serverMessage('Error $code: $msg', false);
}
function getRemoteDataFallback(url:String, callback:(data:VideoData) -> Void):Void {
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage