341 lines
12 KiB
JavaScript
341 lines
12 KiB
JavaScript
/*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("👁️👄👁️ ℬℴ𝓊𝓃𝒿ℴ𝓊𝓇.");
|
||
}
|
||
|
||
/**
|
||
* Handles initial client connection
|
||
*/
|
||
connect(){
|
||
const clientOptions = {
|
||
extraHeaders: {
|
||
//Include CSRF token
|
||
'x-csrf-token': utils.ajax.getCSRFToken()
|
||
}
|
||
};
|
||
|
||
this.socket = io(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("queue", (data) => {
|
||
this.queue = new Map(data.queue);
|
||
});
|
||
|
||
this.socket.on("lock", (data) => {
|
||
this.queueLock = data.locked;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
|
||
//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 for use by the queue panel
|
||
this.queue = new Map(data.queue);
|
||
|
||
//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'){
|
||
//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(); |