diff options
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/Buttons.hx | 61 | ||||
| -rw-r--r-- | src/client/JsApi.hx | 24 | ||||
| -rw-r--r-- | src/client/Main.hx | 45 | ||||
| -rw-r--r-- | src/client/Utils.hx | 56 | ||||
| -rw-r--r-- | src/client/players/Raw.hx | 2 |
5 files changed, 163 insertions, 25 deletions
diff --git a/src/client/Buttons.hx b/src/client/Buttons.hx index fb8337d..334debc 100644 --- a/src/client/Buttons.hx +++ b/src/client/Buttons.hx @@ -1,7 +1,10 @@ package client; +import Types.UploadResponse; +import Types.WsEvent; import client.Main.ge; import haxe.Timer; +import haxe.io.Path; import js.Browser.document; import js.Browser.window; import js.html.Element; @@ -239,6 +242,60 @@ class Buttons { mediaUrl.focus(); } + ge("#mediaurl-upload").onclick = e -> { + Utils.browseFile((buffer, name) -> { + if (name == null || name.length == 0) name = "video"; + + // send last chunk separately to allow server file streaming while uploading + final chunkSize = 1024 * 1024 * 5; // 5 MB + if (buffer.byteLength > chunkSize) { + final lastChunk = buffer.slice(buffer.byteLength - chunkSize); + window.fetch("/upload-last-chunk", { + method: "POST", + headers: { + "content-name": Path.withoutExtension(name), + "client-name": main.getName(), + }, + body: lastChunk, + }); + } + + // send full file + final request = window.fetch("/upload", { + method: "POST", + headers: { + "content-name": Path.withoutExtension(name), + "client-name": main.getName(), + }, + body: buffer, + }); + request.then(e -> { + e.json().then((data:UploadResponse) -> { + trace(data.info); + if (data.errorId == null) return; + main.serverMessage(data.info, true, false); + }); + }).catchError(err -> { + trace(err); + Timer.delay(() -> { + main.hideDynamicChin(); + }, 500); + }); + + // set file url to input after upload starts + function onStartUpload(event:WsEvent):Void { + if (event.type != Progress) return; + final data = event.progress; + if (data.type != Uploading) return; + if (data.data == null) return; + final input:InputElement = ge("#mediaurl"); + input.value = data.data; + JsApi.off(Progress, onStartUpload); + } + JsApi.on(Progress, onStartUpload); + }); + } + final showOptions = ge("#showoptions"); showOptions.onclick = e -> { final isActive = toggleGroup(showOptions); @@ -362,7 +419,7 @@ class Buttons { } final selectLocalVideoBtn = ge("#selectLocalVideoBtn"); selectLocalVideoBtn.onclick = e -> { - Utils.browseFileUrl((url:String, name:String) -> { + Utils.browseFileUrl((url, name) -> { JsApi.setVideoSrc(url); }); } @@ -374,7 +431,7 @@ class Buttons { ge("#getplaylist").title += " (Alt-C)"; ge("#fullscreenbtn").title += " (Alt-F)"; ge("#leader_btn").title += " (Alt-L)"; - window.onkeydown = function(e:KeyboardEvent) { + window.onkeydown = (e:KeyboardEvent) -> { if (!settings.hotkeysEnabled) return; final target:Element = cast e.target; if (isElementEditable(target)) return; diff --git a/src/client/JsApi.hx b/src/client/JsApi.hx index defcc4d..b576b47 100644 --- a/src/client/JsApi.hx +++ b/src/client/JsApi.hx @@ -8,7 +8,7 @@ import js.Browser.window; import js.Syntax; private typedef VideoChangeFunc = (item:VideoItem) -> Void; -private typedef OnceEventFunc = (event:WsEvent) -> Void; +private typedef EventCallback = (event:WsEvent) -> Void; class JsApi { static var main:Main; @@ -16,7 +16,8 @@ class JsApi { static final subtitleFormats = []; static final videoChange:Array<VideoChangeFunc> = []; static final videoRemove:Array<VideoChangeFunc> = []; - static final onceListeners:Array<{type:WsEventType, callback:OnceEventFunc}> = []; + static final onListeners:Array<{type:WsEventType, callback:EventCallback}> = []; + static final onceListeners:Array<{type:WsEventType, callback:EventCallback}> = []; public static function init(main:Main, player:Player):Void { JsApi.main = main; @@ -147,11 +148,26 @@ class JsApi { * `});` */ @:expose - public static function once(type:WsEventType, callback:OnceEventFunc):Void { + public static function once(type:WsEventType, callback:EventCallback):Void { onceListeners.unshift({type: type, callback: callback}); } - public static function fireOnceEvent(event:WsEvent):Void { + public static function on(type:WsEventType, callback:EventCallback):Void { + onListeners.unshift({type: type, callback: callback}); + } + + public static function off(type:WsEventType, callback:EventCallback):Void { + final listener = onListeners.find(item -> { + return item.type == type && item.callback == callback; + }); + onListeners.remove(listener); + } + + public static function fireEvents(event:WsEvent):Void { + for (listener in onListeners.reversed()) { + if (listener.type != event.type) continue; + listener.callback(event); + } for (listener in onceListeners.reversed()) { if (listener.type != event.type) continue; listener.callback(event); diff --git a/src/client/Main.hx b/src/client/Main.hx index 6ec8727..61b3c3a 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -25,8 +25,6 @@ import js.html.URL; import js.html.VideoElement; import js.html.WebSocket; -using ClientTools; - class Main { public static var instance(default, null):Main; static inline var SETTINGS_VERSION = 5; @@ -457,7 +455,7 @@ class Main { final t = t.charAt(0).toLowerCase() + t.substr(1); trace('Event: ${data.type}', Reflect.field(data, t)); } - JsApi.fireOnceEvent(data); + JsApi.fireEvents(data); switch (data.type) { case Connected: onConnected(data); @@ -509,6 +507,26 @@ class Main { } serverMessage(text); + case Progress: + 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 percent = (data.ratio * 100).toFixed(1); + var text = '$text...'; + if (percent > 0) text += ' $percent%'; + showProgressInfo(text); + if (data.ratio == 1) { + Timer.delay(() -> { + hideDynamicChin(); + }, 500); + } + case AddVideo: player.addVideoItem(data.addVideo.item, data.addVideo.atEnd); if (player.itemsLength() == 1) player.setVideo(0); @@ -934,7 +952,7 @@ class Main { return msgBuf.lastElementChild?.className.startsWith("server-msg"); } - public function serverMessage(text:String, isText = true, withTimestamp = true):Void { + public function serverMessage(text:String, isText = true, withTimestamp = true):Element { final div = document.createDivElement(); final time = Date.now().toString().split(" ")[1]; div.className = "server-whisper"; @@ -947,6 +965,7 @@ class Main { else textDiv.innerHTML = text; addMessageDiv(div); scrollChatToEnd(); + return div; } public function serverHtmlMessage(el:Element):Void { @@ -1071,6 +1090,18 @@ class Main { }, {once: true}); } + public function showProgressInfo(text:String):Void { + final chin = ge("#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 showServerUnpause():Void { if (showingServerPause) return; showingServerPause = true; @@ -1096,6 +1127,12 @@ class Main { JsApi.once(SetLeader, event -> removeLeader()); } + showDynamicChin(); + } + + function showDynamicChin():Void { + final chin = ge("#dynamic-chin"); + if (chin.style.display == "") return; chin.style.display = ""; chin.style.transition = "none"; chin.classList.remove("collapsed"); diff --git a/src/client/Utils.hx b/src/client/Utils.hx index 4d85697..a120166 100644 --- a/src/client/Utils.hx +++ b/src/client/Utils.hx @@ -5,7 +5,9 @@ import js.Browser.document; import js.Browser.navigator; import js.Browser.window; import js.html.Element; +import js.html.FileReader; import js.html.URL; +import js.lib.ArrayBuffer; class Utils { public static function nativeTrace(msg:Dynamic, ?infos:haxe.PosInfos):Void { @@ -127,25 +129,51 @@ class Utils { #end } + public static function browseFile( + onFileLoad:(buffer:ArrayBuffer, name:String) -> Void + ):Void { + browseFileImpl(onFileLoad, true, false); + } + public static function browseFileUrl( onFileLoad:(url:String, name:String) -> Void, - isBinary = true, revoke = false ):Void { - final input = document.createElement("input"); + browseFileImpl(onFileLoad, false, revoke); + } + + static function browseFileImpl( + onFileLoad:(data:Dynamic, name:String) -> Void, + isBinary:Bool, + revokeAfterLoad:Bool + ):Void { + final input = document.createInputElement(); input.style.visibility = "hidden"; - input.setAttribute("type", "file"); + input.type = "file"; input.id = "browse"; - input.onclick = function(e) { + input.onclick = e -> { e.cancelBubble = true; e.stopPropagation(); } - input.onchange = function() { - final file:Dynamic = (input : Dynamic).files[0]; - final url = URL.createObjectURL(file); - onFileLoad(url, file.name); - document.body.removeChild(input); - if (revoke) URL.revokeObjectURL(url); + input.onchange = e -> { + final file = input.files[0] ?? return; + if (!isBinary) { + final url = URL.createObjectURL(file); + onFileLoad(url, file.name); + document.body.removeChild(input); + if (revokeAfterLoad) URL.revokeObjectURL(url); + return; + } + final reader = new FileReader(); + reader.onload = e -> { + final result:ArrayBuffer = reader.result; + onFileLoad(result, file.name); + document.body.removeChild(input); + } + reader.onerror = e -> { + document.body.removeChild(input); + } + reader.readAsArrayBuffer(file); } document.body.appendChild(input); input.click(); @@ -156,10 +184,10 @@ class Utils { type: mime }); final url = URL.createObjectURL(blob); - final a = document.createElement("a"); - untyped a.download = name; - untyped a.href = url; - a.onclick = function(e) { + final a = document.createAnchorElement(); + a.download = name; + a.href = url; + a.onclick = e -> { e.cancelBubble = true; e.stopPropagation(); } diff --git a/src/client/players/Raw.hx b/src/client/players/Raw.hx index f054f14..10a54e8 100644 --- a/src/client/players/Raw.hx +++ b/src/client/players/Raw.hx @@ -97,7 +97,7 @@ class Raw implements IPlayer { public function loadVideo(item:VideoItem):Void { final url = main.tryLocalIp(item.url); - final isHls = item.url.contains("m3u8") || item.title.endsWith("m3u8"); + final isHls = url.contains("m3u8") || item.title.endsWith("m3u8"); if (isHls && !isHlsLoaded) { loadHlsPlugin(() -> loadVideo(item)); return; |
