aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2026-07-02 18:49:53 -0700
committerPinapelz <yukais@pinapelz.com>2026-07-02 18:49:53 -0700
commitece9cf2749ac592299a34a2cf0f24672eedbd19e (patch)
tree811ab6991283973a3d1f2dcae127137b1c06893c
parentfe306f7eab9395ebb220b105aee4a36fa2f7f895 (diff)
initial synctube++ fork
Signed-off-by: Pinapelz <yukais@pinapelz.com>
-rw-r--r--LICENSE2
-rw-r--r--README.md40
-rw-r--r--build/server.js280
-rw-r--r--res/client.js74
-rw-r--r--res/css/des.css2
-rw-r--r--res/index.html10
-rw-r--r--src/client/Main.hx55
7 files changed, 352 insertions, 111 deletions
diff --git a/LICENSE b/LICENSE
index d2dd0c7..919b1be 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2020 Maxim Matyukhin
+Copyright (c) 2020 Maxim Matyukhin, Pinapelz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 87cf61b..49f9b6c 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,23 @@
-Fork Key Differences
-- `gatePassword` config option, allowing only people who know the password to enter
-- `adminToken` allows for registering admin on the frontend using a known token
-- `/admin-register` page that is visible only if `adminToken` is set
-
-This fork adds basic password based authentication to prevent unwanted users from joining and strips most existing emotes.
-
-# <img src="./res/img/favicon.svg" width="40" height="40" align="top"> SyncTube
-Synchronized video viewing with chat and other features.
+# <img src="./res/img/favicon.svg" width="40" height="40" align="top"> SyncTube++
+Synchronized video viewing with chat and other features, forked from the original [SyncTube](https://github.com/RblSb/SyncTube).
Lightweight modern implementation and a very easy way to run locally.
-Default channel example: https://synctube.onrender.com/
-
-## Features
+## Original Features
- Control video playback for all users with active `Leader` button
- Start watching local videos while uploading them to the server, before upload completes
- External `vtt`/`srt`/`ass` subtitles support
- External audiotrack / voiceover support
- `/30`, `/-21`, etc chat commands to rewind video playback by seconds
- Hotkeys (`Alt-P` for global play/pause, [etc](https://github.com/RblSb/SyncTube/blob/382f9b2ebedca905028341825350a0fa69d88673/src/client/Buttons.hx#L416-L427))
-- Compact view button with page fullscreen on Android
- Playback rate synchronization (with leader)
- Links mask: `foo.com/bar${1-4}.mp4` to add multiple items
- Override every front-end file you want (`user/res` folder)
-- [Native mobile client](https://github.com/RblSb/SyncTubeApp)
+
+## PlusPlus Features
+- Admin registration via configuring the `adminToken` config key and then registering on the `/admin-register` page
+- `gatePassword` config option to prevent unwanted users from joining
+- Overhauled CSS and mobile view
+
### Easier playback controls for smaller groups
- Enable `requestLeaderOnPause` to allow global pause by any user, without `Leader` button
@@ -37,10 +32,11 @@ Default channel example: https://synctube.onrender.com/
- Iframes (without sync)
## Setup
-- Open `4200` port in your router settings (port is customizable)
+- Open `4200` port in your router settings (port is cust@omizable)
- `npm ci` in this project folder ([NodeJS 14+](https://nodejs.org) required)
+- `haxe build-all.hxml` in this project folder ([HAXE homepage](https://haxe.org/))
- Run `node build/server.js`
-- Open showed "Local" link for yourself and send "Global" link to friends
+- Open "Local" link for yourself and send "Global" link to friends
## Setup (Docker)
As alternative, you can install Docker and run:
@@ -52,18 +48,20 @@ As alternative, you can install Docker and run:
or
> ```shell
-> docker compose up -d
+> docker compose up -d --build
> ```
-
- (Docker container hides real local/global ips, so you need to checkout it manually)
-## Optional dependencies
-If you want to enable `Cache on server` feature for Youtube player, you can also run:
+## "Cache On Server"
+"Cache on Server" is a feature that enables a user to upload videos to the server and serve it to all other users temporarily. The video is deleted after it has been removed from the queue.
+
+This feature requires `ffmpeg` on your server system, it's only used to build single mp4 from downloaded audio/video tracks. Default cache size is 3.0 GiB.
+
+If you want to enable `Cache on server` feature for Youtube player (effectively download the video on the server-side first), you can also run:
```shell
npm i https://github.com/RblSb/ytdlp-nodejs
```
-And install `ffmpeg` on your server system, it's only used to build single mp4 from downloaded audio/video tracks. Default cache size is 3.0 GiB.
## Configuration
It just works, but you can also check [user/ folder](/user/README.md) for server settings and additional customization.
diff --git a/build/server.js b/build/server.js
index 6351cff..c2b42ef 100644
--- a/build/server.js
+++ b/build/server.js
@@ -1304,6 +1304,43 @@ JsonParser_$8d3a3702ec359ae3022bce905b75550b.prototype = $extend(json2object_rea
}
,__class__: JsonParser_$8d3a3702ec359ae3022bce905b75550b
});
+var JsonParser_$9dc54ddefde837b3151ea03ad5d1144e = function(errors,putils,errorType) {
+ if(errorType == null) {
+ errorType = 0;
+ }
+ json2object_reader_BaseParser.call(this,errors,putils,errorType);
+};
+JsonParser_$9dc54ddefde837b3151ea03ad5d1144e.__name__ = true;
+JsonParser_$9dc54ddefde837b3151ea03ad5d1144e.__super__ = json2object_reader_BaseParser;
+JsonParser_$9dc54ddefde837b3151ea03ad5d1144e.prototype = $extend(json2object_reader_BaseParser.prototype,{
+ onIncorrectType: function(pos,variable) {
+ this.errors.push(json2object_Error.IncorrectType(variable,"{ password : String }",pos));
+ json2object_reader_BaseParser.prototype.onIncorrectType.call(this,pos,variable);
+ }
+ ,loadJsonNull: function(pos,variable) {
+ this.value = null;
+ }
+ ,loadJsonObject: function(o,pos,variable) {
+ var assigned = new haxe_ds_StringMap();
+ this.objectSetupAssign(assigned,["password"],[false]);
+ this.value = this.getAuto();
+ var _g = 0;
+ while(_g < o.length) {
+ var field = o[_g];
+ ++_g;
+ if(field.name == "password") {
+ this.value.password = this.loadObjectField(($_=new JsonParser_$27118326006d3829667a400ad23d5d98(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"password",assigned,this.value.password,pos);
+ } else {
+ this.errors.push(json2object_Error.UnknownVariable(field.name,this.putils.convertPosition(field.namePos)));
+ }
+ }
+ this.objectErrors(assigned,pos);
+ }
+ ,getAuto: function() {
+ return { password : new JsonParser_$27118326006d3829667a400ad23d5d98([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1)))};
+ }
+ ,__class__: JsonParser_$9dc54ddefde837b3151ea03ad5d1144e
+});
var JsonParser_$a82c17fc41c21179c58e1d406d2b20cd = function(errors,putils,errorType) {
if(errorType == null) {
errorType = 0;
@@ -1696,6 +1733,54 @@ JsonParser_$d1c1e71f4452df068de6cecc104f6dd6.prototype = $extend(json2object_rea
}
,__class__: JsonParser_$d1c1e71f4452df068de6cecc104f6dd6
});
+var JsonParser_$d581436aae29874719279c60b2eb25d0 = function(errors,putils,errorType) {
+ if(errorType == null) {
+ errorType = 0;
+ }
+ json2object_reader_BaseParser.call(this,errors,putils,errorType);
+};
+JsonParser_$d581436aae29874719279c60b2eb25d0.__name__ = true;
+JsonParser_$d581436aae29874719279c60b2eb25d0.__super__ = json2object_reader_BaseParser;
+JsonParser_$d581436aae29874719279c60b2eb25d0.prototype = $extend(json2object_reader_BaseParser.prototype,{
+ onIncorrectType: function(pos,variable) {
+ this.errors.push(json2object_Error.IncorrectType(variable,"{ token : String, passwordConfirmation : String, password : String, name : String }",pos));
+ json2object_reader_BaseParser.prototype.onIncorrectType.call(this,pos,variable);
+ }
+ ,loadJsonNull: function(pos,variable) {
+ this.value = null;
+ }
+ ,loadJsonObject: function(o,pos,variable) {
+ var assigned = new haxe_ds_StringMap();
+ this.objectSetupAssign(assigned,["name","password","passwordConfirmation","token"],[false,false,false,false]);
+ this.value = this.getAuto();
+ var _g = 0;
+ while(_g < o.length) {
+ var field = o[_g];
+ ++_g;
+ switch(field.name) {
+ case "name":
+ this.value.name = this.loadObjectField(($_=new JsonParser_$27118326006d3829667a400ad23d5d98(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"name",assigned,this.value.name,pos);
+ break;
+ case "password":
+ this.value.password = this.loadObjectField(($_=new JsonParser_$27118326006d3829667a400ad23d5d98(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"password",assigned,this.value.password,pos);
+ break;
+ case "passwordConfirmation":
+ this.value.passwordConfirmation = this.loadObjectField(($_=new JsonParser_$27118326006d3829667a400ad23d5d98(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"passwordConfirmation",assigned,this.value.passwordConfirmation,pos);
+ break;
+ case "token":
+ this.value.token = this.loadObjectField(($_=new JsonParser_$27118326006d3829667a400ad23d5d98(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"token",assigned,this.value.token,pos);
+ break;
+ default:
+ this.errors.push(json2object_Error.UnknownVariable(field.name,this.putils.convertPosition(field.namePos)));
+ }
+ }
+ this.objectErrors(assigned,pos);
+ }
+ ,getAuto: function() {
+ return { name : new JsonParser_$27118326006d3829667a400ad23d5d98([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), password : new JsonParser_$27118326006d3829667a400ad23d5d98([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), passwordConfirmation : new JsonParser_$27118326006d3829667a400ad23d5d98([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), token : new JsonParser_$27118326006d3829667a400ad23d5d98([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1)))};
+ }
+ ,__class__: JsonParser_$d581436aae29874719279c60b2eb25d0
+});
var JsonParser_$d7778d0c64b6ba21494c97f77a66885a = function(errors,putils,errorType) {
if(errorType == null) {
errorType = 0;
@@ -2034,51 +2119,6 @@ JsonParser_$ed5dea09095f671b801bee34ea28a319.prototype = $extend(json2object_rea
}
,__class__: JsonParser_$ed5dea09095f671b801bee34ea28a319
});
-var JsonParser_$f3c29c0813c93ee49a61ccf072b8a177 = function(errors,putils,errorType) {
- if(errorType == null) {
- errorType = 0;
- }
- json2object_reader_BaseParser.call(this,errors,putils,errorType);
-};
-JsonParser_$f3c29c0813c93ee49a61ccf072b8a177.__name__ = true;
-JsonParser_$f3c29c0813c93ee49a61ccf072b8a177.__super__ = json2object_reader_BaseParser;
-JsonParser_$f3c29c0813c93ee49a61ccf072b8a177.prototype = $extend(json2object_reader_BaseParser.prototype,{
- onIncorrectType: function(pos,variable) {
- this.errors.push(json2object_Error.IncorrectType(variable,"{ passwordConfirmation : String, password : String, name : String }",pos));
- json2object_reader_BaseParser.prototype.onIncorrectType.call(this,pos,variable);
- }
- ,loadJsonNull: function(pos,variable) {
- this.value = null;
- }
- ,loadJsonObject: function(o,pos,variable) {
- var assigned = new haxe_ds_StringMap();
- this.objectSetupAssign(assigned,["name","password","passwordConfirmation"],[false,false,false]);
- this.value = this.getAuto();
- var _g = 0;
- while(_g < o.length) {
- var field = o[_g];
- ++_g;
- switch(field.name) {
- case "name":
- this.value.name = this.loadObjectField(($_=new JsonParser_$27118326006d3829667a400ad23d5d98(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"name",assigned,this.value.name,pos);
- break;
- case "password":
- this.value.password = this.loadObjectField(($_=new JsonParser_$27118326006d3829667a400ad23d5d98(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"password",assigned,this.value.password,pos);
- break;
- case "passwordConfirmation":
- this.value.passwordConfirmation = this.loadObjectField(($_=new JsonParser_$27118326006d3829667a400ad23d5d98(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"passwordConfirmation",assigned,this.value.passwordConfirmation,pos);
- break;
- default:
- this.errors.push(json2object_Error.UnknownVariable(field.name,this.putils.convertPosition(field.namePos)));
- }
- }
- this.objectErrors(assigned,pos);
- }
- ,getAuto: function() {
- return { name : new JsonParser_$27118326006d3829667a400ad23d5d98([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), password : new JsonParser_$27118326006d3829667a400ad23d5d98([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), passwordConfirmation : new JsonParser_$27118326006d3829667a400ad23d5d98([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1)))};
- }
- ,__class__: JsonParser_$f3c29c0813c93ee49a61ccf072b8a177
-});
var JsonParser_$f55acea3678203c700715b781ad1ef0c = function(errors,putils,errorType) {
if(errorType == null) {
errorType = 0;
@@ -2302,17 +2342,6 @@ Lambda.findIndex = function(it,f) {
}
return -1;
};
-var haxe_IMap = function() { };
-haxe_IMap.__name__ = true;
-haxe_IMap.__isInterface__ = true;
-var haxe_ds_StringMap = function() {
- this.h = Object.create(null);
-};
-haxe_ds_StringMap.__name__ = true;
-haxe_ds_StringMap.__interfaces__ = [haxe_IMap];
-haxe_ds_StringMap.prototype = {
- __class__: haxe_ds_StringMap
-};
var Lang = function() { };
Lang.__name__ = true;
Lang.request = function(path,callback) {
@@ -2585,6 +2614,9 @@ VideoList.prototype = {
}
,__class__: VideoList
};
+var haxe_IMap = function() { };
+haxe_IMap.__name__ = true;
+haxe_IMap.__isInterface__ = true;
var haxe_Exception = function(message,previous,native) {
Error.call(this,message);
this.message = message;
@@ -2822,6 +2854,14 @@ haxe_crypto_Sha256.prototype = {
}
,__class__: haxe_crypto_Sha256
};
+var haxe_ds_StringMap = function() {
+ this.h = Object.create(null);
+};
+haxe_ds_StringMap.__name__ = true;
+haxe_ds_StringMap.__interfaces__ = [haxe_IMap];
+haxe_ds_StringMap.prototype = {
+ __class__: haxe_ds_StringMap
+};
var haxe_exceptions_PosException = function(message,previous,pos) {
haxe_Exception.call(this,message,previous);
if(pos == null) {
@@ -3958,8 +3998,13 @@ server_HttpServer.prototype = {
break;
}
}
- if(url.pathname == "/setup") {
- this.finishSetup(req,res);
+ switch(url.pathname) {
+ case "/admin-register":
+ this.registerAdmin(req,res);
+ break;
+ case "/gate":
+ this.verifyGate(req,res);
+ break;
}
return;
}
@@ -3978,8 +4023,8 @@ server_HttpServer.prototype = {
res.end("Error getting the file: No access to " + rel + ".");
return;
}
- if(url.pathname == "/setup") {
- if(this.main.hasAdmins()) {
+ if(url.pathname == "/gate") {
+ if(!this.hasGatePassword() || this.hasValidGateCookie(req)) {
tools_HttpServerTools.redirect(res,"/");
return;
}
@@ -3990,6 +4035,22 @@ server_HttpServer.prototype = {
});
return;
}
+ if(url.pathname == "/admin-register") {
+ if(!this.hasAdminToken()) {
+ tools_HttpServerTools.redirect(res,"/");
+ return;
+ }
+ js_node_Fs.readFile("" + this.dir + "/admin-register.html",function(err,data) {
+ if(err != null) {
+ _gthis.readFileError(err,res,"" + _gthis.dir + "/admin-register.html");
+ return;
+ }
+ data = js_node_buffer_Buffer.from(_gthis.localizeHtml(data.toString(),req.headers["accept-language"]));
+ res.setHeader("content-type",_gthis.getMimeType("html"));
+ res.end(data);
+ });
+ return;
+ }
if(url.pathname == "/proxy") {
if(!this.proxyUrl(req,res)) {
res.end("Proxy error: " + req.url);
@@ -4015,8 +4076,8 @@ server_HttpServer.prototype = {
return;
}
if(ext == "html") {
- if(!_gthis.main.isNoState && !_gthis.main.hasAdmins()) {
- tools_HttpServerTools.redirect(res,"/setup");
+ if(_gthis.hasGatePassword() && !_gthis.hasValidGateCookie(req)) {
+ tools_HttpServerTools.redirect(res,"/gate");
return;
}
data = _gthis.localizeHtml(data.toString(),req.headers["accept-language"]);
@@ -4104,7 +4165,7 @@ server_HttpServer.prototype = {
}
});
stream.on("error",function(err) {
- haxe_Log.trace(err,{ fileName : "src/server/HttpServer.hx", lineNumber : 231, className : "server.HttpServer", methodName : "uploadFile"});
+ haxe_Log.trace(err,{ fileName : "src/server/HttpServer.hx", lineNumber : 256, className : "server.HttpServer", methodName : "uploadFile"});
tools_HttpServerTools.json(tools_HttpServerTools.status(res,500),{ info : "File write stream error."});
var _this = _gthis.uploadingFilesSizes;
if(Object.prototype.hasOwnProperty.call(_this.h,filePath)) {
@@ -4117,7 +4178,7 @@ server_HttpServer.prototype = {
_gthis.cache.remove(name);
});
req.on("error",function(err) {
- haxe_Log.trace("Request Error:",{ fileName : "src/server/HttpServer.hx", lineNumber : 238, className : "server.HttpServer", methodName : "uploadFile", customParams : [err]});
+ haxe_Log.trace("Request Error:",{ fileName : "src/server/HttpServer.hx", lineNumber : 263, className : "server.HttpServer", methodName : "uploadFile", customParams : [err]});
stream.destroy();
tools_HttpServerTools.json(tools_HttpServerTools.status(res,500),{ info : "File request error."});
var _this = _gthis.uploadingFilesSizes;
@@ -4131,9 +4192,9 @@ server_HttpServer.prototype = {
_gthis.cache.remove(name);
});
}
- ,finishSetup: function(req,res) {
+ ,verifyGate: function(req,res) {
var _gthis = this;
- if(this.main.hasAdmins()) {
+ if(!this.hasGatePassword()) {
tools_HttpServerTools.redirect(res,"/");
return;
}
@@ -4143,30 +4204,88 @@ server_HttpServer.prototype = {
});
req.on("end",function() {
var body = js_node_buffer_Buffer.concat(bodyChunks).toString();
- var jsonParser = new JsonParser_$f3c29c0813c93ee49a61ccf072b8a177();
+ var jsonParser = new JsonParser_$9dc54ddefde837b3151ea03ad5d1144e();
+ var jsonData = jsonParser.fromJson(body);
+ if(jsonParser.errors.length > 0) {
+ tools_HttpServerTools.json(tools_HttpServerTools.status(res,400),{ success : false});
+ return;
+ }
+ if(jsonData.password == _gthis.main.config.gatePassword) {
+ var token = _gthis.getGateToken();
+ res.setHeader("set-cookie","gate_auth=" + token + "; Path=/; HttpOnly; SameSite=Strict");
+ tools_HttpServerTools.json(tools_HttpServerTools.status(res,200),{ success : true});
+ } else {
+ tools_HttpServerTools.json(tools_HttpServerTools.status(res,401),{ success : false});
+ }
+ });
+ }
+ ,hasGatePassword: function() {
+ var gp = this.main.config.gatePassword;
+ if(gp != null) {
+ return gp.length > 0;
+ } else {
+ return false;
+ }
+ }
+ ,hasValidGateCookie: function(req) {
+ var cookieHeader = req.headers["cookie"];
+ if(cookieHeader == null) {
+ return false;
+ }
+ var needle = "gate_auth=" + this.getGateToken();
+ var _g = 0;
+ var _g1 = cookieHeader.split(";");
+ while(_g < _g1.length) if(StringTools.trim(_g1[_g++]) == needle) {
+ return true;
+ }
+ return false;
+ }
+ ,getGateToken: function() {
+ return haxe_crypto_Sha256.encode("gate_" + this.main.config.gatePassword + "_" + this.main.config.salt);
+ }
+ ,hasAdminToken: function() {
+ var t = this.main.config.adminToken;
+ if(t != null) {
+ return t.length > 0;
+ } else {
+ return false;
+ }
+ }
+ ,registerAdmin: function(req,res) {
+ var _gthis = this;
+ if(!this.hasAdminToken()) {
+ tools_HttpServerTools.json(tools_HttpServerTools.status(res,403),{ success : false, error : "Admin registration is disabled"});
+ return;
+ }
+ var bodyChunks = [];
+ req.on("data",function(chunk) {
+ return bodyChunks.push(chunk);
+ });
+ req.on("end",function() {
+ var body = js_node_buffer_Buffer.concat(bodyChunks).toString();
+ var jsonParser = new JsonParser_$d581436aae29874719279c60b2eb25d0();
var jsonData = jsonParser.fromJson(body);
if(jsonParser.errors.length > 0) {
- haxe_Log.trace(json2object_ErrorUtils.convertErrorArray(jsonParser.errors),{ fileName : "src/server/HttpServer.hx", lineNumber : 264, className : "server.HttpServer", methodName : "finishSetup"});
- tools_HttpServerTools.json(tools_HttpServerTools.status(res,400),{ success : false, errors : []});
+ tools_HttpServerTools.json(tools_HttpServerTools.status(res,400),{ success : false, error : "Invalid request"});
return;
}
- var name = jsonData.name;
+ var name = StringTools.trim(jsonData.name);
var password = jsonData.password;
var passwordConfirmation = jsonData.passwordConfirmation;
- var tmp = req.headers["accept-language"];
- var lang = tmp != null ? tmp : "en";
- var errors = [];
+ if(jsonData.token != _gthis.main.config.adminToken) {
+ tools_HttpServerTools.json(tools_HttpServerTools.status(res,401),{ success : false, error : "Invalid admin token"});
+ return;
+ }
if(_gthis.main.isBadClientName(name)) {
- errors.push({ type : "name", error : StringTools.replace(Lang.get(lang,"usernameError"),"$MAX","" + _gthis.main.config.maxLoginLength)});
+ tools_HttpServerTools.json(tools_HttpServerTools.status(res,400),{ success : false, error : "Invalid username"});
+ return;
}
if(password.length < 4 || password.length > 50) {
- errors.push({ type : "password", error : StringTools.replace(StringTools.replace(Lang.get(lang,"passwordError"),"$MIN","" + 4),"$MAX","" + 50)});
+ tools_HttpServerTools.json(tools_HttpServerTools.status(res,400),{ success : false, error : "Password must be " + 4 + "-" + 50 + " characters"});
+ return;
}
if(password != passwordConfirmation) {
- errors.push({ type : "password", error : Lang.get(lang,"passwordsMismatchError")});
- }
- if(errors.length > 0) {
- tools_HttpServerTools.json(tools_HttpServerTools.status(res,400),{ success : false, errors : errors});
+ tools_HttpServerTools.json(tools_HttpServerTools.status(res,400),{ success : false, error : "Passwords do not match"});
return;
}
_gthis.main.addAdmin(name,password);
@@ -4856,9 +4975,6 @@ server_Main.prototype = {
}));
haxe_Log.trace("Admin " + name + " removed.",{ fileName : "src/server/Main.hx", lineNumber : 443, className : "server.Main", methodName : "removeAdmin"});
}
- ,hasAdmins: function() {
- return this.userList.admins.length > 0;
- }
,replayLog: function(events) {
var _gthis = this;
var timer = new haxe_Timer(1000);
diff --git a/res/client.js b/res/client.js
index faf13b4..4d19790 100644
--- a/res/client.js
+++ b/res/client.js
@@ -1535,6 +1535,78 @@ client_Main.prototype = {
,initListeners: function() {
var _gthis = this;
client_Buttons.init(this);
+ var openBtn = window.document.getElementById("openChatBtn");
+ var openBtnFadeTimer = null;
+ if(openBtn == null) {
+ openBtn = window.document.createElement("button");
+ openBtn.id = "openChatBtn";
+ openBtn.innerText = "Expand";
+ openBtn.setAttribute("style","position: fixed; right: 0; top: 50%; transform: translateY(-50%); z-index: 10000; display: none; padding: .5rem; background: rgba(0,0,0,0.4); color: var(--foreground); border: none; border-radius: .25rem 0 0 .25rem; transition: opacity 200ms; opacity: 1;");
+ window.document.body.appendChild(openBtn);
+ }
+ var toggleChatBtn = window.document.querySelector("#togglechat");
+ openBtn.onclick = function(e) {
+ toggleChatBtn.click();
+ };
+ openBtn.onmouseenter = function(e) {
+ openBtn.style.opacity = "1";
+ if(openBtnFadeTimer != null) {
+ openBtnFadeTimer.stop();
+ }
+ };
+ openBtn.onmouseleave = function(e) {
+ if(openBtnFadeTimer != null) {
+ openBtnFadeTimer.stop();
+ }
+ openBtnFadeTimer = haxe_Timer.delay(function() {
+ openBtn.style.opacity = "0.25";
+ },2000);
+ return openBtnFadeTimer;
+ };
+ window.document.querySelector("#togglechat").onclick = function(e) {
+ var chat = window.document.querySelector("#chat");
+ var bodyEl = window.document.querySelector("body");
+ var sizes = bodyEl.style.gridTemplateColumns;
+ if(sizes == null || sizes == "") {
+ sizes = "1fr 4px 300px";
+ }
+ var parts = sizes.split(" ");
+ var chatIndex = _gthis.settings.isSwapped ? 0 : parts.length - 1;
+ if(chat.style.display == "none") {
+ chat.style.display = "";
+ var prev = bodyEl.getAttribute("data-prev-grid");
+ if(prev != null) {
+ bodyEl.style.gridTemplateColumns = prev;
+ bodyEl.removeAttribute("data-prev-grid");
+ } else {
+ parts[chatIndex] = "" + _gthis.settings.chatSize + "px";
+ if(parts.length > 1) {
+ parts[1] = "4px";
+ }
+ bodyEl.style.gridTemplateColumns = parts.join(" ");
+ }
+ openBtn.style.display = "none";
+ if(openBtnFadeTimer != null) {
+ openBtnFadeTimer.stop();
+ }
+ } else {
+ bodyEl.setAttribute("data-prev-grid",bodyEl.style.gridTemplateColumns);
+ parts[chatIndex] = "0px";
+ if(parts.length > 1) {
+ parts[1] = "0px";
+ }
+ bodyEl.style.gridTemplateColumns = parts.join(" ");
+ chat.style.display = "none";
+ openBtn.style.display = "";
+ openBtn.style.opacity = "1";
+ if(openBtnFadeTimer != null) {
+ openBtnFadeTimer.stop();
+ }
+ openBtnFadeTimer = haxe_Timer.delay(function() {
+ openBtn.style.opacity = "0.25";
+ },2000);
+ }
+ };
var leaderBtn = window.document.querySelector("#leader_btn");
leaderBtn.onclick = $bind(this,this.toggleLeader);
leaderBtn.oncontextmenu = function(e) {
@@ -1764,7 +1836,7 @@ client_Main.prototype = {
var data = JSON.parse(e.data);
if(this.config != null && this.config.isVerbose) {
var t = data.type;
- haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 489, className : "client.Main", methodName : "onMessage", customParams : [Reflect.field(data,t.charAt(0).toLowerCase() + HxOverrides.substr(t,1,null))]});
+ haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 544, className : "client.Main", methodName : "onMessage", customParams : [Reflect.field(data,t.charAt(0).toLowerCase() + HxOverrides.substr(t,1,null))]});
}
client_JsApi.fireEvents(data);
switch(data.type) {
diff --git a/res/css/des.css b/res/css/des.css
index 4da2005..99a2c58 100644
--- a/res/css/des.css
+++ b/res/css/des.css
@@ -1,7 +1,5 @@
@charset "utf-8";
-/* CSS Document */
-
:root {
--cl-black: hsl(140, 1%, 6%);
--cl-gray-0: hsl(140, 2%, 8%);
diff --git a/res/index.html b/res/index.html
index 29f0b1b..76fa65c 100644
--- a/res/index.html
+++ b/res/index.html
@@ -148,7 +148,7 @@
</section>
<!-- Footer -->
<footer id="footer">
- <p>Powered by <a href="https://github.com/RblSb/SyncTube" target="_blank" rel="noreferrer noopener">SyncTube</a>
+ <p>Powered by <a href="https://github.com/pinapelz/SyncTube-plus-plus" target="_blank" rel="noreferrer noopener">SyncTube++</a>
</p>
</footer>
</main>
@@ -165,9 +165,11 @@
<span id="usercount">${connection}...</span>
<ion-icon id="pause-indicator-portrait" name="play" style="display: none;"></ion-icon>
</button>
- <span>
- <button id="leader_btn" class="unselectable" title="${leaderDesc}">${leader}</button>
- <!-- Settings button -->
+ <span>
+ <button id="togglechat" title="Collapse Chat">
+ <ion-icon name="chevron-forward-circle-outline"></ion-icon>
+ </button>
+ <button id="leader_btn" class="unselectable" title="${leaderDesc}">${leader}</button>
<button id="showoptions" class="collapsed" data-toggle="collapse" data-target="#optionsList"
aria-expanded="false">
<ion-icon name="settings-sharp"></ion-icon>
diff --git a/src/client/Main.hx b/src/client/Main.hx
index e23acbe..42aec3e 100644
--- a/src/client/Main.hx
+++ b/src/client/Main.hx
@@ -211,6 +211,61 @@ class Main {
function initListeners():Void {
Buttons.init(this);
+ var openBtn:js.html.Element = js.Browser.document.getElementById("openChatBtn");
+ var openBtnFadeTimer:haxe.Timer = null;
+ if (openBtn == null) {
+ openBtn = js.Browser.document.createElement("button");
+ openBtn.id = "openChatBtn";
+ openBtn.innerText = "Expand";
+ openBtn.setAttribute("style",
+ "position: fixed; right: 0; top: 50%; transform: translateY(-50%); z-index: 10000; display: none; padding: .5rem; background: rgba(0,0,0,0.4); color: var(--foreground); border: none; border-radius: .25rem 0 0 .25rem; transition: opacity 200ms; opacity: 1;");
+ js.Browser.document.body.appendChild(openBtn);
+ }
+ final toggleChatBtn = getEl("#togglechat");
+ openBtn.onclick = e -> (cast toggleChatBtn : js.html.ButtonElement).click();
+
+ openBtn.onmouseenter = e -> {
+ openBtn.style.opacity = "1";
+ if (openBtnFadeTimer != null) openBtnFadeTimer.stop();
+ };
+ openBtn.onmouseleave = e -> {
+ if (openBtnFadeTimer != null) openBtnFadeTimer.stop();
+ openBtnFadeTimer = Timer.delay(() -> openBtn.style.opacity = "0.25", 2000);
+ };
+
+ getEl("#togglechat").onclick = e -> {
+ final chat = getEl("#chat");
+ final bodyEl = getEl("body");
+ var sizes = bodyEl.style.gridTemplateColumns;
+ if (sizes == null || sizes == "") sizes = "1fr 4px 300px";
+ var parts = sizes.split(" ");
+ final chatIndex = if (settings.isSwapped) 0 else parts.length - 1;
+ if (chat.style.display == "none") {
+ chat.style.display = "";
+ final prev = bodyEl.getAttribute("data-prev-grid");
+ if (prev != null) {
+ bodyEl.style.gridTemplateColumns = prev;
+ bodyEl.removeAttribute("data-prev-grid");
+ } else {
+ parts[chatIndex] = '${settings.chatSize}px';
+ if (parts.length > 1) parts[1] = '4px';
+ bodyEl.style.gridTemplateColumns = parts.join(" ");
+ }
+ openBtn.style.display = "none";
+ if (openBtnFadeTimer != null) openBtnFadeTimer.stop();
+ } else {
+ bodyEl.setAttribute("data-prev-grid", bodyEl.style.gridTemplateColumns);
+ parts[chatIndex] = "0px";
+ if (parts.length > 1) parts[1] = "0px";
+ bodyEl.style.gridTemplateColumns = parts.join(" ");
+ chat.style.display = "none";
+ openBtn.style.display = "";
+ openBtn.style.opacity = "1";
+ if (openBtnFadeTimer != null) openBtnFadeTimer.stop();
+ openBtnFadeTimer = Timer.delay(() -> openBtn.style.opacity = "0.25", 2000);
+ }
+ }
+
final leaderBtn = getEl("#leader_btn");
leaderBtn.onclick = toggleLeader;
leaderBtn.oncontextmenu = (e:MouseEvent) -> {
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage