canopy/www/js/channel/channel.js

350 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 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 containing base code for the Canopy channel client.
*/
class channel{
/**
* Instantiates a new channel object
*/
constructor(){
//Establish connetion to the server via socket.io
this.connect();
//Define socket listeners
this.defineListeners();
/**
* Returns true once the ytEmbed API has loaded in from google (eww)
*/
this.ytEmbedAPILoaded = false;
/**
* Current connected channels name
*/
this.channelName = window.location.pathname.split('/c/')[1].split('/')[0];
/**
* Child Video Player object
*/
this.player = new player(this);
/**
* Child Chat Box Object
*/
this.chatBox = new chatBox(this);
/**
* Child User List Object
*/
this.userList = new userList(this);
/**
* Child PM Handler
*/
this.pmHandler = new pmHandler(this);
/**
* Child Canopy Panel Object
*/
this.cPanel = new cPanel(this);
//Set defaults for any unset settings and run any required process steps for the current config
this.setDefaults(false, true);
//Freak out any weirdos who take a peek in the dev console for shits n gigs
console.log("👁️👄👁️ 𝓊𝓃𝒿𝓊𝓇.");
//Preach the good word about software which is Free as in Freedom
console.log(`Did you know Canopy, the software that runs '${utils.ux.getInstanceName()}' is software that is free as in freedom AND free weed?`);
console.log("This means you can read/modify/redistribute the code, run your own server, and even contribute back!");
console.log("https://git.ourfore.st/rainbownapkin/canopy");
}
/**
* Handles initial client connection
*/
connect(){
const clientOptions = {
extraHeaders: {
//Include CSRF token
'x-csrf-token': utils.ajax.getCSRFToken()
}
};
this.socket = io(clientOptions);
this.queueBroadcastSocket = io("/queue-broadcast", clientOptions);
this.pmSocket = io("/pm", clientOptions);
}
/**
* Defines network-related listeners
*/
defineListeners(){
this.socket.on("connect", () => {
document.title = `${this.channelName} - Connected`
});
this.socket.on("kick", async (data) => {
if(data.reason == "Invalid CSRF Token!"){
//Warn the user
new canopyUXUtils.popup('Invalid CSRF Token detected, reloading client...');
//Just reload the fucker
setTimeout(()=>{location.reload();}, 1000);
}else{
new canopyUXUtils.popup(`You have been ${data.type} from the channel for the following reason:<br>${data.reason}`);
}
});
this.socket.on("clientMetadata", this.handleClientInfo.bind(this));
this.socket.on("error", utils.ux.displayResponseError);
this.socket.on("lock", (data) => {
this.queueLock = data.locked;
});
this.queueBroadcastSocket.on("queue", (data) => {
this.queue = new Map(data.queue);
});
}
/**
* Handles initial client-metadata ingestion from server upon connection
* @param {Object} data - Data glob from server
*/
handleClientInfo(data){
//Ingest user data
this.user = data.user;
//Re-hydrate permission maps
this.user.permMap.site = new Map(data.user.permMap.site);
this.user.permMap.chan = new Map(data.user.permMap.chan);
//If we can read the schedule
if(client.user.permMap.chan.get('readSchedule')){
//Display the queue icon
this.chatBox.adminIcon.style.display = "";
}
//Tell the chatbox to handle client info
//should it have its own event listener instead? Guess it's a stylistic choice :P
this.chatBox.handleClientInfo(data);
//Store queue lock status
this.queueLock = data.queueLock;
}
/**
* Processes and applies default config on any unset settings
* @param {Boolean} force - Whether or not to forcefully reset already set settings
* @param {Boolean} processConfig - Whether or not to run the Process Config function once complete
*/
setDefaults(force = false, processConfig = false){
//Iterate through default config
for(let [key, value] of channel.defaultConfig){
//If the setting is unset or function was called forcefully
if(force || localStorage.getItem(key) == null){
//Set item from default map
localStorage.setItem(key, value);
}
//If we're running process steps for the config
if(processConfig){
//Process the current config value
this.processConfig(key, localStorage.getItem(key));
}
}
}
/**
* Run once every config change to ensure settings are properly set
* @param {String} key - Setting to change
* @param {*} value - Value to set setting to
*/
processConfig(key, value){
//Unfortunately we can't scope constants to switch-cases so this is the best we got if we wanna re-use the name
let nowPlaying;
//Switch/case by config key
switch(key){
case 'ytPlayerType':
const embedScript = document.querySelector(".yt-embed-api");
//If the user is running the embedded player and we don't have en embed script loaded
if(value == 'embed' && embedScript == null){
//Find our footer
const footer = document.querySelector('footer');
//Create new script tag
const ytEmbedAPI = document.createElement('script');
//Link the iframe api from youtube
ytEmbedAPI.src = "https://www.youtube.com/iframe_api";
//set the iframe api script id
ytEmbedAPI.classList.add('yt-embed-api');
//Append the script tag to the top of the footer to give everything else access
footer.prepend(ytEmbedAPI);
//If we're not using the embed player but the script is loaded
}else if(embedScript != null){
//Pull all scripts since the main one might have pulled others
const scripts = document.querySelectorAll('script');
//Iterate through all script tags on the page
for(let script of scripts){
//If the script came from youtube
if(script.src.match(/youtube\.com|youtu\.be/)){
//Rip it out
script.remove();
}
}
}
//If the player or mediaHandler isn't loaded
if(this.player == null || this.player.mediaHandler == null){
//We're fuggin done here
return;
}
//Get current video
nowPlaying = this.player.mediaHandler.nowPlaying;
//If we're playing a youtube video
if(nowPlaying != null && nowPlaying.type == 'yt'){
//Restart the video
this.player.hardReload();
}
//Stop while we're ahead
return;
case 'IACDN':
//If the player or mediaHandler isn't loaded
if(this.player == null || this.player.mediaHandler == null){
//We're fuggin done here
return;
}
//Get current video
nowPlaying = this.player.mediaHandler.nowPlaying;
//If we're playing a video from Internet Archive
if(nowPlaying != null && nowPlaying.type == 'ia'){
console.log("RELOAD");
//Hard reload the media, forcing media handler re-creation
this.player.hardReload();
}
return;
case 'syncTolerance':
//If the player isn't loaded
if(this.player == null){
//We're fuckin' done here
return;
}
//Set syncronization tolerance
this.player.syncTolerance = value;
return;
case 'liveSyncTolerance':
//If the player isn't loaded
if(this.player == null){
//We're fuckin' done here
return;
}
//Set syncronization tolerance
this.player.streamSyncTolerance = value;
return;
case 'syncDelta':
//If the player isn't loaded
if(this.player == null){
//We're fuckin' done here
return;
}
//Set syncronization delta
this.player.syncDelta = value;
return;
case 'chatWidthMin':
//If the chat isn't loaded
if(this.chatBox == null){
//We're fuckin' done here
return;
}
//Set Chat Box Width minimum while Locked to Aspect-Ratio
this.chatBox.chatWidthMinimum = value / 100;
return;
case 'userlistHidden':
//If the userlist class isn't loaded in yet
if(this.userList == null){
//We're fuckin' done here
return;
}
//Pass value down to UI toggle, making sure to allow for string conversion
this.userList.toggleUI(!(value == true || value == "true"));
return;
case 'cinemaMode':
//If the userlist class isn't loaded in yet
if(this.player == null){
//We're fuckin' done here
return;
}
//Pass value down to UI toggle, making sure to allow for string conversion
this.player.toggleCinemaMode(value == true || value == "true");
return;
}
}
/**
* Default channel config
*/
static defaultConfig = new Map([
["ytPlayerType","raw"],
["IACDN",""],
["syncTolerance",0.4],
["liveSyncTolerance", 2],
["syncDelta", 6],
["chatWidthMin", 20],
["userlistHidden", false],
["cinemaMode", false],
["rxPMSound", 'unread'],
["txPMSound", false],
["newSeshSound", true],
["endSeshSound", true]
]);
}
/**
* Youtube iframe-embed API entry point
*/
function onYouTubeIframeAPIReady(){
//Set embed api to true
client.ytEmbedAPILoaded = true;
//Get currently playing item
const nowPlaying = client.player.mediaHandler.nowPlaying;
//If we're playing a youtube video and the official embeds are enabled
if(nowPlaying.type == 'yt' && localStorage.getItem('ytPlayerType') == "embed"){
//Restart the video now that the embed api has loaded
client.player.start({media: nowPlaying});
}
}
const client = new channel();