aboutsummaryrefslogtreecommitdiffstats
path: root/src/client/Main.hx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/Main.hx')
-rw-r--r--src/client/Main.hx357
1 files changed, 228 insertions, 129 deletions
diff --git a/src/client/Main.hx b/src/client/Main.hx
index 62c12f8..8775070 100644
--- a/src/client/Main.hx
+++ b/src/client/Main.hx
@@ -27,7 +27,7 @@ import js.html.WebSocket;
class Main {
public static var instance(default, null):Main;
- static inline var SETTINGS_VERSION = 6;
+ static inline var SETTINGS_VERSION = 7;
static inline var MAX_CHAT_MESSAGES = 200;
public final settings:ClientSettings;
@@ -39,6 +39,12 @@ class Main {
public var isPlaylistOpen(default, null) = true;
public var playersCacheSupport(default, null):Array<PlayerType> = [];
public var showingServerPause(default, null) = false;
+ public var hasSeenSkipVote = false;
+ public var chatInput:ChatInput;
+
+ var skipVoteCount = 0;
+ var skipVoteRequired = 0;
+
/** How much time passed since lastState.time update **/
public var timeFromLastState(default, null) = 0.0;
public final lastState:GetTimeEvent = {
@@ -91,6 +97,8 @@ class Main {
latestLinks: [],
latestSubs: [],
hotkeysEnabled: true,
+ pageFullscreen: false,
+ fullscreenAction: Utils.isTouch(),
showHintList: true,
checkboxes: [],
checkedCache: [],
@@ -138,28 +146,14 @@ class Main {
function settingsPatcher(data:Any, version:Int):Any {
switch (version) {
- case 1:
- final data:ClientSettings = data;
- data.hotkeysEnabled = true;
- case 2:
- final data:ClientSettings = data;
- data.latestSubs = [];
- case 3:
- final data:ClientSettings = data;
- data.showHintList = true;
- case 4:
- final data:ClientSettings = data;
- data.checkboxes = [];
+ case 1, 2, 3, 4:
case 5:
+ Settings.reset();
+ data = Settings.read();
+ case 6:
final data:ClientSettings = data;
- data.checkedCache = [];
- Reflect.deleteField(data, "playerSize");
- Reflect.deleteField(data, "isExtendedPlayer");
- final oldCheck = data.checkboxes.find(item -> item.id == "cache-on-server");
- if (oldCheck != null) {
- data.checkboxes.remove(oldCheck);
- data.checkedCache.push(YoutubeType);
- }
+ data.pageFullscreen = false;
+ data.fullscreenAction = Utils.isTouch();
case SETTINGS_VERSION, _:
throw 'skipped version $version';
}
@@ -169,6 +163,7 @@ class Main {
function requestTime():Void {
if (!isSyncActive) return;
if (player.isListEmpty()) return;
+ if (!player.getCurrentItem().isPlayable()) return;
send({type: GetTime});
}
@@ -209,6 +204,7 @@ class Main {
}
function initListeners():Void {
+ chatInput = new ChatInput(this);
Buttons.init(this);
var openBtn:js.html.Element = js.Browser.document.getElementById("openChatBtn");
@@ -275,7 +271,7 @@ class Main {
final voteSkip = getEl("#voteskip");
voteSkip.onclick = e -> {
- if (Utils.isTouch() && !window.confirm(Lang.get("skipItemConfirm"))) return;
+ hasSeenSkipVote = true;
if (player.isListEmpty()) return;
final items = player.getItems();
final pos = player.getItemPos();
@@ -354,7 +350,7 @@ class Main {
final mediaUrl:InputElement = getEl("#mediaurl");
final subsUrl:InputElement = getEl("#subsurl");
final checkboxTemp:InputElement = getEl("#addfromurl .add-temp");
- final isTemp = checkboxTemp.checked;
+ final isTemp = isTempChecked(checkboxTemp);
final checkboxCache:InputElement = getEl("#cache-on-server");
final doCache = isAdmin()
&& checkboxCache.checked
@@ -404,11 +400,12 @@ class Main {
function addVideoArray(links:Array<String>, atEnd:Bool, isTemp:Bool, doCache:Bool):Void {
if (links.length == 0) return;
final link = links.shift();
- addVideo(link, atEnd, isTemp, doCache, () ->
- addVideoArray(links, atEnd, isTemp, doCache));
+ addVideo(link, atEnd, isTemp, doCache, () -> {
+ addVideoArray(links, atEnd, isTemp, doCache);
+ });
}
- public function addVideo(url:String, atEnd:Bool, isTemp:Bool, doCache:Bool, ?callback:() -> Void):Void {
+ public function addVideo(url:String, atEnd:Bool, isTemp:Bool, doCache:Bool, ?afterSend:() -> Void):Void {
final protocol = Browser.location.protocol;
if (url.startsWith("/")) {
final host = Browser.location.hostname;
@@ -420,25 +417,13 @@ class Main {
if (!url.startsWith("http")) url = '$protocol//$url';
}
- final obj:VideoDataRequest = {
- url: url,
- atEnd: atEnd
- };
- if (doCache) showProgressInfo(Lang.get("caching") + "...");
- player.getVideoData(obj, (data:VideoData) -> {
- if (data.duration == 0) {
- if (doCache) hideDynamicChin();
- serverMessage(Lang.get("addVideoError"));
- return;
- }
- data.title ??= Lang.get("rawVideo");
- data.url ??= url;
+ function sendItem(data:VideoData):Void {
send({
type: AddVideo,
addVideo: {
item: {
- url: data.url,
- title: data.title,
+ url: data.url ?? url,
+ title: data.title ?? "",
author: personal.name,
duration: data.duration,
isTemp: isTemp,
@@ -450,7 +435,76 @@ class Main {
atEnd: atEnd
}
});
- if (callback != null) callback();
+ if (afterSend != null) afterSend();
+ }
+
+ final isServerResolvesMeta = doCache && isServerCacheServiceEnabled();
+
+ // skip vk/vimeo api load if we cache anyway
+ final linkPlayerType = getLinkPlayerType(url);
+ if (isServerResolvesMeta && (linkPlayerType == VkType || linkPlayerType == VimeoType)) {
+ sendItem({
+ duration: 0,
+ title: "",
+ url: url,
+ playerType: linkPlayerType
+ });
+ return;
+ }
+
+ final obj:VideoDataRequest = {
+ url: url,
+ atEnd: atEnd
+ };
+ player.getVideoData(obj, (data:VideoData) -> {
+ if (data.duration == 0) {
+ if (!isServerResolvesMeta) {
+ serverMessage(Lang.get("addVideoError"));
+ return;
+ }
+ } else {
+ data.title ??= Lang.get("rawVideo");
+ }
+ sendItem(data);
+ });
+ }
+
+ function isServerCacheServiceEnabled():Bool {
+ return playersCacheSupport.exists(type -> type != RawType);
+ }
+
+ final activeUploads:Map<String, () -> Void> = [];
+
+ public function registerUpload(url:String, cancel:() -> Void):Void {
+ activeUploads[url] = cancel;
+ }
+
+ public function unregisterUpload(url:String):Void {
+ activeUploads.remove(url);
+ }
+
+ function cancelUpload(url:String):Void {
+ final cancel = activeUploads[url] ?? return;
+ activeUploads.remove(url);
+ cancel();
+ }
+
+ public function addUploadedVideo(url:String, title:String, duration:Float, atEnd:Bool, isTemp:Bool):Void {
+ send({
+ type: AddVideo,
+ addVideo: {
+ item: {
+ url: url,
+ title: title,
+ author: personal.name,
+ duration: duration,
+ isTemp: isTemp,
+ doCache: false,
+ isIncomplete: true,
+ playerType: RawType
+ },
+ atEnd: atEnd
+ }
});
}
@@ -462,8 +516,6 @@ class Main {
final mediaTitle:InputElement = getEl("#customembed-title");
final title = mediaTitle.value;
mediaTitle.value = "";
- final checkbox:InputElement = getEl("#customembed .add-temp");
- final isTemp = checkbox.checked;
final obj:VideoDataRequest = {
url: iframe,
atEnd: atEnd
@@ -484,7 +536,7 @@ class Main {
title: data.title,
author: personal.name,
duration: data.duration,
- isTemp: isTemp,
+ isTemp: true,
doCache: false,
playerType: IframeType
},
@@ -615,39 +667,57 @@ class Main {
if (player.itemsLength() == 1) player.setVideo(0);
case VideoLoaded:
- lastState.paused = false;
- lastState.pausedByServer = false;
+ resetSkipVoteUi();
lastState.time = 0;
updateLastStateTime();
+ updatePauseState(false);
player.setTime(0);
player.play();
// try to sync leader after with GetTime events
if (isLeader() && !player.isVideoLoaded()) forceSyncNextTick = true;
case RemoveVideo:
+ final isCurrent = player.getCurrentItem()?.url == data.removeVideo.url;
+ cancelUpload(data.removeVideo.url);
player.removeItem(data.removeVideo.url);
+ if (isCurrent) resetSkipVoteUi();
+ updateServerPauseUi();
if (player.isListEmpty()) player.pause();
case SkipVideo:
+ resetSkipVoteUi();
player.skipItem(data.skipVideo.url);
+ updateServerPauseUi();
if (player.isListEmpty()) player.pause();
+ case SkipVideoStats:
+ if (data.skipVideoStats.voted == 1 && skipVoteCount == 0) {
+ serverMessage(Lang.get("skipVoteInitiated"));
+ }
+ skipVoteCount = data.skipVideoStats.voted;
+ skipVoteRequired = data.skipVideoStats.required;
+
+ final stat = '($skipVoteCount/$skipVoteRequired)';
+ getEl("#voteskip-text").textContent = '$stat ${Lang.get("voteForSkip")}';
+
+ final indicator = getEl("#playlist-menu-indicator");
+ final dropdown = getEl("#playlist-menu-dropdown");
+ if (!hasSeenSkipVote && dropdown.style.display == "none") {
+ indicator.style.display = "";
+ }
+
case Pause:
lastState.time = data.pause.time;
- lastState.paused = true;
updateLastStateTime();
- player.setPauseIndicator(lastState.paused);
- updateUserList();
+ updatePauseState(true, lastState.pausedByServer);
if (isLeader()) return;
player.pause();
player.setTime(data.pause.time);
case Play:
lastState.time = data.play.time;
- lastState.paused = false;
updateLastStateTime();
- player.setPauseIndicator(lastState.paused);
- updateUserList();
+ updatePauseState(false);
if (isLeader()) return;
final synchThreshold = settings.synchThreshold;
final newTime = data.play.time;
@@ -662,21 +732,11 @@ class Main {
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;
updateLastStateTime();
- if (isPauseChanged) updateUserList();
-
- final pausedByServer = data.getTime.pausedByServer;
- if (pausedByServer) {
- showServerUnpause();
- } else if (showingServerPause) {
- hideDynamicChin();
- }
+ updatePauseState(data.getTime.paused, data.getTime.pausedByServer);
if (player.getPlaybackRate() != data.getTime.rate) {
player.setPlaybackRate(data.getTime.rate);
@@ -729,8 +789,10 @@ class Main {
updateUserList();
setLeaderButton(isLeader());
if (isLeader()) player.onSetTime();
+ if (!hasLeader()) updatePauseState(lastState.paused);
case PlayItem:
+ resetSkipVoteUi();
player.setVideo(data.playItem.pos);
case SetNextItem:
@@ -743,7 +805,9 @@ class Main {
clearChat();
case ClearPlaylist:
+ resetSkipVoteUi();
player.clearItems();
+ updateServerPauseUi();
if (player.isListEmpty()) player.pause();
case ShufflePlaylist: // server-only
@@ -762,25 +826,14 @@ class Main {
public function onProgressEvent(data:WsEvent):Void {
final data = data.progress;
- final text = switch data.type {
- case Caching:
- final caching = Lang.get("caching");
- final name = data.data;
- '$caching $name';
- case Downloading: Lang.get("downloading");
- case Uploading: Lang.get("uploading");
+ final url = data.url ?? return;
+ switch data.type {
+ case Caching, Downloading, Uploading:
+ player.setItemProgress(url, data.ratio);
+ case Completed:
+ player.completeItem(url);
case Canceled:
- hideDynamicChin();
- return;
- }
- final percent = (data.ratio * 100).toFixed(1);
- var text = '$text...';
- if (percent > 0) text += ' $percent%';
- showProgressInfo(text);
- if (data.ratio == 1) {
- Timer.delay(() -> {
- hideDynamicChin();
- }, 500);
+ player.removeItem(url);
}
}
@@ -1109,10 +1162,11 @@ class Main {
if (client.isLeader) list.add('<ion-icon name="$iconName"></ion-icon>');
var klass = client.isBanned ? "userlist_banned" : "";
if (client.isAdmin) klass += " userlist_owner";
- list.add('<span class="$klass">${client.name}</span></div>');
+ list.add('<span class="$klass">${client.name.htmlEscape()}</span></div>');
}
final userlist = getEl("#userlist");
userlist.innerHTML = list.toString();
+ if (player != null) player.updateTitle();
}
function getPageTitle():String {
@@ -1184,6 +1238,7 @@ class Main {
showScrollToChatEndBtn();
}
if (onBlinkTab == null) blinkTabWithTitle('*${Lang.get("chat")}*');
+ if (name != personal.name) Buttons.onNewChatMessage();
}
function getFirstMessageDiv():Null<Element> {
@@ -1214,34 +1269,57 @@ class Main {
}, {once: true});
}
- public function showProgressInfo(text:String):Void {
- final chin = getEl("#dynamic-chin");
- var div = chin.querySelector("#progress-info");
- if (div == null) {
- div = document.createDivElement();
- div.id = "progress-info";
- chin.prepend(div);
- }
- div.textContent = text;
- showDynamicChin();
+ public function resetSkipVoteUi():Void {
+ hasSeenSkipVote = false;
+ skipVoteCount = 0;
+ skipVoteRequired = 0;
+ getEl("#voteskip-text").textContent = Lang.get("voteForSkip");
+ getEl("#playlist-menu-indicator").style.display = "none";
+ }
+
+ public function updatePauseState(paused:Bool, pausedByServer = false):Void {
+ final isPauseChanged = lastState.paused != paused;
+ lastState.paused = paused;
+ lastState.pausedByServer = pausedByServer;
+
+ player.setPauseIndicator(lastState.paused);
+ if (isPauseChanged) updateUserList();
+
+ updateServerPauseUi();
+ }
+
+ public function updateServerPauseUi():Void {
+ final show = lastState.pausedByServer && !player.isListEmpty();
+ if (showingServerPause == show) return;
+ showingServerPause = show;
+ renderDynamicChin();
}
- public function showServerUnpause():Void {
- if (showingServerPause) return;
- showingServerPause = true;
+ public function renderDynamicChin():Void {
final chin = getEl("#dynamic-chin");
+ if (!showingServerPause) {
+ hideDynamicChinAnim();
+ return;
+ }
chin.innerHTML = "";
+ final wrapper = document.createDivElement();
+ wrapper.style.display = "flex";
+ wrapper.style.flexDirection = "column";
+ wrapper.style.gap = "0.5rem";
+
final div = document.createDivElement();
div.className = "server-whisper";
+ div.style.textAlign = "center";
div.textContent = Lang.get("leaderDisconnectedServerOnPause");
- chin.appendChild(div);
+ wrapper.appendChild(div);
+
final btn = document.createButtonElement();
btn.id = "unpause-server";
btn.textContent = Lang.get("unpause");
- chin.appendChild(btn);
+ wrapper.appendChild(btn);
btn.onclick = () -> {
- hideDynamicChin();
+ updatePauseState(lastState.paused, false);
send({
type: SetLeader,
setLeader: {
@@ -1250,11 +1328,12 @@ class Main {
});
JsApi.once(SetLeader, event -> removeLeader());
}
+ chin.appendChild(wrapper);
- showDynamicChin();
+ showDynamicChinAnim();
}
- function showDynamicChin():Void {
+ function showDynamicChinAnim():Void {
final chin = getEl("#dynamic-chin");
if (chin.style.display == "") return;
chin.style.display = "";
@@ -1275,9 +1354,9 @@ class Main {
chin.addEventListener("transitionend", onTransitionEnd);
}
- public function hideDynamicChin():Void {
- showingServerPause = false;
+ function hideDynamicChinAnim():Void {
final chin = getEl("#dynamic-chin");
+ if (chin.style.display == "none") return;
final h = chin.clientHeight;
chin.style.height = '${h}px';
Timer.delay(() -> {
@@ -1342,15 +1421,26 @@ class Main {
/* Returns `true` if text should not be sent to chat */
public function handleCommands(command:String):Bool {
if (!command.startsWith("/")) return false;
- final args = command.trim().split(" ");
- command = args.shift().substr(1);
- if (command.length == 0) return false;
+ final parsed = Commands.parse(command);
+ final args = parsed.args;
+ switch (parsed.cmd?.name) {
+ case null:
+ final cmdNameWithoutSlash = parsed.rawCmd.substr(1);
+ if (matchSimpleDate.match(cmdNameWithoutSlash)) {
+ send({
+ type: Rewind,
+ rewind: {
+ time: parseSimpleDate(cmdNameWithoutSlash)
+ }
+ });
+ }
+ return false;
- switch (command) {
- case "help":
+ case Help:
showChatHintList();
return true;
- case "ban":
+
+ case Ban:
mergeRedundantArgs(args, 0, 2);
final name = args[0];
final time = parseSimpleDate(args[1]);
@@ -1363,7 +1453,8 @@ class Main {
}
});
return true;
- case "unban", "removeBan":
+
+ case Unban:
mergeRedundantArgs(args, 0, 1);
final name = args[0];
send({
@@ -1374,7 +1465,8 @@ class Main {
}
});
return true;
- case "kick":
+
+ case Kick:
mergeRedundantArgs(args, 0, 1);
final name = args[0];
send({
@@ -1384,10 +1476,11 @@ class Main {
}
});
return true;
- case "clear":
+
+ case Clear:
send({type: ClearChat});
return true;
- case "giveup":
+ case Giveup:
mergeRedundantArgs(args, 0, 1);
final name = args[0] ?? "";
send({
@@ -1397,13 +1490,17 @@ class Main {
}
});
return true;
- case "flashback", "fb":
+
+
+ case Flashback:
send({type: Flashback});
return false;
- case "ad":
+
+ case Ad:
player.skipAd();
return false;
- case "volume":
+
+ case Volume:
var v = Std.parseFloat(args[0]);
if (Math.isNaN(v)) v = 1;
v = v.clamp(0, 3);
@@ -1418,23 +1515,15 @@ class Main {
final rawPlayer = @:privateAccess player.rawPlayer;
rawPlayer.boostVolume(v);
return true;
- case "dump":
+
+ case Dump:
send({type: Dump});
return true;
- case "crash":
+
+ case Crash:
send({type: CrashTest});
return true;
}
- if (matchSimpleDate.match(command)) {
- send({
- type: Rewind,
- rewind: {
- time: parseSimpleDate(command)
- }
- });
- return false;
- }
- return false;
}
final matchSimpleDate = ~/^-?([0-9]+d)?([0-9]+h)?([0-9]+m)?([0-9]+s?)?$/;
@@ -1553,6 +1642,16 @@ class Main {
return clients.hasLeader();
}
+ public function getLeader():Null<Client> {
+ return clients.getLeader();
+ }
+
+ public static function isTempChecked(checkbox:Null<InputElement>):Bool {
+ if (checkbox == null) return true;
+ final isHidden = checkbox.style.display == "none";
+ return isHidden ? true : checkbox.checked;
+ }
+
public function hasLeaderOnPauseRequest():Bool {
final hasAccess = isPageVisible && !isPageUnloading;
return config.requestLeaderOnPause && hasAccess;
@@ -1593,7 +1692,7 @@ class Main {
}
@:generic
- public static inline function getEl<T:Element>(id:String):T {
+ public static inline function getEl<T:Element>(id:String):Null<T> {
return cast document.querySelector(id);
}
}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage