aboutsummaryrefslogtreecommitdiffstats
path: root/src/client
diff options
context:
space:
mode:
authorRblSb <msrblsb@gmail.com>2025-01-24 08:42:34 +0300
committerRblSb <msrblsb@gmail.com>2025-01-24 19:37:02 +0300
commit6ead98595d71afba9d11d3300756b46f18fd6bda (patch)
treed087094f9a124a31adad290a5040a8f7d4902186 /src/client
parenta843ae530b07b92fc7341754b4722cdcb8dfb831 (diff)
Add server pause when leader disconnects
Diffstat (limited to 'src/client')
-rw-r--r--src/client/Buttons.hx22
-rw-r--r--src/client/JsApi.hx43
-rw-r--r--src/client/Main.hx104
-rw-r--r--src/client/Player.hx30
-rw-r--r--src/client/Utils.hx7
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);
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage