From 07d1955cefc093ffb12002902ed45e963030746e Mon Sep 17 00:00:00 2001 From: RblSb Date: Thu, 13 Feb 2020 16:28:18 +0300 Subject: Initial commit --- .gitignore | 2 + .vscode/launch.json | 12 + .vscode/settings.json | 9 + .vscode/tasks.json | 22 + README.md | 24 + build-all.hxml | 7 + build-client.hxml | 5 + build-server.hxml | 7 + build/client.js | 1240 ++++++++++++++++++++++++++++++++++ build/server.js | 933 ++++++++++++++++++++++++++ package-lock.json | 13 + package.json | 19 + res/css/custom.css | 0 res/css/cytube.css | 725 ++++++++++++++++++++ res/css/des.css | 1350 ++++++++++++++++++++++++++++++++++++++ res/css/mobile-view.css | 81 +++ res/css/sticky-footer-navbar.css | 26 + res/img/favicon.png | Bin 0 -> 171 bytes res/img/stripe-diagonal.png | Bin 0 -> 206 bytes res/img/vertical.png | Bin 0 -> 91 bytes res/index.html | 208 ++++++ res/langs/en.json | 67 ++ res/langs/ru.json | 67 ++ src/Client.hx | 37 ++ src/ClientTools.hx | 26 + src/Lang.hx | 62 ++ src/Types.hx | 77 +++ src/client/Main.hx | 334 ++++++++++ src/client/MobileView.hx | 52 ++ src/client/Player.hx | 182 +++++ src/server/HttpServer.hx | 106 +++ src/server/Main.hx | 251 +++++++ src/server/VideoTimer.hx | 54 ++ 33 files changed, 5998 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 README.md create mode 100644 build-all.hxml create mode 100644 build-client.hxml create mode 100644 build-server.hxml create mode 100644 build/client.js create mode 100644 build/server.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 res/css/custom.css create mode 100644 res/css/cytube.css create mode 100644 res/css/des.css create mode 100644 res/css/mobile-view.css create mode 100644 res/css/sticky-footer-navbar.css create mode 100755 res/img/favicon.png create mode 100755 res/img/stripe-diagonal.png create mode 100644 res/img/vertical.png create mode 100644 res/index.html create mode 100644 res/langs/en.json create mode 100644 res/langs/ru.json create mode 100644 src/Client.hx create mode 100644 src/ClientTools.hx create mode 100644 src/Lang.hx create mode 100644 src/Types.hx create mode 100644 src/client/Main.hx create mode 100644 src/client/MobileView.hx create mode 100644 src/client/Player.hx create mode 100644 src/server/HttpServer.hx create mode 100644 src/server/Main.hx create mode 100644 src/server/VideoTimer.hx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad43d51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/res/temp diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2153bb9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Node: build and run", + "program": "${workspaceFolder}/build/server.js", + "sourceMaps": true, + "preLaunchTask": "Build all" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..50b97fe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "haxe.configurations": [ + ["build-server.hxml"] + ], + "search.exclude": { + "build": true, + "res/temp": true, + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..f68272e --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,22 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Build all", + "type": "hxml", + "file": "build-all.hxml", + "problemMatcher": [ + "$haxe-absolute", + "$haxe", + "$haxe-error", + "$haxe-trace" + ], + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..bbc3986 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +## SyncTube +Synchronized video viewing with chat and other features. +Based on CyTube, but with lightweight implementation and very easy way to run locally. + +### New features +Even if some original features are not implemented yet, there is some new things: +- Multi-Language +- Mobile view with page fullscreen +- Updated Des theme + +TODO: +- Way to play local videos for network users (without NAT loopback feature) +- `/30`, `/-21`, etc to rewind video playback in seconds + +### Setup +- Open `4200` and `4201` ports in your router settings +- `npm install ws` +- Run `node build/server.js` +- Open showed "Local" link for yourself and send "Global" link to friends + +### Development +- Install Haxe 4, VSCode and vshaxe extension. +- `haxelib install all` to install extern libs. +- Open project in VSCode and press `F5` for client+server build and run. diff --git a/build-all.hxml b/build-all.hxml new file mode 100644 index 0000000..8e9fb78 --- /dev/null +++ b/build-all.hxml @@ -0,0 +1,7 @@ +--each + +build-client.hxml + +--next + +build-server.hxml diff --git a/build-client.hxml b/build-client.hxml new file mode 100644 index 0000000..d8a77e3 --- /dev/null +++ b/build-client.hxml @@ -0,0 +1,5 @@ +-cp src +--main client.Main +-D analyzer-optimize +--dce full +--js build/client.js diff --git a/build-server.hxml b/build-server.hxml new file mode 100644 index 0000000..220be8e --- /dev/null +++ b/build-server.hxml @@ -0,0 +1,7 @@ +-lib hxnodejs +-lib hxnodejs-ws +-cp src +--main server.Main +-D analyzer-optimize +--dce full +--js build/server.js diff --git a/build/client.js b/build/client.js new file mode 100644 index 0000000..9ceba8f --- /dev/null +++ b/build/client.js @@ -0,0 +1,1240 @@ +// Generated by Haxe 4.0.5 +(function ($global) { "use strict"; +var $estr = function() { return js_Boot.__string_rec(this,''); },$hxEnums = $hxEnums || {},$_; +function $extend(from, fields) { + var proto = Object.create(from); + for (var name in fields) proto[name] = fields[name]; + if( fields.toString !== Object.prototype.toString ) proto.toString = fields.toString; + return proto; +} +var Client = function(ws,name,isLeader) { + if(isLeader == null) { + isLeader = false; + } + this.ws = ws; + this.name = name; + this.isLeader = isLeader; +}; +Client.__name__ = true; +Client.fromData = function(data) { + return new Client(null,data.name,data.isLeader); +}; +var ClientTools = function() { }; +ClientTools.__name__ = true; +ClientTools.setLeader = function(clients,name) { + var _g = 0; + while(_g < clients.length) { + var client = clients[_g]; + ++_g; + if(client.name == name) { + client.isLeader = true; + } else if(client.isLeader) { + client.isLeader = false; + } + } +}; +ClientTools.getByName = function(clients,name) { + var _g = 0; + while(_g < clients.length) { + var client = clients[_g]; + ++_g; + if(client.name == name) { + return client; + } + } + return null; +}; +var EReg = function(r,opt) { + this.r = new RegExp(r,opt.split("u").join("")); +}; +EReg.__name__ = true; +EReg.prototype = { + match: function(s) { + if(this.r.global) { + this.r.lastIndex = 0; + } + this.r.m = this.r.exec(s); + this.r.s = s; + return this.r.m != null; + } +}; +var HxOverrides = function() { }; +HxOverrides.__name__ = true; +HxOverrides.substr = function(s,pos,len) { + if(len == null) { + len = s.length; + } else if(len < 0) { + if(pos == 0) { + len = s.length + len; + } else { + return ""; + } + } + return s.substr(pos,len); +}; +HxOverrides.remove = function(a,obj) { + var i = a.indexOf(obj); + if(i == -1) { + return false; + } + a.splice(i,1); + return true; +}; +HxOverrides.iter = function(a) { + return { cur : 0, arr : a, hasNext : function() { + return this.cur < this.arr.length; + }, next : function() { + return this.arr[this.cur++]; + }}; +}; +var Lambda = function() { }; +Lambda.__name__ = true; +Lambda.exists = function(it,f) { + var x = $getIterator(it); + while(x.hasNext()) if(f(x.next())) { + return true; + } + return false; +}; +Lambda.find = function(it,f) { + var v = $getIterator(it); + while(v.hasNext()) { + var v1 = v.next(); + if(f(v1)) { + return v1; + } + } + return null; +}; +var Lang = function() { }; +Lang.__name__ = true; +Lang.request = function(path,callback) { + var http = new haxe_http_HttpJs(path); + http.onData = callback; + http.request(); +}; +Lang.init = function(folderPath,callback) { + var _this = Lang.langs; + _this.h = { }; + _this.rh = null; + var count = 0; + var _g = 0; + var _g1 = Lang.ids; + while(_g < _g1.length) { + var name = [_g1[_g]]; + ++_g; + Lang.request("" + folderPath + "/" + name[0] + ".json",(function(name1) { + return function(data) { + var data1 = JSON.parse(data); + var lang = new haxe_ds_StringMap(); + var _g2 = 0; + var _g11 = Reflect.fields(data1); + while(_g2 < _g11.length) { + var key = _g11[_g2]; + ++_g2; + var v = Reflect.field(data1,key); + if(__map_reserved[key] != null) { + lang.setReserved(key,v); + } else { + lang.h[key] = v; + } + } + var id = haxe_io_Path.withoutExtension(name1[0]); + var _this1 = Lang.langs; + if(__map_reserved[id] != null) { + _this1.setReserved(id,lang); + } else { + _this1.h[id] = lang; + } + count += 1; + if(count == Lang.ids.length && callback != null) { + callback(); + } + return; + }; + })(name)); + } +}; +Lang.get = function(key) { + var key1 = Lang.lang; + var _this = Lang.langs; + if((__map_reserved[key1] != null ? _this.getReserved(key1) : _this.h[key1]) == null) { + Lang.lang = "en"; + } + var key2 = Lang.lang; + var _this1 = Lang.langs; + var _this2 = __map_reserved[key2] != null ? _this1.getReserved(key2) : _this1.h[key2]; + var text = __map_reserved[key] != null ? _this2.getReserved(key) : _this2.h[key]; + if(text == null) { + return key; + } else { + return text; + } +}; +Math.__name__ = true; +var Reflect = function() { }; +Reflect.__name__ = true; +Reflect.field = function(o,field) { + try { + return o[field]; + } catch( e ) { + return null; + } +}; +Reflect.fields = function(o) { + var a = []; + if(o != null) { + var hasOwnProperty = Object.prototype.hasOwnProperty; + for( var f in o ) { + if(f != "__id__" && f != "hx__closures__" && hasOwnProperty.call(o,f)) { + a.push(f); + } + } + } + return a; +}; +Reflect.isFunction = function(f) { + if(typeof(f) == "function") { + return !(f.__name__ || f.__ename__); + } else { + return false; + } +}; +Reflect.compareMethods = function(f1,f2) { + if(f1 == f2) { + return true; + } + if(!Reflect.isFunction(f1) || !Reflect.isFunction(f2)) { + return false; + } + if(f1.scope == f2.scope && f1.method == f2.method) { + return f1.method != null; + } else { + return false; + } +}; +var Std = function() { }; +Std.__name__ = true; +Std.string = function(s) { + return js_Boot.__string_rec(s,""); +}; +var client_Main = function(host,port) { + if(port == null) { + port = 4201; + } + this.pageTitle = window.document.title; + this.onTimeGet = new haxe_Timer(2000); + this.isConnected = false; + this.personalHistoryId = -1; + this.personalHistory = []; + this.clients = []; + var _gthis = this; + this.player = new client_Player(this); + if(host == null) { + host = window.location.hostname; + } + if(host == "") { + host = "localhost"; + } + this.initListeners(); + this.onTimeGet.run = function() { + _gthis.send({ type : "GetTime"}); + return; + }; + Lang.init("langs",function() { + _gthis.openWebSocket(host,port); + return; + }); +}; +client_Main.__name__ = true; +client_Main.main = function() { + new client_Main(); +}; +client_Main.prototype = { + openWebSocket: function(host,port) { + var _gthis = this; + this.ws = new WebSocket("ws://" + host + ":" + port); + this.ws.onmessage = $bind(this,this.onMessage); + this.ws.onopen = function() { + _gthis.serverMessage(1); + return _gthis.isConnected = true; + }; + this.ws.onclose = function() { + if(_gthis.isConnected) { + _gthis.serverMessage(2); + } + _gthis.isConnected = false; + _gthis.player.pause(); + return haxe_Timer.delay(function() { + _gthis.openWebSocket(host,port); + return; + },2000); + }; + } + ,initListeners: function() { + var _gthis = this; + var guestName = window.document.querySelector("#guestname"); + guestName.onkeydown = function(e) { + if(e.keyCode == 13) { + _gthis.send({ type : "Login", login : { clientName : guestName.value}}); + } + return; + }; + var chatLine = window.document.querySelector("#chatline"); + chatLine.onkeydown = function(e1) { + switch(e1.keyCode) { + case 13: + _gthis.send({ type : "Message", message : { clientName : "", text : chatLine.value}}); + _gthis.personalHistory.push(chatLine.value); + if(_gthis.personalHistory.length > 50) { + _gthis.personalHistory.shift(); + } + _gthis.personalHistoryId = -1; + chatLine.value = ""; + break; + case 38: + _gthis.personalHistoryId--; + if(_gthis.personalHistoryId == -2) { + _gthis.personalHistoryId = _gthis.personalHistory.length - 1; + if(_gthis.personalHistoryId == -1) { + return; + } + } else if(_gthis.personalHistoryId == -1) { + _gthis.personalHistoryId++; + } + chatLine.value = _gthis.personalHistory[_gthis.personalHistoryId]; + break; + case 40: + if(_gthis.personalHistoryId == -1) { + return; + } + _gthis.personalHistoryId++; + if(_gthis.personalHistoryId > _gthis.personalHistory.length - 1) { + _gthis.personalHistoryId = -1; + chatLine.value = ""; + return; + } + chatLine.value = _gthis.personalHistory[_gthis.personalHistoryId]; + break; + } + }; + client_MobileView.init(); + var leaderBtn = window.document.querySelector("#leader_btn"); + leaderBtn.onclick = function(e2) { + if(_gthis.personal == null) { + return; + } + leaderBtn.classList.toggle("label-success"); + _gthis.send({ type : "SetLeader", setLeader : { clientName : _gthis.personal.isLeader ? "" : _gthis.personal.name}}); + return; + }; + window.document.querySelector("#showmediaurl").onclick = function(e3) { + window.document.querySelector("#showmediaurl").classList.toggle("collapsed"); + window.document.querySelector("#showmediaurl").classList.toggle("active"); + return window.document.querySelector("#addfromurl").classList.toggle("collapse"); + }; + window.document.querySelector("#queue_next").onclick = function(e4) { + _gthis.addVideoUrl(); + return; + }; + window.document.querySelector("#queue_end").onclick = function(e5) { + _gthis.addVideoUrl(); + return; + }; + window.document.querySelector("#mediaurl").onkeydown = function(e6) { + if(e6.keyCode == 13) { + _gthis.addVideoUrl(); + } + }; + } + ,isLeader: function() { + if(this.personal != null) { + return this.personal.isLeader; + } else { + return false; + } + } + ,addVideoUrl: function() { + var _gthis = this; + var mediaUrl = window.document.querySelector("#mediaurl"); + var url = mediaUrl.value; + var name = this.personal == null ? "Unknown" : this.personal.name; + this.getRemoteVideoDuration(mediaUrl.value,function(duration) { + var tmp = Lang.get("rawVideo"); + _gthis.send({ type : "AddVideo", addVideo : { item : { url : url, title : tmp, author : name, duration : duration}}}); + return; + }); + mediaUrl.value = ""; + } + ,getRemoteVideoDuration: function(src,callback) { + var player = window.document.querySelector("#ytapiplayer"); + var video = window.document.createElement("video"); + video.src = src; + video.onloadedmetadata = function() { + haxe_Log.trace(video.duration,{ fileName : "src/client/Main.hx", lineNumber : 162, className : "client.Main", methodName : "getRemoteVideoDuration"}); + 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); + } + } + ,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 : 178, 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); + break; + case "Connected": + if(data.connected.isUnknownClient) { + this.updateClients(data.connected.clients); + window.document.querySelector("#guestlogin").style.display = "block"; + window.document.querySelector("#chatline").style.display = "none"; + } else { + this.onLogin(data.connected.clients,data.connected.clientName); + } + var list = data.connected.videoList; + if(list.length == 0) { + return; + } + this.player.setVideo(list[0]); + var _g = 0; + var _g1 = data.connected.videoList; + while(_g < _g1.length) { + var video = _g1[_g]; + ++_g; + this.player.addVideoItem(video); + } + break; + case "GetTime": + var newTime = data.getTime.time; + var time = this.player.getTime(); + if(Math.abs(time - newTime) < 2) { + return; + } + this.player.setTime(newTime); + if(!data.getTime.paused) { + this.player.play(); + } + break; + case "Login": + this.onLogin(data.login.clients,data.login.clientName); + break; + case "LoginError": + this.serverMessage(4,Lang.get("usernameError")); + break; + case "Logout": + this.updateClients(data.logout.clients); + this.personal = null; + window.document.querySelector("#guestlogin").style.display = "block"; + window.document.querySelector("#chatline").style.display = "none"; + break; + case "Message": + this.addMessage(data.message.clientName,data.message.text); + break; + case "Pause": + this.player.pause(); + this.player.setTime(data.pause.time); + break; + case "Play": + this.player.setTime(data.play.time); + this.player.play(); + break; + case "RemoveVideo": + this.player.removeItem(data.removeVideo.url); + if(this.player.isListEmpty()) { + this.player.pause(); + } + break; + case "SetLeader": + ClientTools.setLeader(this.clients,data.setLeader.clientName); + this.updateUserList(); + if(this.personal == null) { + return; + } + var leaderBtn = window.document.querySelector("#leader_btn"); + if(this.personal.isLeader) { + leaderBtn.classList.add("label-success"); + } else { + leaderBtn.classList.remove("label-success"); + } + break; + case "SetTime": + var newTime1 = data.setTime.time; + var time1 = this.player.getTime(); + if(Math.abs(time1 - newTime1) < 2) { + return; + } + this.player.setTime(newTime1); + break; + case "UpdateClients": + this.updateClients(data.updateClients.clients); + if(this.personal != null) { + this.personal = ClientTools.getByName(this.clients,this.personal.name); + } + break; + case "VideoLoaded": + this.player.setTime(0); + this.player.play(); + break; + } + } + ,onLogin: function(data,clientName) { + this.updateClients(data); + this.personal = ClientTools.getByName(this.clients,clientName); + if(this.personal == null) { + return; + } + window.document.querySelector("#guestlogin").style.display = "none"; + window.document.querySelector("#chatline").style.display = "block"; + } + ,updateClients: function(newClients) { + this.clients.length = 0; + var _g = 0; + while(_g < newClients.length) this.clients.push(Client.fromData(newClients[_g++])); + this.updateUserList(); + } + ,send: function(data) { + if(!this.isConnected) { + return; + } + this.ws.send(JSON.stringify(data)); + } + ,serverMessage: function(type,text) { + var msgBuf = window.document.querySelector("#messagebuffer"); + var div = window.document.createElement("div"); + var time = "[" + new Date().toTimeString().split(" ")[0] + "] "; + switch(type) { + case 1: + div.className = "server-msg-reconnect"; + div.innerHTML = Lang.get("msgConnected"); + break; + case 2: + div.className = "server-msg-disconnect"; + div.innerHTML = Lang.get("msgDisconnected"); + break; + case 3: + div.className = "server-whisper"; + div.innerHTML = time + text + " " + Lang.get("entered"); + break; + case 4: + div.className = "server-whisper"; + div.innerHTML = time + text; + break; + default: + } + msgBuf.appendChild(div); + msgBuf.scrollTop = msgBuf.scrollHeight; + } + ,updateUserList: function() { + window.document.querySelector("#usercount").innerHTML = this.clients.length + " " + Lang.get("online"); + window.document.title = "" + this.pageTitle + " (" + this.clients.length + ")"; + var list_b = ""; + var _g = 0; + var _g1 = this.clients; + while(_g < _g1.length) { + var client1 = _g1[_g]; + ++_g; + if(client1.isLeader) { + list_b += ""; + } + list_b += Std.string("" + client1.name + "
"); + } + window.document.querySelector("#userlist").innerHTML = list_b; + } + ,addMessage: function(name,msg) { + var msgBuf = window.document.querySelector("#messagebuffer"); + var userDiv = window.document.createElement("div"); + userDiv.className = "chat-msg-" + name; + var tstamp = window.document.createElement("span"); + tstamp.className = "timestamp"; + tstamp.innerHTML = "[" + new Date().toTimeString().split(" ")[0] + "] "; + var nameDiv = window.document.createElement("strong"); + nameDiv.className = "username"; + nameDiv.innerHTML = name + ": "; + var textDiv = window.document.createElement("span"); + textDiv.innerHTML = msg; + var 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; + } + } +}; +var client_MobileView = function() { }; +client_MobileView.__name__ = true; +client_MobileView.init = function() { + var mvbtn = window.document.querySelector("#mv_btn"); + mvbtn.onclick = function(e) { + if(client_MobileView.toggleFullScreen()) { + window.document.body.classList.add("mobile-view"); + mvbtn.classList.add("label-success"); + var vwrap = window.document.querySelector("#videowrap"); + if(vwrap.children[0] == window.document.querySelector("currenttitle")) { + vwrap.appendChild(vwrap.children[0]); + } + } else { + window.document.body.classList.remove("mobile-view"); + mvbtn.classList.remove("label-success"); + var vwrap1 = window.document.querySelector("videowrap"); + if(vwrap1.children[0] != window.document.querySelector("currenttitle")) { + vwrap1.insertBefore(vwrap1.children[1],vwrap1.children[0]); + } + } + 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; + this.player = window.document.querySelector("#ytapiplayer"); + this.videoItemsEl = window.document.querySelector("#queue"); + this.items = []; + this.main = main; +}; +client_Player.__name__ = true; +client_Player.prototype = { + setVideo: function(item) { + var _gthis = this; + this.isLoaded = false; + this.video = window.document.createElement("video"); + this.video.id = "videoplayer"; + this.video.src = item.url; + this.video.controls = true; + this.video.oncanplaythrough = function(e) { + if(!_gthis.isLoaded) { + _gthis.main.send({ type : "VideoLoaded"}); + } + return _gthis.isLoaded = true; + }; + this.video.ontimeupdate = function(e1) { + if(_gthis.skipSetTime) { + _gthis.skipSetTime = false; + return; + } + if(!_gthis.main.isLeader()) { + return; + } + _gthis.main.send({ type : "SetTime", setTime : { time : _gthis.video.currentTime}}); + return; + }; + this.video.onpause = function(e2) { + if(!_gthis.main.isLeader()) { + return; + } + _gthis.main.send({ type : "Pause", pause : { time : _gthis.video.currentTime}}); + return; + }; + this.video.onplay = function(e3) { + if(!_gthis.main.isLeader()) { + return; + } + _gthis.main.send({ type : "Play", play : { time : _gthis.video.currentTime}}); + return; + }; + this.player.innerHTML = ""; + this.player.appendChild(this.video); + } + ,addVideoItem: function(item) { + var _gthis = this; + this.items.push(item); + var itemEl = this.nodeFromString("
  • \n\t\t\t\t" + item.title + "\n\t\t\t\t" + this.duration(item.duration) + "\n\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
    \n\t\t\t
  • "); + itemEl.querySelector("#btn-delete").onclick = function(e) { + _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(); + } + ,removeItem: function(url) { + var list = window.document.querySelector("#queue"); + var _g = 0; + var _g1 = list.children; + while(_g < _g1.length) { + var child = _g1[_g]; + ++_g; + if(child.querySelector(".qe_title").getAttribute("href") == url) { + list.removeChild(child); + break; + } + } + HxOverrides.remove(this.items,Lambda.find(this.items,function(item) { + return item.url == url; + })); + if(this.video.src == url) { + if(this.items.length > 0) { + this.setVideo(this.items[0]); + } + } + var tmp = "" + this.items.length + " "; + var tmp1 = Lang.get("videos"); + window.document.querySelector("#plcount").innerHTML = tmp + tmp1; + window.document.querySelector("#pllength").innerHTML = this.totalDuration(); + } + ,duration: function(time) { + var h = time / 60 / 60 | 0; + var m = time / 60 | 0; + var s = time % 60 | 0; + var time1 = "" + m + ":"; + if(m < 10) { + time1 = "0" + time1; + } + if(h > 0) { + time1 = "" + h + ":" + time1; + } + if(s < 10) { + time1 += "0"; + } + time1 += s; + return time1; + } + ,totalDuration: function() { + var time = 0.0; + var _g = 0; + var _g1 = this.items; + while(_g < _g1.length) time += _g1[_g++].duration; + return this.duration(time); + } + ,nodeFromString: function(div) { + var wrapper = window.document.createElement("div"); + wrapper.innerHTML = div; + return wrapper.firstElementChild; + } + ,isListEmpty: function() { + return this.items.length == 0; + } + ,pause: function() { + if(this.video == null) { + return; + } + this.video.pause(); + } + ,play: function() { + if(this.video == null) { + return; + } + this.video.play(); + } + ,setTime: function(time,isLocal) { + if(isLocal == null) { + isLocal = true; + } + if(this.video == null) { + return; + } + this.skipSetTime = isLocal; + this.video.currentTime = time; + } + ,getTime: function() { + if(this.video == null) { + return 0; + } + return this.video.currentTime; + } +}; +var haxe_Log = function() { }; +haxe_Log.__name__ = true; +haxe_Log.formatOutput = function(v,infos) { + var str = Std.string(v); + if(infos == null) { + return str; + } + var pstr = infos.fileName + ":" + infos.lineNumber; + if(infos.customParams != null) { + var _g = 0; + var _g1 = infos.customParams; + while(_g < _g1.length) str += ", " + Std.string(_g1[_g++]); + } + return pstr + ": " + str; +}; +haxe_Log.trace = function(v,infos) { + var str = haxe_Log.formatOutput(v,infos); + if(typeof(console) != "undefined" && console.log != null) { + console.log(str); + } +}; +var haxe_Timer = function(time_ms) { + var me = this; + this.id = setInterval(function() { + me.run(); + },time_ms); +}; +haxe_Timer.__name__ = true; +haxe_Timer.delay = function(f,time_ms) { + var t = new haxe_Timer(time_ms); + t.run = function() { + t.stop(); + f(); + }; + return t; +}; +haxe_Timer.prototype = { + stop: function() { + if(this.id == null) { + return; + } + clearInterval(this.id); + this.id = null; + } + ,run: function() { + } +}; +var haxe_ds_StringMap = function() { + this.h = { }; +}; +haxe_ds_StringMap.__name__ = true; +haxe_ds_StringMap.prototype = { + setReserved: function(key,value) { + if(this.rh == null) { + this.rh = { }; + } + this.rh["$" + key] = value; + } + ,getReserved: function(key) { + if(this.rh == null) { + return null; + } else { + return this.rh["$" + key]; + } + } +}; +var haxe_http_HttpBase = function(url) { + this.url = url; + this.headers = []; + this.params = []; + this.emptyOnData = $bind(this,this.onData); +}; +haxe_http_HttpBase.__name__ = true; +haxe_http_HttpBase.prototype = { + onData: function(data) { + } + ,onBytes: function(data) { + } + ,onError: function(msg) { + } + ,onStatus: function(status) { + } + ,hasOnData: function() { + return !Reflect.compareMethods($bind(this,this.onData),this.emptyOnData); + } + ,success: function(data) { + this.responseBytes = data; + this.responseAsString = null; + if(this.hasOnData()) { + this.onData(this.get_responseData()); + } + this.onBytes(this.responseBytes); + } + ,get_responseData: function() { + if(this.responseAsString == null && this.responseBytes != null) { + this.responseAsString = this.responseBytes.getString(0,this.responseBytes.length,haxe_io_Encoding.UTF8); + } + return this.responseAsString; + } +}; +var haxe_http_HttpJs = function(url) { + this.async = true; + this.withCredentials = false; + haxe_http_HttpBase.call(this,url); +}; +haxe_http_HttpJs.__name__ = true; +haxe_http_HttpJs.__super__ = haxe_http_HttpBase; +haxe_http_HttpJs.prototype = $extend(haxe_http_HttpBase.prototype,{ + request: function(post) { + var _gthis = this; + this.responseAsString = null; + this.responseBytes = null; + var r = this.req = js_Browser.createXMLHttpRequest(); + var onreadystatechange = function(_) { + if(r.readyState != 4) { + return; + } + var s; + try { + s = r.status; + } catch( e ) { + s = null; + } + if(s == 0 && typeof(window) != "undefined") { + var protocol = window.location.protocol.toLowerCase(); + if(new EReg("^(?:about|app|app-storage|.+-extension|file|res|widget):$","").match(protocol)) { + s = r.response != null ? 200 : 404; + } + } + if(s == undefined) { + s = null; + } + if(s != null) { + _gthis.onStatus(s); + } + if(s != null && s >= 200 && s < 400) { + _gthis.req = null; + var onreadystatechange1 = haxe_io_Bytes.ofData(r.response); + _gthis.success(onreadystatechange1); + } else if(s == null) { + _gthis.req = null; + _gthis.onError("Failed to connect or resolve host"); + } else if(s == null) { + _gthis.req = null; + _gthis.responseBytes = haxe_io_Bytes.ofData(r.response); + _gthis.onError("Http Error #" + r.status); + } else { + switch(s) { + case 12007: + _gthis.req = null; + _gthis.onError("Unknown host"); + break; + case 12029: + _gthis.req = null; + _gthis.onError("Failed to connect to host"); + break; + default: + _gthis.req = null; + _gthis.responseBytes = haxe_io_Bytes.ofData(r.response); + _gthis.onError("Http Error #" + r.status); + } + } + }; + if(this.async) { + r.onreadystatechange = onreadystatechange; + } + var _g = this.postBytes; + var _g1 = this.postData; + var uri = _g1 == null ? _g == null ? null : new Blob([_g.b.bufferValue]) : _g == null ? _g1 : null; + if(uri != null) { + post = true; + } else { + var _g2 = 0; + var _g3 = this.params; + while(_g2 < _g3.length) { + var p = _g3[_g2]; + ++_g2; + if(uri == null) { + uri = ""; + } else { + uri = Std.string(uri) + "&"; + } + var s1 = p.name; + var value = Std.string(uri) + encodeURIComponent(s1) + "="; + var s2 = p.value; + uri = value + encodeURIComponent(s2); + } + } + try { + if(post) { + r.open("POST",this.url,this.async); + } else if(uri != null) { + r.open("GET",this.url + (this.url.split("?").length <= 1 ? "?" : "&") + Std.string(uri),this.async); + uri = null; + } else { + r.open("GET",this.url,this.async); + } + r.responseType = "arraybuffer"; + } catch( e1 ) { + var e2 = ((e1) instanceof js__$Boot_HaxeError) ? e1.val : e1; + this.req = null; + this.onError(e2.toString()); + return; + } + r.withCredentials = this.withCredentials; + if(!Lambda.exists(this.headers,function(h) { + return h.name == "Content-Type"; + }) && post && this.postData == null) { + r.setRequestHeader("Content-Type","application/x-www-form-urlencoded"); + } + var _g21 = 0; + var _g31 = this.headers; + while(_g21 < _g31.length) { + var h1 = _g31[_g21]; + ++_g21; + r.setRequestHeader(h1.name,h1.value); + } + r.send(uri); + if(!this.async) { + onreadystatechange(null); + } + } +}); +var haxe_io_Bytes = function(data) { + this.length = data.byteLength; + this.b = new Uint8Array(data); + this.b.bufferValue = data; + data.hxBytes = this; + data.bytes = this.b; +}; +haxe_io_Bytes.__name__ = true; +haxe_io_Bytes.ofData = function(b) { + var hb = b.hxBytes; + if(hb != null) { + return hb; + } + return new haxe_io_Bytes(b); +}; +haxe_io_Bytes.prototype = { + getString: function(pos,len,encoding) { + if(pos < 0 || len < 0 || pos + len > this.length) { + throw new js__$Boot_HaxeError(haxe_io_Error.OutsideBounds); + } + if(encoding == null) { + encoding = haxe_io_Encoding.UTF8; + } + var s = ""; + var b = this.b; + var i = pos; + var max = pos + len; + switch(encoding._hx_index) { + case 0: + while(i < max) { + var c = b[i++]; + if(c < 128) { + if(c == 0) { + break; + } + s += String.fromCodePoint(c); + } else if(c < 224) { + var code = (c & 63) << 6 | b[i++] & 127; + s += String.fromCodePoint(code); + } else if(c < 240) { + var code1 = (c & 31) << 12 | (b[i++] & 127) << 6 | b[i++] & 127; + s += String.fromCodePoint(code1); + } else { + var u = (c & 15) << 18 | (b[i++] & 127) << 12 | (b[i++] & 127) << 6 | b[i++] & 127; + s += String.fromCodePoint(u); + } + } + break; + case 1: + while(i < max) { + var c1 = b[i++] | b[i++] << 8; + s += String.fromCodePoint(c1); + } + break; + } + return s; + } +}; +var haxe_io_Encoding = $hxEnums["haxe.io.Encoding"] = { __ename__ : true, __constructs__ : ["UTF8","RawNative"] + ,UTF8: {_hx_index:0,__enum__:"haxe.io.Encoding",toString:$estr} + ,RawNative: {_hx_index:1,__enum__:"haxe.io.Encoding",toString:$estr} +}; +var haxe_io_Error = $hxEnums["haxe.io.Error"] = { __ename__ : true, __constructs__ : ["Blocked","Overflow","OutsideBounds","Custom"] + ,Blocked: {_hx_index:0,__enum__:"haxe.io.Error",toString:$estr} + ,Overflow: {_hx_index:1,__enum__:"haxe.io.Error",toString:$estr} + ,OutsideBounds: {_hx_index:2,__enum__:"haxe.io.Error",toString:$estr} + ,Custom: ($_=function(e) { return {_hx_index:3,e:e,__enum__:"haxe.io.Error",toString:$estr}; },$_.__params__ = ["e"],$_) +}; +var haxe_io_Path = function(path) { + switch(path) { + case ".":case "..": + this.dir = path; + this.file = ""; + return; + } + var c1 = path.lastIndexOf("/"); + var c2 = path.lastIndexOf("\\"); + if(c1 < c2) { + this.dir = HxOverrides.substr(path,0,c2); + path = HxOverrides.substr(path,c2 + 1,null); + this.backslash = true; + } else if(c2 < c1) { + this.dir = HxOverrides.substr(path,0,c1); + path = HxOverrides.substr(path,c1 + 1,null); + } else { + this.dir = null; + } + var cp = path.lastIndexOf("."); + if(cp != -1) { + this.ext = HxOverrides.substr(path,cp + 1,null); + this.file = HxOverrides.substr(path,0,cp); + } else { + this.ext = null; + this.file = path; + } +}; +haxe_io_Path.__name__ = true; +haxe_io_Path.withoutExtension = function(path) { + var s = new haxe_io_Path(path); + s.ext = null; + return s.toString(); +}; +haxe_io_Path.prototype = { + toString: function() { + return (this.dir == null ? "" : this.dir + (this.backslash ? "\\" : "/")) + this.file + (this.ext == null ? "" : "." + this.ext); + } +}; +var js__$Boot_HaxeError = function(val) { + Error.call(this); + this.val = val; + if(Error.captureStackTrace) { + Error.captureStackTrace(this,js__$Boot_HaxeError); + } +}; +js__$Boot_HaxeError.__name__ = true; +js__$Boot_HaxeError.__super__ = Error; +js__$Boot_HaxeError.prototype = $extend(Error.prototype,{ +}); +var js_Boot = function() { }; +js_Boot.__name__ = true; +js_Boot.__string_rec = function(o,s) { + if(o == null) { + return "null"; + } + if(s.length >= 5) { + return "<...>"; + } + var t = typeof(o); + if(t == "function" && (o.__name__ || o.__ename__)) { + t = "object"; + } + switch(t) { + case "function": + return ""; + case "object": + if(o.__enum__) { + var e = $hxEnums[o.__enum__]; + var n = e.__constructs__[o._hx_index]; + var con = e[n]; + if(con.__params__) { + s = s + "\t"; + return n + "(" + ((function($this) { + var $r; + var _g = []; + { + var _g1 = 0; + var _g2 = con.__params__; + while(true) { + if(!(_g1 < _g2.length)) { + break; + } + var p = _g2[_g1]; + _g1 = _g1 + 1; + _g.push(js_Boot.__string_rec(o[p],s)); + } + } + $r = _g; + return $r; + }(this))).join(",") + ")"; + } else { + return n; + } + } + if(((o) instanceof Array)) { + var str = "["; + s += "\t"; + var _g3 = 0; + var _g11 = o.length; + while(_g3 < _g11) { + var i = _g3++; + str += (i > 0 ? "," : "") + js_Boot.__string_rec(o[i],s); + } + str += "]"; + return str; + } + var tostr; + try { + tostr = o.toString; + } catch( e1 ) { + var e2 = ((e1) instanceof js__$Boot_HaxeError) ? e1.val : e1; + return "???"; + } + if(tostr != null && tostr != Object.toString && typeof(tostr) == "function") { + var s2 = o.toString(); + if(s2 != "[object Object]") { + return s2; + } + } + var str1 = "{\n"; + s += "\t"; + var hasp = o.hasOwnProperty != null; + var k = null; + for( k in o ) { + if(hasp && !o.hasOwnProperty(k)) { + continue; + } + if(k == "prototype" || k == "__class__" || k == "__super__" || k == "__interfaces__" || k == "__properties__") { + continue; + } + if(str1.length != 2) { + str1 += ", \n"; + } + str1 += s + k + " : " + js_Boot.__string_rec(o[k],s); + } + s = s.substring(1); + str1 += "\n" + s + "}"; + return str1; + case "string": + return o; + default: + return String(o); + } +}; +var js_Browser = function() { }; +js_Browser.__name__ = true; +js_Browser.createXMLHttpRequest = function() { + if(typeof XMLHttpRequest != "undefined") { + return new XMLHttpRequest(); + } + if(typeof ActiveXObject != "undefined") { + return new ActiveXObject("Microsoft.XMLHTTP"); + } + throw new js__$Boot_HaxeError("Unable to create XMLHttpRequest object."); +}; +function $getIterator(o) { if( o instanceof Array ) return HxOverrides.iter(o); else return o.iterator(); } +function $bind(o,m) { if( m == null ) return null; if( m.__id__ == null ) m.__id__ = $global.$haxeUID++; var f; if( o.hx__closures__ == null ) o.hx__closures__ = {}; else f = o.hx__closures__[m.__id__]; if( f == null ) { f = m.bind(o); o.hx__closures__[m.__id__] = f; } return f; } +$global.$haxeUID |= 0; +if( String.fromCodePoint == null ) String.fromCodePoint = function(c) { return c < 0x10000 ? String.fromCharCode(c) : String.fromCharCode((c>>10)+0xD7C0)+String.fromCharCode((c&0x3FF)+0xDC00); } +String.__name__ = true; +Array.__name__ = true; +var __map_reserved = {}; +Object.defineProperty(js__$Boot_HaxeError.prototype,"message",{ get : function() { + return String(this.val); +}}); +js_Boot.__toStr = ({ }).toString; +Lang.ids = ["en","ru"]; +Lang.langs = new haxe_ds_StringMap(); +Lang.lang = HxOverrides.substr(window.navigator.language,0,2).toLowerCase(); +client_Main.main(); +})(typeof window != "undefined" ? window : typeof global != "undefined" ? global : typeof self != "undefined" ? self : this); diff --git a/build/server.js b/build/server.js new file mode 100644 index 0000000..49e666c --- /dev/null +++ b/build/server.js @@ -0,0 +1,933 @@ +// Generated by Haxe 4.0.5 +(function ($global) { "use strict"; +function $extend(from, fields) { + var proto = Object.create(from); + for (var name in fields) proto[name] = fields[name]; + if( fields.toString !== Object.prototype.toString ) proto.toString = fields.toString; + return proto; +} +var Client = function(ws,name,isLeader) { + if(isLeader == null) { + isLeader = false; + } + this.ws = ws; + this.name = name; + this.isLeader = isLeader; +}; +Client.__name__ = true; +Client.prototype = { + getData: function() { + return { name : this.name, isLeader : this.isLeader}; + } +}; +var ClientTools = function() { }; +ClientTools.__name__ = true; +ClientTools.setLeader = function(clients,name) { + var _g = 0; + while(_g < clients.length) { + var client = clients[_g]; + ++_g; + if(client.name == name) { + client.isLeader = true; + } else if(client.isLeader) { + client.isLeader = false; + } + } +}; +ClientTools.hasLeader = function(clients) { + var _g = 0; + while(_g < clients.length) if(clients[_g++].isLeader) { + return true; + } + return false; +}; +ClientTools.getByName = function(clients,name) { + var _g = 0; + while(_g < clients.length) { + var client = clients[_g]; + ++_g; + if(client.name == name) { + return client; + } + } + return null; +}; +var EReg = function(r,opt) { + this.r = new RegExp(r,opt.split("u").join("")); +}; +EReg.__name__ = true; +EReg.prototype = { + match: function(s) { + if(this.r.global) { + this.r.lastIndex = 0; + } + this.r.m = this.r.exec(s); + this.r.s = s; + return this.r.m != null; + } + ,matched: function(n) { + if(this.r.m != null && n >= 0 && n < this.r.m.length) { + return this.r.m[n]; + } else { + throw new js__$Boot_HaxeError("EReg::matched"); + } + } + ,matchedPos: function() { + if(this.r.m == null) { + throw new js__$Boot_HaxeError("No string matched"); + } + return { pos : this.r.m.index, len : this.r.m[0].length}; + } + ,matchSub: function(s,pos,len) { + if(len == null) { + len = -1; + } + if(this.r.global) { + this.r.lastIndex = pos; + this.r.m = this.r.exec(len < 0 ? s : HxOverrides.substr(s,0,pos + len)); + var b = this.r.m != null; + if(b) { + this.r.s = s; + } + return b; + } else { + var b1 = this.match(len < 0 ? HxOverrides.substr(s,pos,null) : HxOverrides.substr(s,pos,len)); + if(b1) { + this.r.s = s; + this.r.m.index += pos; + } + return b1; + } + } + ,map: function(s,f) { + var offset = 0; + var buf_b = ""; + while(true) { + if(offset >= s.length) { + break; + } else if(!this.matchSub(s,offset)) { + buf_b += Std.string(HxOverrides.substr(s,offset,null)); + break; + } + var p = this.matchedPos(); + buf_b += Std.string(HxOverrides.substr(s,offset,p.pos - offset)); + buf_b += Std.string(f(this)); + if(p.len == 0) { + buf_b += Std.string(HxOverrides.substr(s,p.pos,1)); + offset = p.pos + 1; + } else { + offset = p.pos + p.len; + } + if(!this.r.global) { + break; + } + } + if(!this.r.global && offset > 0 && offset < s.length) { + buf_b += Std.string(HxOverrides.substr(s,offset,null)); + } + return buf_b; + } +}; +var HxOverrides = function() { }; +HxOverrides.__name__ = true; +HxOverrides.substr = function(s,pos,len) { + if(len == null) { + len = s.length; + } else if(len < 0) { + if(pos == 0) { + len = s.length + len; + } else { + return ""; + } + } + return s.substr(pos,len); +}; +HxOverrides.remove = function(a,obj) { + var i = a.indexOf(obj); + if(i == -1) { + return false; + } + a.splice(i,1); + return true; +}; +HxOverrides.iter = function(a) { + return { cur : 0, arr : a, hasNext : function() { + return this.cur < this.arr.length; + }, next : function() { + return this.arr[this.cur++]; + }}; +}; +var Lambda = function() { }; +Lambda.__name__ = true; +Lambda.find = function(it,f) { + var v = $getIterator(it); + while(v.hasNext()) { + var v1 = v.next(); + if(f(v1)) { + return v1; + } + } + return null; +}; +var haxe_ds_StringMap = function() { + this.h = { }; +}; +haxe_ds_StringMap.__name__ = true; +haxe_ds_StringMap.prototype = { + setReserved: function(key,value) { + if(this.rh == null) { + this.rh = { }; + } + this.rh["$" + key] = value; + } + ,getReserved: function(key) { + if(this.rh == null) { + return null; + } else { + return this.rh["$" + key]; + } + } +}; +var Lang = function() { }; +Lang.__name__ = true; +Lang.request = function(path,callback) { + callback(js_node_Fs.readFileSync(path,{ encoding : "utf8"})); +}; +Lang.init = function(folderPath,callback) { + var _this = Lang.langs; + _this.h = { }; + _this.rh = null; + var count = 0; + var _g = 0; + var _g1 = Lang.ids; + while(_g < _g1.length) { + var name = [_g1[_g]]; + ++_g; + Lang.request("" + folderPath + "/" + name[0] + ".json",(function(name1) { + return function(data) { + var data1 = JSON.parse(data); + var lang = new haxe_ds_StringMap(); + var _g2 = 0; + var _g11 = Reflect.fields(data1); + while(_g2 < _g11.length) { + var key = _g11[_g2]; + ++_g2; + var v = Reflect.field(data1,key); + if(__map_reserved[key] != null) { + lang.setReserved(key,v); + } else { + lang.h[key] = v; + } + } + var id = haxe_io_Path.withoutExtension(name1[0]); + var _this1 = Lang.langs; + if(__map_reserved[id] != null) { + _this1.setReserved(id,lang); + } else { + _this1.h[id] = lang; + } + count += 1; + if(count == Lang.ids.length && callback != null) { + callback(); + } + return; + }; + })(name)); + } +}; +Lang.get = function(lang,key) { + var _this = Lang.langs; + if((__map_reserved[lang] != null ? _this.getReserved(lang) : _this.h[lang]) == null) { + lang = "en"; + } + var _this1 = Lang.langs; + var _this2 = __map_reserved[lang] != null ? _this1.getReserved(lang) : _this1.h[lang]; + var text = __map_reserved[key] != null ? _this2.getReserved(key) : _this2.h[key]; + if(text == null) { + return key; + } else { + return text; + } +}; +Math.__name__ = true; +var Reflect = function() { }; +Reflect.__name__ = true; +Reflect.field = function(o,field) { + try { + return o[field]; + } catch( e ) { + return null; + } +}; +Reflect.fields = function(o) { + var a = []; + if(o != null) { + var hasOwnProperty = Object.prototype.hasOwnProperty; + for( var f in o ) { + if(f != "__id__" && f != "hx__closures__" && hasOwnProperty.call(o,f)) { + a.push(f); + } + } + } + return a; +}; +var Std = function() { }; +Std.__name__ = true; +Std.string = function(s) { + return js_Boot.__string_rec(s,""); +}; +var StringTools = function() { }; +StringTools.__name__ = true; +StringTools.startsWith = function(s,start) { + if(s.length >= start.length) { + return s.lastIndexOf(start,0) == 0; + } else { + return false; + } +}; +var haxe_Log = function() { }; +haxe_Log.__name__ = true; +haxe_Log.formatOutput = function(v,infos) { + var str = Std.string(v); + if(infos == null) { + return str; + } + var pstr = infos.fileName + ":" + infos.lineNumber; + if(infos.customParams != null) { + var _g = 0; + var _g1 = infos.customParams; + while(_g < _g1.length) str += ", " + Std.string(_g1[_g++]); + } + return pstr + ": " + str; +}; +haxe_Log.trace = function(v,infos) { + var str = haxe_Log.formatOutput(v,infos); + if(typeof(console) != "undefined" && console.log != null) { + console.log(str); + } +}; +var haxe_Timer = function(time_ms) { + var me = this; + this.id = setInterval(function() { + me.run(); + },time_ms); +}; +haxe_Timer.__name__ = true; +haxe_Timer.delay = function(f,time_ms) { + var t = new haxe_Timer(time_ms); + t.run = function() { + t.stop(); + f(); + }; + return t; +}; +haxe_Timer.prototype = { + stop: function() { + if(this.id == null) { + return; + } + clearInterval(this.id); + this.id = null; + } + ,run: function() { + } +}; +var haxe_io_Path = function(path) { + switch(path) { + case ".":case "..": + this.dir = path; + this.file = ""; + return; + } + var c1 = path.lastIndexOf("/"); + var c2 = path.lastIndexOf("\\"); + if(c1 < c2) { + this.dir = HxOverrides.substr(path,0,c2); + path = HxOverrides.substr(path,c2 + 1,null); + this.backslash = true; + } else if(c2 < c1) { + this.dir = HxOverrides.substr(path,0,c1); + path = HxOverrides.substr(path,c1 + 1,null); + } else { + this.dir = null; + } + var cp = path.lastIndexOf("."); + if(cp != -1) { + this.ext = HxOverrides.substr(path,cp + 1,null); + this.file = HxOverrides.substr(path,0,cp); + } else { + this.ext = null; + this.file = path; + } +}; +haxe_io_Path.__name__ = true; +haxe_io_Path.withoutExtension = function(path) { + var s = new haxe_io_Path(path); + s.ext = null; + return s.toString(); +}; +haxe_io_Path.extension = function(path) { + var s = new haxe_io_Path(path); + if(s.ext == null) { + return ""; + } + return s.ext; +}; +haxe_io_Path.prototype = { + toString: function() { + return (this.dir == null ? "" : this.dir + (this.backslash ? "\\" : "/")) + this.file + (this.ext == null ? "" : "." + this.ext); + } +}; +var js__$Boot_HaxeError = function(val) { + Error.call(this); + this.val = val; + if(Error.captureStackTrace) { + Error.captureStackTrace(this,js__$Boot_HaxeError); + } +}; +js__$Boot_HaxeError.__name__ = true; +js__$Boot_HaxeError.__super__ = Error; +js__$Boot_HaxeError.prototype = $extend(Error.prototype,{ +}); +var js_Boot = function() { }; +js_Boot.__name__ = true; +js_Boot.__string_rec = function(o,s) { + if(o == null) { + return "null"; + } + if(s.length >= 5) { + return "<...>"; + } + var t = typeof(o); + if(t == "function" && (o.__name__ || o.__ename__)) { + t = "object"; + } + switch(t) { + case "function": + return ""; + case "object": + if(((o) instanceof Array)) { + var str = "["; + s += "\t"; + var _g3 = 0; + var _g11 = o.length; + while(_g3 < _g11) { + var i = _g3++; + str += (i > 0 ? "," : "") + js_Boot.__string_rec(o[i],s); + } + str += "]"; + return str; + } + var tostr; + try { + tostr = o.toString; + } catch( e1 ) { + var e2 = ((e1) instanceof js__$Boot_HaxeError) ? e1.val : e1; + return "???"; + } + if(tostr != null && tostr != Object.toString && typeof(tostr) == "function") { + var s2 = o.toString(); + if(s2 != "[object Object]") { + return s2; + } + } + var str1 = "{\n"; + s += "\t"; + var hasp = o.hasOwnProperty != null; + var k = null; + for( k in o ) { + if(hasp && !o.hasOwnProperty(k)) { + continue; + } + if(k == "prototype" || k == "__class__" || k == "__super__" || k == "__interfaces__" || k == "__properties__") { + continue; + } + if(str1.length != 2) { + str1 += ", \n"; + } + str1 += s + k + " : " + js_Boot.__string_rec(o[k],s); + } + s = s.substring(1); + str1 += "\n" + s + "}"; + return str1; + case "string": + return o; + default: + return String(o); + } +}; +var js_node_Dns = require("dns"); +var js_node_Fs = require("fs"); +var js_node_Http = require("http"); +var js_node_Path = require("path"); +var js_npm_ws_Server = require("ws").Server; +var server_HttpServer = function() { }; +server_HttpServer.__name__ = true; +server_HttpServer.init = function(directory) { + server_HttpServer.dir = directory; +}; +server_HttpServer.serveFiles = function(req,res) { + var filePath = server_HttpServer.dir + req.url; + if(req.url == "/") { + filePath = "" + server_HttpServer.dir + "/index.html"; + } + var extension = haxe_io_Path.extension(filePath).toLowerCase(); + var contentType = server_HttpServer.getMimeType(extension); + if(!server_HttpServer.isChildOf(server_HttpServer.dir,filePath)) { + res.statusCode = 500; + var tmp = "Error getting the file: No access to " + js_node_Path.relative(server_HttpServer.dir,filePath) + "."; + res.end(tmp); + return; + } + if(filePath == "" + server_HttpServer.dir + "/client.js") { + filePath = "" + __dirname + "/client.js"; + } + js_node_Fs.readFile(filePath,function(err,data) { + if(err != null) { + if(err.code == "ENOENT") { + res.statusCode = 404; + var tmp1 = "File " + js_node_Path.relative(server_HttpServer.dir,filePath) + " not found."; + res.end(tmp1); + } else { + res.statusCode = 500; + res.end("Error getting the file: " + Std.string(err) + "."); + } + return; + } + res.setHeader("Content-Type",contentType); + if(extension == "html") { + data = server_HttpServer.localizeHtml(data.toString(),req.headers["accept-language"]); + } + res.end(data); + }); +}; +server_HttpServer.localizeHtml = function(data,lang) { + if(lang != null && server_HttpServer.matchLang.match(lang)) { + lang = server_HttpServer.matchLang.matched(0); + } else { + lang = "en"; + } + data = new EReg("\\${([A-z_]+)}","g").map(data,function(regExp) { + var key = regExp.matched(1); + return Lang.get(lang,key); + }); + return data; +}; +server_HttpServer.isChildOf = function(parent,child) { + var relative = js_node_Path.relative(parent,child); + if(relative.length > 0 && !StringTools.startsWith(relative,"..")) { + return !js_node_Path.isAbsolute(relative); + } else { + return false; + } +}; +server_HttpServer.getMimeType = function(ext) { + var _this = server_HttpServer.mimeTypes; + var contentType = __map_reserved[ext] != null ? _this.getReserved(ext) : _this.h[ext]; + if(contentType == null) { + contentType = "application/octet-stream"; + } + return contentType; +}; +var server_Main = function(port,wsPort) { + if(wsPort == null) { + wsPort = 4201; + } + if(port == null) { + port = 4200; + } + this.loadedClientsCount = 0; + this.videoTimer = new server_VideoTimer(); + this.videoList = []; + this.clients = []; + this.wss = new js_npm_ws_Server({ port : wsPort}); + this.wss.on("connection",$bind(this,this.onConnect)); + var exit = function() { + process.exit(); + }; + process.on("exit",exit); + process.on("SIGINT",exit); + process.on("uncaughtException",function(log) { + haxe_Log.trace(log,{ fileName : "src/server/Main.hx", lineNumber : 34, className : "server.Main", methodName : "new"}); + return; + }); + process.on("unhandledRejection",function(reason,promise) { + haxe_Log.trace("Unhandled Rejection at:",{ fileName : "src/server/Main.hx", lineNumber : 37, className : "server.Main", methodName : "new", customParams : [reason]}); + return; + }); + this.getPublicIp(function(ip) { + haxe_Log.trace("Local: http://127.0.0.1:" + port,{ fileName : "src/server/Main.hx", lineNumber : 41, className : "server.Main", methodName : "new"}); + haxe_Log.trace("Global: http://" + ip + ":" + port,{ fileName : "src/server/Main.hx", lineNumber : 42, className : "server.Main", methodName : "new"}); + return; + }); + var dir = "" + __dirname + "/../res"; + server_HttpServer.init(dir); + Lang.init("" + dir + "/langs"); + js_node_Http.createServer(function(req,res) { + server_HttpServer.serveFiles(req,res); + return; + }).listen(port); +}; +server_Main.__name__ = true; +server_Main.main = function() { + new server_Main(); +}; +server_Main.prototype = { + getPublicIp: function(callback) { + js_node_Dns.resolve("google.com",function(err,arr) { + if(err != null) { + callback("ERROR " + err.code); + return; + } + js_node_Http.get("http://myexternalip.com/raw",function(r) { + r.setEncoding("utf8"); + return r.on("data",callback); + }); + }); + } + ,onConnect: function(ws,req) { + var _gthis = this; + haxe_Log.trace("Client connected (" + req.connection.remoteAddress + ")",{ fileName : "src/server/Main.hx", lineNumber : 69, className : "server.Main", methodName : "onConnect"}); + var client = new Client(ws,"Unknown",false); + this.clients.push(client); + var client1 = client.name; + var _g = []; + var _g1 = 0; + var _g2 = this.clients; + while(_g1 < _g2.length) _g.push(_g2[_g1++].getData()); + this.send(client,{ type : "Connected", connected : { isUnknownClient : true, clientName : client1, clients : _g, videoList : this.videoList}}); + this.sendClientList(); + ws.on("message",function(data) { + var tmp = JSON.parse(data); + _gthis.onMessage(client,tmp); + return; + }); + ws.on("close",function(err) { + haxe_Log.trace("Client " + client.name + " disconnected",{ fileName : "src/server/Main.hx", lineNumber : 90, className : "server.Main", methodName : "onConnect"}); + HxOverrides.remove(_gthis.clients,client); + _gthis.sendClientList(); + if(client.isLeader) { + if(_gthis.videoTimer.isPaused()) { + _gthis.videoTimer.play(); + } + } + return; + }); + } + ,onMessage: function(client,data) { + switch(data.type) { + case "AddVideo": + this.videoList.push(data.addVideo.item); + this.broadcast(data); + if(this.videoList.length == 1) { + this.waitVideoStart = haxe_Timer.delay($bind(this,this.startVideoPlayback),3000); + } + break; + case "Connected": + break; + case "GetTime": + if(this.videoList.length == 0) { + return; + } + if(this.videoTimer.getTime() > this.videoList[0].duration) { + this.videoTimer.stop(); + this.onMessage(client,{ type : "RemoveVideo", removeVideo : { url : this.videoList[0].url}}); + return; + } + this.send(client,{ type : "GetTime", getTime : { time : this.videoTimer.getTime(), paused : this.videoTimer.isPaused()}}); + break; + case "Login": + var name = data.login.clientName; + if(name.length == 0 || name.length > 20 || ClientTools.getByName(this.clients,name) != null) { + this.send(client,{ type : "LoginError"}); + return; + } + client.name = data.login.clientName; + this.send(client,{ type : data.type, login : { isUnknownClient : true, clientName : client.name, clients : this.clientList()}}); + this.sendClientList(); + break; + case "LoginError": + break; + case "Logout": + var oldName = client.name; + client.name = "Unknown"; + this.send(client,{ type : data.type, logout : { clientName : oldName, clients : this.clientList()}}); + this.sendClientList(); + break; + case "Message": + data.message.clientName = client.name; + this.broadcast(data); + break; + case "Pause": + if(this.videoList.length == 0) { + return; + } + if(!client.isLeader) { + return; + } + this.videoTimer.pause(); + this.broadcast(data); + break; + case "Play": + if(this.videoList.length == 0) { + return; + } + if(!client.isLeader) { + return; + } + this.videoTimer.play(); + this.broadcast(data); + break; + case "RemoveVideo": + if(this.videoList.length == 0) { + return; + } + var url = data.removeVideo.url; + if(this.videoList[0].url == url) { + this.videoTimer.stop(); + } + HxOverrides.remove(this.videoList,Lambda.find(this.videoList,function(item) { + return item.url == url; + })); + this.broadcast(data); + break; + case "SetLeader": + ClientTools.setLeader(this.clients,data.setLeader.clientName); + this.sendClientList(); + if(this.videoList.length == 0) { + return; + } + if(!ClientTools.hasLeader(this.clients)) { + if(this.videoTimer.isPaused()) { + this.videoTimer.play(); + } + this.broadcast({ type : "Play", play : { time : this.videoTimer.getTime()}}); + } + break; + case "SetTime": + if(this.videoList.length == 0) { + return; + } + if(!client.isLeader) { + return; + } + this.videoTimer.setTime(data.setTime.time); + this.broadcastExcept(client,data); + break; + case "UpdateClients": + this.sendClientList(); + break; + case "VideoLoaded": + this.prepareVideoPlayback(); + break; + } + } + ,clientList: function() { + var _g = []; + var _g1 = 0; + var _g2 = this.clients; + while(_g1 < _g2.length) _g.push(_g2[_g1++].getData()); + return _g; + } + ,sendClientList: function() { + this.broadcast({ type : "UpdateClients", updateClients : { clients : this.clientList()}}); + } + ,send: function(client,data) { + client.ws.send(JSON.stringify(data),null); + } + ,broadcast: function(data) { + var json = JSON.stringify(data); + var _g = 0; + var _g1 = this.clients; + while(_g < _g1.length) _g1[_g++].ws.send(json,null); + } + ,broadcastExcept: function(skipped,data) { + var json = JSON.stringify(data); + var _g = 0; + var _g1 = this.clients; + while(_g < _g1.length) { + var client = _g1[_g]; + ++_g; + if(client == skipped) { + continue; + } + client.ws.send(json,null); + } + } + ,prepareVideoPlayback: function() { + if(this.videoTimer.isStarted) { + return; + } + this.loadedClientsCount++; + if(this.loadedClientsCount == 1) { + this.waitVideoStart = haxe_Timer.delay($bind(this,this.startVideoPlayback),3000); + } + if(this.loadedClientsCount >= this.clients.length) { + this.startVideoPlayback(); + } + } + ,startVideoPlayback: function() { + if(this.waitVideoStart != null) { + this.waitVideoStart.stop(); + } + this.loadedClientsCount = 0; + this.broadcast({ type : "VideoLoaded"}); + this.videoTimer.start(); + } +}; +var server_VideoTimer = function() { + this.pauseStartTime = 0.0; + this.startTime = 0.0; + this.isStarted = false; +}; +server_VideoTimer.__name__ = true; +server_VideoTimer.prototype = { + start: function() { + this.isStarted = true; + this.startTime = Date.now() / 1000; + this.pauseStartTime = 0; + } + ,stop: function() { + this.isStarted = false; + this.startTime = 0.0; + this.pauseStartTime = 0.0; + } + ,pause: function() { + this.pauseStartTime = Date.now() / 1000; + } + ,play: function() { + if(!this.isStarted) { + this.start(); + } + this.startTime += this.pauseTime(); + this.pauseStartTime = 0; + } + ,getTime: function() { + if(this.startTime == 0) { + return 0; + } + return Date.now() / 1000 - this.startTime - this.pauseTime(); + } + ,setTime: function(secs) { + this.startTime = Date.now() / 1000 - secs; + if(this.isPaused()) { + this.pause(); + } + } + ,isPaused: function() { + return this.pauseStartTime != 0; + } + ,pauseTime: function() { + if(!this.isPaused()) { + return 0; + } + return Date.now() / 1000 - this.pauseStartTime; + } +}; +function $getIterator(o) { if( o instanceof Array ) return HxOverrides.iter(o); else return o.iterator(); } +var $_; +function $bind(o,m) { if( m == null ) return null; if( m.__id__ == null ) m.__id__ = $global.$haxeUID++; var f; if( o.hx__closures__ == null ) o.hx__closures__ = {}; else f = o.hx__closures__[m.__id__]; if( f == null ) { f = m.bind(o); o.hx__closures__[m.__id__] = f; } return f; } +$global.$haxeUID |= 0; +var __map_reserved = {}; +String.__name__ = true; +Array.__name__ = true; +Object.defineProperty(js__$Boot_HaxeError.prototype,"message",{ get : function() { + return String(this.val); +}}); +js_Boot.__toStr = ({ }).toString; +Lang.ids = ["en","ru"]; +Lang.langs = new haxe_ds_StringMap(); +server_HttpServer.mimeTypes = (function($this) { + var $r; + var _g = new haxe_ds_StringMap(); + if(__map_reserved["html"] != null) { + _g.setReserved("html","text/html"); + } else { + _g.h["html"] = "text/html"; + } + if(__map_reserved["js"] != null) { + _g.setReserved("js","text/javascript"); + } else { + _g.h["js"] = "text/javascript"; + } + if(__map_reserved["css"] != null) { + _g.setReserved("css","text/css"); + } else { + _g.h["css"] = "text/css"; + } + if(__map_reserved["json"] != null) { + _g.setReserved("json","application/json"); + } else { + _g.h["json"] = "application/json"; + } + if(__map_reserved["png"] != null) { + _g.setReserved("png","image/png"); + } else { + _g.h["png"] = "image/png"; + } + if(__map_reserved["jpg"] != null) { + _g.setReserved("jpg","image/jpg"); + } else { + _g.h["jpg"] = "image/jpg"; + } + if(__map_reserved["gif"] != null) { + _g.setReserved("gif","image/gif"); + } else { + _g.h["gif"] = "image/gif"; + } + if(__map_reserved["svg"] != null) { + _g.setReserved("svg","image/svg+xml"); + } else { + _g.h["svg"] = "image/svg+xml"; + } + if(__map_reserved["ico"] != null) { + _g.setReserved("ico","image/x-icon"); + } else { + _g.h["ico"] = "image/x-icon"; + } + if(__map_reserved["wav"] != null) { + _g.setReserved("wav","audio/wav"); + } else { + _g.h["wav"] = "audio/wav"; + } + if(__map_reserved["mp3"] != null) { + _g.setReserved("mp3","audio/mpeg"); + } else { + _g.h["mp3"] = "audio/mpeg"; + } + if(__map_reserved["mp4"] != null) { + _g.setReserved("mp4","video/mp4"); + } else { + _g.h["mp4"] = "video/mp4"; + } + if(__map_reserved["woff"] != null) { + _g.setReserved("woff","application/font-woff"); + } else { + _g.h["woff"] = "application/font-woff"; + } + if(__map_reserved["ttf"] != null) { + _g.setReserved("ttf","application/font-ttf"); + } else { + _g.h["ttf"] = "application/font-ttf"; + } + if(__map_reserved["eot"] != null) { + _g.setReserved("eot","application/vnd.ms-fontobject"); + } else { + _g.h["eot"] = "application/vnd.ms-fontobject"; + } + if(__map_reserved["otf"] != null) { + _g.setReserved("otf","application/font-otf"); + } else { + _g.h["otf"] = "application/font-otf"; + } + if(__map_reserved["wasm"] != null) { + _g.setReserved("wasm","application/wasm"); + } else { + _g.h["wasm"] = "application/wasm"; + } + $r = _g; + return $r; +}(this)); +server_HttpServer.matchLang = new EReg("^[A-z]+",""); +server_Main.main(); +})(typeof window != "undefined" ? window : typeof global != "undefined" ? global : typeof self != "undefined" ? self : this); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e7de39d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "synctube", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ws": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz", + "integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c5de207 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "synctube", + "version": "1.0.0", + "description": "Synchronized video viewing with chat and other features", + "main": "build/server.js", + "repository": { + "type": "git", + "url": "git+https://github.com/RblSb/SyncTube.git" + }, + "author": "RblSb", + "license": "MIT", + "bugs": { + "url": "https://github.com/RblSb/SyncTube/issues" + }, + "homepage": "https://github.com/RblSb/SyncTube#readme", + "dependencies": { + "ws": "^7.2.0" + } +} diff --git a/res/css/custom.css b/res/css/custom.css new file mode 100644 index 0000000..e69de29 diff --git a/res/css/cytube.css b/res/css/cytube.css new file mode 100644 index 0000000..ff4411a --- /dev/null +++ b/res/css/cytube.css @@ -0,0 +1,725 @@ +.container-fluid { + padding-left: 15px; + padding-right: 15px; + margin-left: auto; + margin-right: auto; +} + +#loginform > .form-group { + margin-right: 5px; +} + +.center { + text-align: center; +} + +.messagebox > p { + margin-top: 20px; +} + +.vertical-spacer { + margin-top: 10px; +} + +#messagebuffer { + font-size: 17px; + width: auto; + padding-left: 5px; + padding-right: 5px; +} + +#userlist { + width: 90px; + float: left; + border-right: 0; + font-size: 9pt; + list-style: none outside none; + padding: 0; +} + +#messagebuffer, #userlist { + height: 329px; + overflow-x: hidden; + overflow-y: scroll; + margin-bottom: 0; +} + +#chatline, #guestlogin > input, #guestlogin > .input-group-addon { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.linewrap, .linewrap code { + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} + +#from-url, #from-search { + margin-top: 3px; +} + +.clear { + clear: both; +} + +.chat-name { + font-weight: bold; +} + +#customembed > .input-group { + margin-top: 5px; +} + +#chatheader { + padding-left: 5px; +} + +#chatheader > p, #currenttitle { + margin: 0; +} + +.pointer { + cursor: pointer; +} + +#chatwrap, #videowrap { + margin-bottom: 10px; +} + +#smileswrap { + display: none; + background: rgba(0,0,0,0.7); + margin: 10px 15px; + width: 98%; + max-height: 500px; + overflow-y: scroll; + padding: 5px; + text-align: center; + color: #fff; +} +#smileswrap video { + vertical-align: middle; +} +.smile-preview { + padding: 2px; + max-height: 75px; + cursor: pointer; +} + +.embed-responsive-chat { + padding-bottom: 78.75%; +} + +#userpl_list { + list-style: none outside none; + margin-left: 0; + max-height: 500px; + overflow-y: scroll; +} + +/* +#userpl_list li { + display: inline-block; + line-height: 22px; + width: 100%; + clear: both; + margin: 2px 0 0 auto; + padding: 2px; + font-size: 8pt; +} +*/ + +#customembed_wrap { + margin: 5px 0; +} + +#playlistmanagerwrap { + margin-top: 10px; +} + +#library { + padding-left: 0; + padding-right: 0; + margin-bottom: 5px; +} + +#library_search, #queue_next, #ce_queue_next { + border-radius: 0; +} + +#plmeta { + border-radius: 4px; + border-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-top: 1px solid #2c2e31; + padding-left: 5px; + padding-right: 5px; +} + +.videolist { + list-style: none outside none; + margin-left: 0; + max-height: 500px; + overflow-y: auto; +} + + + +#pllength { + float: right; +} + +.queue_temp { + background-image: url(/img/stripe-diagonal.png); +} + +.videolist { + padding: 0; + margin: 0; +} + +#queue > li:last-child { + border-bottom-width: 0; + margin-bottom: 0; +} + +#userpl_list > li:last-child { + border-bottom-width: 1px; +} + +.videolist > li:first-child { + border-top-width: 1px; +} + +li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry { + border-top-width: 1px; +} + +.qe_btn { + height: 20px; + font-family: Monospace; + padding: 0 5px 0 5px; + margin: auto; + overflow: hidden; +} + +.qe_buttons, .qe_title { + float: left; +} + +.qe_time { + float: right; + font-family: Monospace; +} + +.qe_clear { + clear: both; +} + +.clear { + clear: both; +} + +#chatheader .label { + height: 100%; + margin-left: 2px; +} + +.well { + margin-top: 10px; +} + +.well hr { + border-color: #cccccc; +} + +#csstext, #jstext { + font-family: Monospace; +} + +#optedit, #permedit, #filteredit, #motdedit, #cssedit, #jsedit, +#banlist, #loginhistory, #channelranks, #chanlog { + display: none; +} + +#chanlog_contents { + max-height: 400px; + overflow-y: scroll; + margin-top: 10px; +} + +.server-msg-disconnect { + border: 1px solid #ff0000; + line-height: 2; + margin-top: 5px; + margin-bottom: 5px; + color: #ff0000; + text-align: center; + background-color: rgba(129, 20, 21, 0.1); +} + +.server-msg-reconnect { + border: 1px solid #009900; + line-height: 2; + margin-top: 5px; + margin-bottom: 5px; + color: #009900; + text-align: center; + background-color: rgba(18, 100, 18, 0.1); +} + +.queue_sortable li { + cursor: row-resize; +} + +.poll-notify { + color: #0000aa; + font-weight: bold; + font-size: 14pt; +} + +.userlist_item { + cursor: pointer; +} + +.userlist_siteadmin { + color: #cc0000!important; + font-weight: bold!important; +} + +.userlist_owner { + color: #0000cc!important; + font-weight: bold!important; +} + +.userlist_op { + color: #00aa00!important; +} + +.userlist_guest { + color: #888888!important; +} + +.action { + font-style: italic; + color: #888888; +} + +.server-whisper { + font-style: italic; + color: #888888; + font-size: 8pt; +} + +.spoiler { + color: #000000; + background-color: #000000; +} + +.spoiler:hover { + color: #ffffff; +} + +.greentext { + color: #789922; /* Color value directly from 4chan */ +} + +.shout { + color: #ff0000; + font-weight: bold; + font-size: 18pt; +} + +.mono { + font-family: Monospace; +} + +.nick-highlight { + background-color: #ddffdd; +} + +.nick-hover { + background-color: #ffff99; +} + +.timestamp { + color: #808080; + font-size: 8pt; +} + +.profile-box { + z-index: 9999; + position: fixed; + border: 1px solid #aaaaaa; + border-radius: 5px; + padding: 5px; + max-width: 200px; + max-height: 300px; + overflow-y: hidden; +} + +.user-dropdown { + z-index: 9999; + position: absolute; + border: 1px solid #aaaaaa; + border-radius: 5px; + color: #000000; + max-width: 200px; + padding: 5px; +} + +.profile-image { + max-width: 80px; + max-height: 80px; + border: 1px solid #aaaaaa; + border-radius: 5px; +} + +#togglemotd .glyphicon { + font-size: 10pt; +} + +.poll-menu > .btn, .poll-menu > input { + clear: both; + margin-bottom: 10px; +} + +.poll-menu { + margin-top: 10px; +} + +#search_clear { + margin-top: 10px; +} + +#qualitywrap { + margin-right: 5px; +} + +#guestlogin .input-group-addon { + min-width: 120px; +} + +#channeloptions .modal-header { + border-bottom: none; +} + +#pollwrap > div { + margin-top: 10px; +} + +.option { + margin-top: 5px; +} + +.option-selected { + font-weight: bold; +} + +.option > button { + margin-right: 15px; +} + +.option-selected > button { + border-width: 3px !important; + margin-right: 10px; +} + +#useroptions .modal-header { + border-bottom: 0; + padding-bottom: 0; + margin-bottom: 0; +} + +#useroptions .modal-body { + padding-top: 0; + margin-top: 0; +} + +.qfalert { + margin-bottom: 10px; + padding-left: 0!important; + padding-right: 0!important; +} + +#customembed-content { + font-family: Monospace; +} + +#cs-csstext, #cs-jstext, #cs-motdtext { + font-family: Monospace; +} + +#cs-csssubmit, #cs-motdsubmit, #cs-jssubmit { + margin-top: 10px; +} + +#cs-chatfilters input[type='text'], #cs-chatfilters textarea { + font-family: monospace; +} + +#cs-chatfilters-exporttext { + margin-top: 5px; +} + +#cs-emotes input[type='text'], #cs-emotes textarea { + font-family: monospace; +} + +#cs-emotes-exporttext { + margin-top: 5px; +} + +.pagination { + margin: 0; +} + +#cs-chanlog-filter { + border-bottom: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +#cs-chanlog-text { + max-height: 300px; + overflow-y: scroll; + font-size: 8pt; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.channel-emote, .chat-img { + max-width: 200px; + max-height: 200px; +} + +#cs-emotes td:nth-child(3) { + max-width: 300px; +} + +#pmbar { + position: fixed; + bottom: 0; + z-index: 10000; + min-width: 100%; + pointer-events: none; +} + +body.chatOnly .pm-panel, body.chatOnly .pm-panel-placeholder { + margin-left: 0; + margin-right: 5px; + float: right; +} + +.pm-panel, .pm-panel-placeholder { + margin-left: 5px; + margin-bottom: 20px; + float: left; + width: 250px; + pointer-events: auto; +} + +.pm-panel { + margin-bottom: 0!important; + border-radius: 0!important; + border-radius: 0!important; +} + +.pm-panel > .panel-heading { + cursor: pointer; + border-radius: 0!important; + border-radius: 0!important; +} + +.pm-panel > .panel-body { + padding: 0; +} + +.pm-panel > .panel-body > .pm-buffer { + height: 200px; + overflow-y: scroll; +} + +.pm-panel > .panel-body > hr { + margin: 0; +} + +.pm-input { + margin: 0; + width: 100%; + border-top-left-radius: 0!important; + border-top-right-radius: 0!important; +} + +.chat-shadow { + text-decoration: line-through; +} + +#chanjs-allow-prompt { + text-align: center; +} + +#chanjs-allow-prompt-buttons { + margin-top: 10px; +} + +#chanjs-allow-prompt-buttons button:first-child { + margin-right: 5px; +} + +@media screen and (min-width: 768px) { + .modal-dialog { + min-width: 600px!important; + max-width: 1200px!important; + width: auto!important; + } + + .modal-dialog-nonfluid.modal-dialog { + max-width: 600px!important; + } +} + +table td { + max-width: 200px; + word-wrap: break-word; +} + +#cs-chatfilters table .form-group { + max-width: 25%; +} + +#cs-chatfilters table .form-group > input { + max-width: 100%; +} + +#userlisttoggle { + padding-top: 2px; +} + +.queue_entry { + line-height: 22px; + padding: 2px; + font-size: 8pt; + border: 1px solid; + border-top-width: 0; +} + +.emotelist-table { + margin: auto; +} + +.emote-preview-container { + width: 100px; + height: 100px; + float: left; + text-align: center; + white-space: nowrap; + margin: 5px; +} + +.emote-preview-hax { + display: inline-block; + vertical-align: middle; + height: 100%; +} + +.emote-preview { + max-width: 100px; + max-height: 100px; + cursor: pointer; +} + +.emotelist-paginator-container { + text-align: center; +} + +#leftcontrols .btn { + margin-right: 5px; +} + +#videowrap .embed-responsive:-webkit-full-screen { width: 100%; } +#videowrap .embed-responsive:-moz-full-screen { width: 100%; } +#videowrap .embed-responsive:-ms-full-screen { width: 100%; } +#videowrap .embed-responsive:-o-full-screen { width: 100%; } +#videowrap .embed-responsive:full-screen { width: 100%; } + +li.vjs-menu-item.vjs-selected { + background-color: #66a8cc !important; +} + +.video-js video::-webkit-media-text-track-container { + bottom: 50px; +} + +input#logout[type="submit"] { + background: none; + border: none; + padding: 0; +} + +input#logout[type="submit"]:hover { + text-decoration: underline; +} + +#newmessages-indicator { + margin-top: -30px; + line-height: 30px; + height: 30px; + text-align: center; + width: 100%; + font-weight: bold; + cursor: pointer; +} + +#newmessages-indicator .glyphicon { + margin-left: 10px; + margin-right: 10px; +} + +#soundcloud-volume-holder { + position: absolute; + top: 170px; + width: 100%; +} + +#soundcloud-volume-label { + margin-left: 2px; +} + +#soundcloud-volume { + margin-top: 5px; + margin-left: 10px; + margin-right: 10px; +} + +.navbar-inverse { + background: rgba(0,0,0,0.44) !important; +} +#footer { + margin-top: 15px; +} + + +.split { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + float: left; +} +.gutter { + background-color: transparent; + background-repeat: no-repeat; + background-position: 50%; +} +.gutter.gutter-horizontal { + cursor: col-resize; + background-image: url('/img/vertical.png'); + height: 80px; + float: left; + opacity: 0.2; +} diff --git a/res/css/des.css b/res/css/des.css new file mode 100644 index 0000000..583d7bd --- /dev/null +++ b/res/css/des.css @@ -0,0 +1,1350 @@ +@charset "utf-8"; +/* CSS Document */ + + +@import url(http://fonts.googleapis.com/css?family=Play:400italic,700italic,300,700,300italic,400&subset=latin,cyrillic); +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ +html { + font-family:Play; + -ms-text-size-adjust:100%; + -webkit-text-size-adjust:100% +} +body { + margin:0 +} +footer, nav, section { + display:block +} +a { + background-color:transparent +} +a:active, a:hover { + outline:0 +} +b, strong { + font-weight:bold +} +hr { + -moz-box-sizing:content-box; + -webkit-box-sizing:content-box; + box-sizing:content-box; + height:0 +} +pre { + overflow:auto +} +code, pre { + font-family:monospace, monospace; + font-size:1em +} +button, input, select, textarea { + color:inherit; + font:inherit; + margin:0 +} +button { + overflow:visible +} +button, select { + text-transform:none +} +button { + -webkit-appearance:button; + cursor:pointer +} +button[disabled] { + cursor:default +} +button::-moz-focus-inner, input::-moz-focus-inner { + border:0; + padding:0 +} +input { + line-height:normal +} +input[type="checkbox"] { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + padding:0 +} +textarea { + overflow:auto +} +table { + border-collapse:collapse; + border-spacing:0 +} +th { + padding:0 +}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, *:before, *:after { + background:transparent !important; + color:#000 !important; + -webkit-box-shadow:none !important; + box-shadow:none !important; + text-shadow:none !important + } + a, a:visited { + text-decoration:underline + } + a[href]:after { + content:" (" attr(href) ")" + } + a[href^="#"]:after, a[href^="javascript:"]:after { + content:"" + } + pre { + border:1px solid #999; + page-break-inside:avoid + } + thead { + display:table-header-group + } + tr { + page-break-inside:avoid + } + select { + background:#fff !important + } + .navbar { + display:none + } + .label { + border:1px solid #000 + } + .table { + border-collapse:collapse !important + } + .table th { + background-color:#fff !important + } +} +@font-face { +font-family:'Glyphicons Halflings'; +src:url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.2/fonts/glyphicons-halflings-regular.eot'); +src:url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.2/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.2/fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.2/fonts/glyphicons-halflings-regular.woff') format('woff'), url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.2/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.2/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg') +} +.glyphicon { + position:relative; + top:1px; + display:inline-block; + font-family:'Glyphicons Halflings'; + font-style:normal; + font-weight:normal; + line-height:1; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale +} +.glyphicon-plus:before { + content:"\2b" +} +.glyphicon-minus:before { + content:"\2212" +} +.glyphicon-search:before { + content:"\e003" +} +.glyphicon-th-large:before { + content:"\e010" +} +.glyphicon-trash:before { + content:"\e020" +} +.glyphicon-lock:before { + content:"\e033" +} +.glyphicon-flag:before { + content:"\e034" +} +.glyphicon-list:before { + content:"\e056" +} +.glyphicon-play:before { + content:"\e072" +} +.glyphicon-star-empty:before { + content:"\e007" +} +.glyphicon-step-forward:before { + content:"\e077" +} +.glyphicon-share-alt:before { + content:"\e095" +} +.glyphicon-chevron-down:before { + content:"\e114" +} +.glyphicon-retweet:before { + content:"\e115" +} +.glyphicon-fullscreen:before { + content:"\e140" +} +.glyphicon-link:before { + content:"\e144" +} +.glyphicon-sort:before { + content:"\e150" +} +.glyphicon-sound-stereo:before { + content:"\e189" +} +* { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +*:before, *:after { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +html { + font-size:10px; + -webkit-tap-highlight-color:rgba(0, 0, 0, 0) +} +body { + font-family:"Helvetica Neue", Helvetica, Arial, sans-serif; + font-size:14px; + line-height:1.42857143; + color:#c8c8c8; + background-color: #272b30; +} +input, button, select, textarea { + font-family:inherit; + font-size:inherit; + line-height:inherit +} +a { + color:#ffffff; + text-decoration:none +} +a:hover, a:focus { + color:#ffffff; + text-decoration:underline +} +a:focus { + outline:none; + /* outline:thin dotted; + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px */ +} +hr { + margin-top:20px; + margin-bottom:20px; + border:0; + border-top:1px solid #282a2d +} +h3, h4 { + font-family:"Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight:500; + line-height:1.1; + color:inherit +} +h3 { + margin-top:20px; + margin-bottom:10px +} +h4 { + margin-top:10px; + margin-bottom:10px +} +h3 { + font-size:24px +} +h4 { + font-size:18px +} +p { + margin:0 0 10px +} +.text-muted { + color:rgba(120, 126, 132, 0.85) +} +.text-info { + color:#ffffff +} +.text-danger { + color:#ffffff +} +ul { + margin-top:0; + margin-bottom:10px +} +ul ul { + margin-bottom:0 +} +code, pre { + font-family:Menlo, Monaco, Consolas, "Courier New", monospace +} +code { + padding:2px 4px; + font-size:90%; + color:#c7254e; + background-color:#f9f2f4; + border-radius:0px +} +pre { + display:block; + padding:9.5px; + margin:0 0 10px; + font-size:13px; + line-height:1.42857143; + word-break:break-all; + word-wrap:break-word; + color:rgba(20, 22, 26, 0.7); + background-color:#f5f5f5; + border:1px solid #cccccc; + border-radius:0px +} +.container { + margin-right:auto; + margin-left:auto; + padding-left:15px; + padding-right:15px +} +@media (min-width:768px) { + .container { + width:750px + } +} +@media (min-width:992px) { + .container { + width:970px + } +} +@media (min-width:1200px) { + .container { + width:1170px + } +} +.container-fluid { + margin-right:auto; + margin-left:auto; + padding-left:15px; + padding-right:15px +} +.row { + margin-left:-15px; + margin-right:-15px +} +.col-sm-4, .col-md-5, .col-lg-5, .col-md-7, .col-lg-7, .col-sm-8, .col-md-12, .col-lg-12 { + position:relative; + min-height:1px; + padding-left:15px; + padding-right:15px +} +@media (min-width:768px) { + .col-sm-4, .col-sm-8 { + float:left + } + .col-sm-8 { + width:66.66666667% + } + .col-sm-4 { + width:33.33333333% + } + .col-sm-offset-4 { + margin-left:33.33333333% + } +} +@media (min-width:992px) { + .col-md-5, .col-md-7, .col-md-12 { + float:left + } + .col-md-12 { + width:100% + } + .col-md-7 { + width:58.33333333% + } + .col-md-5 { + width:41.66666667% + } +} +@media (min-width:1200px) { + .col-lg-5, .col-lg-7, .col-lg-12 { + float:left + } + .col-lg-12 { + width:100% + } + .col-lg-7 { + width:58.33333333% + } + .col-lg-5 { + width:41.66666667% + } +} +table { + background-color:rgba(22, 24, 29, 0.7) +} +th { + text-align:left +} +.table { + width:100%; + max-width:100%; + margin-bottom:20px +} +.table>thead>tr>th { + padding:8px; + line-height:1.42857143; + vertical-align:top; + border-top:1px solid rgba(0, 0, 0, 0.7) +} +.table>thead>tr>th { + vertical-align:bottom; + border-bottom:2px solid rgba(0, 0, 0, 0.7) +} +.table>thead:first-child>tr:first-child>th { + border-top:0 +} +.table-condensed>thead>tr>th { + padding:5px +} +label { + display:inline-block; + max-width:100%; + margin-bottom:5px; + font-weight:bold +} +input[type="checkbox"] { + margin:4px 0 0; + margin-top:1px \9; + line-height:normal +} +select[multiple] { + height:auto +} +input[type="checkbox"]:focus { + outline:none; + /* outline:thin dotted; + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px */ +} +.form-control { + display:block; + width:100%; + height:38px; + padding:8px 12px; + font-size:14px; + line-height:1.42857143; + color:#34373a; + background-color:rgba(20, 22, 26, 0.7); + background-image:none; + border:1px solid #000000; + border-radius:0px; + -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition:border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s +} +.form-control:focus { + border-color:#646464; + outline:0; + -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(100, 100, 100, 0.6); + box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(100, 100, 100, 0.6) +} +.form-control::-moz-placeholder { + color:rgba(120, 126, 132, 0.85); + opacity: 1; +} +.form-control::-moz-placeholder, +.form-control:-ms-input-placeholder, +.form-control::-webkit-input-placeholder, +.form-control::placeholder { + color:rgba(120, 126, 132, 0.85); +} +textarea.form-control { + height:auto +} +.form-group { + margin-bottom:15px +} +.checkbox { + position:relative; + display:block; + margin-top:10px; + margin-bottom:10px +} +.checkbox label { + min-height:20px; + padding-left:20px; + margin-bottom:0; + font-weight:normal; + cursor:pointer +} +.checkbox input[type="checkbox"] { + position:absolute; + margin-left:-20px; + margin-top:4px \9 +} +@media (min-width:768px) { +.form-inline .form-group { +display:inline-block; +margin-bottom:0; +vertical-align:middle +} +.form-inline .form-control { +display:inline-block; +width:auto; +vertical-align:middle +} +.form-inline .input-group { +display:inline-table; +vertical-align:middle +} +.form-inline .input-group .input-group-btn, .form-inline .input-group .form-control { +width:auto +} +.form-inline .input-group>.form-control { +width:100% +} +.form-inline .checkbox { +display:inline-block; +margin-top:0; +margin-bottom:0; +vertical-align:middle +} +.form-inline .checkbox label { +padding-left:0 +} +.form-inline .checkbox input[type="checkbox"] { +position:relative; +margin-left:0 +} +} +.form-horizontal .checkbox { + margin-top:0; + margin-bottom:0; + padding-top:9px +} +.form-horizontal .checkbox { + min-height:29px +} +.form-horizontal .form-group { + margin-left:-15px; + margin-right:-15px +} +@media (min-width:768px) { +.form-horizontal .control-label { +text-align:right; +margin-bottom:0; +padding-top:9px +} +} +.btn { + display:inline-block; + margin-bottom:0; + font-weight:normal; + text-align:center; + vertical-align:middle; + -ms-touch-action:manipulation; + touch-action:manipulation; + cursor:pointer; + background-image:none; + white-space:nowrap; + padding:8px 12px; + font-size:14px; + line-height:1.42857143; + border-radius:0px; + -webkit-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none +} +.btn:focus, .btn:active:focus { + outline:none; + /* outline:thin dotted; + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px */ +} +.btn:hover, .btn:focus { + color:#ffffff; + text-decoration:none +} +.btn:active { + outline:none; + /* outline:0; + background-image:none; + -webkit-box-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125) */ +} +.btn[disabled] { + cursor:not-allowed; + pointer-events:none; + opacity:0.65; + filter:alpha(opacity=65); + -webkit-box-shadow:none; + box-shadow:none +} +.btn-default:hover, .btn-default:focus, .btn-default:active { + color:#ffffff; +} +.btn-default:active { + background-image:none +} +.btn-default[disabled], .btn-default[disabled]:hover, .btn-default[disabled]:focus, .btn-default[disabled]:active { + background-color:rgba(20, 22, 26, 0.7); + border-color:rgba(0, 0, 0, 0.7) +} +.btn-primary { + color:#ffffff; + background-color:rgba(120, 126, 132, 0.85); + border-color:rgba(0, 0, 0, 0.7) +} +.btn-primary:hover, .btn-primary:focus, .btn-primary:active { + color:#ffffff; + background-color:rgba(96, 101, 105, 0.85); + border-color:rgba(0, 0, 0, 0.7) +} +.btn-primary:active { + background-image:none +} +.btn-success { + color:#ffffff; + background-color:rgba(76, 174, 76, 0.75); + border-color:rgba(6, 14, 6, 0.75) +} +.btn-success:hover, .btn-success:focus, .btn-success:active { + color:#ffffff; + background-color:rgba(60, 139, 60, 0.75); + border-color:rgba(0, 0, 0, 0.75) +} +.btn-success:active { + background-image:none +} +.btn-info { + color:#ffffff; + background-color:rgba(78, 185, 217, 0.75); + border-color:rgba(7, 27, 33, 0.75) +} +.btn-info:hover, .btn-info:focus, .btn-info:active { + color:#ffffff; + background-color:rgba(43, 165, 201, 0.75); + border-color:rgba(0, 0, 0, 0.75) +} +.btn-info:active { + background-image:none +} +.btn-danger { + color:#ffffff; + background-color:rgba(238, 72, 67, 0.75); + border-color:rgba(46, 5, 4, 0.75) +} +.btn-danger:hover, .btn-danger:focus, .btn-danger:active { + color:#ffffff; + background-color:rgba(233, 27, 21, 0.75); + border-color:rgba(0, 0, 0, 0.75) +} +.btn-danger:active { + background-image:none +} +.btn-sm { + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:0px +} +.btn-xs { + padding:1px 5px; + font-size:12px; + line-height:1.5; + border-radius:0px +} +.fade { + opacity:0; + -webkit-transition:opacity 0.15s linear; + -o-transition:opacity 0.15s linear; + transition:opacity 0.15s linear +} +.collapse { + display:none; + visibility:hidden +} +.caret { + display:inline-block; + width:0; + height:0; + margin-left:2px; + vertical-align:middle; + border-top:4px solid; + border-right:4px solid transparent; + border-left:4px solid transparent +} +.dropdown { + position:relative +} +.dropdown-toggle:focus { + outline:0 +} +.dropdown-menu { + position:absolute; + top:100%; + left:0; + z-index:1000; + display:none; + float:left; + min-width:160px; + padding:5px 0; + margin:2px 0 0; + list-style:none; + font-size:14px; + text-align:left; + background-color:rgba(20, 22, 26, 0.96); + border:1px solid #34373a; + border:1px solid #000000; + border-radius:0px; + -webkit-box-shadow:0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow:0 6px 12px rgba(0, 0, 0, 0.175); + -webkit-background-clip:padding-box; + background-clip:padding-box +} +.dropdown-menu .divider { + height: 1px; + margin: 3px 0 0; + overflow: hidden; + background-color: rgba(0, 0, 0, 0.75) +} +.dropdown-menu>li>a { + display:block; + padding:3px 20px; + clear:both; + font-weight:normal; + line-height:1.42857143; + color:#c8c8c8; + white-space:nowrap +} +.dropdown-menu>li>a:hover, .dropdown-menu>li>a:focus { + text-decoration:none; + color:#ffffff; + background-color:rgba(0, 2, 4, 0.4) +} +.btn-group { + position:relative; + display:inline-block; + vertical-align:middle +} +.btn-group>.btn { + position:relative; + float:left +} +.btn-group>.btn:hover, .btn-group>.btn:focus, .btn-group>.btn:active { + z-index:2 +} +.btn-group .btn+.btn { + margin-left:-1px +} +.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { +border-radius:0 +} +.btn-group>.btn:first-child { + margin-left:0 +} +.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle) { +border-bottom-right-radius:0; +border-top-right-radius:0 +} +.btn-group>.btn:last-child:not(:first-child) { +border-bottom-left-radius:0; +border-top-left-radius:0 +} +.input-group { + position:relative; + display:table; + border-collapse:separate +} +.input-group .form-control { + position:relative; + z-index:2; + float:left; + width:100%; + margin-bottom:0 +} +.input-group-addon, .input-group-btn, .input-group .form-control { + display:table-cell +} +.input-group-btn:not(:first-child):not(:last-child), .input-group .form-control:not(:first-child):not(:last-child) { +border-radius:0 +} +.input-group-addon, .input-group-btn { + width:1%; + white-space:nowrap; + vertical-align:middle +} +.input-group-addon { + padding:8px 12px; + font-size:14px; + font-weight:normal; + line-height:1; + color:#34373a; + text-align:center; + background-color:rgba(70, 76, 82, 0.4); +} +.input-group .form-control:first-child, .input-group-addon:first-child, .input-group-btn:first-child>.btn, .input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle) { +border-bottom-right-radius:0; +border-top-right-radius:0 +} +.input-group-addon:first-child { + border-right:0 +} +.input-group .form-control:last-child, .input-group-btn:last-child>.btn { +border-bottom-left-radius:0; +border-top-left-radius:0 +} +.input-group-btn { + position:relative; + font-size:0; + white-space:nowrap +} +.input-group-btn>.btn { + position:relative +} +.input-group-btn>.btn+.btn { + margin-left:-1px +} +.input-group-btn>.btn:hover, .input-group-btn>.btn:focus, .input-group-btn>.btn:active { + z-index:2 +} +.input-group-btn:first-child>.btn { + margin-right:-1px +} +.input-group-btn:last-child>.btn { + margin-left:-1px +} +.nav { + margin-bottom:0; + padding-left:0; + list-style:none +} +.nav>li { + position:relative; + display:block +} +.nav>li>a { + position:relative; + display:block; + padding:10px 15px +} +.nav>li>a:hover, .nav>li>a:focus { + text-decoration:none; + background-color:rgba(7, 7, 9, 0.7) +} +.nav-tabs { + border-bottom:1px solid rgba(0, 0, 0, 0.7) +} +.nav-tabs>li { + float:left; + margin-bottom:-1px +} +.nav-tabs>li>a { + margin-right:2px; + line-height:1.42857143; + border:1px solid transparent; + border-radius:0px 0px 0 0 +} +.nav-tabs>li>a:hover { + border-color:rgba(0, 0, 0, 0.7) rgba(0, 0, 0, 0.7) rgba(0, 0, 0, 0.7) +} +.nav-tabs>li.active>a, .nav-tabs>li.active>a:hover, .nav-tabs>li.active>a:focus { + color:#ffffff; + background-color:rgba(20, 22, 26, 0.7); + border:1px solid rgba(0, 0, 0, 0.7); + border-bottom-color:transparent; + cursor:default +} +.tab-content>.tab-pane { + display:none; + visibility:hidden +} +.tab-content>.active { + display:block; + visibility:visible +} +.nav-tabs .dropdown-menu { + margin-top:-1px; + border-top-right-radius:0; + border-top-left-radius:0 +} +.navbar { + position: relative; + min-height: 0; + margin-bottom: 10px; + border: 1px solid transparent; +} +.navbar li a { + padding: 5px 15px !important; +} +.navbar-text { + margin-top: 0; + margin-bottom: 0; +} +@media (min-width:768px) { + .navbar { + border-radius:0px + } +} +@media (min-width:768px) { + .navbar-header { + float:left + } +} +.navbar-collapse { + overflow-x:visible; + padding-right:15px; + padding-left:15px; + border-top:1px solid transparent; + -webkit-overflow-scrolling:touch +} +@media (min-width:768px) { + .navbar-collapse { + width:auto; + border-top:0; + -webkit-box-shadow:none; + box-shadow:none + } + .navbar-collapse.collapse { + display:block !important; + visibility:visible !important; + height:auto !important; + padding-bottom:0; + overflow:visible !important + } +} +.navbar-brand { + float:left; + padding: 5px; + font-size: 15px; + line-height: 20px; + height: 20px; +} +.navbar-brand:hover, .navbar-brand:focus { + text-decoration:none +} +.navbar-nav { + margin:7.5px -15px +} +.navbar-nav>li>a { + padding-top:10px; + padding-bottom:10px; + line-height:20px +} +@media (min-width:768px) { + .navbar-nav { + float:left; + margin:0 + } + .navbar-nav>li { + float:left + } + .navbar-nav>li>a { + padding-top:15px; + padding-bottom:15px + } +} +.navbar-nav>li>.dropdown-menu { + margin-top:0; + border-top-right-radius:0; + border-top-left-radius:0 +} +.navbar-inverse { + background-color:rgba(20, 22, 26, 0.7); + border-color:rgba(0, 0, 0, 0.7) +} +.navbar-inverse .navbar-brand { + color:#c8c8c8 +} +.navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus { + color:#ffffff; + background-color:none +} +.navbar-inverse .navbar-nav>li>a { + color:#c8c8c8 +} +.navbar-inverse .navbar-nav>li>a:hover, .navbar-inverse .navbar-nav>li>a:focus { + color:#ffffff; + background-color:rgba(14, 16, 19, 0.7) +} +.navbar-inverse { + border-color:rgba(2, 2, 3, 0.7) +} +.navbar-inverse, .navbar-inverse { + background-color:rgba(7, 7, 9, 0.7) +} +.navbar-inverse .navbar-collapse { + border-color:rgba(4, 5, 6, 0.7) +} +.label { + display:inline; + padding:.2em .6em .3em; + font-size:75%; + font-weight:bold; + line-height:1; + color:#ffffff; + text-align:center; + white-space:nowrap; + vertical-align:baseline; + border-radius:.25em +} +.label-default { + background-color:#1e1e1e; +} +.label-success { + background-color: #377339; +} + +.embed-responsive { + position:relative; + display:block; + height:0; + padding:0; + overflow:hidden +} +.embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, .embed-responsive object, .embed-responsive video { + position:absolute; + top:0; + left:0; + bottom:0; + height:100%; + width:100%; + border:0 +} +.embed-responsive.embed-responsive-16by9 { + /* padding-bottom:56.25%; */ + padding-bottom:60%; + background-color: rgb(26, 29, 32); +} +.embed-responsive.embed-responsive-4by3 { + padding-bottom:75% +} +video { + background-color: #000; +} +.well { + min-height:20px; + padding:19px; + margin-bottom:20px; + background-color:rgba(30, 30, 30, 0.5) !important; + border:1px solid rgba(0, 0, 0, 0.5) !important; + border-radius:0px; + -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05) +} +.close { + float:right; + font-size:21px; + font-weight:bold; + line-height:1; + color:#000000; + text-shadow:0 1px 0 #ffffff; + opacity:0.2; + filter:alpha(opacity=20) +} +.close:hover, .close:focus { + color:#000000; + text-decoration:none; + cursor:pointer; + opacity:0.5; + filter:alpha(opacity=50) +} +button.close { + padding:0; + cursor:pointer; + background:transparent; + border:0; + -webkit-appearance:none +} +.modal { + display:none; + overflow:hidden; + position:fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index:1040; + -webkit-overflow-scrolling:touch; + outline:0 +} +.modal.fade .modal-dialog { + -webkit-transform:translate(0, -25%); + -ms-transform:translate(0, -25%); + -o-transform:translate(0, -25%); + transform:translate(0, -25%); + -webkit-transition:-webkit-transform .3s ease-out; + -o-transition:-o-transform .3s ease-out; + transition:transform .3s ease-out +} +.modal-dialog { + position:relative; + width:auto; + margin:10px +} +.modal-content { + position:relative; + background-color:#2a2d30; + border:1px solid #999999; + border:1px solid rgba(0, 0, 0, 0.7); + border-radius:0px; + -webkit-box-shadow:0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow:0 3px 9px rgba(0, 0, 0, 0.5); + -webkit-background-clip:padding-box; + background-clip:padding-box; + outline:0 +} +.modal-header { + padding:12px; + border-bottom:1px solid rgba(0, 0, 0, 0.7); + min-height:13.42857143px +} +.modal-header .close { + margin-top:-2px +} +.modal-body { + position:relative; + padding:16px +} +.modal-footer { + padding:16px; + text-align:right; + border-top:1px solid rgba(0, 0, 0, 0.7) +} +.modal-footer .btn+.btn { + margin-left:5px; + margin-bottom:0 +} +@media (min-width:768px) { + .modal-dialog { + width:600px; + margin:30px auto + } + .modal-content { + -webkit-box-shadow:0 5px 15px rgba(0, 0, 0, 0.5); + box-shadow:0 5px 15px rgba(0, 0, 0, 0.5) + } +} +.container:before, .container:after, .container-fluid:before, .container-fluid:after, .row:before, .row:after, .form-horizontal .form-group:before, .form-horizontal .form-group:after, .nav:before, .nav:after, .navbar:before, .navbar:after, .navbar-header:before, .navbar-header:after, .navbar-collapse:before, .navbar-collapse:after, .modal-footer:before, .modal-footer:after { + content:" "; + display:table +} +.container:after, .container-fluid:after, .row:after, .form-horizontal .form-group:after, .nav:after, .navbar:after, .navbar-header:after, .navbar-collapse:after, .modal-footer:after { + clear:both +} +.pull-right { + float:right !important +} +.pull-left { + float:left !important +} +@-ms-viewport { + width:device-width +} +.navbar { + border:1px solid rgba(0, 0, 0, 0.6); + text-shadow:1px 1px 1px rgba(0, 0, 0, 0.3) +} +.navbar-nav>li>a { + border-right:transparent; + border-left:transparent +} +.navbar-nav>li>a:hover { + border-left-color:transparent +} +.btn, .btn:hover { + border-top: 0; + background-color: #171A1D; + text-shadow:1px 1px 1px rgba(0, 0, 0, 0.3) +} +h3 { + text-shadow:2px 2px 0 rgba(0, 0, 0, 0.2) +} +h4 { + text-shadow:-1px 1px 2px rgba(0, 0, 0, 0.4) +} +.text-danger, .text-danger:hover { + color:rgba(238, 72, 67, 0.75) +} +.text-info, .text-info:hover { + color:rgba(78, 185, 217, 0.75) +} +.table { + border:1px solid rgba(0, 0, 0, 0.7) +} +input, textarea { + color:#34373a +} +.input-group-addon { + text-shadow:1px 1px 1px rgba(0, 0, 0, 0.3); + color:#ffffff +} +html, body { + /* font-family: 'Roboto', sans-serif!important; */ + font-family:Play !important; + font-weight: 300; +} +p, h3, h4 { + font-family: 'Roboto', sans-serif; + font-weight: 300; +} +hr { + border-color: #191919; +} +.navbar-inverse { + background-color: #141414; + border-color: #1e1e1e; +} +/* set the fixed height of the footer here */ +footer { + background-color: rgba(0, 0, 0, 0.44) !important; + border: 0 solid #080808; +} +body { + color: #bbbbbb; +} +a { + color: #eeeeee; +} +a:hover, a:focus { + color: #0099CC; + text-decoration: underline; +} +.modal-content { + background: #1E1E1E; +} +/* theme */ +.btn, .form-control, .well { + border-radius: 1px; + box-shadow: 0 0 0; +} +.btn { + border: 0px; +} +.btn-default { + background-color: #1A1D20; +} +.btn.active { + background-color: #0C0D0E; +} +/* .btn-default, .well { + background-color: #272727; + color: #bbbbbb; +} */ +/* .btn-default:focus { + background-color: rgb(39, 39, 39); + border-color: #0099CC; + border-top: 3px solid #0099CC; + color: #ffffff; + transition: all .3s ease-in-out; +} */ +/* .btn-success { + background-color: #669900; +} */ +.btn-success { + background-color: #377339; +} +.btn-success:hover { + background-color: #438D45; +} +.btn-danger { + background-color: #D33C3C; +} +.btn-danger:hover { + background-color: #FF4444; +} +.btn-success, .btn-danger { + margin-left: 0px !important; +} +.nav-tabs > li > a { + border-radius: 0; +} +.navbar-nav > li > .dropdown-menu { + border-radius: 0; +} +.dropdown-menu { + background-color: #080808; + color: #bbbbbb; +} +.dropdown-menu > li > a { + color: #bbbbbb; +} +.dropdown-menu > li > a:hover { + color: #bbbbbb; + background-color: #1e1e1e; +} +.dropdown-menu .divider { + background-color: #333333; +} +.nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus { + color: #cccccc; + cursor: default; + background-color: #080808; + border: 1px solid #080808; +} +.nav-tabs > li > a:hover { + border-color: #171717; + border-bottom-color: #1e1e1e; +} +.nav > li > a:hover, .nav > li > a:focus { + text-decoration: none; + background-color: #171717; + color: #cccccc; +} +.nav-tabs { + border-bottom: 1px solid #080808; +} +.modal-footer { + border-top: 1px solid #080808; +} +#chatheader, #currenttitle, #userlist, #messagebuffer, .queue_entry { + border-radius: 0px; + border-color: #1e1e1e; + background-color: #171A1D; +} +#plmeta, #userlist, #messagebuffer, .queue_entry { + background-color: #1A1D20; +} +#userlist { + text-shadow: 1px 1px 1px #000000; + border-left: 0; + border-right: 1px solid #171A1D; +} +.form-control, input.form-control[type="text"], textarea.form-control { + background-color: rgba(0, 0, 0, 0.7) !important; + color: #c8c8c8; + border-color: #1e1e1e; +} +select option { + background-color: #171717; + color: #cccccc; +} +.form-control:focus { + border-color: #5E9ACA !important; + box-shadow: none; +} +#mainpage { + padding-top: 0; +} +#wrap { + min-height: 100%; + height: auto; + margin: 0 auto -60px; + padding: 0 0 60px; +} +#messagebuffer, #userlist, #queue { + overflow-y: auto !important; +} +#currenttitle { + text-align: center; +} +.queue_entry { + background-color: #16191C; + border-color: #2c2e31; +} +.queue_entry.queue_active { + background-color: #1f2224; + border-color: #2c2e31; +} diff --git a/res/css/mobile-view.css b/res/css/mobile-view.css new file mode 100644 index 0000000..a23b963 --- /dev/null +++ b/res/css/mobile-view.css @@ -0,0 +1,81 @@ +/* Mobile view */ + +.mobile-view .navbar { + display: none; +} +.mobile-view #mainpage { + padding-top: 0; +} +.mobile-view #messagebuffer :nth-child(n+5):last-child { + margin-bottom: 0; +} +.mobile-view #chatwrap { + padding-left: 0; + padding-right: 0; + margin-bottom: 0; +} +.mobile-view #videowrap { + padding-left: 0; + padding-right: 0; +} +.mobile-view #currenttitle { + display: inline; + background-color: transparent; + text-align: center; + border: none; +} +.mobile-view #plcontrol { + display: none; +} +.mobile-view #leftcontrols { + text-align: center; +} +.mobile-view #rightcontrols { + text-align: center; +} +.mobile-view #videocontrols { + float: none !important; + width: 100%; +} +.mobile-view #extendplayer { + display: none; +} +.mobile-view #videocontrols .btn { + width: 20%; +} +/* .mobile-view .btn-xs { + padding: 4px 5px; +} */ +.mobile-view #rightpane-inner { + padding-left: 0; + padding-right: 0; +} +.mobile-view #wrap { + padding-bottom: 0; + margin-bottom: 0; +} +.mobile-view #footer { + display: none; +} + +@media (max-width:799px) { + .navbar { + /* display: none; */ + border-color: #272b30 !important; + background: rgb(39, 43, 48) !important; + margin-bottom: 0; + } + #chatwrap { + width: 100% !important; + margin-bottom: 0; + } + #videowrap { + width: 100% !important; + } + .gutter.gutter-horizontal { + display: none; + } + #currenttitle { + display: none; + } +} diff --git a/res/css/sticky-footer-navbar.css b/res/css/sticky-footer-navbar.css new file mode 100644 index 0000000..249aa42 --- /dev/null +++ b/res/css/sticky-footer-navbar.css @@ -0,0 +1,26 @@ +html, +body { + height: 100%; + /* The html and body elements cannot have any padding or margin. */ +} + +/* Wrapper for page content to push down footer */ +#wrap { + min-height: 100%; + height: auto; + /* Negative indent footer by its height */ + margin: 0 auto -60px; + /* Pad bottom by footer height */ + padding: 0 0 60px; +} + +/* Set the fixed height of the footer here */ +#footer { + min-height: 60px; + background-color: #f5f5f5; +} + +.container .credit { + margin: 20px 0; + text-align: center; +} diff --git a/res/img/favicon.png b/res/img/favicon.png new file mode 100755 index 0000000..5d46d1c Binary files /dev/null and b/res/img/favicon.png differ diff --git a/res/img/stripe-diagonal.png b/res/img/stripe-diagonal.png new file mode 100755 index 0000000..ae44b91 Binary files /dev/null and b/res/img/stripe-diagonal.png differ diff --git a/res/img/vertical.png b/res/img/vertical.png new file mode 100644 index 0000000..0ac8fa1 Binary files /dev/null and b/res/img/vertical.png differ diff --git a/res/index.html b/res/index.html new file mode 100644 index 0000000..b174733 --- /dev/null +++ b/res/index.html @@ -0,0 +1,208 @@ + + + + + + + SyncTube + + + + + + + + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + ${connection}... + ${mobileViewBtn} + ${leader} +
    +
    +
    + + +
    +
    +
    +

    ${nothingPlaying}

    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + + + + + + + +
    +
    + + + + + + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + + + +
    +
    +
    +
    +
      +
      0 ${videos}00:00
      +
      +
      +
      +
      +
      +
      +
      +
      + +
      +
      +
      +
      +
      +
      +

      Based on CyTube by Calvin Montgomery

      +
      +
      + + + + + + diff --git a/res/langs/en.json b/res/langs/en.json new file mode 100644 index 0000000..5181f5f --- /dev/null +++ b/res/langs/en.json @@ -0,0 +1,67 @@ +{ + "connection": "Connection", + "msgConnected": "Connected", + "msgDisconnected": "Disconnected", + "joined": "joined", + "online": "online", + "nothingPlaying": "Nothing Playing", + "usernameError": "Username must be from 1 to 20 characters and don't repeat another's.", + "rawVideo": "Raw video", + "videos": "videos", + "addedBy": "Added by", + "play": "Play", + "skip": "Skip", + "makePermanent": "Make Permanent", + "delete": "Delete", + "account": "Account", + "exportSettings": "Export Settings", + "importSettings": "Import Settings", + "exit": "Exit", + "settings": "Settings", + "channel": "Channel", + "layout": "Layout", + "chatOnly": "Chat Only", + "removeVideo": "Remove Video", + "toggleUserList": "Show/Hide Userlist", + "mobileViewBtn": "Mobile View", + "leader": "Leader", + "enterAsGuest": "Enter As Guest", + "yourName": "Your Name", + "emotes": "Emotes", + "clearChat": "Clear Chat", + "uploadImage": "Upload Image", + "searchVideo": "Search video", + "addVideoFromUrl": "Add video from URL", + "embedCustomFrame": "Embed a custom frame", + "managePlaylists": "Manage playlists", + "clearPlaylist": "Clear playlist", + "shufflePlaylist": "Shuffle playlist", + "playlistOpen": "Playlist open", + "playlistLocked": "Playlist locked", + "expandPlayer": "Expand player", + "toggleVideoSync": "Toggle video synchronization", + "refreshPlayer": "Refresh player", + "fullscreenPlayer": "Fullscreen player", + "retrievePlaylistLinks": "Retrieve playlist links", + "voteForSkip": "Vote for skip", + "searchQuery": "Search query", + "addAsTemporary": "Add as temporary", + "mediaUrl": "Media URL", + "optionalTitle": "Title (optional)", + "queueNext": "Queue next", + "queueLast": "Queue last", + "or": "or", + "pasteEmbedCodeAndClick": "Paste the embed code below and click", + "acceptableEmbedCodesAre": "Acceptable embed codes are", + "customEmbedsCannotBeSynchronized": "CUSTOM EMBEDS CANNOT BE SYNCHRONIZED", + "playlistName": "Playlist Name", + "save": "Save", + + "yes": "Yes", + "no": "No", + "on": "On", + "off": "Off", + + "areYouSure": "Are you sure?", + "dataWillBeLost": "The data will be lost." +} diff --git a/res/langs/ru.json b/res/langs/ru.json new file mode 100644 index 0000000..d4c4e85 --- /dev/null +++ b/res/langs/ru.json @@ -0,0 +1,67 @@ +{ + "connection": "Подключение", + "msgConnected": "Соединение установлено", + "msgDisconnected": "Соединение потеряно", + "joined": "вошел", + "online": "онлайн", + "nothingPlaying": "Ничего не играет", + "usernameError": "Ник должен быть от 1 до 20 символов и не повторять чужие.", + "rawVideo": "Исходное видео", + "videos": "видео", + "addedBy": "Добавлено", + "play": "Воспроизвести", + "skip": "Пропустить", + "makePermanent": "Закрепить", + "delete": "Удалить", + "account": "Аккаунт", + "exportSettings": "Экспорт настроек", + "importSettings": "Импорт настроек", + "exit": "Выход", + "settings": "Настройки", + "channel": "Канал", + "layout": "Разметка", + "chatOnly": "Только чат", + "removeVideo": "Убрать видео", + "toggleUserList": "Показать/Скрыть список юзеров", + "mobileViewBtn": "Моб. вид", + "leader": "Лидер", + "enterAsGuest": "Войти как гость:", + "yourName": "Ваш ник", + "emotes": "Смайлы", + "clearChat": "Очистить", + "uploadImage": "Загрузить картинку", + "searchVideo": "Найти видео", + "addVideoFromUrl": "Добавить видео по ссылке", + "embedCustomFrame": "Добавить iframe", + "managePlaylists": "Менеджер плейлистов", + "clearPlaylist": "Очистить плейлист", + "shufflePlaylist": "Перемешать плейлист", + "playlistOpen": "Плейлист открыт", + "playlistLocked": "Плейлист заблокирован", + "expandPlayer": "Расширить плеер", + "toggleVideoSync": "Переключить синхронизацию видео", + "refreshPlayer": "Обновить плеер", + "fullscreenPlayer": "Полноэкранный режим видео", + "retrievePlaylistLinks": "Получить ссылки на видео из плейлиста", + "voteForSkip": "Голосовать за пропуск", + "searchQuery": "Что ищем?", + "addAsTemporary": "Добавить как временный", + "mediaUrl": "Ссылка на видео", + "optionalTitle": "Заголовок (необязательно)", + "queueNext": "След.", + "queueLast": "В конец", + "or": "или", + "pasteEmbedCodeAndClick": "Вставьте код видео в поле ниже и нажмите", + "acceptableEmbedCodesAre": "Можно добавить видео с тегами", + "customEmbedsCannotBeSynchronized": "СИНХРОНИЗАЦИЯ БУДЕТ НЕДОСТУПНА", + "playlistName": "Название плейлиста", + "save": "Сохранить", + + "yes": "Да", + "no": "Нет", + "on": "Вкл.", + "off": "Откл.", + + "areYouSure": "Вы уверены?", + "dataWillBeLost": "Данные будут потеряны." +} diff --git a/src/Client.hx b/src/Client.hx new file mode 100644 index 0000000..e44417a --- /dev/null +++ b/src/Client.hx @@ -0,0 +1,37 @@ +package; + +#if nodejs +import js.npm.ws.WebSocket; +#elseif js +import js.html.WebSocket; +#end + +typedef ClientData = { + name:String, + isLeader:Bool +} + +class Client { + + public final ws:WebSocket; + public var name:String; + public var isLeader:Bool; + + public function new(?ws:WebSocket, name:String, isLeader = false) { + this.ws = ws; + this.name = name; + this.isLeader = isLeader; + } + + public function getData():ClientData { + return { + name: name, + isLeader: isLeader + } + } + + public static function fromData(data:ClientData):Client { + return new Client(data.name, data.isLeader); + } + +} diff --git a/src/ClientTools.hx b/src/ClientTools.hx new file mode 100644 index 0000000..4503ada --- /dev/null +++ b/src/ClientTools.hx @@ -0,0 +1,26 @@ +package; + +class ClientTools { + + public static function setLeader(clients:Array, name:String):Void { + for (client in clients) { + if (client.name == name) client.isLeader = true; + else if (client.isLeader) client.isLeader = false; + } + } + + public static function hasLeader(clients:Array):Bool { + for (client in clients) { + if (client.isLeader) return true; + } + return false; + } + + public static function getByName(clients:Array, name:String):Null { + for (client in clients) { + if (client.name == name) return client; + } + return null; + } + +} diff --git a/src/Lang.hx b/src/Lang.hx new file mode 100644 index 0000000..8632812 --- /dev/null +++ b/src/Lang.hx @@ -0,0 +1,62 @@ +package; + +import haxe.Json; +import haxe.io.Path; +#if (sys || nodejs) +import sys.io.File; +#else +import haxe.Http; +#end + +private typedef LangMap = Map; + +class Lang { + + static final ids = ["en", "ru"]; + static final langs:Map = []; + + static function request(path:String, callback:(data:String)->Void):Void { + #if (sys || nodejs) + callback(File.getContent(path)); + #else + final http = new Http(path); + http.onData = callback; + http.request(); + #end + } + + public static function init(folderPath:String, ?callback:()->Void):Void { + langs.clear(); + var count = 0; + for (name in ids) { + request('$folderPath/$name.json', data -> { + final data = Json.parse(data); + final lang = new LangMap(); + for (key in Reflect.fields(data)) { + lang[key] = Reflect.field(data, key); + } + final id = Path.withoutExtension(name); + langs[id] = lang; + count++; + if (count == ids.length && callback != null) callback(); + }); + } + } + + #if (sys || nodejs) + public static function get(lang:String, ?key:String):String { + if (langs[lang] == null) lang = "en"; + final text = langs[lang][key]; + return text == null ? key : text; + } + #else + static var lang = js.Browser.navigator.language.substr(0, 2).toLowerCase(); + + public static function get(key:String):String { + if (langs[lang] == null) lang = "en"; + final text = langs[lang][key]; + return text == null ? key : text; + } + #end + +} diff --git a/src/Types.hx b/src/Types.hx new file mode 100644 index 0000000..3d4ac4f --- /dev/null +++ b/src/Types.hx @@ -0,0 +1,77 @@ +package; + +import Client.ClientData; + +typedef VideoItem = { + url:String, + title:String, + author:String, + duration:Float +} + +typedef WsEvent = { + type:WsEventType, + ?connected:{ + clients:Array, + isUnknownClient:Bool, + clientName:String, + videoList:Array + }, + ?login:{ + clientName:String, + ?clients:Array, + ?isUnknownClient:Bool, + }, + ?logout:{ + clientName:String, + clients:Array, + }, + ?message:{ + clientName:String, + text:String + }, + ?updateClients:{ + clients:Array, + }, + ?addVideo:{ + item:VideoItem + }, + ?removeVideo:{ + url:String + }, + ?pause:{ + time:Float + }, + ?play:{ + time:Float + }, + ?getTime:{ + time:Float, + paused:Bool + }, + ?setTime:{ + time:Float + }, + ?setLeader:{ + clientName:String + } +} + +enum abstract WsEventType(String) { + var Connected; + var Login; + var LoginError; + var Logout; + var Message; + var UpdateClients; + // var AddClient; + // var RemoveClient; + var AddVideo; + var RemoveVideo; + var VideoLoaded; + var Pause; + var Play; + var GetTime; + var SetTime; + var SetLeader; +} 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 = []; + final personalHistory:Array = []; + var personal:Null; + 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, 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):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(''); + list.add('${client.name}
      '); + } + 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 = []; + 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( + '
    • + ${item.title} + ${duration(item.duration)} +
      +
      + + + + +
      +
    • ' + ); + 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; + } + +} diff --git a/src/server/HttpServer.hx b/src/server/HttpServer.hx new file mode 100644 index 0000000..b49301b --- /dev/null +++ b/src/server/HttpServer.hx @@ -0,0 +1,106 @@ +package server; + +import js.node.Buffer; +import haxe.io.Path; +import js.node.Fs; +import sys.io.File; +import js.node.http.IncomingMessage; +import js.node.http.ServerResponse; +import js.Node.__dirname; +import js.node.Path as JsPath; +using StringTools; + +class HttpServer { + + static final mimeTypes = [ + "html" => "text/html", + "js" => "text/javascript", + "css" => "text/css", + "json" => "application/json", + "png" => "image/png", + "jpg" => "image/jpg", + "gif" => "image/gif", + "svg" => "image/svg+xml", + "ico" => "image/x-icon", + "wav" => "audio/wav", + "mp3" => "audio/mpeg", + "mp4" => "video/mp4", + "woff" => "application/font-woff", + "ttf" => "application/font-ttf", + "eot" => "application/vnd.ms-fontobject", + "otf" => "application/font-otf", + "wasm" => "application/wasm" + ]; + + static var dir:String; + + public static function init(directory:String):Void { + dir = directory; + } + + public static function serveFiles(req:IncomingMessage, res:ServerResponse):Void { + var filePath = dir + req.url; + if (req.url == "/") filePath = '$dir/index.html'; + + final extension = Path.extension(filePath).toLowerCase(); + final contentType = getMimeType(extension); + + if (!isChildOf(dir, filePath)) { + res.statusCode = 500; + var rel = JsPath.relative(dir, filePath); + res.end('Error getting the file: No access to $rel.'); + return; + } + + // load client code from build folder + if (filePath == '$dir/client.js') { + filePath = '$__dirname/client.js'; + } + + Fs.readFile(filePath, function(err:Dynamic, data:Buffer) { + if (err != null) { + if (err.code == "ENOENT") { + res.statusCode = 404; + var rel = JsPath.relative(dir, filePath); + res.end('File $rel not found.'); + } else { + res.statusCode = 500; + res.end('Error getting the file: $err.'); + } + return; + } + res.setHeader("Content-Type", contentType); + if (extension == "html") { + // replace ${textId} to localized strings + data = cast localizeHtml(data.toString(), req.headers["accept-language"]); + } + res.end(data); + }); + } + + static final matchLang = ~/^[A-z]+/; + + static function localizeHtml(data:String, lang:String):String { + if (lang != null && matchLang.match(lang)) { + lang = matchLang.matched(0); + } else lang = "en"; + data = ~/\${([A-z_]+)}/g.map(data, (regExp) -> { + final key = regExp.matched(1); + return Lang.get(lang, key); + }); + return data; + } + + static function isChildOf(parent:String, child:String):Bool { + final path = JsPath; + final relative = path.relative(parent, child); + return relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative); + } + + static function getMimeType(ext:String):String { + var contentType = mimeTypes[ext]; + if (contentType == null) contentType = "application/octet-stream"; + return contentType; + } + +} diff --git a/src/server/Main.hx b/src/server/Main.hx new file mode 100644 index 0000000..8ec7e87 --- /dev/null +++ b/src/server/Main.hx @@ -0,0 +1,251 @@ +package server; + +import haxe.Timer; +import Client.ClientData; +import haxe.Json; +import js.Node.process; +import js.Node.__dirname; +import js.npm.ws.Server as WSServer; +import js.npm.ws.WebSocket; +import js.node.Http; +import js.node.Dns; +import Types; +using ClientTools; +using Lambda; + +class Main { + + final wss:WSServer; + final clients:Array = []; + final videoList:Array = []; + final videoTimer = new VideoTimer(); + + static function main():Void new Main(); + + public function new(port = 4200, wsPort = 4201) { + wss = new WSServer({port: wsPort}); + wss.on("connection", onConnect); + function exit() { + process.exit(); + } + process.on("exit", exit); + process.on("SIGINT", exit); // ctrl+c + process.on("uncaughtException", (log) -> { + trace(log); + }); + process.on("unhandledRejection", (reason, promise) -> { + trace("Unhandled Rejection at:", reason); + }); + + getPublicIp(ip -> { + trace('Local: http://127.0.0.1:$port'); + trace('Global: http://$ip:$port'); + }); + + final dir = '$__dirname/../res'; + HttpServer.init(dir); + Lang.init('$dir/langs'); + + Http.createServer((req, res) -> { + HttpServer.serveFiles(req, res); + }).listen(port); + } + + function getPublicIp(callback:(ip:String)->Void):Void { + Dns.resolve("google.com", function(err, arr) { + if (err != null) { + callback("ERROR " + err.code); + return; + } + Http.get("http://myexternalip.com/raw", r -> { + r.setEncoding("utf8"); + r.on("data", callback); + }); + }); + } + + function onConnect(ws:WebSocket, req):Void { + final ip = req.connection.remoteAddress; + trace('Client connected ($ip)'); + final client = new Client(ws, "Unknown", false); + clients.push(client); + + send(client, { + type: Connected, + connected: { + isUnknownClient: true, + clientName: client.name, + clients: [ + for (client in clients) client.getData() + ], + videoList: videoList + } + }); + sendClientList(); + + ws.on("message", data -> { + onMessage(client, Json.parse(data)); + }); + ws.on("close", err -> { + trace('Client ${client.name} disconnected'); + clients.remove(client); + sendClientList(); + if (client.isLeader) { + if (videoTimer.isPaused()) videoTimer.play(); + } + }); + } + + function onMessage(client:Client, data:WsEvent):Void { + switch (data.type) { + case Connected: + case UpdateClients: + sendClientList(); + case Login: + final name = data.login.clientName; + if (name.length == 0 || name.length > 20 || clients.getByName(name) != null) { + send(client, {type: LoginError}); + return; + } + client.name = data.login.clientName; + send(client, { + type: data.type, + login: { + isUnknownClient: true, + clientName: client.name, + clients: clientList() + } + }); + sendClientList(); + case LoginError: + case Logout: + final oldName = client.name; + client.name = "Unknown"; + send(client, { + type: data.type, + logout: { + clientName: oldName, + clients: clientList() + } + }); + sendClientList(); + case Message: + // todo message log, max items + // todo message max length check + data.message.clientName = client.name; + broadcast(data); + case AddVideo: + videoList.push(data.addVideo.item); + broadcast(data); + if (videoList.length == 1) { + waitVideoStart = Timer.delay(startVideoPlayback, 3000); + } + case VideoLoaded: + prepareVideoPlayback(); + case RemoveVideo: + if (videoList.length == 0) return; + final url = data.removeVideo.url; + if (videoList[0].url == url) videoTimer.stop(); + videoList.remove( + videoList.find(item -> item.url == url) + ); + broadcast(data); + case Pause: + if (videoList.length == 0) return; + if (!client.isLeader) return; + videoTimer.pause(); + broadcast(data); + case Play: + if (videoList.length == 0) return; + if (!client.isLeader) return; + videoTimer.play(); + broadcast(data); + case GetTime: + if (videoList.length == 0) return; + if (videoTimer.getTime() > videoList[0].duration) { + videoTimer.stop(); + onMessage(client, { + type: RemoveVideo, + removeVideo: { + url: videoList[0].url + } + }); + return; + } + send(client, { + type: GetTime, getTime: { + time: videoTimer.getTime(), + paused: videoTimer.isPaused() + }}); + case SetTime: + if (videoList.length == 0) return; + if (!client.isLeader) return; + videoTimer.setTime(data.setTime.time); + broadcastExcept(client, data); + case SetLeader: + clients.setLeader(data.setLeader.clientName); + sendClientList(); + if (videoList.length == 0) return; + if (!clients.hasLeader()) { + if (videoTimer.isPaused()) videoTimer.play(); + broadcast({ + type: Play, play: { + time: videoTimer.getTime() + } + }); + } + } + } + + function clientList():Array { + return [ + for (client in clients) client.getData() + ]; + } + + function sendClientList():Void { + broadcast({ + type: UpdateClients, + updateClients: { + clients: clientList() + } + }); + } + + function send(client:Client, data:WsEvent):Void { + client.ws.send(Json.stringify(data), null); + } + + function broadcast(data:WsEvent):Void { + final json = Json.stringify(data); + for (client in clients) client.ws.send(json, null); + } + + function broadcastExcept(skipped:Client, data:WsEvent):Void { + final json = Json.stringify(data); + for (client in clients) { + if (client == skipped) continue; + client.ws.send(json, null); + } + } + + var waitVideoStart:Timer; + var loadedClientsCount = 0; + + function prepareVideoPlayback():Void { + if (videoTimer.isStarted) return; + loadedClientsCount++; + if (loadedClientsCount == 1) { + waitVideoStart = Timer.delay(startVideoPlayback, 3000); + } + if (loadedClientsCount >= clients.length) startVideoPlayback(); + } + + function startVideoPlayback():Void { + if (waitVideoStart != null) waitVideoStart.stop(); + loadedClientsCount = 0; + broadcast({type: VideoLoaded}); + videoTimer.start(); + } + +} diff --git a/src/server/VideoTimer.hx b/src/server/VideoTimer.hx new file mode 100644 index 0000000..695ea5d --- /dev/null +++ b/src/server/VideoTimer.hx @@ -0,0 +1,54 @@ +package server; + +import haxe.Timer; + +class VideoTimer { + + public var isStarted(default, null) = false; + var startTime = 0.0; + var pauseStartTime = 0.0; + + public function new() {} + + public function start():Void { + isStarted = true; + startTime = Timer.stamp(); + pauseStartTime = 0; + } + + public function stop():Void { + isStarted = false; + startTime = 0.0; + pauseStartTime = 0.0; + } + + public function pause():Void { + pauseStartTime = Timer.stamp(); + } + + public function play():Void { + if (!isStarted) start(); + startTime += pauseTime(); + pauseStartTime = 0; + } + + public function getTime():Float { + if (startTime == 0) return 0; + return Timer.stamp() - startTime - pauseTime(); + } + + public function setTime(secs:Float):Void { + startTime = Timer.stamp() - secs; + if (isPaused()) pause(); + } + + public function isPaused():Bool { + return pauseStartTime != 0; + } + + function pauseTime():Float { + if (!isPaused()) return 0; + return Timer.stamp() - pauseStartTime; + } + +} -- cgit v1.2.3