/*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 .*/ /** * Class for Object containing code for managing the Canopy Panel UX */ class cPanel{ /** * Instantiates a new Canopy Panel Management object * @param {channel} client - Parent client Management Object */ constructor(client){ /** * Parent Client Management object */ this.client = client; /** * Active Panel Object */ this.activePanel = null; /** * Pinned Panel Object */ this.pinnedPanel = null; /** * Popped Panel Objects */ this.poppedPanels = []; /** * Click-Dragger object for re-sizable active panel */ this.activePanelDragger = new canopyUXUtils.clickDragger("#cpanel-active-drag-handle", "#cpanel-active-div", false, null, false); /** * Click-Dragger object for re-sizable pinned panel */ this.pinnedPanelDragger = new canopyUXUtils.clickDragger("#cpanel-pinned-drag-handle", "#cpanel-pinned-div", false, this.client.chatBox.clickDragger); //Element Nodes //Active Panel /** * Active Panel Container */ this.activePanelDiv = document.querySelector("#cpanel-active-div"); /** * Active Panel Title */ this.activePanelTitle = document.querySelector("#cpanel-active-title"); /** * Active Title Document Div */ this.activePanelDoc = document.querySelector("#cpanel-active-doc"); /** * Active Panel Pin Icon */ this.activePanelPinIcon = document.querySelector("#cpanel-active-pin-icon"); /** * Active Panel Pop-Out Icon */ this.activePanelPopoutIcon = document.querySelector("#cpanel-active-popout-icon"); /** * Active Panel Close Icon */ this.activePanelCloseIcon = document.querySelector("#cpanel-active-close-icon"); //Pinned Panel /** * Pinned Panel Contianer */ this.pinnedPanelDiv = document.querySelector("#cpanel-pinned-div"); /** * Pinned Panel Title */ this.pinnedPanelTitle = document.querySelector("#cpanel-pinned-title"); /** * Pinned Panel Document Div */ this.pinnedPanelDoc = document.querySelector("#cpanel-pinned-doc"); /** * Pinned Panel Un-Pin Icon */ this.pinnedPanelUnpinIcon = document.querySelector("#cpanel-pinned-unpin-icon"); /** * Pinned Panel Pop-Out Icon */ this.pinnedPanelPopoutIcon = document.querySelector("#cpanel-pinned-popout-icon"); /** * Pinned Panel Close Icon */ this.pinnedPanelCloseIcon = document.querySelector("#cpanel-pinned-close-icon"); this.setupInput(); } /** * Defines input-related event listeners */ setupInput(){ this.activePanelCloseIcon.addEventListener("click", this.hideActivePanel.bind(this)); this.activePanelPinIcon.addEventListener("click", this.pinPanel.bind(this)); this.activePanelPopoutIcon.addEventListener("click", this.popActivePanel.bind(this)); this.pinnedPanelCloseIcon.addEventListener("click", this.hidePinnedPanel.bind(this)); this.pinnedPanelUnpinIcon.addEventListener("click", this.unpinPanel.bind(this)); this.pinnedPanelPopoutIcon.addEventListener("click", this.popPinnedPanel.bind(this)); } /** * Sets Active Panel * @param {panelObj} panel - Panel Object to set as active * @param {String} panelBody - innerHTML of Panel, pulls from panelObj.getPage() if empty */ async setActivePanel(panel, panelBody){ //Set active panel this.activePanel = panel; //Grab panel hypertext content and load it into div this.activePanelDoc.innerHTML = (panelBody == null || panelBody == "") ? await this.activePanel.getPage() : panelBody; //Display panel this.activePanelDiv.style.display = "flex"; this.activePanelTitle.textContent = this.activePanel.name; //Call panel initialization function this.activePanel.panelDocument = this.activePanelDoc; this.activePanel.docSwitch(); } /** * Hides active panel * @param {Event} event - Event passed down from Input Handler * @param {Boolean} keepAlive - Prevents closing panel if true */ hideActivePanel(event, keepAlive = false){ if(!keepAlive){ this.activePanel.closer(); } //Hide the panel this.activePanelDiv.style.display = "none"; //Clear out the panel this.activePanelDoc.innerHTML = ''; //Set active panel to null this.activePanel = null; } /** * Pins active panel */ pinPanel(){ this.setPinnedPanel(this.activePanel, this.activePanelDoc.innerHTML); this.hideActivePanel(null, true); } /** * Pop's out active panel */ popActivePanel(){ this.popPanel(this.activePanel, this.activePanelDoc.innerHTML); this.hideActivePanel(null, true); } /** * Sets pinned panel * @param {panelObj} panel - Panel Object to apply to panel * @param {String} panelBody - Raw HTML to inject into panel body, defaults to panel page if null */ async setPinnedPanel(panel, panelBody){ //Set pinned panel this.pinnedPanel = panel; //Set Title this.pinnedPanelTitle.textContent = this.pinnedPanel.name; //Grab panel hypertext content and load it into div this.pinnedPanelDoc.innerHTML = (panelBody == null || panelBody == "") ? await this.pinnedPanel.getPage() : panelBody; //Display panel this.pinnedPanelDiv.style.display = "flex"; //Call panel initialization function this.pinnedPanel.panelDocument = this.pinnedPanelDoc; this.pinnedPanel.docSwitch(); //Resize to window/content this.pinnedPanelDragger.fixCutoff(); } /** * Hides pinned panel * @param {Event} event - Passed down input event * @param {Boolean} keepAlive - Prevents panel.closer() from running if true */ hidePinnedPanel(event, keepAlive = false){ this.pinnedPanelDiv.style.display = "none"; if(!keepAlive){ this.pinnedPanel.closer(); } this.pinnedPanel = null; } /** * Sets pinned panel to active */ unpinPanel(){ this.setActivePanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML); this.hidePinnedPanel(null, true); } /** * Pops pinned panel */ popPinnedPanel(){ this.popPanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML); this.hidePinnedPanel(null, true); } /** * Pops a new pop-out panel * @param {panelObj} panel - panelObj to apply to the panel * @param {String} panelBody - Raw HTML to inject into panel body, injects panel default if left to null */ popPanel(panel, panelBody){ var newPanel = new poppedPanel(panel, panelBody, this) this.poppedPanels.push(newPanel); } } /** * Template Class for other Classes for Objects which represent a single Canopy Panel */ class panelObj{ /** * Instantiates a new Panel Object * @param {channel} client - Parent client Management Object * @param {String} name - Panel Name * @param {String} pageURL - Panel Default Page URL * @param {Document} panelDocument - Panel Document */ constructor(client, name = "Placeholder Panel", pageURL = "/panel/placeholder", panelDocument = window.document){ /** * Panel Name */ this.name = name; /** * Panel Default Page URL */ this.pageURL = pageURL; /** * Panel Document */ this.panelDocument = panelDocument; /** * Current root document panel doc lives within */ this.ownerDoc = this.panelDocument.ownerDocument == null ? this.panelDocument : this.panelDocument.ownerDocument; /** * Parent Client Management object */ this.client = client; } /** * Fetches panel page from the server * @returns {String} Raw panel doc HTML */ async getPage(){ var response = await fetch(this.pageURL,{ method: "GET", }); return await response.text(); } /** * Handles Document/Panel Changes */ docSwitch(){ //Set owner doc this.ownerDoc = this.panelDocument.ownerDocument == null ? this.panelDocument : this.panelDocument.ownerDocument; } /** * Called upon panel close/exit */ closer(){ } } /** * Class for Objects which represent a single instance of a popped-out panel */ class poppedPanel{ /** * Instantiates a new Popped Panel Object * @param {panelObj} panel - Panel Object to apply to Popped Panel * @param {String} panelBody - Raw HTML to inject into panel body, defaults to panel page if null * @param {cPanel} cPanel - Parent Canopy Panel Management Object */ constructor(panel, panelBody, cPanel){ /** * Panel Object to apply to Popped Panel */ this.panel = panel; /** * Raw HTML to inject into panel body, defaults to panel page if null */ this.panelBody = panelBody; /** * Browser Window taken up by the Popped Panel */ this.window = null; /** * Popped Panel Container Div */ this.pinnedPanelDiv = null; /** * Popped Panel Title */ this.pinnedPanelTitle = null; /** * Popped Panel Document Div */ this.pinnedPanelDoc = null; /** * Popped Panel Close Icon */ this.pinnedPanelCloseIcon = null; /** * Parent Canopy Panel Management Object */ this.cPanel = cPanel; /** * Disables this.panel.closer() calls from this.closer() */ this.keepAlive = false; //Continue constructor asynchrnously this.asyncConstructor(); } /** * Continuation of constructor method for asynchronous function calls */ async asyncConstructor(){ //Set panel body properly this.panelBody = (this.panelBody == null || this.panelBody == "") ? await this.panel.getPage() : this.panelBody; //Pop the panel this.popContainer(); } /** * Pops/Opens container window upon start */ popContainer(){ //Set Window Object this.window = window.open("/panel/popoutContainer","",`menubar=no,height=850,width=600`); this.window.addEventListener("load", this.fillContainer.bind(this)); } /** * Fills container window with Popped Panel container elements */ fillContainer(){ //Set Element Nodes this.panelDiv = this.window.document.querySelector("#cpanel-div"); this.panelTitle = this.window.document.querySelector("#cpanel-title"); this.panelDoc = this.window.document.querySelector("#cpanel-doc"); this.panelPopinIcon = this.window.document.querySelector("#cpanel-popin-icon"); this.panelPinIcon = this.window.document.querySelector("#cpanel-pin-icon"); //Set Window Title this.window.document.title = this.window.document.title.replace("NULL_POPOUT", `${this.panel.name} (${client.channelName})`); //Set Panel Content this.panelTitle.innerText = this.panel.name; this.panelDoc.innerHTML = this.panelBody; //Set panel object document and call the related function this.panel.panelDocument = this.window.document; this.panel.docSwitch(); this.setupInput(); } /** * Defines default input-related popped-panel Event Listeners */ setupInput(){ this.panelPopinIcon.addEventListener("click", this.unpop.bind(this)); this.panelPinIcon.addEventListener("click", this.pin.bind(this)); this.window.addEventListener("unload", this.closer.bind(this)); } /** * Called upon close/exit of panel */ closer(){ if(!this.keepAlive){ this.panel.closer(); } this.cPanel.poppedPanels.splice(this.cPanel.poppedPanels.indexOf(this),1); } /** * Un-pops panel into active-panel slot */ unpop(){ //Set active panel this.cPanel.setActivePanel(this.panel, this.panelDoc.innerHTML); this.keepAlive = true; //Close the popped window this.window.close(); } /** * Pins panel next to chat */ pin(){ this.cPanel.setPinnedPanel(this.panel, this.panelDoc.innerHTML); this.keepAlive = true; this.window.close(); } }