diff options
| author | RblSb <msrblsb@gmail.com> | 2025-01-24 08:42:34 +0300 |
|---|---|---|
| committer | RblSb <msrblsb@gmail.com> | 2025-01-24 19:37:02 +0300 |
| commit | 6ead98595d71afba9d11d3300756b46f18fd6bda (patch) | |
| tree | d087094f9a124a31adad290a5040a8f7d4902186 /src/client | |
| parent | a843ae530b07b92fc7341754b4722cdcb8dfb831 (diff) | |
Add server pause when leader disconnects
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/Buttons.hx | 22 | ||||
| -rw-r--r-- | src/client/JsApi.hx | 43 | ||||
| -rw-r--r-- | src/client/Main.hx | 104 | ||||
| -rw-r--r-- | src/client/Player.hx | 30 | ||||
| -rw-r--r-- | src/client/Utils.hx | 7 |
5 files changed, 158 insertions, 48 deletions
diff --git a/src/client/Buttons.hx b/src/client/Buttons.hx index 36f5729..fb8337d 100644 --- a/src/client/Buttons.hx +++ b/src/client/Buttons.hx @@ -8,6 +8,7 @@ import js.html.Element; import js.html.ImageElement; import js.html.InputElement; import js.html.KeyboardEvent; +import js.html.TransitionEvent; import js.html.VisualViewport; class Buttons { @@ -50,12 +51,14 @@ class Buttons { final isActive = smilesBtn.classList.toggle("active"); if (isActive) { wrap.style.display = ""; - wrap.style.height = outerHeight(list) + "px"; + wrap.style.height = Utils.outerHeight(list) + "px"; } else { wrap.style.height = "0"; - wrap.addEventListener("transitionend", e -> { + function onTransitionEnd(e:TransitionEvent):Void { + if (e.propertyName != "height") return; wrap.style.display = "none"; - }, {once: true}); + wrap.removeEventListener("transitionend", onTransitionEnd); + } } if (list.firstElementChild.dataset.src == null) return; for (child in list.children) { @@ -225,7 +228,7 @@ class Buttons { final panel = ge("#addfromurl"); final oldH = panel.style.height; // save for animation panel.style.height = ""; // to calculate height from content - final newH = outerHeight(panel) + "px"; + final newH = Utils.outerHeight(panel) + "px"; panel.style.height = oldH; Timer.delay(() -> panel.style.height = newH, 0); } @@ -279,18 +282,13 @@ class Buttons { } else { final list = target.firstElementChild; if (target.style.height == "") target.style.height = "0"; - target.style.height = outerHeight(list) + "px"; + Timer.delay(() -> { + target.style.height = Utils.outerHeight(list) + "px"; + }, 0); } return el.classList.toggle("active"); } - static function outerHeight(el:Element):Float { - final style = window.getComputedStyle(el); - return (el.getBoundingClientRect().height - + Std.parseFloat(style.marginTop) - + Std.parseFloat(style.marginBottom)); - } - static function swapPlayerAndChat():Void { settings.isSwapped = ge("body").classList.toggle("swap"); final sizes = document.body.style.gridTemplateColumns.split(" "); diff --git a/src/client/JsApi.hx b/src/client/JsApi.hx index abf9a6a..defcc4d 100644 --- a/src/client/JsApi.hx +++ b/src/client/JsApi.hx @@ -16,7 +16,7 @@ class JsApi { static final subtitleFormats = []; static final videoChange:Array<VideoChangeFunc> = []; static final videoRemove:Array<VideoChangeFunc> = []; - static final onceListeners:Array<{type:WsEventType, func:OnceEventFunc}> = []; + static final onceListeners:Array<{type:WsEventType, callback:OnceEventFunc}> = []; public static function init(main:Main, player:Player):Void { JsApi.main = main; @@ -147,52 +147,47 @@ class JsApi { * `});` */ @:expose - public static function once(type:WsEventType, func:OnceEventFunc):Void { - onceListeners.push({type: type, func: func}); + public static function once(type:WsEventType, callback:OnceEventFunc):Void { + onceListeners.unshift({type: type, callback: callback}); } public static function fireOnceEvent(event:WsEvent):Void { - var i = 0; - while (i < onceListeners.length) { - final listener = onceListeners[i]; - if (listener.type == event.type) { - listener.func(event); - onceListeners.remove(listener); - continue; - } - i++; + for (listener in onceListeners.reversed()) { + if (listener.type != event.type) continue; + listener.callback(event); + onceListeners.remove(listener); } } @:expose - static function notifyOnVideoChange(func:VideoChangeFunc):Void { - videoChange.push(func); + static function notifyOnVideoChange(callback:VideoChangeFunc):Void { + videoChange.push(callback); } @:expose - static function removeFromVideoChange(func:VideoChangeFunc):Void { - videoChange.remove(func); + static function removeFromVideoChange(callback:VideoChangeFunc):Void { + videoChange.remove(callback); } public static function fireVideoChangeEvents(item:VideoItem):Void { - for (func in videoChange) { - func(item); + for (callback in videoChange) { + callback(item); } } @:expose - static function notifyOnVideoRemove(func:VideoChangeFunc):Void { - videoRemove.push(func); + static function notifyOnVideoRemove(callback:VideoChangeFunc):Void { + videoRemove.push(callback); } @:expose - static function removeFromVideoRemove(func:VideoChangeFunc):Void { - videoRemove.remove(func); + static function removeFromVideoRemove(callback:VideoChangeFunc):Void { + videoRemove.remove(callback); } public static function fireVideoRemoveEvents(item:VideoItem):Void { - for (func in videoRemove) { - func(item); + for (callback in videoRemove) { + callback(item); } } } diff --git a/src/client/Main.hx b/src/client/Main.hx index 9a4b426..62fa7de 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -2,6 +2,7 @@ package client; import Client.ClientData; import Types.Config; +import Types.GetTimeEvent; import Types.Permission; import Types.PlayerType; import Types.VideoData; @@ -19,6 +20,7 @@ import js.html.Event; import js.html.InputElement; import js.html.KeyboardEvent; import js.html.MouseEvent; +import js.html.TransitionEvent; import js.html.URL; import js.html.VideoElement; import js.html.WebSocket; @@ -37,6 +39,13 @@ class Main { public var globalIp(default, null) = ""; public var isPlaylistOpen(default, null) = true; public var playersCacheSupport(default, null):Array<PlayerType> = []; + public var showingServerPause(default, null) = false; + public final lastState:GetTimeEvent = { + time: 0, + rate: 1.0, + paused: false, + pausedByServer: false + }; final clients:Array<Client> = []; var pageTitle = document.title; @@ -109,7 +118,7 @@ class Main { if (!player.isVideoLoaded()) return; gotFirstPageInteraction = true; player.unmute(); - if (!hasLeader()) player.play(); + if (!hasLeader() && !showingServerPause) player.play(); document.removeEventListener("click", onFirstInteraction); } @@ -515,14 +524,16 @@ class Main { if (player.isListEmpty()) player.pause(); case Pause: - player.setPauseIndicator(false); + lastState.paused = true; + player.setPauseIndicator(lastState.paused); updateUserList(); if (isLeader()) return; player.pause(); player.setTime(data.pause.time); case Play: - player.setPauseIndicator(true); + lastState.paused = false; + player.setPauseIndicator(lastState.paused); updateUserList(); if (isLeader()) return; final synchThreshold = settings.synchThreshold; @@ -535,8 +546,24 @@ class Main { case GetTime: data.getTime.paused ??= false; + data.getTime.pausedByServer ??= false; data.getTime.rate ??= 1; + final isPauseChanged = lastState.paused != data.getTime.paused; + lastState.time = data.getTime.time; + lastState.paused = data.getTime.paused; + lastState.pausedByServer = data.getTime.pausedByServer; + lastState.rate = data.getTime.rate; + + if (isPauseChanged) updateUserList(); + + final pausedByServer = data.getTime.pausedByServer; + if (pausedByServer) { + showServerUnpause(); + } else if (showingServerPause) { + hideDynamicChin(); + } + if (player.getPlaybackRate() != data.getTime.rate) { player.setPlaybackRate(data.getTime.rate); } @@ -558,7 +585,7 @@ class Main { } else { if (data.getTime.paused) player.pause(); } - player.setPauseIndicator(!data.getTime.paused); + player.setPauseIndicator(data.getTime.paused); if (Math.abs(time - newTime) < synchThreshold) return; // +0.5s for buffering if (!data.getTime.paused) player.setTime(newTime + 0.5); @@ -913,7 +940,7 @@ class Main { final list = new StringBuf(); for (client in clients) { list.add('<div class="userlist_item">'); - final iconName = player.isPaused() ? "pause" : "play"; + final iconName = lastState.paused ? "pause" : "play"; if (client.isLeader) list.add('<ion-icon name="$iconName"></ion-icon>'); var klass = client.isBanned ? "userlist_banned" : ""; if (client.isAdmin) klass += " userlist_owner"; @@ -1014,6 +1041,73 @@ class Main { }, {once: true}); } + public function showServerUnpause():Void { + if (showingServerPause) return; + showingServerPause = true; + final chin = ge("#dynamic-chin"); + chin.innerHTML = ""; + + final div = document.createDivElement(); + div.className = "server-whisper"; + div.textContent = Lang.get("leaderDisconnectedServerOnPause"); + chin.appendChild(div); + final btn = document.createButtonElement(); + btn.id = "unpause-server"; + btn.textContent = Lang.get("unpause"); + chin.appendChild(btn); + btn.onclick = () -> { + hideDynamicChin(); + send({ + type: SetLeader, + setLeader: { + clientName: personal.name + } + }); + JsApi.once(SetLeader, event -> { + send({ + type: SetLeader, + setLeader: { + clientName: "" + } + }); + }); + } + + chin.style.display = ""; + chin.style.transition = "none"; + chin.classList.remove("collapsed"); + final h = chin.clientHeight; + chin.classList.add("collapsed"); + Timer.delay(() -> { + chin.style.transition = ""; + chin.classList.remove("collapsed"); + chin.style.height = '${h}px'; + }, 0); + function onTransitionEnd(e:TransitionEvent):Void { + if (e.propertyName != "height") return; + chin.style.height = ""; + chin.removeEventListener("transitionend", onTransitionEnd); + } + chin.addEventListener("transitionend", onTransitionEnd); + } + + public function hideDynamicChin():Void { + showingServerPause = false; + final chin = ge("#dynamic-chin"); + final h = chin.clientHeight; + chin.style.height = '${h}px'; + Timer.delay(() -> { + chin.style.height = ""; + chin.classList.add("collapsed"); + }, 0); + function onTransitionEnd(e:TransitionEvent):Void { + if (e.propertyName != "height") return; + chin.style.display = "none"; + chin.removeEventListener("transitionend", onTransitionEnd); + } + chin.addEventListener("transitionend", onTransitionEnd); + } + function onChatImageLoaded(e:Event):Void { scrollChatToEnd(); (cast e.target : Element).onload = null; diff --git a/src/client/Player.hx b/src/client/Player.hx index 0efa2e3..911092e 100644 --- a/src/client/Player.hx +++ b/src/client/Player.hx @@ -239,15 +239,19 @@ class Player { JsApi.fireVideoRemoveEvents(videoList.currentItem); player.removeVideo(); ge("#currenttitle").textContent = Lang.get("nothingPlaying"); - setPauseIndicator(true); + setPauseIndicator(false); } - public function setPauseIndicator(flag:Bool):Void { + public function setPauseIndicator(isPause:Bool):Void { if (!main.isSyncActive) return; - final state = flag ? "play" : "pause"; + final state = isPause ? "pause" : "play"; final el = ge("#pause-indicator"); - if (el.getAttribute("name") == state) return; el.setAttribute("name", state); + + final el2 = ge("#pause-indicator-portrait"); + el2.setAttribute("name", "pause"); + var isVisible = isPause || main.hasLeader(); + el2.style.display = isVisible ? "" : "none"; } public function onCanBePlayed():Void { @@ -259,7 +263,11 @@ class Player { public function onPlay():Void { audioTrack?.play(); - if (!main.isLeader()) return; + if (!main.isLeader()) { + // paused and no leader - instant pause + if (main.lastState.paused) pause(); + return; + } main.send({ type: Play, play: { @@ -279,10 +287,13 @@ class Player { final item = videoList.currentItem ?? return; // do not send pause if video is ended if (getTime() >= item.duration - 0.01) return; - final hasAutoPause = main.hasLeaderOnPauseRequest() + var hasAutoPause = main.hasLeaderOnPauseRequest() && videoList.length > 0 && getTime() > 1 && isLoaded; + // do not set leader on pause if user tried to play server-paused video + if (main.showingServerPause) hasAutoPause = false; + // set leader and pause if (hasAutoPause && !main.hasLeader()) { JsApi.once(SetLeader, event -> { final name = event.setLeader.clientName; @@ -298,7 +309,12 @@ class Player { main.toggleLeader(); return; } - if (!main.isLeader()) return; + if (!main.isLeader()) { + // no pause and no permission - instant play + if (!main.lastState.paused) play(); + return; + } + // we are leader, so just send pause main.send({ type: Pause, pause: { diff --git a/src/client/Utils.hx b/src/client/Utils.hx index 155292f..717d64f 100644 --- a/src/client/Utils.hx +++ b/src/client/Utils.hx @@ -47,6 +47,13 @@ class Utils { return wrapper.firstElementChild; } + public static function outerHeight(el:Element):Float { + final style = window.getComputedStyle(el); + return (el.getBoundingClientRect().height + + Std.parseFloat(style.marginTop) + + Std.parseFloat(style.marginBottom)); + } + public static function prepend(parent:Element, child:Element):Void { if (parent.firstChild == null) parent.appendChild(child); else parent.insertBefore(child, parent.firstChild); |
