Initial commit.

This commit is contained in:
rainbownapkin 2024-11-15 17:44:03 -05:00
commit f0c91b4e55
78 changed files with 5054 additions and 0 deletions

47
www/js/channel/channel.js Normal file
View file

@ -0,0 +1,47 @@
/*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 <https://www.gnu.org/licenses/>.*/
class channel{
constructor(){
//Establish connetion to the server via socket.io
this.connect();
//Define socket listeners
this.defineListeners();
//Scrape channel name off URL
this.channelName = window.location.pathname.split('/c/')[`1`];
//Create the Video Player Object
this.player = new player(this);
//Create the Chat Box Object
this.chatBox = new chatBox(this);
//Create the User List Object
this.userList = new userList(this);
}
connect(){
this.socket = io();
}
defineListeners(){
//This function should serve mostly to glue functions from channel and it's children to it's socket's listeners.
this.socket.on("connect", () => {
document.title = `${this.channelName} - Connected`
});
}
}
const client = new channel();

149
www/js/channel/chat.js Normal file
View file

@ -0,0 +1,149 @@
/*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 <https://www.gnu.org/licenses/>.*/
class chatBox{
constructor(client){
//Client Object
this.client = client
//Booleans
this.aspectLock = true;
//clickDragger object
this.clickDragger = new canopyUXUtils.clickDragger("#chat-panel-drag-handle", "#chat-panel-div", "#chat-panel-user-count");
//Element Nodes
this.chatPanel = document.querySelector("#chat-panel-div");
this.chatBuffer = document.querySelector("#chat-panel-buffer-div");
this.chatPrompt = document.querySelector("#chat-panel-prompt");
this.sendButton = document.querySelector("#chat-panel-send-button");
this.highLevel = document.querySelector("#chat-panel-high-level-select");
//Seems weird to stick this in here, but the split is dictated by chat width :P
this.aspectLockIcon = document.querySelector("#media-panel-aspect-lock-icon");
this.hideChatIcon = document.querySelector("#chat-panel-div-hide");
this.showChatIcon = document.querySelector("#media-panel-show-chat-icon");
//Setup functions
this.setupInput();
this.defineListeners();
this.sizeToAspect();
}
setupInput(){
//Chat bar
this.chatPrompt.addEventListener("keydown", this.send.bind(this));
this.sendButton.addEventListener("click", this.send.bind(this));
//Header icons
this.aspectLockIcon.addEventListener("click", this.lockAspect.bind(this));
this.showChatIcon.addEventListener("click", ()=>{this.toggleUI()});
this.hideChatIcon.addEventListener("click", ()=>{this.toggleUI()});
//Clickdragger/Resize
this.clickDragger.handle.addEventListener("mousedown", this.unlockAspect.bind(this));
window.addEventListener("resize", this.resizeAspect.bind(this));
}
defineListeners(){
this.client.socket.on("chat-message", (data) => {
this.displayChat(data);
});
}
lockAspect(event){
//prevent the user from breaking shit :P
if(this.chatPanel.style.display != "none"){
this.aspectLock = true;
this.aspectLockIcon.style.display = "none";
this.sizeToAspect();
}
}
unlockAspect(event){
this.aspectLock = false;
this.aspectLockIcon.style.display = "inline";
}
resizeAspect(event){
if(this.aspectLock){
this.sizeToAspect();
}
}
sizeToAspect(){
if(this.chatPanel.style.display != "none"){
var targetVidWidth = this.client.player.getRatio() * this.chatPanel.getBoundingClientRect().height;
this.chatPanel.style.width = `${(document.body.getBoundingClientRect().width - targetVidWidth)}px`;
//Fix busted layout
var pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width;
this.chatPanel.style.width = `${this.chatPanel.getBoundingClientRect().width + pageBreak}px`;
}
}
toggleUI(show = this.chatPanel.style.display == "none"){
if(show){
this.chatPanel.style.display = "flex";
this.showChatIcon.style.display = "none";
this.client.player.hideVideoIcon.style.display = "flex";
}else{
this.chatPanel.style.display = "none";
this.showChatIcon.style.display = "flex";
this.client.player.hideVideoIcon.style.display = "none";
}
}
displayChat(chat){
//Create chat-entry span
var chatEntry = document.createElement('span');
chatEntry.classList.add("chat-panel-buffer","chat-entry",`chat-entry-${chat.user}`);
//Create high-level label
var highLevel = document.createElement('p');
highLevel.classList.add("chat-panel-buffer","chat-entry-high-level");
highLevel.innerHTML = `${chat.high}`;
chatEntry.appendChild(highLevel);
//Create username label
var userLabel = document.createElement('p');
userLabel.classList.add("chat-panel-buffer","chat-entry-username");
//Create color span
var colorSpan = document.createElement('span');
colorSpan.classList.add(this.client.userList.colorMap.get(chat.user));
colorSpan.innerHTML = `${chat.user}`;
userLabel.innerHTML = `${colorSpan.outerHTML}: `;
chatEntry.appendChild(userLabel);
//Create chat body
var chatBody = document.createElement('p');
chatBody.classList.add("chat-panel-buffer","chat-entry-body");
chatBody.innerHTML = chat.msg;
chatEntry.appendChild(chatBody);
this.chatBuffer.appendChild(chatEntry);
}
async send(event){
if((!event || !event.key || event.key == "Enter") && this.chatPrompt.value){
this.client.socket.emit("chat-message",{msg: this.chatPrompt.value, high: this.highLevel.value});
this.chatPrompt.value = "";
}
}
}

97
www/js/channel/player.js Normal file
View file

@ -0,0 +1,97 @@
/*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 <https://www.gnu.org/licenses/>.*/
class player{
constructor (client){
//client obj
this.client = client;
//booleans
this.onUI = false;
//timers
this.uiTimer = setTimeout(this.toggleUI.bind(this), 1500, false);
//elements
this.playerDiv = document.querySelector("#media-panel-div");
this.navBar = document.querySelector("#navbar");
this.video = document.querySelector("#media-panel-video");
this.uiBar = document.querySelector("#media-panel-head-div");
this.showVideoIcon = document.querySelector("#chat-panel-show-video-icon");
this.hideVideoIcon = document.querySelector("#media-panel-div-toggle-icon");
this.cinemaModeIcon = document.querySelector("#media-panel-cinema-mode-icon");
//run setup functions
this.setupInput();
}
setupInput(){
//UIBar Movement Detection
this.playerDiv.addEventListener("mousemove", this.popUI.bind(this));
this.uiBar.addEventListener("mouseenter", ()=>{this.setOnUI(true)});
this.uiBar.addEventListener("mouseleave", ()=>{this.setOnUI(false)});
//UIBar/header icons
this.showVideoIcon.addEventListener("click", ()=>{this.toggleVideo()});
this.hideVideoIcon.addEventListener("click", ()=>{this.toggleVideo()});
this.cinemaModeIcon.addEventListener("click", ()=>{this.toggleCinemaMode()});
}
popUI(event){
this.toggleUI(true);
clearTimeout(this.uiTimer);
if(!this.onUI){
this.uiTimer = setTimeout(this.toggleUI.bind(this), 1500, false);
}
}
toggleUI(show = this.uiBar.style.display == "none"){
this.uiBar.style.display = show ? "flex" : "none";
}
toggleVideo(show = this.playerDiv.style.display == "none"){
if(show){
this.playerDiv.style.display = "flex";
this.showVideoIcon.style.display = "none";
this.client.chatBox.hideChatIcon.style.display = "flex";
//Lock the chat to aspect ratio of the video, to make sure the chat width isn't breaking shit
this.client.chatBox.lockAspect();
}else{
this.playerDiv.style.display = "none";
this.showVideoIcon.style.display = "flex";
this.client.chatBox.hideChatIcon.style.display = "none";
//Need to clear the width from the split, or else it doesn't display properly
this.client.chatBox.chatPanel.style.width = "100%";
}
}
toggleCinemaMode(cinema = this.navBar.style.display == "none"){
if(cinema){
this.navBar.style.display = "flex";
}else{
this.navBar.style.display = "none";
}
}
setOnUI(onUI){
this.onUI = onUI;
this.popUI();
}
getRatio(){
return this.video.videoWidth / this.video.videoHeight;
}
}

107
www/js/channel/userlist.js Normal file
View file

@ -0,0 +1,107 @@
/*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 <https://www.gnu.org/licenses/>.*/
class userList{
constructor(client){
//Client object
this.client = client
//Click Dragger Object
this.clickDragger = new canopyUXUtils.clickDragger("#chat-panel-users-drag-handle", "#chat-panel-users-div");
//Strings
this.userColors = [
"userlist-color0",
"userlist-color1",
"userlist-color2",
"userlist-color3",
"userlist-color4",
"userlist-color5",
"userlist-color6"];
//Maps
this.colorMap = new Map();
//Element Nodes
this.userDiv = document.querySelector("#chat-panel-users-div");
this.userList = document.querySelector("#chat-panel-users-list-div");
this.userCount = document.querySelector("#chat-panel-user-count");
this.toggleIcon = document.querySelector("#chat-panel-users-toggle");
//Call setup functions
this.setupInput();
this.defineListeners();
}
//Setup functions
setupInput(){
this.toggleIcon.addEventListener("click", ()=>{this.toggleUI()});
this.userCount.addEventListener("click", ()=>{this.toggleUI()});
}
defineListeners(){
this.client.socket.on('user-list', (data) => {
this.updateList(data);
});
}
updateList(list){
//Clear list and set user count
this.userCount.textContent = list.length == 1 ? '1 User' : `${list.length} Users`;
this.userList.innerHTML = null;
//create a new map
var newMap = new Map();
//for each user
list.forEach((user) => {
//randomly pick a color
var color = this.userColors[Math.floor(Math.random()*this.userColors.length)]
//if this user was in the previous colormap
if(this.colorMap.get(user) != null){
//Override with previous color
color = this.colorMap.get(user);
}
newMap.set(user, color);
this.renderUser(user, color);
});
this.colorMap = newMap;
}
renderUser(user, color){
var userEntry = document.createElement('p');
userEntry.innerText = user;
userEntry.id = `user-entry-${user}`;
userEntry.classList.add("chat-panel-users","user-entry",color);
this.userList.appendChild(userEntry);
}
toggleUI(show = this.userDiv.style.display == "none"){
if(show){
this.userDiv.style.display = "flex";
this.toggleIcon.classList.replace("bi-caret-left-fill","bi-caret-down-fill");
}else{
this.userDiv.style.display = "none";
this.toggleIcon.classList.replace("bi-caret-down-fill","bi-caret-left-fill");
}
}
}

54
www/js/channelSettings.js Normal file
View file

@ -0,0 +1,54 @@
/*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 <https://www.gnu.org/licenses/>.*/
class channelSettingsPrompt{
constructor(){
this.channel = window.location.pathname.slice(3).replace('/settings','');
this.hidden = document.querySelector("#channel-hidden");
this.delete = document.querySelector("#channel-delete");
this.hidden.addEventListener('change', this.submitUpdate.bind(this));
this.delete.addEventListener('click', this.promptDelete.bind(this));
}
async submitUpdate(event){
//probably not the cleanest way to get the chan name :P
const key = event.target.id.split("-").pop();
const value = event.target.type == "checkbox" ? event.target.checked : event.target.value;
const settingsMap = new Map([
[key, value]
]);
this.handleUpdate(await utils.ajax.setChannelSetting(this.channel, settingsMap));
}
handleUpdate(updateObj){
this.hidden.checked = updateObj.hidden;
}
promptDelete(){
var confirm = window.prompt(`Warning: You are about to nuke ${this.channel} off of the face of the fucking planet, no taksie-backsies. \n \n Type in ${this.channel} to confirm.`);
this.deleteChannel(confirm);
}
async deleteChannel(confirm){
if(this.channel === confirm){
utils.ajax.deleteChannel(this.channel, confirm);
}
}
}
new channelSettingsPrompt();

36
www/js/navbar.js Normal file
View file

@ -0,0 +1,36 @@
/*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 <https://www.gnu.org/licenses/>.*/
//I could make a class like the others but it's so god-damned basic why bother?
async function navbarLogin(event){
if(!event || !event.key || event.key == "Enter"){
var user = document.querySelector("#username-prompt").value;
var pass = document.querySelector("#password-prompt").value;
utils.ajax.login(user, pass);
}
}
//assign events
if(document.querySelector("#username-prompt")){
document.querySelector("#username-prompt").addEventListener("keydown", navbarLogin);
document.querySelector("#password-prompt").addEventListener("keydown", navbarLogin);
document.querySelector("#login-button").addEventListener("click", navbarLogin);
}
if(document.querySelector("#logout-button")){
document.querySelector("#logout-button").addEventListener("click", utils.ajax.logout);
}

31
www/js/newChannel.js Normal file
View file

@ -0,0 +1,31 @@
/*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 <https://www.gnu.org/licenses/>.*/
//I could make a class like the others but it's so god-damned basic why bother?
async function registerPrompt(event){
if(!event || event.key == "Enter"){
var name = document.querySelector("#register-channel-name").value;
var description = document.querySelector("#register-description").value;
var thumbnail = document.querySelector("#register-thumbnail").value;
utils.ajax.newChannel(name, description, thumbnail);
}
}
//assign events
document.querySelector("#register-channel-name").addEventListener("keydown", registerPrompt)
document.querySelector("#register-description").addEventListener("keydown", registerPrompt)
document.querySelector("#register-thumbnail").addEventListener("keydown", registerPrompt)

184
www/js/profile.js Normal file
View file

@ -0,0 +1,184 @@
/*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 <https://www.gnu.org/licenses/>.*/
//Base Class
class profileEditPrompt{
constructor(field, content, useTextArea = false){
this.field = field;
this.useTextArea = useTextArea;
this.content = content;
this.link = document.querySelector(`#profile-${field}-edit`);
//Bail out if something ain't right
if(!this.link || !this.content){
return;
}
this.setupPrompt();
}
setupPrompt(){
if(this.link != null){
this.link.addEventListener("click", this.prompt.bind(this));
}
}
prompt(){
//Create input element
if(this.useTextArea){
this.prompt = document.createElement("textArea");
}else{
this.prompt = document.createElement("input");
}
//Setup properties
this.prompt.id = `profile-${this.field}-prompt`;
this.prompt.classList.add("profile-edit-prompt");
this.prompt.placeholder = this.content.innerHTML;
//Setup event listener
this.prompt.addEventListener("keydown", this.update.bind(this));
//replace label
this.content.replaceWith(this.prompt);
}
async update(event){
if((!event || event.key == "Enter") && this.prompt.value){
//setup object
var updateObj = {};
updateObj[this.field] = this.prompt.value;
//contact server, and collect response
var response = await utils.ajax.updateProfile(updateObj);
var updated_content = (await response.json())[this.field];
//Update label
if(response.status == 200){
if(this.field == "img"){
this.content.src = updated_content;
}else{
this.content.innerHTML = updated_content;
}
}
this.finish();
}else if(event.key == "Escape" || event.key == "Enter"){
this.finish();
}
}
finish(){
this.prompt.replaceWith(this.content);
}
}
class profileTextEditPrompt extends profileEditPrompt{
constructor(field, useTextArea = false){
//Get content based on field name
var content = document.querySelector(`#profile-${field}-content`);
//Derived Constructor
super(field, content, useTextArea);
}
prompt(){
super.prompt();
}
async update(event){
await super.update(event)
}
}
//Child Classes
class profileImgEditPrompt extends profileEditPrompt{
constructor(){
//Get content based on field name
var content = document.querySelector(`#profile-img`);
//Derived constructor
super("img", content, false);
}
}
class passwordResetPrompt{
constructor(){
this.oldPassNode = document.querySelector('#account-settings-password-reset-old');
this.newPassNode = document.querySelector('#account-settings-password-reset-new');
this.confirmPassNode = document.querySelector('#account-settings-password-reset-confirm');
this.setupInput(this.oldPassNode);
this.setupInput(this.newPassNode);
this.setupInput(this.confirmPassNode);
}
setupInput(node){
if(node != null){
node.addEventListener("keydown", this.update.bind(this));
}
}
async update(event){
var hasVal = (this.oldPassNode.value && this.newPassNode.value && this.confirmPassNode.value);
if((!event || event.key == "Enter") && hasVal){
if(this.newPassNode.value == this.confirmPassNode.value){
const updateObj = {};
updateObj.passChange = {
oldPass: this.oldPassNode.value,
newPass: this.newPassNode.value,
confirmPass: this.confirmPassNode.value
};
const response = await utils.ajax.updateProfile(updateObj);
if(response.status == 200){
//Return user homepage after good pass change, as we've probably been logged out by the server for security.
window.location.pathname = '/';
}
}
}
}
}
class deleteAccountPrompt{
constructor(){
this.deleteLink = document.querySelector('#account-settings-delete-link');
this.setupEvent();
}
setupEvent(){
if(this.deleteLink != null){
this.deleteLink.addEventListener("click",this.deletePrompt);
}
}
async deletePrompt(event){
const pass = window.prompt("Warning: You are about to nuke your account off of the face of the fucking planet, no taksie-backsies.\n \n (todo: replace with dialog that has obscured password input) \n Enter your password to confirm.");
const response = await utils.ajax.deleteAccount(pass);
if(response.status == 200){
window.location.pathname = '/';
}else{
displayResponseError(await response.json());
}
}
}
//Object Instantiation
new profileTextEditPrompt("signature");
new profileTextEditPrompt("bio", true);
new profileImgEditPrompt();
new passwordResetPrompt();
new deleteAccountPrompt();

33
www/js/register.js Normal file
View file

@ -0,0 +1,33 @@
/*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 <https://www.gnu.org/licenses/>.*/
//I could make a class like the others but it's so god-damned basic why bother?
async function registerPrompt(event){
if(!event || event.key == "Enter"){
var user = document.querySelector("#register-username").value;
var pass = document.querySelector("#register-password").value;
var passConfirm = document.querySelector("#register-password-confirm").value;
var email = document.querySelector("#register-email").value;
utils.ajax.register(user, pass, passConfirm, email);
}
}
//assign events
document.querySelector("#register-username").addEventListener("keydown", registerPrompt)
document.querySelector("#register-password").addEventListener("keydown", registerPrompt)
document.querySelector("#register-password-confirm").addEventListener("keydown", registerPrompt)
document.querySelector("#register-email").addEventListener("keydown", registerPrompt)

214
www/js/utils.js Normal file
View file

@ -0,0 +1,214 @@
/*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 <https://www.gnu.org/licenses/>.*/
class canopyUtils{
constructor(){
this.ajax = new canopyAjaxUtils();
this.ux = new canopyUXUtils();
}
}
class canopyUXUtils{
constructor(){
}
static clickDragger = class{
constructor(handle, element, breakPoint){
//Pull needed nodes
this.handle = document.querySelector(handle);
this.element = document.querySelector(element);
//True while dragging
this.dragLock = false;
//Little hacky but it could be worse :P
this.fixWidth = false;
//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;
}
endDrag(event){
//we're no longer dragging
this.dragLock = false;
//if we broke the page we need to fix it
if(this.fixWidth){
//Pop the element width up just a bit to compensate for the extra pixel
this.element.style.width = `${this.calcWidth(this.element.getBoundingClientRect().width + 1)}%`;
//if this is true, it no longer needs to be, though it *should* be reset by the drag function by the time it matters anywho :P
this.fixWidth = false;
}
}
drag(event){
if(this.dragLock){
//get difference between mouse and right edge of element
var difference = this.element.getBoundingClientRect().right - event.clientX;
//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(pageBreak <= 0 || event.clientX < this.handle.getBoundingClientRect().left){
//Apply difference to width
this.element.style.width = `${this.calcWidth(difference)}%`;
//If we let go here, the width isn't breaking anything so there's nothing to fix.
this.fixWidth = false;
}else{
//We need to move the element back, but we can't do it all the way while we're still dragging as it will thrash
this.element.style.width = `${this.calcWidth(this.element.getBoundingClientRect().width + pageBreak - 1)}%`;
//If we stop dragging here, let the endDrag function know to fix the pixel difference used to prevent thrashing
this.fixWidth = true;
}
}
}
calcWidth(px){
return (px / this.element.parentElement.getBoundingClientRect().width) * 100;
}
}
}
class canopyAjaxUtils{
constructor(){
}
//Profile
async displayResponseError(body){
const err = body.msg;
window.alert(`ERROR:\n${err}`);
}
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 = "/";
}
}
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();
}
}
async logout(){
var response = await fetch(`/api/account/logout`,{
method: "GET",
});
if(response.status == 200){
location.reload();
}
}
async updateProfile(update){
return await fetch(`/api/account/update`,{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(update)
});
}
async deleteAccount(pass){
return await fetch(`/api/account/delete`,{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({pass})
});
}
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 = "/";
}
}
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();
}
}
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 = "/";
}
}
}
const utils = new canopyUtils()