aboutsummaryrefslogtreecommitdiffstats
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/Buttons.hx61
-rw-r--r--src/client/JsApi.hx24
-rw-r--r--src/client/Main.hx45
-rw-r--r--src/client/Utils.hx56
-rw-r--r--src/client/players/Raw.hx2
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;
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage