578 lines
19 KiB
JavaScript
578 lines
19 KiB
JavaScript
/*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 channelSettingsPage{
|
|
constructor(){
|
|
//Get channel name off of the URL
|
|
this.channel = window.location.pathname.slice(3).replace('/settings','');
|
|
//Instantiate UX handling objects, making sure to pass the channel name.
|
|
this.rankList = new rankList(this.channel);
|
|
this.banList = new banList(this.channel);
|
|
this.permList = new permList(this.channel);
|
|
this.prefrenceList = new prefrenceList(this.channel);
|
|
this.tokeCommandList = new tokeCommandList(this.channel);
|
|
this.emoteList = new emoteList(this.channel);
|
|
this.deleteBtn = new deleteBtn(this.channel);
|
|
}
|
|
}
|
|
|
|
class rankList{
|
|
constructor(channel){
|
|
this.channel = channel
|
|
this.table = document.querySelector(".admin-list-table");
|
|
this.userPrompt = document.querySelector("#new-rank-input");
|
|
this.rankSelect = document.querySelector("#new-rank-select");
|
|
|
|
//Load the userlist and setup input
|
|
this.loadList();
|
|
this.setupInput();
|
|
}
|
|
|
|
setupInput(){
|
|
this.userPrompt.addEventListener("keydown", this.submitNewRank.bind(this));
|
|
}
|
|
|
|
async loadList(){
|
|
const list = await utils.ajax.getChannelRank(this.channel);
|
|
|
|
this.updateList(list);
|
|
}
|
|
|
|
async submitNewRank(event){
|
|
if(event.key != "Enter" && event.key != null){
|
|
//Bail out if we didn't hit enter
|
|
return;
|
|
}
|
|
|
|
//Send new rank
|
|
this.submitUserRank(this.userPrompt.value, this.rankSelect.value);
|
|
|
|
//Clear out prompt
|
|
this.userPrompt.value = "";
|
|
}
|
|
|
|
async submitUpdate(event){
|
|
const user = event.target.id.replace("channel-rank-select-","");
|
|
const rank = event.target.value;
|
|
|
|
await this.submitUserRank(user, rank);
|
|
}
|
|
|
|
async submitUserRank(user, rank){
|
|
await this.updateList(await utils.ajax.setChannelRank(this.channel, user, rank));
|
|
}
|
|
|
|
async updateList(data){
|
|
//If no data
|
|
if(!data){
|
|
//Do not pass go, do not collect $200
|
|
return;
|
|
}
|
|
//Get name/rank of logged in user
|
|
const curName = document.querySelector("#username").textContent
|
|
const curUser = data[curName];
|
|
const rankEnum = await utils.ajax.getRankEnum();
|
|
|
|
//clear the table
|
|
this.clearTable();
|
|
|
|
//For each user in the list
|
|
Object.entries(data).forEach((userAr) => {
|
|
//pull user object from entry array
|
|
const user = userAr[1];
|
|
|
|
//Create IMG node inside of IMG cell
|
|
const imgNode = document.createElement('img');
|
|
imgNode.classList.add("admin-list-entry","admin-list-entry-item");
|
|
imgNode.src = user.img;
|
|
|
|
//If the listed user rank is equal or higher than the signed-in user
|
|
if(rankEnum.indexOf(user.rank) >= rankEnum.indexOf(curUser.rank)){
|
|
var rankContent = user.rank;
|
|
}else{
|
|
//Create rank select
|
|
var rankContent = document.createElement('select');
|
|
rankContent.id = `channel-rank-select-${user.user}`
|
|
rankContent.classList.add("channel-rank-select")
|
|
rankContent.addEventListener("change", this.submitUpdate.bind(this));
|
|
|
|
//for each rank in the enum
|
|
rankEnum.slice().reverse().forEach((rank) => {
|
|
//Create an option for the given rank
|
|
const rankOption = document.createElement('option');
|
|
rankOption.value = rank;
|
|
rankOption.innerHTML = rank;
|
|
rankOption.selected = user.rank == rank;
|
|
rankContent.appendChild(rankOption);
|
|
});
|
|
}
|
|
|
|
//Generate row and append to table
|
|
this.table.appendChild(utils.ux.newTableRow([
|
|
imgNode,
|
|
user.id,
|
|
user.user,
|
|
rankContent
|
|
]));
|
|
});
|
|
}
|
|
|
|
clearTable(){
|
|
//get all non-title table rows
|
|
const rows = this.table.querySelectorAll("tr.gen-row");
|
|
|
|
//for each row
|
|
rows.forEach((row) => {
|
|
//The Lord Yeeteth, and The Lord Yoinketh away...
|
|
row.remove();
|
|
});
|
|
}
|
|
}
|
|
|
|
class banList{
|
|
constructor(channel){
|
|
this.channel = channel;
|
|
this.table = document.querySelector("#admin-ban-list-table");
|
|
this.banPrompt = document.querySelector("#new-ban-input");
|
|
this.banButton = document.querySelector("#new-ban-button");
|
|
|
|
this.loadList();
|
|
this.setupInput();
|
|
}
|
|
|
|
setupInput(){
|
|
this.banButton.addEventListener('click', this.ban.bind(this));
|
|
}
|
|
|
|
async loadList(){
|
|
const data = await utils.ajax.getChanBans(this.channel);
|
|
this.updateList(data);
|
|
}
|
|
|
|
clearList(){
|
|
const oldRows = this.table.querySelectorAll('tr.gen-row');
|
|
oldRows.forEach((row) => {
|
|
row.remove();
|
|
});
|
|
}
|
|
|
|
async ban(event){
|
|
new banUserPopup(this.channel, this.banPrompt.value, this.updateList.bind(this));
|
|
}
|
|
|
|
async unban(event){
|
|
//Rip user outta the target id
|
|
const user = event.target.id.replace("admin-user-list-unban-icon-","");
|
|
//Tell the server to unban them and get the list returned
|
|
const list = await utils.ajax.chanUnban(this.channel, user);
|
|
|
|
//Use the list to update the UI
|
|
this.updateList(list);
|
|
}
|
|
|
|
updateList(bans){
|
|
//for each ban listed
|
|
this.clearList();
|
|
bans.forEach((ban)=>{
|
|
//Create IMG node
|
|
const imgNode = document.createElement('img');
|
|
imgNode.classList.add("admin-list-entry","admin-list-entry-item");
|
|
imgNode.src = ban.user.img;
|
|
|
|
//Create banAlts check
|
|
const banAlts = document.createElement('i');
|
|
banAlts.classList.add("admin-user-list-ban-alts",ban.banAlts ? "bi-check" : "bi-x");
|
|
|
|
//Create unban icon node
|
|
const unbanIcon = document.createElement('i');
|
|
unbanIcon.classList.add("bi-emoji-smile-fill","admin-user-list-icon","admin-user-list-unban-icon");
|
|
unbanIcon.id = `admin-user-list-unban-icon-${ban.user.user}`;
|
|
unbanIcon.title = `Unban ${ban.user.user}`;
|
|
unbanIcon.addEventListener("click", this.unban.bind(this));
|
|
|
|
//THIS IS NOT YET PROPERLY IMPLEMENTED AS getDaysUntilExpiration has not been made for chan bans
|
|
const expirationString = ban.expirationDays < 0 ? "Never" : `${new Date(ban.expirationDate).toDateString()}<br>(${ban.daysUntilExpiration} day(s) left)`;
|
|
|
|
//Generate row and append to table
|
|
this.table.appendChild(utils.ux.newTableRow([
|
|
imgNode,
|
|
ban.user.id,
|
|
ban.user.user,
|
|
banAlts,
|
|
new Date(ban.banDate).toDateString(),
|
|
expirationString,
|
|
unbanIcon
|
|
]));
|
|
});
|
|
}
|
|
}
|
|
|
|
class banUserPopup{
|
|
constructor(channel, target, cb){
|
|
this.channel = channel;
|
|
this.target = target;
|
|
this.cb = cb;
|
|
this.popup = new canopyUXUtils.popup("channelUserBan", true, this.asyncConstruction.bind(this));
|
|
}
|
|
|
|
asyncConstruction(){
|
|
this.title = document.querySelector(".popup-title");
|
|
//Setup title text real quick-like :P
|
|
this.title.innerHTML = `Ban ${this.target}`;
|
|
|
|
this.permBan = document.querySelector("#ban-popup-perm");
|
|
this.expiration = document.querySelector("#ban-popup-expiration");
|
|
this.expirationPrefix = document.querySelector("#ban-popup-expiration-prefix");
|
|
this.banAlts = document.querySelector("#ban-popup-alts");
|
|
this.banButton = document.querySelector("#ban-popup-ban-button");
|
|
this.cancelButton = document.querySelector("#ban-popup-cancel-button");
|
|
|
|
this.setupInput();
|
|
}
|
|
|
|
setupInput(){
|
|
this.permBan.addEventListener("change", this.permaBanLabel.bind(this));
|
|
this.cancelButton.addEventListener("click", this.popup.closePopup.bind(this.popup));
|
|
this.banButton.addEventListener("click",this.ban.bind(this));
|
|
}
|
|
|
|
permaBanLabel(event){
|
|
if(event.target.checked){
|
|
this.expiration.disabled = true;
|
|
}else{
|
|
this.expiration.disabled = false;
|
|
}
|
|
}
|
|
|
|
async ban(event){
|
|
//Get expiration days
|
|
const expirationDays = this.permBan.checked ? -1 : this.expiration.value;
|
|
//Send ban request off to server and retrieve new ban list
|
|
const data = await utils.ajax.chanBan(this.channel, this.target, expirationDays, this.banAlts.checked);
|
|
|
|
//Close the popup
|
|
this.popup.closePopup();
|
|
|
|
//If we have data and a callback, run the callback with our data
|
|
if(data != null && this.cb != null){
|
|
await this.cb(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
class prefrenceList{
|
|
constructor(channel){
|
|
this.channel = channel;
|
|
this.inputs = document.querySelectorAll(".channel-preference-list-item");
|
|
|
|
this.setupInput();
|
|
}
|
|
|
|
setupInput(){
|
|
this.inputs.forEach((input) => {
|
|
input.addEventListener("change", this.submitUpdate.bind(this));
|
|
});
|
|
}
|
|
|
|
async submitUpdate(event){
|
|
const key = event.target.id.replace("channel-preference-","");
|
|
const value = event.target.checked;
|
|
const settingsMap = new Map([
|
|
[key, value]
|
|
]);
|
|
|
|
this.handleUpdate(await utils.ajax.setChannelSetting(this.channel, settingsMap), event.target, key);
|
|
}
|
|
|
|
handleUpdate(data, target, key){
|
|
if(data){
|
|
target = data[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
class permList{
|
|
constructor(channel){
|
|
this.channel = channel
|
|
this.inputs = document.querySelectorAll(".channel-perm-select");
|
|
this.setupInput();
|
|
}
|
|
|
|
setupInput(){
|
|
this.inputs.forEach((input) => {
|
|
input.addEventListener("change", this.submitUpdate.bind(this));
|
|
});
|
|
}
|
|
|
|
async submitUpdate(event){
|
|
const key = event.target.id.replace("admin-perm-list-rank-select-","");
|
|
const value = event.target.value;
|
|
const permMap = new Map([
|
|
[key, value]
|
|
]);
|
|
|
|
this.handleUpdate(await utils.ajax.setChannelPermissions(this.channel, permMap), event.target, key);
|
|
}
|
|
|
|
handleUpdate(data, target, key){
|
|
target.value = data[key];
|
|
}
|
|
}
|
|
|
|
class tokeCommandList{
|
|
constructor(channel){
|
|
this.channel = channel;
|
|
this.tokeCommandList = document.querySelector('div.toke-command-list');
|
|
this.newTokeCommandPrompt = document.querySelector('#new-toke-command-input');
|
|
this.newTokeCommandButton = document.querySelector('#new-toke-command-button');
|
|
|
|
//Setup input
|
|
this.setupInput();
|
|
|
|
//Pull the toke list on load
|
|
this.getTokeList();
|
|
}
|
|
|
|
setupInput(){
|
|
this.newTokeCommandButton.addEventListener('click', this.addToke.bind(this));
|
|
}
|
|
|
|
async addToke(event){
|
|
//Send out the new toke command and get the new list
|
|
const tokeList = await utils.ajax.addChanToke(this.channel, this.newTokeCommandPrompt.value);
|
|
//clear the prompt
|
|
this.newTokeCommandPrompt.value = "";
|
|
//render the returned list
|
|
this.renderTokeList(tokeList);
|
|
}
|
|
|
|
async getTokeList(){
|
|
const tokeList = await utils.ajax.getChanTokes(this.channel);
|
|
this.renderTokeList(tokeList);
|
|
}
|
|
|
|
clearTokeList(){
|
|
this.tokeCommandList.innerHTML = "";
|
|
}
|
|
|
|
async deleteToke(event){
|
|
const name = event.target.id.replace("toke-command-delete-","");
|
|
|
|
const tokeList = await utils.ajax.deleteChanToke(this.channel, name);
|
|
|
|
this.renderTokeList(tokeList);
|
|
}
|
|
|
|
renderTokeList(tokeList){
|
|
if(tokeList != null){
|
|
//Clear our the toke list
|
|
this.clearTokeList();
|
|
|
|
//For each toke in the received list
|
|
tokeList.forEach((toke)=>{
|
|
//generate a toke command span, and append it to the toke list div
|
|
this.tokeCommandList.appendChild(this.generateTokeSpan(toke));
|
|
});
|
|
}
|
|
}
|
|
|
|
generateTokeSpan(toke){
|
|
//Create toke command span
|
|
const tokeSpan = document.createElement('span');
|
|
tokeSpan.classList.add('toke-command-list');
|
|
|
|
//Create toke command label
|
|
const tokeLabel = document.createElement('p');
|
|
tokeLabel.innerHTML = `!${toke}`;
|
|
tokeLabel.classList.add('toke-command-list');
|
|
|
|
//Create toke command delete icon
|
|
const tokeDelete = document.createElement('i');
|
|
tokeDelete.classList.add('toke-command-list', 'bi-trash-fill', 'toke-command-delete');
|
|
tokeDelete.id = `toke-command-delete-${toke}`;
|
|
tokeDelete.addEventListener('click', this.deleteToke.bind(this));
|
|
|
|
//append span contents to tokeSpan
|
|
tokeSpan.appendChild(tokeLabel);
|
|
tokeSpan.appendChild(tokeDelete);
|
|
|
|
//return the toke span
|
|
return tokeSpan
|
|
}
|
|
}
|
|
|
|
class emoteList{
|
|
constructor(channel){
|
|
this.channel = channel
|
|
this.linkPrompt = document.querySelector('#new-emote-link-input');
|
|
this.namePrompt = document.querySelector('#new-emote-name-input');
|
|
this.addButton = document.querySelector('#new-emote-button');
|
|
this.emoteList = document.querySelector('#emote-list');
|
|
|
|
//Setup input
|
|
this.setupInput();
|
|
|
|
//Pull and render emote list
|
|
this.updateList();
|
|
}
|
|
|
|
setupInput(){
|
|
this.addButton.addEventListener('click', this.addEmote.bind(this));
|
|
}
|
|
|
|
async deleteEmote(event){
|
|
//Strip name from element id
|
|
const name = event.target.id.replace('emote-list-delete-','');
|
|
|
|
//Delete emote and pull list
|
|
const list = await utils.ajax.deleteChanEmote(this.channel, name);
|
|
|
|
//If we received a list
|
|
if(list != null){
|
|
//Pass updated liste to renderEmoteList function instead of pulling it twice
|
|
this.renderEmoteList(list);
|
|
}
|
|
}
|
|
|
|
async addEmote(event){
|
|
//Add emote to list and ingest returned updates list
|
|
const list = await utils.ajax.addChanEmote(this.channel, this.namePrompt.value, this.linkPrompt.value);
|
|
|
|
//If we received a list
|
|
if(list != null){
|
|
//Pass updated liste to renderEmoteList function instead of pulling it twice
|
|
this.renderEmoteList(list);
|
|
|
|
//Clear out the prompts
|
|
this.namePrompt.value = '';
|
|
this.linkPrompt.value = '';
|
|
}
|
|
}
|
|
|
|
async updateList(){
|
|
const list = await utils.ajax.getChanEmotes(this.channel);
|
|
this.renderEmoteList(list);
|
|
}
|
|
|
|
renderEmoteList(list){
|
|
//Clear the current list
|
|
this.emoteList.innerHTML = "";
|
|
|
|
//For each emote in the list
|
|
list.forEach((emote) => {
|
|
//Create span to hold emote
|
|
const emoteDiv = document.createElement('div');
|
|
emoteDiv.classList.add('emote-list-emote');
|
|
|
|
//If the emote is an image
|
|
if(emote.type == 'image'){
|
|
//Create image node
|
|
var emoteMedia = document.createElement('img');
|
|
//if emote is a video
|
|
}else if(emote.type == 'video'){
|
|
//create video node
|
|
var emoteMedia = document.createElement('video');
|
|
//Set video properties
|
|
emoteMedia.autoplay = true;
|
|
emoteMedia.muted = true;
|
|
emoteMedia.controls = false;
|
|
emoteMedia.loop = true;
|
|
}
|
|
|
|
//set media link as source
|
|
emoteMedia.src = emote.link;
|
|
//Set media class
|
|
emoteMedia.classList.add('emote-list-media');
|
|
|
|
//Create title span
|
|
const titleSpan = document.createElement('span');
|
|
titleSpan.classList.add('emote-list-title');
|
|
|
|
//Create paragraph tag
|
|
const emoteTitle = document.createElement('p');
|
|
//Set title class
|
|
emoteTitle.classList.add('emote-list-title');
|
|
//Set emote title
|
|
emoteTitle.innerHTML = `[${emote.name}]`;
|
|
|
|
//Create delete icon
|
|
const deleteIcon = document.createElement('i');
|
|
//Set delete icon id and class
|
|
deleteIcon.classList.add('bi-trash-fill', 'emote-list-delete');
|
|
deleteIcon.id = `emote-list-delete-${emote.name}`;
|
|
//Add delete icon event listener
|
|
deleteIcon.addEventListener('click',this.deleteEmote.bind(this));
|
|
|
|
|
|
//Add the emote media to the emote span
|
|
emoteDiv.appendChild(emoteMedia);
|
|
//Add title paragraph node
|
|
titleSpan.appendChild(emoteTitle);
|
|
//Add trash icon node
|
|
titleSpan.appendChild(deleteIcon);
|
|
//Add title span
|
|
emoteDiv.appendChild(titleSpan);
|
|
|
|
//Append the mote span to the emote list
|
|
this.emoteList.appendChild(emoteDiv);
|
|
});
|
|
}
|
|
}
|
|
|
|
class deleteBtn{
|
|
constructor(channel){
|
|
this.channel = channel;
|
|
this.delete = document.querySelector("#chan-delete");
|
|
|
|
this.setupInput();
|
|
}
|
|
|
|
setupInput(){
|
|
this.delete.addEventListener('click', () => {new deleteAccountPopup(this.channel)});
|
|
}
|
|
|
|
}
|
|
|
|
class deleteAccountPopup{
|
|
constructor(channel){
|
|
this.channel = channel;
|
|
this.popup = new canopyUXUtils.popup("nukeChannel", true, this.asyncConstructor.bind(this));
|
|
}
|
|
|
|
asyncConstructor(){
|
|
this.prompt = document.querySelector("#delete-channel-popup-prompt");
|
|
this.label = document.querySelector("#delete-channel-popup-content");
|
|
|
|
//Fill channel label
|
|
this.label.innerHTML = this.label.innerHTML.replaceAll("[CHANNEL]", this.channel);
|
|
|
|
this.setupInput();
|
|
}
|
|
|
|
setupInput(){
|
|
this.prompt.addEventListener("keydown", this.nukeChannel.bind(this));
|
|
}
|
|
|
|
async nukeChannel(event){
|
|
if(event.key == "Enter"){
|
|
if(this.channel === event.target.value){
|
|
await utils.ajax.deleteChannel(this.channel, event.target.value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const channelSettings = new channelSettingsPage(); |