diff options
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/Buttons.hx | 64 | ||||
| -rw-r--r-- | src/client/Main.hx | 88 | ||||
| -rw-r--r-- | src/client/Player.hx | 17 | ||||
| -rw-r--r-- | src/client/Utils.hx | 22 | ||||
| -rw-r--r-- | src/client/players/Raw.hx | 2 | ||||
| -rw-r--r-- | src/client/players/RawSubs.hx | 2 | ||||
| -rw-r--r-- | src/client/players/Youtube.hx | 2 |
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 { |
