aboutsummaryrefslogtreecommitdiffstats
path: root/src/client
diff options
context:
space:
mode:
authorRblSb <msrblsb@gmail.com>2020-02-13 16:28:18 +0300
committerRblSb <msrblsb@gmail.com>2020-02-15 19:45:40 +0300
commit07d1955cefc093ffb12002902ed45e963030746e (patch)
tree8833eca2dc2ef07891aa8eb66daf7ad90f2ab0ce /src/client
Initial commit
Diffstat (limited to 'src/client')
-rw-r--r--src/client/Main.hx334
-rw-r--r--src/client/MobileView.hx52
-rw-r--r--src/client/Player.hx182
3 files changed, 568 insertions, 0 deletions
diff --git a/src/client/Main.hx b/src/client/Main.hx
new file mode 100644
index 0000000..2779202
--- /dev/null
+++ b/src/client/Main.hx
@@ -0,0 +1,334 @@
+package client;
+
+import haxe.Timer;
+import js.html.MouseEvent;
+import js.html.ButtonElement;
+import js.html.KeyboardEvent;
+import js.html.Element;
+import haxe.Json;
+import js.html.InputElement;
+import js.html.WebSocket;
+import js.Browser;
+import js.Browser.document;
+import js.lib.Date;
+import Client.ClientData;
+import Types;
+using ClientTools;
+
+class Main {
+
+ final clients:Array<Client> = [];
+ final personalHistory:Array<String> = [];
+ var personal:Null<Client>;
+ var personalHistoryId = -1;
+ var isConnected = false;
+ var ws:WebSocket;
+ final player:Player;
+ final onTimeGet = new Timer(2000);
+
+ static function main():Void new Main();
+
+ public function new(?host:String, port = 4201) {
+ player = new Player(this);
+ if (host == null) host = Browser.location.hostname;
+ if (host == "") host = "localhost";
+
+ initListeners();
+ onTimeGet.run = () -> send({type: GetTime});
+ Lang.init("langs", () -> {
+ openWebSocket(host, port);
+ });
+ }
+
+ function openWebSocket(host:String, port:Int):Void {
+ ws = new WebSocket('ws://$host:$port');
+ ws.onmessage = onMessage;
+ ws.onopen = () -> {
+ serverMessage(1);
+ isConnected = true;
+ }
+ ws.onclose = () -> {
+ // if initial connection refused
+ // or server/client offline
+ if (isConnected) serverMessage(2);
+ isConnected = false;
+ player.pause();
+ Timer.delay(() -> openWebSocket(host, port), 2000);
+ }
+ }
+
+ function initListeners():Void {
+ final guestName:InputElement = cast ge("#guestname");
+ guestName.onkeydown = (e:KeyboardEvent) -> {
+ if (e.keyCode == 13) send({
+ type: Login,
+ login: {
+ clientName: guestName.value
+ }
+ });
+ }
+
+ final chatLine:InputElement = cast ge("#chatline");
+ chatLine.onkeydown = function(e:KeyboardEvent) {
+ switch (e.keyCode) {
+ case 13: // Enter
+ send({
+ type: Message,
+ message: {
+ clientName: "",
+ text: chatLine.value
+ }
+ });
+ personalHistory.push(chatLine.value);
+ if (personalHistory.length > 50) personalHistory.shift();
+ personalHistoryId = -1;
+ chatLine.value = "";
+ case 38: // Up
+ personalHistoryId--;
+ if (personalHistoryId == -2) {
+ personalHistoryId = personalHistory.length - 1;
+ if (personalHistoryId == -1) return;
+ } else if (personalHistoryId == -1) personalHistoryId++;
+ chatLine.value = personalHistory[personalHistoryId];
+ case 40: // Down
+ if (personalHistoryId == -1) return;
+ personalHistoryId++;
+ if (personalHistoryId > personalHistory.length - 1) {
+ personalHistoryId = -1;
+ chatLine.value = "";
+ return;
+ }
+ chatLine.value = personalHistory[personalHistoryId];
+ }
+ }
+
+ MobileView.init();
+
+ final leaderBtn:InputElement = cast ge("#leader_btn");
+ leaderBtn.onclick = (e) -> {
+ if (personal == null) return;
+ leaderBtn.classList.toggle('label-success');
+ final name = personal.isLeader ? "" : personal.name;
+ send({
+ type: SetLeader,
+ setLeader: {
+ clientName: name
+ }
+ });
+ }
+
+ final showMediaUrl:ButtonElement = cast ge("#showmediaurl");
+ showMediaUrl.onclick = (e:MouseEvent) -> {
+ ge("#showmediaurl").classList.toggle("collapsed");
+ ge("#showmediaurl").classList.toggle("active");
+ ge("#addfromurl").classList.toggle("collapse");
+ }
+ ge("#queue_next").onclick = (e:MouseEvent) -> addVideoUrl();
+ ge("#queue_end").onclick = (e:MouseEvent) -> addVideoUrl();
+ ge("#mediaurl").onkeydown = function(e:KeyboardEvent) {
+ if (e.keyCode == 13) addVideoUrl();
+ }
+ }
+
+ public function isLeader():Bool {
+ return personal != null && personal.isLeader;
+ }
+
+ function addVideoUrl():Void {
+ final mediaUrl:InputElement = cast ge("#mediaurl");
+ final url = mediaUrl.value;
+ final name = personal == null ? "Unknown" : personal.name;
+ getRemoteVideoDuration(mediaUrl.value, (duration:Float) -> {
+ send({
+ type: AddVideo,
+ addVideo: {
+ item: {
+ url: url,
+ title: Lang.get("rawVideo"),
+ author: name,
+ duration: duration
+ }
+ }
+ });
+ });
+ mediaUrl.value = "";
+ }
+
+ function getRemoteVideoDuration(src:String, callback:(duration:Float)->Void):Void {
+ final player:Element = ge("#ytapiplayer");
+ final video = document.createVideoElement();
+ video.src = src;
+ video.onloadedmetadata = () -> {
+ trace(video.duration);
+ player.removeChild(video);
+ callback(video.duration);
+ }
+ prepend(player, video);
+ }
+
+ function prepend(parent:Element, child:Element):Void {
+ if (parent.firstChild == null) parent.appendChild(child);
+ else parent.insertBefore(child, parent.firstChild);
+ }
+
+ function onMessage(e):Void {
+ final data:WsEvent = Json.parse(e.data);
+ final t:String = cast data.type;
+ final t = t.charAt(0).toLowerCase() + t.substr(1);
+ trace('Event: ${data.type}', untyped data[t]);
+ switch (data.type) {
+ case Connected:
+ if (data.connected.isUnknownClient) {
+ updateClients(data.connected.clients);
+ ge("#guestlogin").style.display = "block";
+ ge("#chatline").style.display = "none";
+ } else {
+ onLogin(data.connected.clients, data.connected.clientName);
+ }
+ final list = data.connected.videoList;
+ if (list.length == 0) return;
+ player.setVideo(list[0]);
+ for (video in data.connected.videoList) {
+ player.addVideoItem(video);
+ }
+ case Login:
+ onLogin(data.login.clients, data.login.clientName);
+ case LoginError:
+ serverMessage(4, Lang.get("usernameError"));
+ case Logout:
+ updateClients(data.logout.clients);
+ personal = null;
+ ge("#guestlogin").style.display = "block";
+ ge("#chatline").style.display = "none";
+ case UpdateClients:
+ updateClients(data.updateClients.clients);
+ if (personal != null) personal = clients.getByName(personal.name);
+ case Message:
+ addMessage(data.message.clientName, data.message.text);
+ case AddVideo:
+ if (player.isListEmpty()) player.setVideo(data.addVideo.item);
+ player.addVideoItem(data.addVideo.item);
+ case VideoLoaded:
+ player.setTime(0);
+ player.play();
+ case RemoveVideo:
+ player.removeItem(data.removeVideo.url);
+ if (player.isListEmpty()) player.pause();
+ case Pause:
+ player.pause();
+ player.setTime(data.pause.time);
+ case Play:
+ player.setTime(data.play.time);
+ player.play();
+ case GetTime:
+ final newTime = data.getTime.time;
+ final time = player.getTime();
+ if (Math.abs(time - newTime) < 2) return;
+ player.setTime(newTime);
+ if (!data.getTime.paused) player.play();
+ case SetTime:
+ final newTime = data.setTime.time;
+ final time = player.getTime();
+ if (Math.abs(time - newTime) < 2) return;
+ player.setTime(newTime);
+ case SetLeader:
+ clients.setLeader(data.setLeader.clientName);
+ updateUserList();
+ if (personal == null) return;
+ final leaderBtn:InputElement = cast ge("#leader_btn");
+ if (personal.isLeader) leaderBtn.classList.add('label-success');
+ else leaderBtn.classList.remove('label-success');
+ }
+ }
+
+ function onLogin(data:Array<ClientData>, clientName:String):Void {
+ updateClients(data);
+ personal = clients.getByName(clientName);
+ if (personal == null) return;
+ ge("#guestlogin").style.display = "none";
+ ge("#chatline").style.display = "block";
+ }
+
+ function updateClients(newClients:Array<ClientData>):Void {
+ clients.resize(0);
+ for (client in newClients) {
+ clients.push(Client.fromData(client));
+ }
+ updateUserList();
+ }
+
+ public function send(data:WsEvent):Void {
+ if (!isConnected) return;
+ ws.send(Json.stringify(data));
+ }
+
+ function serverMessage(type:Int, ?text:String):Void {
+ final msgBuf = ge("#messagebuffer");
+ final div = document.createDivElement();
+ final time = "[" + new Date().toTimeString().split(" ")[0] + "] ";
+ switch (type) {
+ case 1:
+ div.className = "server-msg-reconnect";
+ div.innerHTML = Lang.get("msgConnected");
+ case 2:
+ div.className = "server-msg-disconnect";
+ div.innerHTML = Lang.get("msgDisconnected");
+ case 3:
+ div.className = "server-whisper";
+ div.innerHTML = time + text + " " + Lang.get("entered");
+ case 4:
+ div.className = "server-whisper";
+ div.innerHTML = time + text;
+ default:
+ }
+ msgBuf.appendChild(div);
+ msgBuf.scrollTop = msgBuf.scrollHeight;
+ }
+
+ final pageTitle = document.title;
+
+ function updateUserList():Void {
+ final userCount = ge("#usercount");
+ userCount.innerHTML = clients.length + " " + Lang.get("online");
+ document.title = '$pageTitle (${clients.length})';
+
+ final list = new StringBuf();
+ for (client in clients) {
+ // final klass = client.isLeader ? "userlist_owner" : "userlist_item";
+ final klass = "userlist_item";
+ if (client.isLeader) list.add('<span class="glyphicon glyphicon-star-empty"></span>');
+ list.add('<span class="$klass">${client.name}</span></br>');
+ }
+ final userlist = ge("#userlist");
+ userlist.innerHTML = list.toString();
+ }
+
+ function addMessage(name:String, msg:String):Void {
+ final msgBuf = ge("#messagebuffer");
+ final userDiv = document.createDivElement();
+ userDiv.className = 'chat-msg-$name';
+
+ final tstamp = document.createSpanElement();
+ tstamp.className = "timestamp";
+ tstamp.innerHTML = "[" + new Date().toTimeString().split(" ")[0] + "] ";
+
+ final nameDiv = document.createElement("strong");
+ nameDiv.className = "username";
+ nameDiv.innerHTML = name + ": ";
+
+ final textDiv = document.createSpanElement();
+ textDiv.innerHTML = msg;
+
+ final isInChatEnd = msgBuf.scrollHeight - msgBuf.scrollTop == msgBuf.clientHeight;
+ userDiv.appendChild(tstamp);
+ userDiv.appendChild(nameDiv);
+ userDiv.appendChild(textDiv);
+ msgBuf.appendChild(userDiv);
+ if (isInChatEnd) msgBuf.scrollTop = msgBuf.scrollHeight;
+ }
+
+ public static inline function ge(id:String):Element {
+ return document.querySelector(id);
+ }
+
+}
diff --git a/src/client/MobileView.hx b/src/client/MobileView.hx
new file mode 100644
index 0000000..558df61
--- /dev/null
+++ b/src/client/MobileView.hx
@@ -0,0 +1,52 @@
+package client;
+
+import js.html.InputElement;
+import js.Browser.document;
+import client.Main.ge;
+
+class MobileView {
+
+ public static function init():Void {
+ final mvbtn:InputElement = cast ge("#mv_btn");
+ mvbtn.onclick = (e) -> {
+ final mobile_view = toggleFullScreen();
+ if (mobile_view) {
+ document.body.classList.add('mobile-view');
+ mvbtn.classList.add('label-success');
+ final vwrap = ge("#videowrap");
+ if (vwrap.children[0] == ge("currenttitle")) {
+ vwrap.appendChild(vwrap.children[0]);
+ }
+ } else {
+ document.body.classList.remove('mobile-view');
+ mvbtn.classList.remove('label-success');
+ final vwrap = ge("videowrap");
+ if (vwrap.children[0] != ge("currenttitle")) {
+ vwrap.insertBefore(vwrap.children[1],vwrap.children[0]);
+ }
+ }
+ }
+ }
+
+ static function toggleFullScreen():Bool {
+ var state = true;
+ final doc:Dynamic = document;
+ if (document.fullscreenElement == null &&
+ doc.mozFullScreenElement == null &&
+ doc.webkitFullscreenElement == null) {
+ if (document.documentElement.requestFullscreen != null) {
+ document.documentElement.requestFullscreen();
+ } else if (doc.documentElement.mozRequestFullScreen != null) {
+ doc.documentElement.mozRequestFullScreen();
+ } else if (doc.documentElement.webkitRequestFullscreen != null) {
+ doc.documentElement.webkitRequestFullscreen(untyped Element.ALLOW_KEYBOARD_INPUT);
+ } else state = false;
+ } else {
+ if (doc.cancelFullScreen != null) doc.cancelFullScreen();
+ else if (doc.mozCancelFullScreen != null) doc.mozCancelFullScreen();
+ else if (doc.webkitCancelFullScreen != null) doc.webkitCancelFullScreen();
+ state = false;
+ }
+ return state;
+ }
+}
diff --git a/src/client/Player.hx b/src/client/Player.hx
new file mode 100644
index 0000000..2973e9b
--- /dev/null
+++ b/src/client/Player.hx
@@ -0,0 +1,182 @@
+package client;
+
+import js.html.LIElement;
+import js.html.UListElement;
+import js.html.Element;
+import js.html.VideoElement;
+import js.Browser.document;
+import client.Main.ge;
+import Types.VideoItem;
+using Lambda;
+
+class Player {
+
+ final main:Main;
+ final items:Array<VideoItem> = [];
+ final videoItemsEl = ge("#queue");
+ final player:Element = ge("#ytapiplayer");
+ var isLoaded = false;
+ var skipSetTime = false;
+ var video:VideoElement;
+
+ public function new(main:Main):Void {
+ this.main = main;
+ }
+
+ public function setVideo(item:VideoItem):Void {
+ isLoaded = false;
+ video = document.createVideoElement();
+ video.id = "videoplayer";
+ video.src = item.url;
+ video.controls = true;
+ video.oncanplaythrough = (e) -> {
+ if (!isLoaded) main.send({type: VideoLoaded});
+ isLoaded = true;
+ }
+ video.ontimeupdate = (e) -> {
+ if (skipSetTime) {
+ skipSetTime = false;
+ return;
+ }
+ if (!main.isLeader()) return;
+ main.send({
+ type: SetTime,
+ setTime: {
+ time: video.currentTime
+ }
+ });
+ }
+ video.onpause = (e) -> {
+ if (!main.isLeader()) return;
+ main.send({
+ type: Pause,
+ pause: {
+ time: video.currentTime
+ }
+ });
+ }
+ video.onplay = (e) -> {
+ if (!main.isLeader()) return;
+ main.send({
+ type: Play,
+ play: {
+ time: video.currentTime
+ }
+ });
+ }
+ player.innerHTML = "";
+ player.appendChild(video);
+ }
+
+ public function addVideoItem(item:VideoItem):Void {
+ items.push(item);
+ final itemEl:LIElement = cast nodeFromString(
+ '<li class="queue_entry pluid-0 queue_temp queue_active" title="${Lang.get("addedBy")}: ${item.author}">
+ <a class="qe_title" href="${item.url}" target="_blank">${item.title}</a>
+ <span class="qe_time">${duration(item.duration)}</span>
+ <div class="qe_clear"></div>
+ <div class="btn-group" style="display: inline-block;">
+ <button class="btn btn-xs btn-default qbtn-play">
+ <span class="glyphicon glyphicon-play"></span>${Lang.get("play")}
+ </button>
+ <button class="btn btn-xs btn-default qbtn-next">
+ <span class="glyphicon glyphicon-share-alt"></span>${Lang.get("skip")}
+ </button>
+ <button class="btn btn-xs btn-default qbtn-tmp">
+ <span class="glyphicon glyphicon-flag"></span>${Lang.get("makePermanent")}
+ </button>
+ <button class="btn btn-xs btn-default qbtn-delete" id="btn-delete">
+ <span class="glyphicon glyphicon-trash"></span>${Lang.get("delete")}
+ </button>
+ </div>
+ </li>'
+ );
+ final deleteBtn = itemEl.querySelector("#btn-delete");
+ deleteBtn.onclick = (e) -> {
+ main.send({
+ type: RemoveVideo,
+ removeVideo: {
+ url: itemEl.querySelector(".qe_title").getAttribute("href")
+ }
+ });
+ }
+ videoItemsEl.appendChild(itemEl);
+ ge("#plcount").innerHTML = '${items.length} ${Lang.get("videos")}';
+ ge("#pllength").innerHTML = totalDuration();
+ }
+
+ public function removeVideo():Void {
+ player.removeChild(video);
+ video = null;
+ }
+
+ public function removeItem(url:String):Void {
+ final list = ge("#queue");
+ for (child in list.children) {
+ if (child.querySelector(".qe_title").getAttribute("href") == url) {
+ list.removeChild(child);
+ break;
+ }
+ }
+
+ items.remove(
+ items.find(item -> item.url == url)
+ );
+
+ if (video.src == url) {
+ if (items.length > 0) setVideo(items[0]);
+ }
+ ge("#plcount").innerHTML = '${items.length} ${Lang.get("videos")}';
+ ge("#pllength").innerHTML = totalDuration();
+ }
+
+ function duration(time:Float):String {
+ final h = Std.int(time / 60 / 60);
+ final m = Std.int(time / 60);
+ final s = Std.int(time % 60);
+ var time = '$m:';
+ if (m < 10) time = '0$time';
+ if (h > 0) time = '$h:$time';
+ if (s < 10) time = time + "0";
+ time += s;
+ return time;
+ }
+
+ function totalDuration():String {
+ var time = 0.0;
+ for (item in items) time += item.duration;
+ return duration(time);
+ }
+
+ function nodeFromString(div:String):Element {
+ final wrapper = document.createDivElement();
+ wrapper.innerHTML = div;
+ return wrapper.firstElementChild;
+ }
+
+ public function isListEmpty():Bool {
+ return items.length == 0;
+ }
+
+ public function pause():Void {
+ if (video == null) return;
+ video.pause();
+ }
+
+ public function play():Void {
+ if (video == null) return;
+ video.play();
+ }
+
+ public function setTime(time:Float, isLocal = true):Void {
+ if (video == null) return;
+ skipSetTime = isLocal;
+ video.currentTime = time;
+ }
+
+ public function getTime():Float {
+ if (video == null) return 0;
+ return video.currentTime;
+ }
+
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage