/*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 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.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()}
(${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 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.nukeAccount.bind(this));
}
async nukeAccount(event){
if(event.key == "Enter"){
console.log(this.channel);
if(this.channel === event.target.value){
await utils.ajax.deleteChannel(this.channel, event.target.value);
}
}
}
}
const channelSettings = new channelSettingsPage();