/*Canopy - The next generation of stoner streaming software Copyright (C) 2024 Rainbownapkin and the TTN Community This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 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 .*/ class canopyUtils{ constructor(){ this.ajax = new canopyAjaxUtils(); this.ux = new canopyUXUtils(); } } class canopyUXUtils{ constructor(){ } //Update this and popup class to use nodes //and display multiple errors in one popup displayResponseError(body){ const errors = body.errors; errors.forEach((err)=>{ new canopyUXUtils.popup(`

Server Error:


${err.msg}

`); }); } //We should really move this over to uxutils along with newrow & newtable functions newTableCell(content, firstCol = false){ //Create a new 'td' element const cell = document.createElement('td'); cell.classList.add("admin-list-entry","admin-list-entry-item"); //If it's not the first column, mention it! if(!firstCol){ cell.classList.add("not-first-col"); } //check for arrays if(content.forEach == null){ //add single items addContent(content); }else{ //Crawl through content array content.forEach((item)=>{ //add each item addContent(item); }); } //return the resulting cell return cell; function addContent(ct){ //If we're adding as node if(ct.cloneNode != null){ //append it like it's a node cell.appendChild(ct); }else{ //otherwise use it as innerHTML cell.innerHTML = ct; } } } newTableRow(cellContent){ //Create an empty table row to hold the cells const entryRow = document.createElement('tr'); entryRow.classList.add("admin-list-entry"); entryRow.appendChild(this.newTableCell(cellContent[0], true)); cellContent.forEach((content, i)=>{ if(i > 0){ entryRow.append(this.newTableCell(content)); } }); return entryRow; } static popup = class{ constructor(content, ajaxPopup = false, cb){ //Define non-popup node values this.content = content; this.ajaxPopup = ajaxPopup; this.cb = cb; //define popup nodes this.createPopup(); //fill popup nodes this.fillPopupContent(); } createPopup(){ this.popupBacker = document.createElement('div'); this.popupBacker.classList.add('popup-backer'); this.popupDiv = document.createElement('div'); this.popupDiv.classList.add('popup-div'); this.closeIcon = document.createElement('i'); this.closeIcon.classList.add('bi-x','popup-close-icon'); this.closeIcon.addEventListener("click", this.closePopup.bind(this)); this.contentDiv = document.createElement('div'); this.contentDiv.classList.add('popup-content-div'); this.popupDiv.appendChild(this.closeIcon); this.popupDiv.appendChild(this.contentDiv); } async fillPopupContent(){ if(this.ajaxPopup){ this.contentDiv.innerHTML = await utils.ajax.getPopup(this.content); }else{ this.contentDiv.innerHTML = this.content; } //display popup nodes this.displayPopup(); if(this.cb){ //Callbacks are kinda out of vogue, but there really isn't a good way to handle asynchronously constructed objects/classes this.cb(); } } displayPopup(){ document.body.prepend(this.popupDiv); document.body.prepend(this.popupBacker); } closePopup(){ this.popupDiv.remove(); this.popupBacker.remove(); } } static clickDragger = class{ constructor(handle, element, leftHandle = true, parent){ //Pull needed nodes this.handle = document.querySelector(handle); this.element = document.querySelector(element); //True while dragging this.dragLock = false; //Come to the ~~dark~~ left side this.leftHandle = leftHandle //True when the user is actively breaking the screen by dragging too far off to the side this.breakingScreen = false; //we put a click dragger in yo click dragger so you could click and drag while you click and drag this.parent = parent; //Setup our event listeners this.setupInput(); } setupInput(){ this.handle.addEventListener("mousedown", this.startDrag.bind(this)); this.element.parentElement.addEventListener("mouseup", this.endDrag.bind(this)); this.element.parentElement.addEventListener("mousemove", this.drag.bind(this)); } startDrag(event){ //we are now dragging this.dragLock = true; //Keep user from selecting text or changing cursor as we drag window.document.body.style.userSelect = "none"; window.document.body.style.cursor = "ew-resize"; } endDrag(event){ //we're no longer dragging this.dragLock = false; //This is only relevant when dragging, keeping this on can cause issues after content changes this.breakingScreen = false; //Return cursor to normal, and allow user to select text again window.document.body.style.userSelect = "auto"; window.document.body.style.cursor = "auto"; } drag(event){ if(this.dragLock){ if(this.leftHandle){ //get difference between mouse and right edge of element var difference = this.element.getBoundingClientRect().right - event.clientX; }else{ //get difference between mouse and left edge of element var difference = event.clientX - this.element.getBoundingClientRect().left; } //check if we have a scrollbar because we're breaking shit var pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width; //if we're not breaking the page, or we're moving left if((!this.breakingScreen && pageBreak <= 0) || event.clientX < this.handle.getBoundingClientRect().left){ //Apply difference to width this.element.style.width = `${this.calcWidth(difference)}vw`; //If we let go here, the width isn't breaking anything so there's nothing to fix. this.breakingScreen = false; }else{ //call fixCutoff with standalone mode off, and a pre-calculated pageBreak if(!this.breakingScreen){ this.fixCutoff(false, pageBreak); } } } } calcWidth(px){ return (px / window.innerWidth) * 100; } fixCutoff(standalone = true, pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width){ //Fix the page width this.element.style.width = `${this.calcWidth(this.element.getBoundingClientRect().width + pageBreak)}vw`; //If we're calling this outside of drag() (regardless of draglock unless set otherwise) if(standalone){ //call end drag to finish the job this.endDrag(); }else{ //We should let the rest of the object know we're breaking the screen this.breakingScreen = true; } //If we have a parent dragger if(this.parent != null){ //Make sure to fix it's cutoff too this.parent.fixCutoff(standalone); } } } } class canopyAjaxUtils{ constructor(){ } async register(user, pass, passConfirm, email){ var response = await fetch(`/api/account/register`,{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(email ? {user, pass, passConfirm, email} : {user, pass, passConfirm}) }); if(response.status == 200){ location = "/"; }else{ utils.ux.displayResponseError(await response.json()); } } async login(user, pass){ var response = await fetch(`/api/account/login`,{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({user, pass}) }); if(response.status == 200){ location.reload(); }else{ utils.ux.displayResponseError(await response.json()); } } async logout(){ var response = await fetch(`/api/account/logout`,{ method: "GET", }); if(response.status == 200){ location.reload(); }else{ utils.ux.displayResponseError(await response.json()); } } //We need to fix this one to use displayResponseError function async updateProfile(update){ const response = await fetch(`/api/account/update`,{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(update) }); if(response.status == 200){ return await response.json(); }else{ utils.ux.displayResponseError(await response.json()); } } async getRankEnum(){ var response = await fetch(`/api/account/rankEnum`,{ method: "GET" }); if(response.status == 200){ return (await response.json()) }else{ utils.ux.displayResponseError(await response.json()); } } async deleteAccount(pass){ const response = await fetch(`/api/account/delete`,{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({pass}) }); if(response.status == 200){ window.location.pathname = '/'; }else{ utils.ux.displayResponseError(await response.json()); } } async newChannel(name, description, thumbnail){ var response = await fetch(`/api/channel/register`,{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(thumbnail ? {name, description, thumbnail} : {name, description}) }); if(response.status == 200){ location = "/"; }else{ utils.ux.displayResponseError(await response.json()); } } async setChannelSetting(chanName, settingsMap){ var response = await fetch(`/api/channel/settings`,{ method: "POST", headers: { "Content-Type": "application/json" }, //Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible... body: JSON.stringify({chanName, settingsMap: Object.fromEntries(settingsMap)}) }); if(response.status == 200){ return await response.json(); }else{ utils.ux.displayResponseError(await response.json()); } } async setChannelPermissions(chanName, permissionsMap){ var response = await fetch(`/api/channel/permissions`,{ method: "POST", headers: { "Content-Type": "application/json" }, //Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible... body: JSON.stringify({chanName, channelPermissionsMap: Object.fromEntries(permissionsMap)}) }); if(response.status == 200){ return await response.json(); }else{ utils.ux.displayResponseError(await response.json()); } } async getChannelRank(chanName){ var response = await fetch(`/api/channel/rank?chanName=${chanName}`,{ method: "GET" }); if(response.status == 200){ return (await response.json()) }else{ utils.ux.displayResponseError(await response.json()); } } async setChannelRank(chanName, user, rank){ var response = await fetch(`/api/channel/rank`,{ method: "POST", headers: { "Content-Type": "application/json" }, //Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible... body: JSON.stringify({chanName, user, rank}) }); if(response.status == 200){ return await response.json(); }else{ utils.ux.displayResponseError(await response.json()); } } async deleteChannel(chanName, confirm){ var response = await fetch(`/api/channel/delete`,{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({chanName, confirm}) }); if(response.status == 200){ location = "/"; }else{ utils.ux.displayResponseError(await response.json()); } } async getPopup(popup){ var response = await fetch(`/popup/${popup}`,{ method: "GET" }); if(response.status == 200){ return (await response.text()) }else{ utils.ux.displayResponseError(await response.json()); } } async getChanBans(chanName){ var response = await fetch(`/api/channel/ban?chanName=${chanName}`,{ method: "GET", headers: { "Content-Type": "application/json" } }); if(response.status == 200){ return await response.json(); }else{ utils.ux.displayResponseError(await response.json()); } } async chanBan(chanName, user, expirationDays, banAlts){ var response = await fetch(`/api/channel/ban`,{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({chanName, user, expirationDays, banAlts}) }); if(response.status == 200){ return await response.json(); }else{ utils.ux.displayResponseError(await response.json()); } } async chanUnban(chanName, user){ var response = await fetch(`/api/channel/ban`,{ method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({chanName, user}) }); if(response.status == 200){ return await response.json(); }else{ utils.ux.displayResponseError(await response.json()); } } } const utils = new canopyUtils()