aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.vscode/launch.json12
-rw-r--r--.vscode/settings.json9
-rw-r--r--.vscode/tasks.json22
-rw-r--r--README.md24
-rw-r--r--build-all.hxml7
-rw-r--r--build-client.hxml5
-rw-r--r--build-server.hxml7
-rw-r--r--build/client.js1240
-rw-r--r--build/server.js933
-rw-r--r--package-lock.json13
-rw-r--r--package.json19
-rw-r--r--res/css/custom.css0
-rw-r--r--res/css/cytube.css725
-rw-r--r--res/css/des.css1350
-rw-r--r--res/css/mobile-view.css81
-rw-r--r--res/css/sticky-footer-navbar.css26
-rwxr-xr-xres/img/favicon.pngbin0 -> 171 bytes
-rwxr-xr-xres/img/stripe-diagonal.pngbin0 -> 206 bytes
-rw-r--r--res/img/vertical.pngbin0 -> 91 bytes
-rw-r--r--res/index.html208
-rw-r--r--res/langs/en.json67
-rw-r--r--res/langs/ru.json67
-rw-r--r--src/Client.hx37
-rw-r--r--src/ClientTools.hx26
-rw-r--r--src/Lang.hx62
-rw-r--r--src/Types.hx77
-rw-r--r--src/client/Main.hx334
-rw-r--r--src/client/MobileView.hx52
-rw-r--r--src/client/Player.hx182
-rw-r--r--src/server/HttpServer.hx106
-rw-r--r--src/server/Main.hx251
-rw-r--r--src/server/VideoTimer.hx54
33 files changed, 5998 insertions, 0 deletions
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 += "<span class=\"glyphicon glyphicon-star-empty\"></span>";
+ }
+ list_b += Std.string("<span class=\"" + "userlist_item" + "\">" + client1.name + "</span></br>");
+ }
+ 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("<li class=\"queue_entry pluid-0 queue_temp queue_active\" title=\"" + Lang.get("addedBy") + ": " + item.author + "\">\n\t\t\t\t<a class=\"qe_title\" href=\"" + item.url + "\" target=\"_blank\">" + item.title + "</a>\n\t\t\t\t<span class=\"qe_time\">" + this.duration(item.duration) + "</span>\n\t\t\t\t<div class=\"qe_clear\"></div>\n\t\t\t\t<div class=\"btn-group\" style=\"display: inline-block;\">\n\t\t\t\t\t<button class=\"btn btn-xs btn-default qbtn-play\">\n\t\t\t\t\t\t<span class=\"glyphicon glyphicon-play\"></span>" + Lang.get("play") + "\n\t\t\t\t\t</button>\n\t\t\t\t\t<button class=\"btn btn-xs btn-default qbtn-next\">\n\t\t\t\t\t\t<span class=\"glyphicon glyphicon-share-alt\"></span>" + Lang.get("skip") + "\n\t\t\t\t\t</button>\n\t\t\t\t\t<button class=\"btn btn-xs btn-default qbtn-tmp\">\n\t\t\t\t\t\t<span class=\"glyphicon glyphicon-flag\"></span>" + Lang.get("makePermanent") + "\n\t\t\t\t\t</button>\n\t\t\t\t\t<button class=\"btn btn-xs btn-default qbtn-delete\" id=\"btn-delete\">\n\t\t\t\t\t\t<span class=\"glyphicon glyphicon-trash\"></span>" + Lang.get("delete") + "\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</li>");
+ 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 "<function>";
+ 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 "<function>";
+ 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
--- /dev/null
+++ b/res/css/custom.css
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
--- /dev/null
+++ b/res/img/favicon.png
Binary files differ
diff --git a/res/img/stripe-diagonal.png b/res/img/stripe-diagonal.png
new file mode 100755
index 0000000..ae44b91
--- /dev/null
+++ b/res/img/stripe-diagonal.png
Binary files differ
diff --git a/res/img/vertical.png b/res/img/vertical.png
new file mode 100644
index 0000000..0ac8fa1
--- /dev/null
+++ b/res/img/vertical.png
Binary files 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 @@
+<!DOCTYPE html>
+<html lang="ru"><head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <style class="vjs-styles-defaults">
+ .video-js {
+ width: 300px;
+ height: 150px;
+ }
+ .vjs-fluid {
+ padding-top: 56.25%
+ }
+ </style>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>SyncTube</title>
+ <link rel="icon" type="image/png" href="img/favicon.png" />
+ <link href="css/sticky-footer-navbar.css" rel="stylesheet">
+ <link href="css/cytube.css" rel="stylesheet">
+ <link href="css/mobile-view.css" rel="stylesheet">
+ <link id="usertheme" href="css/des.css" rel="stylesheet">
+ <link id="customcss" href="css/custom.css" rel="stylesheet">
+</head>
+ <body class="fluid">
+ <div id="wrap">
+ <nav class="navbar navbar-inverse" role="navigation">
+ <div class="navbar-header">
+ <a class="navbar-brand" href="#">SyncTube</a>
+ </div>
+ <div class="collapse navbar-collapse" id="nav-collapsible">
+ <ul class="nav navbar-nav">
+ <li class="dropdown"><a class="dropdown-toggle" href="#" data-toggle="dropdown">${account}<b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="#">${exportSettings}</a></li>
+ <li><a href="#">${importSettings}</a></li>
+ <li class="divider"></li>
+ <li><a href="#">${exit}</a></li>
+ </ul>
+ </li>
+ <li><a href="javascript:void(0)" onclick="javascript:showUserOptions()">${settings}</a></li>
+ <li><a id="showchansettings" href="javascript:void(0)" onclick="javascript:showChannelSettings()">${channel}</a></li>
+ <li class="dropdown"><a class="dropdown-toggle" href="#" data-toggle="dropdown">${layout}<b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="#" onclick="javascript:chatOnly()">${chatOnly}</a></li>
+ <li><a href="#" onclick="javascript:removeVideo(event)">${removeVideo}</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </nav>
+ <section id="mainpage">
+ <div class="container-fluid">
+ <div class="row" id="motdrow">
+ <div class="col-lg-12 col-md-12">
+ <div class="well" id="motdwrap" style="display: none;">
+ <button class="close pull-right" id="togglemotd" type="button"><span class="glyphicon glyphicon-minus"></span></button>
+ <div id="motd"></div>
+ <div class="clear"></div>
+ </div>
+ </div>
+ </div>
+ <div class="row" id="announcements"></div>
+ <div class="row" id="main">
+ <div class="split split-horizontal" id="chatwrap" style="width: calc(40% - 5px);">
+ <div id="chatheader"><i class="glyphicon glyphicon-chevron-down pull-left pointer" id="userlisttoggle" title="${toggleUserList}"></i>
+ <span class="pointer" id="usercount">${connection}...</span>
+ <span class="label label-default pull-right pointer" id="mv_btn">${mobileViewBtn}</span>
+ <span class="label label-default pull-right pointer" id="leader_btn">${leader}</span>
+ </div>
+ <div id="userlist" style="height: 389px;"></div>
+ <div class="linewrap" id="messagebuffer" style="height: 389px;"></div>
+ <input class="form-control" id="chatline" type="text" maxlength="500">
+ <div class="input-group" id="guestlogin" style="display: none;"><span class="input-group-addon">${enterAsGuest}</span>
+ <input class="form-control" id="guestname" type="text" placeholder="${yourName}">
+ </div>
+ </div>
+ <div class="gutter gutter-horizontal" style="width: 10px;"></div>
+ <div class="split split-horizontal" id="videowrap" style="width: calc(60% - 5px);">
+ <p id="currenttitle">${nothingPlaying}</p>
+ <div id="ytapiplayer" class="embed-responsive embed-responsive-16by9"></div>
+ </div>
+ <div class="clear"></div>
+ </div>
+ <div class="row" id="controlsrow">
+ <div class="col-lg-5 col-md-5" id="leftcontrols">
+ <button class="btn btn-sm btn-default" id="clearchatbtn">${clearChat}</button>
+ <button class="btn btn-sm btn-default" id="smilesbtn">${emotes}</button>
+ <button id="image-btn" class="btn btn-sm btn-default">${uploadImage}</button></div>
+ <div class="col-lg-7 col-md-7" id="rightcontrols">
+ <div class="btn-group" id="plcontrol">
+ <button class="btn btn-sm btn-default collapsed" id="showsearch" title="${searchVideo}" data-toggle="collapse" data-target="#searchcontrol" aria-expanded="false"><span class="glyphicon glyphicon-search"></span></button>
+ <button class="btn btn-sm btn-default collapsed" id="showmediaurl" title="${addVideoFromUrl}" data-toggle="collapse" data-target="#addfromurl" aria-expanded="false"><span class="glyphicon glyphicon-plus"></span></button>
+ <button class="btn btn-sm btn-default collapsed" id="showcustomembed" title="${embedCustomFrame}" data-toggle="collapse" data-target="#customembed" aria-expanded="false"><span class="glyphicon glyphicon-th-large"></span></button>
+ <button class="btn btn-sm btn-default collapsed" id="showplaylistmanager" title="${managePlaylists}" data-toggle="collapse" data-target="#playlistmanager" aria-expanded="false"><span class="glyphicon glyphicon-list"></span></button>
+ <button class="btn btn-sm btn-default" id="clearplaylist" title="${clearPlaylist}"><span class="glyphicon glyphicon-trash"></span></button>
+ <button class="btn btn-sm btn-default" id="shuffleplaylist" title="${shufflePlaylist}"><span class="glyphicon glyphicon-sort"></span></button>
+ <button class="btn btn-sm btn-danger" id="qlockbtn" title="${playlistLocked}"><span class="glyphicon glyphicon-lock"></span></button>
+ </div>
+ <div class="btn-group pull-right" id="videocontrols">
+ <button class="btn btn-sm btn-default" id="extendplayer" title="${expandPlayer}"><span class="glyphicon glyphicon-sound-stereo"></span></button>
+ <button class="btn btn-sm btn-default" id="togglesynch" title="${toggleVideoSync}"><span class="glyphicon glyphicon-play"></span></button>
+ <button class="btn btn-sm btn-default" id="mediarefresh" title="${refreshPlayer}"><span class="glyphicon glyphicon-retweet"></span></button>
+ <button class="btn btn-sm btn-default" id="fullscreenbtn" title="${fullscreenPlayer}"><span class="glyphicon glyphicon-fullscreen"></span></button>
+ <button class="btn btn-sm btn-default" id="getplaylist" title="${retrievePlaylistLinks}"><span class="glyphicon glyphicon-link"></span></button>
+ <button class="btn btn-sm btn-default" id="voteskip" title="${voteForSkip}" disabled="disabled"><span class="glyphicon glyphicon-step-forward"></span></button>
+ </div>
+ </div>
+ </div>
+ <div class="row" id="playlistrow">
+ <div class="col-lg-5 col-md-5" id="leftpane">
+ <div class="row" id="leftpane-inner">
+ <div class="col-lg-12 col-md-12" id="smileswrap">
+ <!-- <img class="smile-preview" src="css/Sfich1B.png" title=":pinkie:"> -->
+ </div>
+ <div class="col-lg-12 col-md-12" id="playlistmanagerwrap"></div>
+ </div>
+ </div>
+ <div class="col-lg-7 col-md-7" id="rightpane">
+ <div class="row" id="rightpane-inner">
+ <div class="plcontrol-collapse col-lg-12 col-md-12 collapse" id="searchcontrol" aria-expanded="false" style="height: 95px;">
+ <div class="vertical-spacer"></div>
+ <div class="input-group">
+ <input class="form-control" id="library_query" type="text" placeholder="${searchQuery}"><span class="input-group-btn">
+ <button class="btn btn-default" id="library_search">Library</button></span><span class="input-group-btn">
+ <button class="btn btn-default" id="youtube_search">YouTube</button></span>
+ </div>
+ <div class="checkbox">
+ <label>
+ <input class="add-temp" type="checkbox" checked="checked">${addAsTemporary}
+ </label>
+ </div>
+ <ul class="videolist col-lg-12 col-md-12" id="library"></ul>
+ </div>
+ <div class="plcontrol-collapse col-lg-12 col-md-12 collapse" id="addfromurl" aria-expanded="false" style="height: 89px;">
+ <div class="vertical-spacer"></div>
+ <div class="input-group"><span class="input-group-btn">
+ <button class="btn btn-default" id="insert_template">&gt;</button></span>
+ <input class="form-control" id="mediaurl" type="text" placeholder="${mediaUrl}"><span class="input-group-btn">
+ <button class="btn btn-default" id="queue_next">${queueNext}</button></span><span class="input-group-btn">
+ <button class="btn btn-default" id="queue_end">${queueLast}</button></span>
+ </div>
+ <div class="checkbox">
+ <label>
+ <input class="add-temp" type="checkbox" checked="checked">${addAsTemporary}
+ </label>
+ </div>
+ <div id="addfromurl-queue"></div>
+ </div>
+ <div class="plcontrol-collapse col-lg-12 col-md-12 collapse" id="customembed" aria-expanded="false" style="height: 209px;">
+ <div class="vertical-spacer"></div>
+ <div class="input-group">
+ <input class="form-control" id="customembed-title" type="text" placeholder="${optionalTitle}"><span class="input-group-btn">
+ <button class="btn btn-default" id="ce_queue_next">${queueNext}</button></span><span class="input-group-btn">
+ <button class="btn btn-default" id="ce_queue_end">${queueLast}</button></span>
+ </div>
+ <div class="checkbox">
+ <label>
+ <input class="add-temp" type="checkbox" checked="checked">${addAsTemporary}
+ </label>
+ </div>${pasteEmbedCodeAndClick} "${queueNext}" ${or} "${queueLast}".
+ ${acceptableEmbedCodesAre} <code>&lt;iframe&gt;</code> ${or} <code>&lt;object&gt;</code>.
+ <strong>${customEmbedsCannotBeSynchronized}.</strong>
+ <textarea class="input-block-level form-control" id="customembed-content" rows="3"></textarea>
+ </div>
+ <div class="plcontrol-collapse col-lg-12 col-md-12 collapse" id="playlistmanager" aria-expanded="false" style="height: 105px;">
+ <div class="vertical-spacer"></div>
+ <div class="input-group">
+ <input class="form-control" id="userpl_name" type="text" placeholder="${playlistName}"><span class="input-group-btn">
+ <button class="btn btn-default" id="userpl_save">${save}</button></span>
+ </div>
+ <div class="checkbox">
+ <label>
+ <input class="add-temp" type="checkbox" checked="checked">${addAsTemporary}
+ </label>
+ </div>
+ <ul class="videolist" id="userpl_list"></ul>
+ </div>
+ <div class="col-lg-12 col-md-12" id="queuefail">
+ <div class="vertical-spacer"></div>
+ </div>
+ <div class="col-lg-12 col-md-12">
+ <ul class="videolist ui-sortable queue_sortable" id="queue"></ul>
+ <div id="plmeta"><span id="plcount">0 ${videos}</span><span id="pllength">00:00</span></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="row" id="resizewrap">
+ <div class="col-lg-5 col-md-5"></div>
+ <div class="col-lg-7 col-md-7" id="videowidth"></div>
+ </div>
+ <div id="sitefooter"></div>
+ </div>
+ </section>
+ </div>
+ <div id="pmbar"></div>
+ <footer id="footer">
+ <div class="container">
+ <p class="text-muted credit">Based on CyTube by Calvin Montgomery</p>
+ </div>
+ </footer>
+
+ <!-- <script src="js/custom.js"></script> -->
+ <script src="client.js"></script>
+ <!-- <script defer="" src="https://www.youtube.com/iframe_api"></script>
+ <script defer="" src="js/sc.js"></script>
+ <script defer="" src="https://player.twitch.tv/js/embed/v1.js"></script> -->
+ </body>
+</html>
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<Client>, 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<Client>):Bool {
+ for (client in clients) {
+ if (client.isLeader) return true;
+ }
+ return false;
+ }
+
+ public static function getByName(clients:Array<Client>, name:String):Null<Client> {
+ 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<String, String>;
+
+class Lang {
+
+ static final ids = ["en", "ru"];
+ static final langs:Map<String, LangMap> = [];
+
+ 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<ClientData>,
+ isUnknownClient:Bool,
+ clientName:String,
+ videoList:Array<VideoItem>
+ },
+ ?login:{
+ clientName:String,
+ ?clients:Array<ClientData>,
+ ?isUnknownClient:Bool,
+ },
+ ?logout:{
+ clientName:String,
+ clients:Array<ClientData>,
+ },
+ ?message:{
+ clientName:String,
+ text:String
+ },
+ ?updateClients:{
+ clients:Array<ClientData>,
+ },
+ ?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<Client> = [];
+ final personalHistory:Array<String> = [];
+ var personal:Null<Client>;
+ var personalHistoryId = -1;
+ var isConnected = false;
+ var ws:WebSocket;
+ final player:Player;
+ final onTimeGet = new Timer(2000);
+
+ static function main():Void new Main();
+
+ public function new(?host:String, port = 4201) {
+ player = new Player(this);
+ if (host == null) host = Browser.location.hostname;
+ if (host == "") host = "localhost";
+
+ initListeners();
+ onTimeGet.run = () -> send({type: GetTime});
+ Lang.init("langs", () -> {
+ openWebSocket(host, port);
+ });
+ }
+
+ function openWebSocket(host:String, port:Int):Void {
+ ws = new WebSocket('ws://$host:$port');
+ ws.onmessage = onMessage;
+ ws.onopen = () -> {
+ serverMessage(1);
+ isConnected = true;
+ }
+ ws.onclose = () -> {
+ // if initial connection refused
+ // or server/client offline
+ if (isConnected) serverMessage(2);
+ isConnected = false;
+ player.pause();
+ Timer.delay(() -> openWebSocket(host, port), 2000);
+ }
+ }
+
+ function initListeners():Void {
+ final guestName:InputElement = cast ge("#guestname");
+ guestName.onkeydown = (e:KeyboardEvent) -> {
+ if (e.keyCode == 13) send({
+ type: Login,
+ login: {
+ clientName: guestName.value
+ }
+ });
+ }
+
+ final chatLine:InputElement = cast ge("#chatline");
+ chatLine.onkeydown = function(e:KeyboardEvent) {
+ switch (e.keyCode) {
+ case 13: // Enter
+ send({
+ type: Message,
+ message: {
+ clientName: "",
+ text: chatLine.value
+ }
+ });
+ personalHistory.push(chatLine.value);
+ if (personalHistory.length > 50) personalHistory.shift();
+ personalHistoryId = -1;
+ chatLine.value = "";
+ case 38: // Up
+ personalHistoryId--;
+ if (personalHistoryId == -2) {
+ personalHistoryId = personalHistory.length - 1;
+ if (personalHistoryId == -1) return;
+ } else if (personalHistoryId == -1) personalHistoryId++;
+ chatLine.value = personalHistory[personalHistoryId];
+ case 40: // Down
+ if (personalHistoryId == -1) return;
+ personalHistoryId++;
+ if (personalHistoryId > personalHistory.length - 1) {
+ personalHistoryId = -1;
+ chatLine.value = "";
+ return;
+ }
+ chatLine.value = personalHistory[personalHistoryId];
+ }
+ }
+
+ MobileView.init();
+
+ final leaderBtn:InputElement = cast ge("#leader_btn");
+ leaderBtn.onclick = (e) -> {
+ if (personal == null) return;
+ leaderBtn.classList.toggle('label-success');
+ final name = personal.isLeader ? "" : personal.name;
+ send({
+ type: SetLeader,
+ setLeader: {
+ clientName: name
+ }
+ });
+ }
+
+ final showMediaUrl:ButtonElement = cast ge("#showmediaurl");
+ showMediaUrl.onclick = (e:MouseEvent) -> {
+ ge("#showmediaurl").classList.toggle("collapsed");
+ ge("#showmediaurl").classList.toggle("active");
+ ge("#addfromurl").classList.toggle("collapse");
+ }
+ ge("#queue_next").onclick = (e:MouseEvent) -> addVideoUrl();
+ ge("#queue_end").onclick = (e:MouseEvent) -> addVideoUrl();
+ ge("#mediaurl").onkeydown = function(e:KeyboardEvent) {
+ if (e.keyCode == 13) addVideoUrl();
+ }
+ }
+
+ public function isLeader():Bool {
+ return personal != null && personal.isLeader;
+ }
+
+ function addVideoUrl():Void {
+ final mediaUrl:InputElement = cast ge("#mediaurl");
+ final url = mediaUrl.value;
+ final name = personal == null ? "Unknown" : personal.name;
+ getRemoteVideoDuration(mediaUrl.value, (duration:Float) -> {
+ send({
+ type: AddVideo,
+ addVideo: {
+ item: {
+ url: url,
+ title: Lang.get("rawVideo"),
+ author: name,
+ duration: duration
+ }
+ }
+ });
+ });
+ mediaUrl.value = "";
+ }
+
+ function getRemoteVideoDuration(src:String, callback:(duration:Float)->Void):Void {
+ final player:Element = ge("#ytapiplayer");
+ final video = document.createVideoElement();
+ video.src = src;
+ video.onloadedmetadata = () -> {
+ trace(video.duration);
+ player.removeChild(video);
+ callback(video.duration);
+ }
+ prepend(player, video);
+ }
+
+ function prepend(parent:Element, child:Element):Void {
+ if (parent.firstChild == null) parent.appendChild(child);
+ else parent.insertBefore(child, parent.firstChild);
+ }
+
+ function onMessage(e):Void {
+ final data:WsEvent = Json.parse(e.data);
+ final t:String = cast data.type;
+ final t = t.charAt(0).toLowerCase() + t.substr(1);
+ trace('Event: ${data.type}', untyped data[t]);
+ switch (data.type) {
+ case Connected:
+ if (data.connected.isUnknownClient) {
+ updateClients(data.connected.clients);
+ ge("#guestlogin").style.display = "block";
+ ge("#chatline").style.display = "none";
+ } else {
+ onLogin(data.connected.clients, data.connected.clientName);
+ }
+ final list = data.connected.videoList;
+ if (list.length == 0) return;
+ player.setVideo(list[0]);
+ for (video in data.connected.videoList) {
+ player.addVideoItem(video);
+ }
+ case Login:
+ onLogin(data.login.clients, data.login.clientName);
+ case LoginError:
+ serverMessage(4, Lang.get("usernameError"));
+ case Logout:
+ updateClients(data.logout.clients);
+ personal = null;
+ ge("#guestlogin").style.display = "block";
+ ge("#chatline").style.display = "none";
+ case UpdateClients:
+ updateClients(data.updateClients.clients);
+ if (personal != null) personal = clients.getByName(personal.name);
+ case Message:
+ addMessage(data.message.clientName, data.message.text);
+ case AddVideo:
+ if (player.isListEmpty()) player.setVideo(data.addVideo.item);
+ player.addVideoItem(data.addVideo.item);
+ case VideoLoaded:
+ player.setTime(0);
+ player.play();
+ case RemoveVideo:
+ player.removeItem(data.removeVideo.url);
+ if (player.isListEmpty()) player.pause();
+ case Pause:
+ player.pause();
+ player.setTime(data.pause.time);
+ case Play:
+ player.setTime(data.play.time);
+ player.play();
+ case GetTime:
+ final newTime = data.getTime.time;
+ final time = player.getTime();
+ if (Math.abs(time - newTime) < 2) return;
+ player.setTime(newTime);
+ if (!data.getTime.paused) player.play();
+ case SetTime:
+ final newTime = data.setTime.time;
+ final time = player.getTime();
+ if (Math.abs(time - newTime) < 2) return;
+ player.setTime(newTime);
+ case SetLeader:
+ clients.setLeader(data.setLeader.clientName);
+ updateUserList();
+ if (personal == null) return;
+ final leaderBtn:InputElement = cast ge("#leader_btn");
+ if (personal.isLeader) leaderBtn.classList.add('label-success');
+ else leaderBtn.classList.remove('label-success');
+ }
+ }
+
+ function onLogin(data:Array<ClientData>, clientName:String):Void {
+ updateClients(data);
+ personal = clients.getByName(clientName);
+ if (personal == null) return;
+ ge("#guestlogin").style.display = "none";
+ ge("#chatline").style.display = "block";
+ }
+
+ function updateClients(newClients:Array<ClientData>):Void {
+ clients.resize(0);
+ for (client in newClients) {
+ clients.push(Client.fromData(client));
+ }
+ updateUserList();
+ }
+
+ public function send(data:WsEvent):Void {
+ if (!isConnected) return;
+ ws.send(Json.stringify(data));
+ }
+
+ function serverMessage(type:Int, ?text:String):Void {
+ final msgBuf = ge("#messagebuffer");
+ final div = document.createDivElement();
+ final time = "[" + new Date().toTimeString().split(" ")[0] + "] ";
+ switch (type) {
+ case 1:
+ div.className = "server-msg-reconnect";
+ div.innerHTML = Lang.get("msgConnected");
+ case 2:
+ div.className = "server-msg-disconnect";
+ div.innerHTML = Lang.get("msgDisconnected");
+ case 3:
+ div.className = "server-whisper";
+ div.innerHTML = time + text + " " + Lang.get("entered");
+ case 4:
+ div.className = "server-whisper";
+ div.innerHTML = time + text;
+ default:
+ }
+ msgBuf.appendChild(div);
+ msgBuf.scrollTop = msgBuf.scrollHeight;
+ }
+
+ final pageTitle = document.title;
+
+ function updateUserList():Void {
+ final userCount = ge("#usercount");
+ userCount.innerHTML = clients.length + " " + Lang.get("online");
+ document.title = '$pageTitle (${clients.length})';
+
+ final list = new StringBuf();
+ for (client in clients) {
+ // final klass = client.isLeader ? "userlist_owner" : "userlist_item";
+ final klass = "userlist_item";
+ if (client.isLeader) list.add('<span class="glyphicon glyphicon-star-empty"></span>');
+ list.add('<span class="$klass">${client.name}</span></br>');
+ }
+ final userlist = ge("#userlist");
+ userlist.innerHTML = list.toString();
+ }
+
+ function addMessage(name:String, msg:String):Void {
+ final msgBuf = ge("#messagebuffer");
+ final userDiv = document.createDivElement();
+ userDiv.className = 'chat-msg-$name';
+
+ final tstamp = document.createSpanElement();
+ tstamp.className = "timestamp";
+ tstamp.innerHTML = "[" + new Date().toTimeString().split(" ")[0] + "] ";
+
+ final nameDiv = document.createElement("strong");
+ nameDiv.className = "username";
+ nameDiv.innerHTML = name + ": ";
+
+ final textDiv = document.createSpanElement();
+ textDiv.innerHTML = msg;
+
+ final isInChatEnd = msgBuf.scrollHeight - msgBuf.scrollTop == msgBuf.clientHeight;
+ userDiv.appendChild(tstamp);
+ userDiv.appendChild(nameDiv);
+ userDiv.appendChild(textDiv);
+ msgBuf.appendChild(userDiv);
+ if (isInChatEnd) msgBuf.scrollTop = msgBuf.scrollHeight;
+ }
+
+ public static inline function ge(id:String):Element {
+ return document.querySelector(id);
+ }
+
+}
diff --git a/src/client/MobileView.hx b/src/client/MobileView.hx
new file mode 100644
index 0000000..558df61
--- /dev/null
+++ b/src/client/MobileView.hx
@@ -0,0 +1,52 @@
+package client;
+
+import js.html.InputElement;
+import js.Browser.document;
+import client.Main.ge;
+
+class MobileView {
+
+ public static function init():Void {
+ final mvbtn:InputElement = cast ge("#mv_btn");
+ mvbtn.onclick = (e) -> {
+ final mobile_view = toggleFullScreen();
+ if (mobile_view) {
+ document.body.classList.add('mobile-view');
+ mvbtn.classList.add('label-success');
+ final vwrap = ge("#videowrap");
+ if (vwrap.children[0] == ge("currenttitle")) {
+ vwrap.appendChild(vwrap.children[0]);
+ }
+ } else {
+ document.body.classList.remove('mobile-view');
+ mvbtn.classList.remove('label-success');
+ final vwrap = ge("videowrap");
+ if (vwrap.children[0] != ge("currenttitle")) {
+ vwrap.insertBefore(vwrap.children[1],vwrap.children[0]);
+ }
+ }
+ }
+ }
+
+ static function toggleFullScreen():Bool {
+ var state = true;
+ final doc:Dynamic = document;
+ if (document.fullscreenElement == null &&
+ doc.mozFullScreenElement == null &&
+ doc.webkitFullscreenElement == null) {
+ if (document.documentElement.requestFullscreen != null) {
+ document.documentElement.requestFullscreen();
+ } else if (doc.documentElement.mozRequestFullScreen != null) {
+ doc.documentElement.mozRequestFullScreen();
+ } else if (doc.documentElement.webkitRequestFullscreen != null) {
+ doc.documentElement.webkitRequestFullscreen(untyped Element.ALLOW_KEYBOARD_INPUT);
+ } else state = false;
+ } else {
+ if (doc.cancelFullScreen != null) doc.cancelFullScreen();
+ else if (doc.mozCancelFullScreen != null) doc.mozCancelFullScreen();
+ else if (doc.webkitCancelFullScreen != null) doc.webkitCancelFullScreen();
+ state = false;
+ }
+ return state;
+ }
+}
diff --git a/src/client/Player.hx b/src/client/Player.hx
new file mode 100644
index 0000000..2973e9b
--- /dev/null
+++ b/src/client/Player.hx
@@ -0,0 +1,182 @@
+package client;
+
+import js.html.LIElement;
+import js.html.UListElement;
+import js.html.Element;
+import js.html.VideoElement;
+import js.Browser.document;
+import client.Main.ge;
+import Types.VideoItem;
+using Lambda;
+
+class Player {
+
+ final main:Main;
+ final items:Array<VideoItem> = [];
+ final videoItemsEl = ge("#queue");
+ final player:Element = ge("#ytapiplayer");
+ var isLoaded = false;
+ var skipSetTime = false;
+ var video:VideoElement;
+
+ public function new(main:Main):Void {
+ this.main = main;
+ }
+
+ public function setVideo(item:VideoItem):Void {
+ isLoaded = false;
+ video = document.createVideoElement();
+ video.id = "videoplayer";
+ video.src = item.url;
+ video.controls = true;
+ video.oncanplaythrough = (e) -> {
+ if (!isLoaded) main.send({type: VideoLoaded});
+ isLoaded = true;
+ }
+ video.ontimeupdate = (e) -> {
+ if (skipSetTime) {
+ skipSetTime = false;
+ return;
+ }
+ if (!main.isLeader()) return;
+ main.send({
+ type: SetTime,
+ setTime: {
+ time: video.currentTime
+ }
+ });
+ }
+ video.onpause = (e) -> {
+ if (!main.isLeader()) return;
+ main.send({
+ type: Pause,
+ pause: {
+ time: video.currentTime
+ }
+ });
+ }
+ video.onplay = (e) -> {
+ if (!main.isLeader()) return;
+ main.send({
+ type: Play,
+ play: {
+ time: video.currentTime
+ }
+ });
+ }
+ player.innerHTML = "";
+ player.appendChild(video);
+ }
+
+ public function addVideoItem(item:VideoItem):Void {
+ items.push(item);
+ final itemEl:LIElement = cast nodeFromString(
+ '<li class="queue_entry pluid-0 queue_temp queue_active" title="${Lang.get("addedBy")}: ${item.author}">
+ <a class="qe_title" href="${item.url}" target="_blank">${item.title}</a>
+ <span class="qe_time">${duration(item.duration)}</span>
+ <div class="qe_clear"></div>
+ <div class="btn-group" style="display: inline-block;">
+ <button class="btn btn-xs btn-default qbtn-play">
+ <span class="glyphicon glyphicon-play"></span>${Lang.get("play")}
+ </button>
+ <button class="btn btn-xs btn-default qbtn-next">
+ <span class="glyphicon glyphicon-share-alt"></span>${Lang.get("skip")}
+ </button>
+ <button class="btn btn-xs btn-default qbtn-tmp">
+ <span class="glyphicon glyphicon-flag"></span>${Lang.get("makePermanent")}
+ </button>
+ <button class="btn btn-xs btn-default qbtn-delete" id="btn-delete">
+ <span class="glyphicon glyphicon-trash"></span>${Lang.get("delete")}
+ </button>
+ </div>
+ </li>'
+ );
+ final deleteBtn = itemEl.querySelector("#btn-delete");
+ deleteBtn.onclick = (e) -> {
+ main.send({
+ type: RemoveVideo,
+ removeVideo: {
+ url: itemEl.querySelector(".qe_title").getAttribute("href")
+ }
+ });
+ }
+ videoItemsEl.appendChild(itemEl);
+ ge("#plcount").innerHTML = '${items.length} ${Lang.get("videos")}';
+ ge("#pllength").innerHTML = totalDuration();
+ }
+
+ public function removeVideo():Void {
+ player.removeChild(video);
+ video = null;
+ }
+
+ public function removeItem(url:String):Void {
+ final list = ge("#queue");
+ for (child in list.children) {
+ if (child.querySelector(".qe_title").getAttribute("href") == url) {
+ list.removeChild(child);
+ break;
+ }
+ }
+
+ items.remove(
+ items.find(item -> item.url == url)
+ );
+
+ if (video.src == url) {
+ if (items.length > 0) setVideo(items[0]);
+ }
+ ge("#plcount").innerHTML = '${items.length} ${Lang.get("videos")}';
+ ge("#pllength").innerHTML = totalDuration();
+ }
+
+ function duration(time:Float):String {
+ final h = Std.int(time / 60 / 60);
+ final m = Std.int(time / 60);
+ final s = Std.int(time % 60);
+ var time = '$m:';
+ if (m < 10) time = '0$time';
+ if (h > 0) time = '$h:$time';
+ if (s < 10) time = time + "0";
+ time += s;
+ return time;
+ }
+
+ function totalDuration():String {
+ var time = 0.0;
+ for (item in items) time += item.duration;
+ return duration(time);
+ }
+
+ function nodeFromString(div:String):Element {
+ final wrapper = document.createDivElement();
+ wrapper.innerHTML = div;
+ return wrapper.firstElementChild;
+ }
+
+ public function isListEmpty():Bool {
+ return items.length == 0;
+ }
+
+ public function pause():Void {
+ if (video == null) return;
+ video.pause();
+ }
+
+ public function play():Void {
+ if (video == null) return;
+ video.play();
+ }
+
+ public function setTime(time:Float, isLocal = true):Void {
+ if (video == null) return;
+ skipSetTime = isLocal;
+ video.currentTime = time;
+ }
+
+ public function getTime():Float {
+ if (video == null) return 0;
+ return video.currentTime;
+ }
+
+}
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<Client> = [];
+ final videoList:Array<VideoItem> = [];
+ 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<ClientData> {
+ 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;
+ }
+
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage