Merge branch 'dev'
This commit is contained in:
commit
3e4b036ab5
31 changed files with 221 additions and 67 deletions
|
|
@ -9,7 +9,7 @@ Canopy
|
|||
<a href="https://git.ourfore.st/rainbownapkin/canopy/issues" target="_blank"><img src="https://git.ourfore.st/rainbownapkin/canopy/badges/issues/closed.svg"></a>
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank"><img src="https://img.shields.io/badge/License-AGPL_v3-663366.svg"></a>
|
||||
|
||||
0.1-Alpha (Panama Red) - Hotfix 2
|
||||
0.1-Alpha (Panama Red) - Hotfix 3
|
||||
=========
|
||||
|
||||
Canopy - /ˈkæ.nə.pi/:
|
||||
|
|
|
|||
|
|
@ -34,5 +34,10 @@
|
|||
"address": "toke@42069.weed",
|
||||
"pass": "CHANGE_ME"
|
||||
},
|
||||
"links":{
|
||||
"About": "/about",
|
||||
"Code": "https://git.ourfore.st/rainbownapkin/canopy",
|
||||
"HRT": "/hrt"
|
||||
},
|
||||
"aboutText":"<a href=\"https://ourfore.st/\">ourfore.st</a> is the one and only original canopy instance. Setup, ran, and administered by rainbownapkin herself. This site exists to provide a featureful, preformant, and comfy replacement for the TTN community."
|
||||
}
|
||||
|
|
@ -65,6 +65,12 @@
|
|||
"address": "toke@42069.weed",
|
||||
"pass": "CHANGE_ME"
|
||||
},
|
||||
//Provides customizable links for navbar
|
||||
"links":{
|
||||
"About": "/about",
|
||||
"Code": "https://git.ourfore.st/rainbownapkin/canopy",
|
||||
"HRT": "/hrt"
|
||||
},
|
||||
//Fills the 'about ${instanceName}' section on the /about page, lets users know about your specific instance
|
||||
"aboutText":"<a href=\"https://ourfore.st/\">ourfore.st</a> is the one and only original canopy instance. Setup, ran, and administered by rainbownapkin herself. This site exists to provide a featureful, preformant, and comfy replacement for the TTN community."
|
||||
}
|
||||
12
package.json
12
package.json
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"name": "canopy-of-alpha",
|
||||
"version": "0.1.2",
|
||||
"canopyDisplayVersion": "0.1-Alpha (Panama Red) - Hotfix 2",
|
||||
"version": "0.1.3",
|
||||
"canopyDisplayVersion": "0.1-Alpha (Panama Red) - Hotfix 3",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.1.1",
|
||||
"altcha": "^1.0.7",
|
||||
"altcha": "^2.3.0",
|
||||
"altcha-lib": "^1.2.0",
|
||||
"argon2": "^0.44.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"connect-mongo": "^5.1.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
|
|
@ -20,8 +20,8 @@
|
|||
"hls.js": "^1.6.2",
|
||||
"mongoose": "^8.4.3",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^7.0.9",
|
||||
"socket.io": "^4.8.1",
|
||||
"nodemailer": "^8.0.7",
|
||||
"socket.io": "^4.2.0",
|
||||
"youtube-dl-exec": "^3.0.20"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class chatPreprocessor{
|
|||
|
||||
//If we don't pass sanatization/validation turn this car around
|
||||
if(!this.sanatizeCommand(commandObj)){
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
//split the command
|
||||
|
|
|
|||
|
|
@ -26,5 +26,5 @@ module.exports = async function(req, res){
|
|||
res.status(404);
|
||||
|
||||
//Render page
|
||||
return res.render('404', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
|
||||
return res.render('404', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
|
||||
}
|
||||
|
|
@ -24,5 +24,5 @@ const csrfUtils = require('../utils/csrfUtils');
|
|||
//register page functions
|
||||
module.exports.get = async function(req, res){
|
||||
//Render page
|
||||
return res.render('about', {aboutText: config.aboutText, instance: config.instanceName, user: req.session.user, version: package.canopyDisplayVersion, csrfToken: csrfUtils.generateToken(req)});
|
||||
return res.render('about', {aboutText: config.aboutText, instance: config.instanceName, links: config.links, user: req.session.user, version: package.canopyDisplayVersion, csrfToken: csrfUtils.generateToken(req)});
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ module.exports.get = async function(req, res){
|
|||
|
||||
//Render out the page
|
||||
return res.render('adminPanel', {
|
||||
instance: config.instanceName,
|
||||
instance: config.instanceName, links: config.links,
|
||||
user: req.session.user,
|
||||
rankEnum: permissionModel.rankEnum,
|
||||
chanGuide: chanGuide,
|
||||
|
|
|
|||
|
|
@ -22,5 +22,5 @@ const csrfUtils = require('../utils/csrfUtils');
|
|||
|
||||
//channel functions
|
||||
module.exports.get = function(req, res){
|
||||
res.render('channel', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
|
||||
res.render('channel', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ module.exports.get = async function(req, res){
|
|||
throw loggerUtils.exceptionSmith("Channel not found.", "queue");
|
||||
}
|
||||
|
||||
return res.render('channelSettings', {instance: config.instanceName, user: req.session.user, channel: chanDB, reqRank, rankEnum: permissionModel.rankEnum, csrfToken: csrfUtils.generateToken(req), unescape: validator.unescape});
|
||||
return res.render('channelSettings', {instance: config.instanceName, links: config.links, user: req.session.user, channel: chanDB, reqRank, rankEnum: permissionModel.rankEnum, csrfToken: csrfUtils.generateToken(req), unescape: validator.unescape});
|
||||
}catch(err){
|
||||
return exceptionHandler(res, err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,18 +40,18 @@ module.exports.get = async function(req, res){
|
|||
|
||||
//If we have an invalid request
|
||||
if(requestDB == null){
|
||||
return res.render('emailChange', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: false});
|
||||
return res.render('emailChange', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: false});
|
||||
}
|
||||
|
||||
//Speak of our success (don't wait for the emails to be sent)
|
||||
res.render('emailChange', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: true});
|
||||
res.render('emailChange', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: true});
|
||||
|
||||
//Consume the request
|
||||
await requestDB.consume();
|
||||
}else{
|
||||
return res.render('emailChange', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: false});
|
||||
return res.render('emailChange', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: false});
|
||||
}
|
||||
}catch(err){
|
||||
return res.render('emailChange', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: false});
|
||||
return res.render('emailChange', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req), valid: false});
|
||||
}
|
||||
}
|
||||
|
|
@ -24,5 +24,5 @@ const csrfUtils = require('../utils/csrfUtils');
|
|||
//register page functions
|
||||
module.exports.get = async function(req, res){
|
||||
//Render page
|
||||
return res.render('hrt', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
|
||||
return res.render('hrt', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ const {exceptionHandler, errorHandler} = require('../utils/loggerUtils');
|
|||
module.exports.get = async function(req, res){
|
||||
try{
|
||||
const chanGuide = await channelModel.getChannelList();
|
||||
return res.render('index', {instance: config.instanceName, user: req.session.user, chanGuide: chanGuide, csrfToken: csrfUtils.generateToken(req), unescape: validator.unescape});
|
||||
return res.render('index', {instance: config.instanceName, links: config.links, user: req.session.user, chanGuide: chanGuide, csrfToken: csrfUtils.generateToken(req), unescape: validator.unescape});
|
||||
}catch(err){
|
||||
return exceptionHandler(res, err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ module.exports.get = async function(req, res){
|
|||
//if we have previous attempts for this user
|
||||
if(attempts != null){
|
||||
if(attempts.count > sessionUtils.maxAttempts){
|
||||
return res.render('lockedAccount', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
|
||||
return res.render('lockedAccount', {instance: config.instanceName, links: config.links, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
|
||||
}
|
||||
|
||||
//If the users login's are being throttled
|
||||
|
|
@ -56,16 +56,16 @@ module.exports.get = async function(req, res){
|
|||
const challenge = await altchaUtils.genCaptcha(difficulty, user);
|
||||
|
||||
//Render page
|
||||
return res.render('login', {instance: config.instanceName, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
|
||||
return res.render('login', {instance: config.instanceName, links: config.links, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
|
||||
}
|
||||
//otherwise
|
||||
}else{
|
||||
//Render generic page
|
||||
return res.render('login', {instance: config.instanceName, user: req.session.user, challenge: null, csrfToken: csrfUtils.generateToken(req)});
|
||||
return res.render('login', {instance: config.instanceName, links: config.links, user: req.session.user, challenge: null, csrfToken: csrfUtils.generateToken(req)});
|
||||
}
|
||||
//if we received invalid input
|
||||
}else{
|
||||
//Render pretend nothing happened, send out a generic page
|
||||
return res.render('login', {instance: config.instanceName, user: req.session.user, challenge: null, csrfToken: csrfUtils.generateToken(req)});
|
||||
return res.render('login', {instance: config.instanceName, links: config.links, user: req.session.user, challenge: null, csrfToken: csrfUtils.generateToken(req)});
|
||||
}
|
||||
}
|
||||
|
|
@ -27,5 +27,5 @@ module.exports.get = async function(req, res){
|
|||
const challenge = await altchaUtils.genCaptcha();
|
||||
|
||||
//Render page
|
||||
return res.render('migrate', {instance: config.instanceName, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
|
||||
return res.render('migrate', {instance: config.instanceName, links: config.links, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
|
||||
}
|
||||
|
|
@ -27,5 +27,5 @@ module.exports.get = async function(req, res){
|
|||
const challenge = await altchaUtils.genCaptcha();
|
||||
|
||||
//render the page
|
||||
return res.render('newChannel', {instance: config.instanceName, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
|
||||
return res.render('newChannel', {instance: config.instanceName, links: config.links, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
|
||||
}
|
||||
|
|
@ -19,5 +19,5 @@ const config = require('../../../config.json');
|
|||
|
||||
//popout panel container functions
|
||||
module.exports.get = async function(req, res){
|
||||
res.render('popoutContainer', {instance: config.instanceName});
|
||||
res.render('popoutContainer', {instance: config.instanceName, links: config.links});
|
||||
}
|
||||
|
|
@ -47,11 +47,11 @@ module.exports.get = async function(req, res){
|
|||
*/
|
||||
|
||||
//Render page
|
||||
return res.render('passwordReset', {instance: config.instanceName, user: req.session.user, challenge, token, csrfToken: csrfUtils.generateToken(req)});
|
||||
return res.render('passwordReset', {instance: config.instanceName, links: config.links, user: req.session.user, challenge, token, csrfToken: csrfUtils.generateToken(req)});
|
||||
//If we didn't get a valid token
|
||||
}else{
|
||||
//otherwise render generic page
|
||||
return res.render('passwordReset', {instance: config.instanceName, user: req.session.user, challenge, token: null, csrfToken: csrfUtils.generateToken(req)});
|
||||
return res.render('passwordReset', {instance: config.instanceName, links: config.links, user: req.session.user, challenge, token: null, csrfToken: csrfUtils.generateToken(req)});
|
||||
}
|
||||
}catch(err){
|
||||
return exceptionHandler(res, err);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ module.exports.get = async function(req, res){
|
|||
const presence = await presenceUtils.getPresence(profile.user);
|
||||
|
||||
res.render('profile', {
|
||||
instance: config.instanceName,
|
||||
instance: config.instanceName, links: config.links,
|
||||
user: req.session.user,
|
||||
profile,
|
||||
selfProfile,
|
||||
|
|
@ -52,7 +52,7 @@ module.exports.get = async function(req, res){
|
|||
});
|
||||
}else{
|
||||
res.render('profile', {
|
||||
instance: config.instanceName,
|
||||
instance: config.instanceName, links: config.links,
|
||||
user: req.session.user,
|
||||
profile: null,
|
||||
selfProfile: false,
|
||||
|
|
|
|||
|
|
@ -27,5 +27,5 @@ module.exports.get = async function(req, res){
|
|||
const challenge = await altchaUtils.genCaptcha();
|
||||
|
||||
//Render page
|
||||
return res.render('register', {instance: config.instanceName, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
|
||||
return res.render('register', {instance: config.instanceName, links: config.links, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)});
|
||||
}
|
||||
|
|
@ -14,6 +14,9 @@ GNU Affero General Public License for more details.
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
//Config
|
||||
const config = require('../../config.json');
|
||||
|
||||
//NPM Imports
|
||||
const validator = require('validator');//No express here, so regular validator it is!
|
||||
const {sanitizeUrl} = require("@braintree/sanitize-url");
|
||||
|
|
@ -32,6 +35,15 @@ module.exports.cache = new Map();
|
|||
module.exports.markLink = async function(dirtyLink){
|
||||
const link = sanitizeUrl(dirtyLink);
|
||||
|
||||
//If this link is referencing this web server
|
||||
if(link.match(new RegExp(`^${config.protocol}://${config.domain}`)) != null){
|
||||
//Lazily return it as a good link, since we know it'll at least return a good 404 page XP
|
||||
return {
|
||||
link,
|
||||
type: "link"
|
||||
}
|
||||
}
|
||||
|
||||
//Check link cache for the requested link
|
||||
const cachedLink = module.exports.cache.get(link);
|
||||
|
||||
|
|
@ -105,4 +117,4 @@ module.exports.markLink = async function(dirtyLink){
|
|||
|
||||
//return the link
|
||||
return linkObj;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ module.exports.getMediaType = async function(dirtyURL){
|
|||
}
|
||||
|
||||
//If we have link to a resource from archive.org
|
||||
if(match = url.match(/archive\.org\/(?:details|download)\/([a-zA-Z0-9\/._-\s\%]+)/)){
|
||||
if(match = url.match(/archive\.org\/(?:details|download)\/(.+)/)){
|
||||
//return internet archive upload id and filepath
|
||||
return {
|
||||
type: "ia",
|
||||
|
|
|
|||
|
|
@ -20,13 +20,24 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
|||
</span>
|
||||
<span class="navbar-item" id="right-controls">
|
||||
<% if(user){ %>
|
||||
<p class="navbar-item">Welcome, <a class="navbar-item" id="username" href="/profile"><%= user.user %></a> - <% if(user.rank == "admin"){ %><a href="/adminPanel" title="Admin Panel" class="bi bi-server navbar-item"></a> - <% } %> <a class="navbar-item" href="/about">About</a> - <a class="navbar-item" href="javascript:" id="logout-button">Logout</a></p>
|
||||
<p class="navbar-item">Welcome, <a class="navbar-item" id="username" href="/profile"><%= user.user %></a> - <% if(user.rank == "admin"){ %>
|
||||
<a href="/adminPanel" title="Admin Panel" class="bi bi-server navbar-item"></a> -
|
||||
<% } %>
|
||||
|
||||
<% for(link of Object.keys(links)){ %>
|
||||
<a target="_blank" class="navbar-item" href="<%- links[link] %>"><%= link %></a> -
|
||||
<% } %>
|
||||
|
||||
<a class="navbar-item" href="javascript:" id="logout-button">Logout</a></p>
|
||||
<% }else{ %>
|
||||
<p class="navbar-item">Remember Me:</p>
|
||||
<input class="navbar-item login-prompt" id="remember-me" type="checkbox">
|
||||
<input class="navbar-item login-prompt" id="username-prompt" placeholder="username">
|
||||
<input class="navbar-item login-prompt" id="password-prompt" placeholder="password" type="password">
|
||||
<p class="navbar-item"><a class="navbar-item" href="javascript:" id="login-button">Login</a> - <a class="navbar-item" href="/passwordReset">Forgot Password</a> - <a class="navbar-item" href="/register">Register</a> - <a class="navbar-item" href="/about">About</a></p>
|
||||
<p class="navbar-item"><a class="navbar-item" href="javascript:" id="login-button">Login</a> - <a class="navbar-item" href="/passwordReset">Forgot Password</a> - <a class="navbar-item" href="/register">Register</a>
|
||||
<% for(link of Object.keys(links)){ %>
|
||||
- <a target="_blank" class="navbar-item" href="<%- links[link] %>"><%= link %></a>
|
||||
<% } %></p>
|
||||
<% } %>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
|||
<link rel="stylesheet" type="text/css" href="/css/panel/settings.css">
|
||||
<div id="settings-panel">
|
||||
<h2>Client Settings</h2>
|
||||
<h4>Player Settings</h4>
|
||||
<h4>Playeback Settings</h4>
|
||||
<span id="settings-panel-youtube-source" class="settings-panel-setting">
|
||||
<p>Youtube Player Type: </p>
|
||||
<select>
|
||||
|
|
@ -40,11 +40,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
|||
<p>Syncronization Delta: </p>
|
||||
<input type="number">
|
||||
</span>
|
||||
<h4>Chat Settings</h4>
|
||||
<h4>Display Settings</h4>
|
||||
<span id="settings-panel-min-chat-width" class="settings-panel-setting">
|
||||
<p>Aspect-Ratio Lock Chat Width Minimum: </p>
|
||||
<p>Chat Width Minimum While Locked to Aspect Ratio: </p>
|
||||
<input type="number">
|
||||
</span>
|
||||
<span id="settings-panel-disable-portrait" class="settings-panel-setting">
|
||||
<p>Disable Portrait/Mobile Layout: </p>
|
||||
<input type="checkbox">
|
||||
</span>
|
||||
<h4>Notification Settings</h4>
|
||||
<span id="settings-panel-ping-on-pm-rx" class="settings-panel-setting">
|
||||
<p>Play Sound for received PMs: </p>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<filter id="strikethroughFilter">
|
||||
<filter id="strikethroughFilter" primitiveUnits="objectBoundingBox">
|
||||
<feFlood
|
||||
result="floodFill"
|
||||
x="0"
|
||||
y="50%"
|
||||
height="2%"
|
||||
width="100%"
|
||||
height="1"
|
||||
x="0"
|
||||
y="49%"
|
||||
flood-color="black"
|
||||
flood-opacity="1"
|
||||
/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 376 B After Width: | Height: | Size: 413 B |
|
|
@ -288,6 +288,16 @@ class channel{
|
|||
//Set Chat Box Width minimum while Locked to Aspect-Ratio
|
||||
this.chatBox.chatWidthMinimum = value / 100;
|
||||
return;
|
||||
case 'disablePortrait':
|
||||
//If the chat isn't loaded
|
||||
if(this.chatBox == null){
|
||||
//We're fuckin' done here
|
||||
return;
|
||||
}
|
||||
|
||||
//Toggle portrait mode
|
||||
this.chatBox.togglePortrait();
|
||||
return;
|
||||
case 'userlistHidden':
|
||||
//If the userlist class isn't loaded in yet
|
||||
if(this.userList == null){
|
||||
|
|
@ -326,7 +336,8 @@ class channel{
|
|||
["rxPMSound", 'unread'],
|
||||
["txPMSound", false],
|
||||
["newSeshSound", true],
|
||||
["endSeshSound", true]
|
||||
["endSeshSound", true],
|
||||
["disablePortrait", false]
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ class chatBox{
|
|||
*/
|
||||
this.autoScroll = true;
|
||||
|
||||
/**
|
||||
* Whether or not the screen is currently in portrait mode
|
||||
*/
|
||||
this.portrait = false;
|
||||
|
||||
/**
|
||||
* Chat-Width Minimum while sized to media Aspect-Ratio
|
||||
*/
|
||||
|
|
@ -74,6 +79,11 @@ class chatBox{
|
|||
this.chatPostprocessor = new chatPostprocessor(client);
|
||||
|
||||
//Element Nodes
|
||||
/**
|
||||
* Channel Div
|
||||
*/
|
||||
this.channelDiv = document.querySelector("#channel-flexbox");
|
||||
|
||||
/**
|
||||
* Chat Panel Container Div
|
||||
*/
|
||||
|
|
@ -473,8 +483,14 @@ class chatBox{
|
|||
resizeAspect(event){
|
||||
const playerHidden = this.client.player.playerDiv.style.display == "none";
|
||||
|
||||
//If the aspect is locked and the player is hidden
|
||||
if(this.aspectLock && !playerHidden){
|
||||
//If window is taller than wide and not in portrait mode, or vice-versa
|
||||
if(this.portrait != (window.innerWidth <= window.innerHeight)){
|
||||
//Toggle portrait mode
|
||||
this.togglePortrait();
|
||||
}
|
||||
|
||||
//If the aspect is locked/the window is portrait and the player isn't hidden
|
||||
if((this.aspectLock || this.portrait) && !playerHidden){
|
||||
this.sizeToAspect();
|
||||
//Otherwise
|
||||
}else{
|
||||
|
|
@ -490,24 +506,72 @@ class chatBox{
|
|||
* Re-sizes chat box relative to media aspect ratio
|
||||
*/
|
||||
sizeToAspect(){
|
||||
//If the chat panel is visible
|
||||
if(this.chatPanel.style.display != "none"){
|
||||
var targetVidWidth = this.client.player.getRatio() * this.chatPanel.getBoundingClientRect().height;
|
||||
const targetChatWidth = window.innerWidth - targetVidWidth;
|
||||
//This should be changeable in settings later on, for now it defaults to 20%
|
||||
const limit = window.innerWidth * this.chatWidthMinimum;
|
||||
//If our window width is more than or equal to window height (not portrait mode)
|
||||
if(!this.portrait){
|
||||
//Get target video width by multiplying media ratio by window height
|
||||
var targetVidWidth = this.client.player.getRatio() * this.chatPanel.getBoundingClientRect().height;
|
||||
//Get target chat width my subtracting target media width from total window width
|
||||
const targetChatWidth = window.innerWidth - targetVidWidth;
|
||||
//This should be changeable in settings later on, for now it defaults to 20%
|
||||
const limit = window.innerWidth * this.chatWidthMinimum;
|
||||
|
||||
//Set width to target or 20vw depending on whether or not we've hit the width limit
|
||||
this.chatPanel.style.flexBasis = targetChatWidth > limit ? `${targetChatWidth}px` : `${this.chatWidthMinimum * 100}vw`;
|
||||
//Set width to target or 20vw depending on whether or not we've hit the width limit
|
||||
this.chatPanel.style.flexBasis = targetChatWidth > limit ? `${targetChatWidth}px` : `${this.chatWidthMinimum * 100}vw`;
|
||||
|
||||
//Fix busted layout
|
||||
var pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width;
|
||||
this.chatPanel.style.flexBasis = `${this.chatPanel.getBoundingClientRect().width + pageBreak}px`;
|
||||
}else{
|
||||
//Calculate target video height from media aspect ratio and window width
|
||||
var targetVidHeight = window.innerWidth / this.client.player.getRatio();
|
||||
//Calculate target chat height from the difference between the channel div height and the target video height
|
||||
var targetChatHeight = this.channelDiv.getBoundingClientRect().height - targetVidHeight;
|
||||
|
||||
//Set div heights accordingly
|
||||
this.client.player.playerDiv.style.height = `${targetVidHeight}px`;
|
||||
this.chatPanel.style.height = `${targetChatHeight}px`;
|
||||
}
|
||||
|
||||
//Fix busted layout
|
||||
var pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width;
|
||||
this.chatPanel.style.flexBasis = `${this.chatPanel.getBoundingClientRect().width + pageBreak}px`;
|
||||
//This sometimes gets called before userList ahs been initiated :p
|
||||
if(this.client.userList != null){
|
||||
this.client.userList.clickDragger.fixCutoff();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
togglePortrait(){
|
||||
//If our window width is more than or equal to window height (not portrait mode), or portrait mode is on while its supposed to be disabled
|
||||
if(window.innerWidth >= window.innerHeight || (localStorage.getItem("disablePortrait") == 'true' && this.portrait)){
|
||||
//Disable portrait CSS modifiers
|
||||
this.channelDiv.style.flexDirection = "row";
|
||||
this.clickDragger.enabled = true;
|
||||
this.chatPanel.style.width = "";
|
||||
this.client.player.playerDiv.style.height = "";
|
||||
this.chatPanel.style.height = "";
|
||||
|
||||
//Disable portrait behavior modifiers
|
||||
this.portrait = false;
|
||||
|
||||
//resize player in-case of empty flex basis
|
||||
this.resizeAspect();
|
||||
|
||||
//Otherwise, if portrait mode is enabled
|
||||
}else if(localStorage.getItem("disablePortrait") != 'true'){
|
||||
//Modify CSS for portrait displays
|
||||
this.channelDiv.style.flexDirection = "column";
|
||||
this.clickDragger.enabled = false;
|
||||
this.chatPanel.style.width = "100%";
|
||||
this.chatPanel.style.flexBasis = "";
|
||||
|
||||
//Enable portrait behavior modifiers
|
||||
this.portrait = true;
|
||||
|
||||
//resize player to correct height
|
||||
this.resizeAspect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles Chat Box UX
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class chatPostprocessor{
|
|||
//with negative lookaheads to exclude file seperators so we don't split link placeholders, dashes so we dont split usernames and other things, and accented characters to keep those from splitting boundries too
|
||||
//Also split by any invisble whitespace as a crutch to handle mushed links/emotes
|
||||
//If we can one day figure out how to split non-repeating special chars instead of special chars with whitespace, that would be perf, unfortunately my brain hasn't rotted enough to understand regex like that just yet.
|
||||
const splitString = utils.unescapeEntities(this.rawData.msg).split(/(?<!-)(?<!␜)(?=\w)\b|(?!-|[\u00C0-\u017F])(?<=\w)\b|(?=\s)\B|(?<=\s)\B|ㅤ/g);
|
||||
const splitString = utils.unescapeEntities(this.rawData.msg).split(/(?<!-)(?<!␜)(?=\w)\b|(?!-|[\u00C0-\u017F])(?<=\w)\b|(?=\s)\B|(?<=\s)\B|/g);
|
||||
|
||||
//for each word in the splitstring
|
||||
splitString.forEach((string) => {
|
||||
|
|
@ -474,7 +474,7 @@ class chatPostprocessor{
|
|||
//After eight characters
|
||||
if(charIndex > 8){
|
||||
//Push an invisible line-break character between every character
|
||||
wordArray.push("ㅤ");
|
||||
wordArray.push("");
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -116,14 +116,14 @@ class commandPreprocessor{
|
|||
*/
|
||||
processEmotes(){
|
||||
//inject invisible whitespace in-between emotes to prevent from mushing links together
|
||||
this.message = this.message.replaceAll('][',']ㅤ[');
|
||||
this.message = this.message.replaceAll('][','][');
|
||||
|
||||
//For each list of emotes
|
||||
Object.keys(this.emotes).forEach((key) => {
|
||||
//For each emote in the current list
|
||||
this.emotes[key].forEach((emote) => {
|
||||
//Inject emote links into the message, pad with invisible whitespace to keep link from getting mushed
|
||||
this.message = this.message.replaceAll(`[${emote.name}]`, `ㅤ${emote.link}ㅤ`);
|
||||
this.message = this.message.replaceAll(`[${emote.name}]`, `${emote.link}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -135,13 +135,13 @@ class commandPreprocessor{
|
|||
//Strip out file seperators in-case the user is being a smart-ass
|
||||
this.message = this.message.replaceAll('␜','');
|
||||
//Split message by links
|
||||
var splitMessage = this.message.split(/(https?:\/\/[^\sㅤ]+)/g);
|
||||
var splitMessage = this.message.split(/(https?:\/\/[^\s]+)/g);
|
||||
//Create an empty array to hold links
|
||||
this.links = [];
|
||||
|
||||
splitMessage.forEach((chunk, chunkIndex) => {
|
||||
//For each chunk that is a link
|
||||
if(chunk.match(/(https?:\/\/[^\sㅤ]+)/g)){
|
||||
if(chunk.match(/(https?:\/\/[^\s]+)/g)){
|
||||
//I looked online for obscure characters that no one would use to prevent people from chatting embed placeholders
|
||||
//Then I found this fucker, turns out it's literally made for the job lmao (even if it was originally intended for paper/magnetic tape)
|
||||
//Replace link with indexed placeholder
|
||||
|
|
|
|||
|
|
@ -1233,6 +1233,11 @@ class queuePanel extends panelObj{
|
|||
//Convert start epoch to JS date object
|
||||
const started = new Date(nowPlaying.startTime);
|
||||
|
||||
//If the date the scheduler is set to isn't within the livestream
|
||||
if(!utils.isSameDate(started, this.day) && !utils.dateWithinRange(started, new Date(), this.day)){
|
||||
return;
|
||||
}
|
||||
|
||||
//If this started today
|
||||
if(utils.isSameDate(this.day, started)){
|
||||
//Set entryDiv top-border location based on start time
|
||||
|
|
@ -1246,15 +1251,29 @@ class queuePanel extends panelObj{
|
|||
entryDiv.style.top = `${this.offsetByDate(dawn)}px`;
|
||||
|
||||
//Apply rest of the styling rules for items that started yestarday
|
||||
entryDiv.classList.add('started-yesterday')
|
||||
entryDiv.classList.add('started-yesterday');
|
||||
}
|
||||
|
||||
//Create entry title
|
||||
const entryTitle = document.createElement('p');
|
||||
entryTitle.textContent = utils.unescapeEntities(nowPlaying.title);
|
||||
|
||||
//Set entry div bottom-border location based on current time, round to match time marker
|
||||
entryDiv.style.bottom = `${Math.round(this.offsetByDate(date, true))}px`
|
||||
|
||||
//If we're looking at today
|
||||
if(utils.isSameDate(this.day, new Date())){
|
||||
//Set entry div bottom-border location based on current time, round to match time marker
|
||||
entryDiv.style.bottom = `${Math.round(this.offsetByDate(date, true))}px`;
|
||||
}else{
|
||||
//Get midnight
|
||||
const dusk = new Date();
|
||||
dusk.setHours(23,59,59,999);
|
||||
|
||||
//Set stream to continue to run into the next morning
|
||||
entryDiv.style.bottom = `${Math.round(this.offsetByDate(dusk, true))}px`;
|
||||
|
||||
//Apply rest of the styling rules for items that end after today
|
||||
entryDiv.classList.add('ends-tomorrow');
|
||||
}
|
||||
|
||||
//Assembly entryDiv
|
||||
entryDiv.appendChild(entryTitle);
|
||||
|
|
@ -1285,8 +1304,11 @@ class queuePanel extends panelObj{
|
|||
//Append entry div to queue container
|
||||
this.queueContainer.appendChild(entryDiv);
|
||||
}else{
|
||||
//Update existing entry, round offset to match time marker
|
||||
staleEntry.style.bottom = `${Math.round(this.offsetByDate(date, true))}px`
|
||||
//If we're looking at today
|
||||
if(utils.isSameDate(this.day, new Date())){
|
||||
//Update existing entry, round offset to match time marker
|
||||
staleEntry.style.bottom = `${Math.round(this.offsetByDate(date, true))}px`
|
||||
}
|
||||
}
|
||||
|
||||
//Keep tooltip date seperate so it re-calculates live duration properly
|
||||
|
|
@ -1541,7 +1563,7 @@ class reschedulePopup extends schedulePopup{
|
|||
this.media = media;
|
||||
}
|
||||
|
||||
startSesh(event){
|
||||
schedule(event){
|
||||
//If we clicked or hit enter
|
||||
if(event.key == null || event.key == "Enter"){
|
||||
//Get localized input date
|
||||
|
|
|
|||
|
|
@ -62,6 +62,11 @@ class settingsPanel extends panelObj{
|
|||
*/
|
||||
this.chatWidthMinimum = this.panelDocument.querySelector("#settings-panel-min-chat-width input");
|
||||
|
||||
/**
|
||||
* Disable Portrait/Mobile Layout
|
||||
*/
|
||||
this.disablePortrait = this.panelDocument.querySelector("#settings-panel-disable-portrait input");
|
||||
|
||||
/**
|
||||
* Audible Ping on PM Recieved
|
||||
*/
|
||||
|
|
@ -99,6 +104,7 @@ class settingsPanel extends panelObj{
|
|||
this.liveSyncTolerance.addEventListener('change', this.updateLiveSyncTolerance.bind(this));
|
||||
this.syncDelta.addEventListener('change', this.updateSyncDelta.bind(this));
|
||||
this.chatWidthMinimum.addEventListener('change', this.updateChatWidthMinimum.bind(this));
|
||||
this.disablePortrait.addEventListener('change', this.updateDisablePortrait.bind(this));
|
||||
this.rxPMSound.addEventListener('change', this.updateRXPMSound.bind(this));
|
||||
this.txPMSound.addEventListener('change', this.updateTXPMSound.bind(this));
|
||||
this.newSeshSound.addEventListener('change', this.updateNewPMSeshSound.bind(this));
|
||||
|
|
@ -115,6 +121,7 @@ class settingsPanel extends panelObj{
|
|||
this.liveSyncTolerance.value = localStorage.getItem("liveSyncTolerance");
|
||||
this.syncDelta.value = localStorage.getItem("syncDelta");
|
||||
this.chatWidthMinimum.value = localStorage.getItem("chatWidthMin");
|
||||
this.disablePortrait.checked = localStorage.getItem("disablePortrait") == 'true';
|
||||
this.rxPMSound.value = localStorage.getItem('rxPMSound');
|
||||
this.txPMSound.checked = localStorage.getItem('txPMSound') == 'true';
|
||||
this.newSeshSound.checked = localStorage.getItem('newSeshSound') == 'true';
|
||||
|
|
@ -218,11 +225,20 @@ class settingsPanel extends panelObj{
|
|||
client.processConfig("chatWidthMin", this.chatWidthMinimum.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles change toggle of disable/enable portrait
|
||||
*/
|
||||
updateDisablePortrait(){
|
||||
localStorage.setItem('disablePortrait', this.disablePortrait.checked);
|
||||
client.processConfig("disablePortrait", this.disablePortrait.checked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles changes to RX PM Sound setting
|
||||
*/
|
||||
updateRXPMSound(){
|
||||
localStorage.setItem('rxPMSound', this.rxPMSound.value);
|
||||
client.processConfig("rxPMSound", this.rxPMSound.value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -230,6 +246,7 @@ class settingsPanel extends panelObj{
|
|||
*/
|
||||
updateTXPMSound(){
|
||||
localStorage.setItem('txPMSound', this.txPMSound.checked);
|
||||
client.processConfig("txPMSound", this.txPMSound.checked);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -237,6 +254,7 @@ class settingsPanel extends panelObj{
|
|||
*/
|
||||
updateNewPMSeshSound(){
|
||||
localStorage.setItem('newSeshSound', this.newSeshSound.checked);
|
||||
client.processConfig("newSeshSound", this.newSeshSound.checked);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -244,5 +262,6 @@ class settingsPanel extends panelObj{
|
|||
*/
|
||||
updateEndPMSeshSound(){
|
||||
localStorage.setItem('endSeshSound', this.endSeshSound.checked);
|
||||
client.processConfig("endSeshSound", this.endSeshSound.checked);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue