diff options
| author | Pinapelz <yukais@pinapelz.com> | 2026-07-02 18:49:53 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2026-07-02 18:49:53 -0700 |
| commit | ece9cf2749ac592299a34a2cf0f24672eedbd19e (patch) | |
| tree | 811ab6991283973a3d1f2dcae127137b1c06893c | |
| parent | fe306f7eab9395ebb220b105aee4a36fa2f7f895 (diff) | |
initial synctube++ fork
Signed-off-by: Pinapelz <yukais@pinapelz.com>
| -rw-r--r-- | LICENSE | 2 | ||||
| -rw-r--r-- | README.md | 40 | ||||
| -rw-r--r-- | build/server.js | 280 | ||||
| -rw-r--r-- | res/client.js | 74 | ||||
| -rw-r--r-- | res/css/des.css | 2 | ||||
| -rw-r--r-- | res/index.html | 10 | ||||
| -rw-r--r-- | src/client/Main.hx | 55 |
7 files changed, 352 insertions, 111 deletions
@@ -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 @@ -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) -> { |
