diff options
| -rw-r--r-- | build/client.js | 277 | ||||
| -rw-r--r-- | build/server.js | 58 | ||||
| -rw-r--r-- | res/css/des.css | 3 | ||||
| -rw-r--r-- | res/css/mobile-view.css | 3 | ||||
| -rw-r--r-- | res/langs/en.json | 2 | ||||
| -rw-r--r-- | res/langs/ru.json | 2 | ||||
| -rw-r--r-- | src/Types.hx | 9 | ||||
| -rw-r--r-- | src/client/Buttons.hx | 42 | ||||
| -rw-r--r-- | src/client/Main.hx | 80 | ||||
| -rw-r--r-- | src/client/MobileView.hx | 23 | ||||
| -rw-r--r-- | src/client/Player.hx | 59 | ||||
| -rw-r--r-- | src/client/Utils.hx | 63 | ||||
| -rw-r--r-- | src/server/Main.hx | 42 | ||||
| -rw-r--r-- | src/server/Utils.hx | 11 |
14 files changed, 519 insertions, 155 deletions
diff --git a/build/client.js b/build/client.js index 8d796a3..4b9954b 100644 --- a/build/client.js +++ b/build/client.js @@ -359,7 +359,40 @@ client_Buttons.init = function(main) { extendPlayer.classList.toggle("active"); return window.dispatchEvent(new Event("resize")); }; - window.document.querySelector("#showmediaurl").onclick = function(e5) { + window.document.querySelector("#mediarefresh").onclick = function(e5) { + main.refreshPlayer(); + return; + }; + window.document.querySelector("#fullscreenbtn").onclick = function(e6) { + return client_Utils.toggleFullScreen(window.document.querySelector("#ytapiplayer")); + }; + var getPlaylist = window.document.querySelector("#getplaylist"); + getPlaylist.onclick = function(e7) { + client_Utils.copyToClipboard(main.getPlaylistLinks().join(",")); + var icon = getPlaylist.firstElementChild; + icon.classList.remove("glyphicon-link"); + icon.classList.add("glyphicon-ok"); + return haxe_Timer.delay(function() { + icon.classList.add("glyphicon-link"); + icon.classList.remove("glyphicon-ok"); + return; + },2000); + }; + window.document.querySelector("#clearplaylist").onclick = function(e8) { + if(!window.confirm(Lang.get("clearPlaylistConfirm"))) { + return; + } + main.send({ type : "ClearPlaylist"}); + return; + }; + window.document.querySelector("#shuffleplaylist").onclick = function(e9) { + if(!window.confirm(Lang.get("shufflePlaylistConfirm"))) { + return; + } + main.send({ type : "ShufflePlaylist"}); + return; + }; + window.document.querySelector("#showmediaurl").onclick = function(e10) { window.document.querySelector("#showmediaurl").classList.toggle("collapsed"); window.document.querySelector("#showmediaurl").classList.toggle("active"); return window.document.querySelector("#addfromurl").classList.toggle("collapse"); @@ -497,23 +530,49 @@ client_Main.prototype = { return; }; window.document.querySelector("#queue_next").onclick = function(e1) { - _gthis.addVideoUrl(); + _gthis.addVideoUrl(false); return; }; window.document.querySelector("#queue_end").onclick = function(e2) { - _gthis.addVideoUrl(); + _gthis.addVideoUrl(true); return; }; window.document.querySelector("#mediaurl").onkeydown = function(e3) { if(e3.keyCode == 13) { - _gthis.addVideoUrl(); + _gthis.addVideoUrl(true); } }; } - ,addVideoUrl: function() { + ,addVideoUrl: function(atEnd) { var _gthis = this; var mediaUrl = window.document.querySelector("#mediaurl"); var url = mediaUrl.value; + if(url.length == 0) { + return; + } + mediaUrl.value = ""; + var _this_r = new RegExp(",(https?)","g".split("u").join("")); + var links = url.replace(_this_r,"|$1").split("|"); + this.addVideo(atEnd || this.player.isListEmpty() ? links.shift() : links.pop(),atEnd,function() { + _gthis.addVideoArray(links,atEnd); + return; + }); + } + ,addVideoArray: function(links,atEnd) { + var _gthis = this; + if(links.length == 0) { + return; + } + this.addVideo(atEnd ? links.shift() : links.pop(),atEnd,function() { + _gthis.addVideoArray(links,atEnd); + return; + }); + } + ,addVideo: function(url,atEnd,callback) { + var _gthis = this; + if(!StringTools.startsWith(url,"http")) { + url = "" + window.location.protocol + "//" + url; + } var pos = url.lastIndexOf("/") + 1; var name = HxOverrides.substr(url,pos,null); var matchName = new EReg("^(.+)\\.",""); @@ -522,11 +581,21 @@ client_Main.prototype = { } else { name = Lang.get("rawVideo"); } - this.getRemoteVideoDuration(mediaUrl.value,function(duration) { - _gthis.send({ type : "AddVideo", addVideo : { item : { url : url, title : name, author : _gthis.personal.name, duration : duration}}}); + this.getRemoteVideoDuration(url,function(duration) { + _gthis.send({ type : "AddVideo", addVideo : { item : { url : url, title : name, author : _gthis.personal.name, duration : duration}, atEnd : atEnd}}); + callback(); return; }); - mediaUrl.value = ""; + } + ,refreshPlayer: function() { + this.player.refresh(); + } + ,getPlaylistLinks: function() { + var items = this.player.getItems(); + var _g = []; + var _g1 = 0; + while(_g1 < items.length) _g.push(items[_g1++].url); + return _g; } ,getRemoteVideoDuration: function(src,callback) { var player = window.document.querySelector("#ytapiplayer"); @@ -537,36 +606,38 @@ client_Main.prototype = { return; }; video.onloadedmetadata = function() { - player.removeChild(video); + if(player.contains(video)) { + player.removeChild(video); + } callback(video.duration); return; }; - this.prepend(player,video); - } - ,prepend: function(parent,child) { - if(parent.firstChild == null) { - parent.appendChild(child); - } else { - parent.insertBefore(child,parent.firstChild); - } + client_Utils.prepend(player,video); } ,onMessage: function(e) { var data = JSON.parse(e.data); var t = data.type; var t1 = t.charAt(0).toLowerCase() + HxOverrides.substr(t,1,null); - haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 149, className : "client.Main", methodName : "onMessage", customParams : [data[t1]]}); + haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 171, className : "client.Main", methodName : "onMessage", customParams : [data[t1]]}); switch(data.type) { case "AddVideo": if(this.player.isListEmpty()) { this.player.setVideo(data.addVideo.item); } - this.player.addVideoItem(data.addVideo.item); + this.player.addVideoItem(data.addVideo.item,data.addVideo.atEnd); break; case "ClearChat": window.document.querySelector("#messagebuffer").innerHTML = ""; break; + case "ClearPlaylist": + this.player.clearItems(); + if(this.player.isListEmpty()) { + this.player.pause(); + } + break; case "Connected": this.onConnected(data); + this.onTimeGet.run(); break; case "GetTime": var newTime = data.getTime.time; @@ -578,15 +649,15 @@ client_Main.prototype = { this.player.setTime(time,false); return; } - if(Math.abs(time - newTime) < 2) { - return; - } - this.player.setTime(newTime); if(!data.getTime.paused) { this.player.play(); } else { this.player.pause(); } + if(Math.abs(time - newTime) < 2) { + return; + } + this.player.setTime(newTime); break; case "Login": this.onLogin(data.login.clients,data.login.clientName); @@ -642,10 +713,15 @@ client_Main.prototype = { } this.player.setTime(newTime1); break; + case "ShufflePlaylist": + break; case "UpdateClients": this.updateClients(data.updateClients.clients); this.personal = ClientTools.getByName(this.clients,this.personal.name,this.personal); break; + case "UpdatePlaylist": + this.player.setItems(data.updatePlaylist.videoList); + break; case "VideoLoaded": this.player.setTime(0); this.player.play(); @@ -673,14 +749,7 @@ client_Main.prototype = { ++_g; this.addMessage(message.name,message.text,message.time); } - var list = connected.videoList; - if(list.length == 0) { - return; - } - this.player.setVideo(list[0]); - var _g2 = 0; - var _g3 = connected.videoList; - while(_g2 < _g3.length) this.player.addVideoItem(_g3[_g2++]); + this.player.setItems(connected.videoList); } ,setConfig: function(config) { this.config = config; @@ -852,6 +921,11 @@ client_Main.prototype = { } } ,handleCommands: function(text) { + if(text == "clear") { + if((this.personal.group & 4) != 0) { + this.send({ type : "ClearChat"}); + } + } if(this.matchNumbers.match(text)) { this.send({ type : "Rewind", rewind : { time : Std.parseInt(text)}}); } @@ -874,7 +948,7 @@ client_MobileView.__name__ = true; client_MobileView.init = function() { var mvbtn = window.document.querySelector("#mv_btn"); mvbtn.onclick = function(e) { - if(client_MobileView.toggleFullScreen()) { + if(client_Utils.toggleFullScreen(window.document.documentElement)) { window.document.body.classList.add("mobile-view"); mvbtn.classList.add("active"); var vwrap = window.document.querySelector("#videowrap"); @@ -892,31 +966,6 @@ client_MobileView.init = function() { return; }; }; -client_MobileView.toggleFullScreen = function() { - var state = true; - var doc = window.document; - if(window.document.fullscreenElement == null && doc.mozFullScreenElement == null && doc.webkitFullscreenElement == null) { - if(window.document.documentElement.requestFullscreen != null) { - window.document.documentElement.requestFullscreen(); - } else if(doc.documentElement.mozRequestFullScreen != null) { - doc.documentElement.mozRequestFullScreen(); - } else if(doc.documentElement.webkitRequestFullscreen != null) { - doc.documentElement.webkitRequestFullscreen(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; -}; var client_Player = function(main) { this.skipSetTime = false; this.isLoaded = false; @@ -969,7 +1018,7 @@ client_Player.prototype = { this.player.appendChild(this.video); window.document.querySelector("#currenttitle").innerHTML = item.title; } - ,addVideoItem: function(item) { + ,addVideoItem: function(item,atEnd) { var _gthis = this; this.items.push(item); var itemEl = this.nodeFromString("<li class=\"queue_entry pluid-0 queue_temp queue_active\" title=\"" + Lang.get("addedBy") + ": " + item.author + "\">\n\t\t\t\t<a class=\"qe_title\" href=\"" + item.url + "\" target=\"_blank\">" + item.title + "</a>\n\t\t\t\t<span class=\"qe_time\">" + this.duration(item.duration) + "</span>\n\t\t\t\t<div class=\"qe_clear\"></div>\n\t\t\t\t<div class=\"btn-group\" style=\"display: inline-block;\">\n\t\t\t\t\t<button class=\"btn btn-xs btn-default qbtn-play\">\n\t\t\t\t\t\t<span class=\"glyphicon glyphicon-play\"></span>" + Lang.get("play") + "\n\t\t\t\t\t</button>\n\t\t\t\t\t<button class=\"btn btn-xs btn-default qbtn-next\">\n\t\t\t\t\t\t<span class=\"glyphicon glyphicon-share-alt\"></span>" + Lang.get("skip") + "\n\t\t\t\t\t</button>\n\t\t\t\t\t<button class=\"btn btn-xs btn-default qbtn-tmp\">\n\t\t\t\t\t\t<span class=\"glyphicon glyphicon-flag\"></span>" + Lang.get("makePermanent") + "\n\t\t\t\t\t</button>\n\t\t\t\t\t<button class=\"btn btn-xs btn-default qbtn-delete\" id=\"btn-delete\">\n\t\t\t\t\t\t<span class=\"glyphicon glyphicon-trash\"></span>" + Lang.get("delete") + "\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</li>"); @@ -977,21 +1026,29 @@ client_Player.prototype = { _gthis.main.send({ type : "RemoveVideo", removeVideo : { url : itemEl.querySelector(".qe_title").getAttribute("href")}}); return; }; - this.videoItemsEl.appendChild(itemEl); - var tmp = "" + this.items.length + " "; - var tmp1 = Lang.get("videos"); - window.document.querySelector("#plcount").innerHTML = tmp + tmp1; - window.document.querySelector("#pllength").innerHTML = this.totalDuration(); + if(atEnd) { + this.videoItemsEl.appendChild(itemEl); + } else { + client_Utils.insertAtIndex(this.videoItemsEl,itemEl,1); + } + this.updateCounters(); + } + ,removeVideo: function() { + if(this.video == null) { + return; + } + this.player.removeChild(this.video); + this.video = null; + window.document.querySelector("#currenttitle").innerHTML = Lang.get("nothingPlaying"); } ,removeItem: function(url) { - var list = window.document.querySelector("#queue"); var _g = 0; - var _g1 = list.children; + var _g1 = this.videoItemsEl.children; while(_g < _g1.length) { var child = _g1[_g]; ++_g; if(child.querySelector(".qe_title").getAttribute("href") == url) { - list.removeChild(child); + this.videoItemsEl.removeChild(child); break; } } @@ -1003,14 +1060,43 @@ client_Player.prototype = { this.setVideo(this.items[0]); } } + this.updateCounters(); + } + ,updateCounters: function() { var tmp = "" + this.items.length + " "; var tmp1 = Lang.get("videos"); window.document.querySelector("#plcount").innerHTML = tmp + tmp1; window.document.querySelector("#pllength").innerHTML = this.totalDuration(); } + ,getItems: function() { + return this.items; + } + ,setItems: function(list) { + this.clearItems(); + if(list.length == 0) { + return; + } + if(this.video == null || this.video.src != list[0].url) { + this.setVideo(list[0]); + } + var _g = 0; + while(_g < list.length) this.addVideoItem(list[_g++],true); + } + ,clearItems: function() { + this.items.length = 0; + this.videoItemsEl.innerHTML = ""; + this.updateCounters(); + } + ,refresh: function() { + if(this.items.length == 0) { + return; + } + this.removeVideo(); + this.setVideo(this.items[0]); + } ,duration: function(time) { var h = time / 60 / 60 | 0; - var m = time / 60 | 0; + var m = (time / 60 | 0) - h * 60; var s = time % 60 | 0; var time1 = "" + m + ":"; if(m < 10) { @@ -1069,6 +1155,63 @@ client_Player.prototype = { return this.video.currentTime; } }; +var client_Utils = function() { }; +client_Utils.__name__ = true; +client_Utils.prepend = function(parent,child) { + if(parent.firstChild == null) { + parent.appendChild(child); + } else { + parent.insertBefore(child,parent.firstChild); + } +}; +client_Utils.insertAtIndex = function(parent,child,i) { + if(i >= parent.children.length) { + parent.appendChild(child); + } else { + parent.insertBefore(child,parent.children[i]); + } +}; +client_Utils.toggleFullScreen = function(el) { + var state = true; + var doc = window.document; + var el2 = el; + if(window.document.fullscreenElement == null && doc.mozFullScreenElement == null && doc.webkitFullscreenElement == null) { + if(el.requestFullscreen != null) { + el.requestFullscreen(); + } else if(el2.mozRequestFullScreen != null) { + el2.mozRequestFullScreen(); + } else if(el2.webkitRequestFullscreen != null) { + el2.webkitRequestFullscreen(HTMLElement.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; +}; +client_Utils.copyToClipboard = function(text) { + var clipboardData = window.clipboardData; + if(clipboardData != null && clipboardData.setData != null) { + clipboardData.setData("Text",text); + return; + } else if(window.document.queryCommandSupported != null) { + var textarea = window.document.createElement("textarea"); + textarea.textContent = text; + textarea.style.position = "fixed"; + window.document.body.appendChild(textarea); + textarea.select(); + window.document.execCommand("copy"); + window.document.body.removeChild(textarea); + } +}; var haxe_Log = function() { }; haxe_Log.__name__ = true; haxe_Log.formatOutput = function(v,infos) { diff --git a/build/server.js b/build/server.js index 611ca9c..68ad69d 100644 --- a/build/server.js +++ b/build/server.js @@ -292,6 +292,13 @@ Std.__name__ = true; Std.string = function(s) { return js_Boot.__string_rec(s,""); }; +Std.random = function(x) { + if(x <= 0) { + return 0; + } else { + return Math.floor(Math.random() * x); + } +}; var StringTools = function() { }; StringTools.__name__ = true; StringTools.startsWith = function(s,start) { @@ -652,7 +659,7 @@ server_Main.prototype = { client.group |= 4; } this.clients.push(client); - if(this.clients.length == 1) { + if(this.clients.length == 1 && this.videoList.length > 0) { if(this.videoTimer.isPaused()) { this.videoTimer.play(); } @@ -682,6 +689,9 @@ server_Main.prototype = { } } if(_gthis.clients.length == 0) { + if(_gthis.waitVideoStart != null) { + _gthis.waitVideoStart.stop(); + } _gthis.videoTimer.pause(); } return; @@ -702,10 +712,14 @@ server_Main.prototype = { ,onMessage: function(client,data) { switch(data.type) { case "AddVideo": - this.videoList.push(data.addVideo.item); + if(data.addVideo.atEnd) { + this.videoList.push(data.addVideo.item); + } else { + this.videoList.splice(1,0,data.addVideo.item); + } this.broadcast(data); if(this.videoList.length == 1) { - this.waitVideoStart = haxe_Timer.delay($bind(this,this.startVideoPlayback),3000); + this.restartWaitTimer(); } break; case "ClearChat": @@ -713,6 +727,11 @@ server_Main.prototype = { this.broadcast(data); } break; + case "ClearPlaylist": + this.videoTimer.stop(); + this.videoList.length = 0; + this.broadcast(data); + break; case "Connected": break; case "GetTime": @@ -793,6 +812,9 @@ server_Main.prototype = { return item.url == url; })); this.broadcast(data); + if(this.videoList.length > 0) { + this.restartWaitTimer(); + } break; case "Rewind": if(this.videoList.length == 0) { @@ -828,9 +850,20 @@ server_Main.prototype = { this.videoTimer.setTime(data.setTime.time); this.broadcastExcept(client,data); break; + case "ShufflePlaylist": + if(this.videoList.length == 0) { + return; + } + var first = this.videoList.shift(); + server_Utils.shuffle(this.videoList); + this.videoList.unshift(first); + this.broadcast({ type : "UpdatePlaylist", updatePlaylist : { videoList : this.videoList}}); + break; case "UpdateClients": this.sendClientList(); break; + case "UpdatePlaylist": + break; case "VideoLoaded": this.prepareVideoPlayback(); break; @@ -868,13 +901,19 @@ server_Main.prototype = { client.ws.send(json,null); } } + ,restartWaitTimer: function() { + if(this.waitVideoStart != null) { + this.waitVideoStart.stop(); + } + this.waitVideoStart = haxe_Timer.delay($bind(this,this.startVideoPlayback),3000); + } ,prepareVideoPlayback: function() { if(this.videoTimer.isStarted) { return; } this.loadedClientsCount++; if(this.loadedClientsCount == 1) { - this.waitVideoStart = haxe_Timer.delay($bind(this,this.startVideoPlayback),3000); + this.restartWaitTimer(); } if(this.loadedClientsCount >= this.clients.length) { this.startVideoPlayback(); @@ -915,6 +954,17 @@ server_Utils.getLocalIp = function() { } return "127.0.0.1"; }; +server_Utils.shuffle = function(arr) { + var _g = 0; + var _g1 = arr.length; + while(_g < _g1) { + var i = _g++; + var n = Std.random(arr.length); + var a = arr[i]; + arr[i] = arr[n]; + arr[n] = a; + } +}; var server_VideoTimer = function() { this.pauseStartTime = 0.0; this.startTime = 0.0; diff --git a/res/css/des.css b/res/css/des.css index e1f960b..e3f81bd 100644 --- a/res/css/des.css +++ b/res/css/des.css @@ -185,6 +185,9 @@ src:url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.2/fonts/gl .glyphicon-link:before { content:"\e144" } +.glyphicon-ok:before { + content:"\e013" +} .glyphicon-sort:before { content:"\e150" } diff --git a/res/css/mobile-view.css b/res/css/mobile-view.css index a23b963..596dcde 100644 --- a/res/css/mobile-view.css +++ b/res/css/mobile-view.css @@ -54,9 +54,6 @@ padding-bottom: 0; margin-bottom: 0; } -.mobile-view #footer { - display: none; -} @media (max-width:799px) { .navbar { diff --git a/res/langs/en.json b/res/langs/en.json index 87f7d84..5fa5d53 100644 --- a/res/langs/en.json +++ b/res/langs/en.json @@ -54,6 +54,8 @@ "acceptableEmbedCodesAre": "Acceptable embed codes are", "customEmbedsCannotBeSynchronized": "CUSTOM EMBEDS CANNOT BE SYNCHRONIZED", "save": "Save", + "clearPlaylistConfirm": "Are you sure you want to clear the playlist?", + "shufflePlaylistConfirm": "Are you sure you want to shuffle the playlist?", "yes": "Yes", "no": "No", diff --git a/res/langs/ru.json b/res/langs/ru.json index e13a6d5..f9944d1 100644 --- a/res/langs/ru.json +++ b/res/langs/ru.json @@ -54,6 +54,8 @@ "acceptableEmbedCodesAre": "Можно добавить видео с тегами", "customEmbedsCannotBeSynchronized": "СИНХРОНИЗАЦИЯ БУДЕТ НЕДОСТУПНА", "save": "Сохранить", + "clearPlaylistConfirm": "Вы уверены что хотите очистить плейлист?", + "shufflePlaylistConfirm": "Вы уверены что хотите перемешать плейлист?", "yes": "Да", "no": "Нет", diff --git a/src/Types.hx b/src/Types.hx index 643fd4f..f2b506f 100644 --- a/src/Types.hx +++ b/src/Types.hx @@ -66,7 +66,8 @@ typedef WsEvent = { clients:Array<ClientData>, }, ?addVideo:{ - item:VideoItem + item:VideoItem, + atEnd:Bool }, ?removeVideo:{ url:String @@ -89,6 +90,9 @@ typedef WsEvent = { }, ?setLeader:{ clientName:String + }, + ?updatePlaylist:{ + videoList:Array<VideoItem> } } @@ -111,4 +115,7 @@ enum abstract WsEventType(String) { var Rewind; var SetLeader; var ClearChat; + var ClearPlaylist; + var ShufflePlaylist; + var UpdatePlaylist; } diff --git a/src/client/Buttons.hx b/src/client/Buttons.hx index a5fbfa4..f72fbb3 100644 --- a/src/client/Buttons.hx +++ b/src/client/Buttons.hx @@ -1,8 +1,8 @@ package client; +import haxe.Timer; import js.html.KeyboardEvent; import js.html.InputElement; -import js.html.ButtonElement; import js.html.Element; import client.Main.ge; import js.Browser.window; @@ -77,16 +77,48 @@ class Buttons { extendPlayer.onclick = e -> { if (extendPlayer.classList.contains("active")) { split.setSizes([40, 60]); - ge('#userlist').style.width = "90px"; + ge("#userlist").style.width = "90px"; } else { split.setSizes([20, 80]); - ge('#userlist').style.width = "80px"; + ge("#userlist").style.width = "80px"; } extendPlayer.classList.toggle("active"); - window.dispatchEvent(new Event('resize')); + window.dispatchEvent(new Event("resize")); } - final showMediaUrl:ButtonElement = cast ge("#showmediaurl"); + final mediaRefresh = ge("#mediarefresh"); + mediaRefresh.onclick = e -> { + main.refreshPlayer(); + } + final fullscreenBtn = ge("#fullscreenbtn"); + fullscreenBtn.onclick = e -> { + final el = ge("#ytapiplayer"); + Utils.toggleFullScreen(el); + } + final getPlaylist = ge("#getplaylist"); + getPlaylist.onclick = e -> { + final text = main.getPlaylistLinks().join(","); + Utils.copyToClipboard(text); + final icon = getPlaylist.firstElementChild; + icon.classList.remove("glyphicon-link"); + icon.classList.add("glyphicon-ok"); + Timer.delay(() -> { + icon.classList.add("glyphicon-link"); + icon.classList.remove("glyphicon-ok"); + }, 2000); + } + final clearPlaylist = ge("#clearplaylist"); + clearPlaylist.onclick = e -> { + if (!window.confirm(Lang.get("clearPlaylistConfirm"))) return; + main.send({type: ClearPlaylist}); + } + final shufflePlaylist = ge("#shuffleplaylist"); + shufflePlaylist.onclick = e -> { + if (!window.confirm(Lang.get("shufflePlaylistConfirm"))) return; + main.send({type: ShufflePlaylist}); + } + + final showMediaUrl = ge("#showmediaurl"); showMediaUrl.onclick = e -> { ge("#showmediaurl").classList.toggle("collapsed"); ge("#showmediaurl").classList.toggle("active"); diff --git a/src/client/Main.hx b/src/client/Main.hx index dc5a803..96353f8 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -71,8 +71,7 @@ class Main { Buttons.init(this); MobileView.init(); - final leaderBtn = ge("#leader_btn"); - leaderBtn.onclick = (e) -> { + ge("#leader_btn").onclick = e -> { // change button style before answer setLeaderButton(!personal.isLeader); final name = personal.isLeader ? "" : personal.name; @@ -84,11 +83,10 @@ class Main { }); } - // TODO next/end - ge("#queue_next").onclick = (e:MouseEvent) -> addVideoUrl(); - ge("#queue_end").onclick = (e:MouseEvent) -> addVideoUrl(); + ge("#queue_next").onclick = (e:MouseEvent) -> addVideoUrl(false); + ge("#queue_end").onclick = (e:MouseEvent) -> addVideoUrl(true); ge("#mediaurl").onkeydown = function(e:KeyboardEvent) { - if (e.keyCode == 13) addVideoUrl(); + if (e.keyCode == 13) addVideoUrl(true); } } @@ -100,26 +98,55 @@ class Main { return personal.isAdmin; } - function addVideoUrl():Void { + function addVideoUrl(atEnd:Bool):Void { final mediaUrl:InputElement = cast ge("#mediaurl"); final url = mediaUrl.value; + if (url.length == 0) return; + mediaUrl.value = ""; + final url = ~/,(https?)/g.replace(url, "|$1"); + final links = url.split("|"); + // if videos added as next, we need to load it in reverse order + final link = (atEnd || player.isListEmpty()) ? links.shift() : links.pop(); + addVideo(link, atEnd, () -> addVideoArray(links, atEnd)); + } + + function addVideoArray(links:Array<String>, atEnd:Bool):Void { + if (links.length == 0) return; + final link = atEnd ? links.shift() : links.pop(); + addVideo(link, atEnd, () -> addVideoArray(links, atEnd)); + } + + function addVideo(url:String, atEnd:Bool, callback:()->Void):Void { + if (!url.startsWith("http")) url = '${Browser.location.protocol}//$url'; var name = url.substr(url.lastIndexOf('/') + 1); final matchName = ~/^(.+)\./; if (matchName.match(name)) name = matchName.matched(1); else name = Lang.get("rawVideo"); - getRemoteVideoDuration(mediaUrl.value, (duration:Float) -> { + getRemoteVideoDuration(url, (duration:Float) -> { send({ type: AddVideo, addVideo: { item: { url: url, title: name, author: personal.name, - duration: duration - } - }}); + duration: duration, + }, + atEnd: atEnd + }}); + callback(); }); - mediaUrl.value = ""; + } + + public function refreshPlayer():Void { + player.refresh(); + } + + public function getPlaylistLinks():Array<String> { + final items = player.getItems(); + return [ + for (item in items) item.url + ]; } function getRemoteVideoDuration(src:String, callback:(duration:Float)->Void):Void { @@ -131,15 +158,10 @@ class Main { callback(0); } video.onloadedmetadata = () -> { - player.removeChild(video); + if (player.contains(video)) 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); + Utils.prepend(player, video); } function onMessage(e):Void { @@ -150,6 +172,7 @@ class Main { switch (data.type) { case Connected: onConnected(data); + onTimeGet.run(); case Login: onLogin(data.login.clients, data.login.clientName); case LoginError: @@ -167,7 +190,7 @@ class Main { addMessage(data.message.clientName, data.message.text); case AddVideo: if (player.isListEmpty()) player.setVideo(data.addVideo.item); - player.addVideoItem(data.addVideo.item); + player.addVideoItem(data.addVideo.item, data.addVideo.atEnd); case VideoLoaded: player.setTime(0); player.play(); @@ -192,10 +215,10 @@ class Main { player.setTime(time, false); return; } - if (Math.abs(time - newTime) < 2) return; - player.setTime(newTime); if (!data.getTime.paused) player.play(); else player.pause(); + if (Math.abs(time - newTime) < 2) return; + player.setTime(newTime); case SetTime: final newTime = data.setTime.time; final time = player.getTime(); @@ -210,6 +233,12 @@ class Main { if (isLeader()) player.setTime(player.getTime(), false); case ClearChat: ge("#messagebuffer").innerHTML = ""; + case ClearPlaylist: + player.clearItems(); + if (player.isListEmpty()) player.pause(); + case ShufflePlaylist: // server-only + case UpdatePlaylist: + player.setItems(data.updatePlaylist.videoList); } } @@ -233,12 +262,7 @@ class Main { for (message in connected.history) { addMessage(message.name, message.text, message.time); } - final list = connected.videoList; - if (list.length == 0) return; - player.setVideo(list[0]); - for (video in connected.videoList) { - player.addVideoItem(video); - } + player.setItems(connected.videoList); } function setConfig(config:Config):Void { diff --git a/src/client/MobileView.hx b/src/client/MobileView.hx index 976958b..ad788df 100644 --- a/src/client/MobileView.hx +++ b/src/client/MobileView.hx @@ -8,7 +8,7 @@ class MobileView { public static function init():Void { final mvbtn = ge("#mv_btn"); mvbtn.onclick = e -> { - final mobileView = toggleFullScreen(); + final mobileView = Utils.toggleFullScreen(document.documentElement); if (mobileView) { document.body.classList.add('mobile-view'); mvbtn.classList.add('active'); @@ -27,25 +27,4 @@ class MobileView { } } - 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 index 19e0c84..34ef716 100644 --- a/src/client/Player.hx +++ b/src/client/Player.hx @@ -1,7 +1,5 @@ package client; -import js.html.LIElement; -import js.html.UListElement; import js.html.Element; import js.html.VideoElement; import js.Browser.document; @@ -29,7 +27,7 @@ class Player { video.id = "videoplayer"; video.src = item.url; video.controls = true; - video.oncanplaythrough = (e) -> { + video.oncanplaythrough = e -> { if (!isLoaded) main.send({type: VideoLoaded}); isLoaded = true; } @@ -46,7 +44,7 @@ class Player { } }); } - video.onpause = (e) -> { + video.onpause = e -> { if (!main.isLeader()) return; main.send({ type: Pause, @@ -55,7 +53,7 @@ class Player { } }); } - video.onplay = (e) -> { + video.onplay = e -> { if (!main.isLeader()) return; main.send({ type: Play, @@ -69,9 +67,9 @@ class Player { ge("#currenttitle").innerHTML = item.title; } - public function addVideoItem(item:VideoItem):Void { + public function addVideoItem(item:VideoItem, atEnd:Bool):Void { items.push(item); - final itemEl:LIElement = cast nodeFromString( + final itemEl = 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> @@ -93,7 +91,7 @@ class Player { </li>' ); final deleteBtn = itemEl.querySelector("#btn-delete"); - deleteBtn.onclick = (e) -> { + deleteBtn.onclick = e -> { main.send({ type: RemoveVideo, removeVideo: { @@ -101,22 +99,22 @@ class Player { } }); } - videoItemsEl.appendChild(itemEl); - ge("#plcount").innerHTML = '${items.length} ${Lang.get("videos")}'; - ge("#pllength").innerHTML = totalDuration(); + if (atEnd) videoItemsEl.appendChild(itemEl); + else Utils.insertAtIndex(videoItemsEl, itemEl, 1); + updateCounters(); } public function removeVideo():Void { + if (video == null) return; player.removeChild(video); video = null; ge("#currenttitle").innerHTML = Lang.get("nothingPlaying"); } public function removeItem(url:String):Void { - final list = ge("#queue"); - for (child in list.children) { + for (child in videoItemsEl.children) { if (child.querySelector(".qe_title").getAttribute("href") == url) { - list.removeChild(child); + videoItemsEl.removeChild(child); break; } } @@ -128,13 +126,44 @@ class Player { if (video.src == url) { if (items.length > 0) setVideo(items[0]); } + updateCounters(); + } + + function updateCounters():Void { ge("#plcount").innerHTML = '${items.length} ${Lang.get("videos")}'; ge("#pllength").innerHTML = totalDuration(); } + public function getItems():Array<VideoItem> { + return items; + } + + public function setItems(list:Array<VideoItem>):Void { + clearItems(); + if (list.length == 0) return; + if (video == null || video.src != list[0].url) { + setVideo(list[0]); + } + for (video in list) { + addVideoItem(video, true); + } + } + + public function clearItems():Void { + items.resize(0); + videoItemsEl.innerHTML = ""; + updateCounters(); + } + + public function refresh():Void { + if (items.length == 0) return; + removeVideo(); + setVideo(items[0]); + } + function duration(time:Float):String { final h = Std.int(time / 60 / 60); - final m = Std.int(time / 60); + final m = Std.int(time / 60) - h * 60; final s = Std.int(time % 60); var time = '$m:'; if (m < 10) time = '0$time'; diff --git a/src/client/Utils.hx b/src/client/Utils.hx new file mode 100644 index 0000000..37de672 --- /dev/null +++ b/src/client/Utils.hx @@ -0,0 +1,63 @@ +package client; + +import js.html.Element; +import js.Browser.document; +import js.Browser.window; + +class Utils { + + public static function prepend(parent:Element, child:Element):Void { + if (parent.firstChild == null) parent.appendChild(child); + else parent.insertBefore(child, parent.firstChild); + } + + public static function insertAtIndex(parent:Element, child:Element, i:Int) { + if (i >= parent.children.length) parent.appendChild(child); + else parent.insertBefore(child, parent.children[i]); + } + + public static function toggleFullScreen(el:Element):Bool { + var state = true; + final doc:Dynamic = document; + final el2:Dynamic = el; + if (document.fullscreenElement == null && + doc.mozFullScreenElement == null && + doc.webkitFullscreenElement == null) { + if (el.requestFullscreen != null) { + el.requestFullscreen(); + } else if (el2.mozRequestFullScreen != null) { + el2.mozRequestFullScreen(); + } else if (el2.webkitRequestFullscreen != null) { + el2.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; + } + + public static function copyToClipboard(text:String):Void { + final clipboardData = (window : Dynamic).clipboardData; + if (clipboardData != null && clipboardData.setData != null) { + // IE-specific code path to prevent textarea being shown while dialog is visible. + clipboardData.setData("Text", text); + return; + } else if ((document : Dynamic).queryCommandSupported != null) { + final textarea = document.createTextAreaElement(); + textarea.textContent = text; + // Prevent scrolling to bottom of page in Microsoft Edge. + textarea.style.position = "fixed"; + document.body.appendChild(textarea); + textarea.select(); + try { + // Security exception may be thrown by some browsers. + document.execCommand("copy"); + } + document.body.removeChild(textarea); + } + } + +} diff --git a/src/server/Main.hx b/src/server/Main.hx index cdf6924..e2b9b18 100644 --- a/src/server/Main.hx +++ b/src/server/Main.hx @@ -80,7 +80,7 @@ class Main { final client = new Client(ws, id, name, 0); if (isAdmin) client.group.set(Admin); clients.push(client); - if (clients.length == 1) + if (clients.length == 1 && videoList.length > 0) if (videoTimer.isPaused()) videoTimer.play(); send(client, { @@ -109,7 +109,10 @@ class Main { if (client.isLeader) { if (videoTimer.isPaused()) videoTimer.play(); } - if (clients.length == 0) videoTimer.pause(); + if (clients.length == 0) { + if (waitVideoStart != null) waitVideoStart.stop(); + videoTimer.pause(); + } }); } @@ -131,7 +134,8 @@ class Main { sendClientList(); case Login: final name = data.login.clientName; - if (name.length == 0 || name.length > config.maxLoginLength || clients.getByName(name) != null) { + if (name.length == 0 || name.length > config.maxLoginLength + || clients.getByName(name) != null) { send(client, {type: LoginError}); return; } @@ -172,12 +176,13 @@ class Main { if (messages.length > config.serverChatHistory) messages.shift(); broadcast(data); case AddVideo: - videoList.push(data.addVideo.item); + if (data.addVideo.atEnd) videoList.push(data.addVideo.item); + else videoList.insert(1, data.addVideo.item); broadcast(data); - if (videoList.length == 1) { - waitVideoStart = Timer.delay(startVideoPlayback, 3000); - } + // Initial timer start if VideoLoaded is not happen + if (videoList.length == 1) restartWaitTimer(); case VideoLoaded: + // Called if client loads next video and can play it prepareVideoPlayback(); case RemoveVideo: if (videoList.length == 0) return; @@ -187,6 +192,7 @@ class Main { videoList.find(item -> item.url == url) ); broadcast(data); + if (videoList.length > 0) restartWaitTimer(); case Pause: if (videoList.length == 0) return; if (!client.isLeader) return; @@ -244,6 +250,19 @@ class Main { } case ClearChat: if (client.isAdmin) broadcast(data); + case ClearPlaylist: + videoTimer.stop(); + videoList.resize(0); + broadcast(data); + case ShufflePlaylist: + if (videoList.length == 0) return; + final first = videoList.shift(); + Utils.shuffle(videoList); + videoList.unshift(first); + broadcast({type: UpdatePlaylist, updatePlaylist: { + videoList: videoList + }}); + case UpdatePlaylist: } } @@ -282,12 +301,15 @@ class Main { var waitVideoStart:Timer; var loadedClientsCount = 0; + function restartWaitTimer():Void { + if (waitVideoStart != null) waitVideoStart.stop(); + waitVideoStart = Timer.delay(startVideoPlayback, 3000); + } + function prepareVideoPlayback():Void { if (videoTimer.isStarted) return; loadedClientsCount++; - if (loadedClientsCount == 1) { - waitVideoStart = Timer.delay(startVideoPlayback, 3000); - } + if (loadedClientsCount == 1) restartWaitTimer(); if (loadedClientsCount >= clients.length) startVideoPlayback(); } diff --git a/src/server/Utils.hx b/src/server/Utils.hx index c3510c9..2ecbd42 100644 --- a/src/server/Utils.hx +++ b/src/server/Utils.hx @@ -27,4 +27,15 @@ class Utils { } return "127.0.0.1"; } + + public static function shuffle<T>(arr:Array<T>):Void { + for (i in 0...arr.length) { + final n = Std.random(arr.length); + final a = arr[i]; + final b = arr[n]; + arr[i] = b; + arr[n] = a; + } + } + } |
