diff options
| author | RblSb <msrblsb@gmail.com> | 2020-02-13 16:28:18 +0300 |
|---|---|---|
| committer | RblSb <msrblsb@gmail.com> | 2020-02-15 19:45:40 +0300 |
| commit | 07d1955cefc093ffb12002902ed45e963030746e (patch) | |
| tree | 8833eca2dc2ef07891aa8eb66daf7ad90f2ab0ce /src/client | |
Initial commit
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/Main.hx | 334 | ||||
| -rw-r--r-- | src/client/MobileView.hx | 52 | ||||
| -rw-r--r-- | src/client/Player.hx | 182 |
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; + } + +} |
