diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js
index 107119d..e591858 100644
--- a/src/app/channel/media/queue.js
+++ b/src/app/channel/media/queue.js
@@ -44,16 +44,38 @@ module.exports = class{
defineListeners(socket){
socket.on("queue", (data) => {this.queueURL(socket, data)});
+ socket.on("delete", (data => {this.deleteMedia(socket, data)}));
+ socket.on("move", (data => {this.moveMedia(socket, data)}));
}
async queueURL(socket, data){
try{
//pull URL and start time from data
- let {url, start} = data;
+ let {url, start, title} = data;
//Pull media list
- const mediaList = await yanker.yankMedia(url);
+ const mediaList = await yanker.yankMedia(url, title);
+
+ //If we didn't find any media
+ if(mediaList == null || mediaList.length <= 0){
+ //Bitch, moan, complain...
+ loggerUtils.socketErrorHandler(socket, "No media found!", "queue");
+ //and ignore it!
+ return;
+ }
+
+ //If we have an invalid time
+ if(start == null || start < (new Date).getTime()){
+ //Get last item from schedule
+ const lastItem = (Array.from(this.schedule)[this.schedule.size - 1]);
+
+ //if we have a last item
+ if(lastItem != null){
+ //Throw it on five ms after the last item
+ start = lastItem[1].startTime + (lastItem[1].duration * 1000) + 5;
+ }
+ }
//Queue the first media object given
this.queueMedia(mediaList[0], start, socket);
@@ -62,6 +84,24 @@ module.exports = class{
}
}
+ deleteMedia(socket, data){
+ try{
+ //Remove media by UUID
+ this.removeMedia(data.uuid, socket);
+ }catch(err){
+ return loggerUtils.socketExceptionHandler(socket, err);
+ }
+ }
+
+ moveMedia(socket, data){
+ try{
+ //Move media by UUID
+ this.rescheduleMedia(data.uuid, data.start, socket);
+ }catch(err){
+ return loggerUtils.socketExceptionHandler(socket, err);
+ }
+ }
+
//Default start time to now + half a second to give everyone time to process shit
queueMedia(inputMedia, start = new Date().getTime() + 50, socket){
//Create a new media queued object, set start time to now
@@ -69,9 +109,6 @@ module.exports = class{
//schedule the media
this.scheduleMedia(mediaObj, socket);
-
- //Refresh the next timer to ensure whatever comes on next is right
- this.refreshNextTimer();
}
refreshNextTimer(){
@@ -93,6 +130,62 @@ module.exports = class{
this.nextTimer = setTimeout(()=>{this.start(nextItem)}, startsIn);
}
+ rescheduleMedia(uuid, start = new Date().getTime() + 50, socket){
+ //Find and remove media from the schedule by UUID
+ const media = this.removeMedia(uuid);
+
+ //If we got a bad request
+ if(media == null){
+ //If an originating socket was provided for this request
+ if(socket != null){
+ //Yell at the user for being an asshole
+ loggerUtils.socketErrorHandler(socket, "Cannot move non-existant item!", "queue");
+ }
+ //Ignore it
+ return;
+ }
+
+ //Set media time
+ media.startTime = start;
+
+ //Re-schedule the media for the given time
+ this.scheduleMedia(media, socket);
+ }
+
+ removeMedia(uuid, socket){
+ //Get requested media
+ const media = this.getItemByUUID(uuid);
+
+ //If we got a bad request
+ if(media == null){
+ //If an originating socket was provided for this request
+ if(socket != null){
+ //Yell at the user for being an asshole
+ loggerUtils.socketErrorHandler(socket, "Cannot delete non-existant item!", "queue");
+ }
+ //Ignore it
+ return;
+ }
+
+ //If we're currently playing the requested item.
+ if(this.nowPlaying != null && this.nowPlaying.uuid == uuid){
+ //End playback
+ this.end();
+ }
+
+ //Take the item out of the schedule
+ this.schedule.delete(media.startTime);
+
+ //Refresh next timer
+ this.refreshNextTimer();
+
+ //Broadcast the channel queue
+ this.broadcastQueue();
+
+ //return found media in-case our calling function needs it :P
+ return media;
+ }
+
scheduleMedia(mediaObj, socket){
/* This is a fun method and I think it deserves it's own little explination...
Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option
@@ -159,6 +252,9 @@ module.exports = class{
//Broadcast the channel queue
this.broadcastQueue();
+
+ //Refresh the next timer to ensure whatever comes on next is right
+ this.refreshNextTimer();
}
start(mediaObj){
@@ -270,6 +366,17 @@ module.exports = class{
}
}
+ getItemByUUID(uuid){
+ //Iterate through the schedule
+ for(let item of this.schedule){
+ //If the uuid matches
+ if(item[1].uuid == uuid){
+ //return the found item
+ return item[1];
+ }
+ }
+ }
+
sendMedia(socket){
//Create data object
const data = {
diff --git a/src/utils/media/yanker.js b/src/utils/media/yanker.js
index 638e0fa..191a58c 100644
--- a/src/utils/media/yanker.js
+++ b/src/utils/media/yanker.js
@@ -21,7 +21,7 @@ const validator = require('validator');//No express here, so regular validator i
const iaUtil = require('./internetArchiveUtils');
const media = require('../../app/channel/media/media');
-module.exports.yankMedia = async function(url){
+module.exports.yankMedia = async function(url, title){
const pullType = await this.getMediaType(url);
if(pullType == 'ia'){
@@ -34,13 +34,21 @@ module.exports.yankMedia = async function(url){
for(let file of mediaInfo.files){
//Split file path by directories
const path = file.name.split('/');
+
//pull filename from path
const name = path[path.length - 1];
+
//Construct link from pulled info
const link = `https://archive.org/download/${mediaInfo.metadata.identifier}/${file.name}`;
- //Create new media object from file info
- mediaList.push(new media(name, name, link, link, 'ia', Number(file.length)));
+ //if we where handed a null title
+ if(title == null || title == ''){
+ //Create new media object from file info substituting filename for title
+ mediaList.push(new media(name, name, link, link, 'ia', Number(file.length)));
+ }else{
+ //Create new media object from file info
+ mediaList.push(new media(title, name, link, link, 'ia', Number(file.length)));
+ }
}
//return media object list
@@ -52,12 +60,16 @@ module.exports.yankMedia = async function(url){
}
module.exports.getMediaType = async function(url){
+ //Encode URI in-case we where handed something a little too humie friendly
+ url = encodeURI(url);
+
//Check if we have a valid url
if(!validator.isURL(url)){
//If not toss the fucker out
return null;
}
+
//If we have link to a resource from archive.org
if(url.match(/^https\:\/\/archive.org\//g)){
//return internet archive code
diff --git a/src/views/partial/panels/queue.ejs b/src/views/partial/panels/queue.ejs
index e7b5ed5..c52e8aa 100644
--- a/src/views/partial/panels/queue.ejs
+++ b/src/views/partial/panels/queue.ejs
@@ -16,7 +16,16 @@ along with this program. If not, see . %>
-
+
+
+
+
+
+
+
+
+
+
@@ -24,9 +33,11 @@ along with this program. If not, see . %>
<%# Probably not the cleanest way to do this but fuggit %>
+
-
+
+
diff --git a/src/views/partial/popup/scheduleMedia.ejs b/src/views/partial/popup/scheduleMedia.ejs
new file mode 100644
index 0000000..3f83583
--- /dev/null
+++ b/src/views/partial/popup/scheduleMedia.ejs
@@ -0,0 +1,21 @@
+<%# 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 . %>
+<%# %>
+
Schedule Media
+
+
+
+
\ No newline at end of file
diff --git a/src/views/partial/styles.ejs b/src/views/partial/styles.ejs
index 9cc8977..0fa4e09 100644
--- a/src/views/partial/styles.ejs
+++ b/src/views/partial/styles.ejs
@@ -1,4 +1,4 @@
-<%# Canopy - The next generation of stoner streaming software
+
<%# Technically favicon has nothing to do with .css, but it's still looks related, uses a link tag, and globally used :P %>
diff --git a/www/css/channel.css b/www/css/channel.css
index 3be0ab3..04a8200 100644
--- a/www/css/channel.css
+++ b/www/css/channel.css
@@ -249,6 +249,7 @@ span.user-entry{
#cpanel-pinned-div{
position: relative;
scrollbar-width: thin;
+ flex: 0 0;
}
.cpanel-div{
diff --git a/www/css/panel/queue.css b/www/css/panel/queue.css
index d7f9410..b6cb460 100644
--- a/www/css/panel/queue.css
+++ b/www/css/panel/queue.css
@@ -20,6 +20,29 @@ along with this program. If not, see .*/
height: 100%;
}
+#queue-controls{
+ position: absolute;
+ left: 0;
+ right: 0;
+ z-index: 3;
+}
+
+#queue-control-offset{
+ margin-bottom: 2em;
+}
+
+#queue-control-buttons{
+ height: 2em;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+
+#queue-control-buttons button{
+ width: 4em;
+}
+
#queue{
display: flex;
flex: 1;
@@ -58,6 +81,7 @@ div.queue-entry{
margin: 0 1em;
right: 0;
left: 0;
+ cursor:grab;
}
div.queue-entry a{
@@ -79,4 +103,8 @@ div.queue-entry a{
right: 0;
text-align: center;
font-size: 3em;
+}
+
+.media-tooltip p{
+ margin: 0;
}
\ No newline at end of file
diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css
index b4c66fb..b17b3ce 100644
--- a/www/css/theme/movie-night.css
+++ b/www/css/theme/movie-night.css
@@ -460,6 +460,10 @@ span.emote-list-trash-icon{
border: 1px solid var(--accent0)
}
+#queue-controls{
+ background-color: var(--bg0-alpha1);
+}
+
span.queue-marker{
background-color: var(--accent0);
}
@@ -477,8 +481,14 @@ div.queue-entry{
border: 1px solid var(--accent1);
}
-.queue-entry a{
+.queue-entry p{
color: var(--accent1);
+ margin: 0;
+ text-align: center;
+}
+
+.media-tooltip p{
+ font-family: monospace;
}
/* altcha theming*/
diff --git a/www/js/channel/channel.js b/www/js/channel/channel.js
index 87a4ad8..11f773e 100644
--- a/www/js/channel/channel.js
+++ b/www/js/channel/channel.js
@@ -57,7 +57,7 @@ class channel{
this.socket.on("error", console.log);
this.socket.on("queue", (data) => {
- this.queue = data.queue;
+ this.queue = new Map(data.queue);
})
}
@@ -74,7 +74,7 @@ class channel{
this.chatBox.handleClientInfo(data);
//Store queue for use by the queue panel
- this.queue = data.queue;
+ this.queue = new Map(data.queue);
}
}
diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js
index 6ca3b7c..305c88a 100644
--- a/www/js/channel/chat.js
+++ b/www/js/channel/chat.js
@@ -59,7 +59,7 @@ class chatBox{
this.autocompleteDisplay.addEventListener("click", this.tabComplete.bind(this));
this.sendButton.addEventListener("click", this.send.bind(this));
this.settingsIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new panelObj(client))});
- this.adminIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new panelObj(client))});
+ this.adminIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new queuePanel(client))});
this.emoteIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new emotePanel(client))});
//Header icons
@@ -310,11 +310,11 @@ class chatBox{
const limit = window.innerWidth * .2;
//Set width to target or 20vh depending on whether or not we've hit the width limit
- this.chatPanel.style.width = targetChatWidth > limit ? `${targetChatWidth}px` : '20vh';
+ this.chatPanel.style.flexBasis = targetChatWidth > limit ? `${targetChatWidth}px` : '20vh';
//Fix busted layout
var pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width;
- this.chatPanel.style.width = `${this.chatPanel.getBoundingClientRect().width + pageBreak}px`;
+ this.chatPanel.style.flexBasis = `${this.chatPanel.getBoundingClientRect().width + pageBreak}px`;
//This sometimes gets called before userList ahs been initiated :p
if(this.client.userList != null){
this.client.userList.clickDragger.fixCutoff();
diff --git a/www/js/channel/cpanel.js b/www/js/channel/cpanel.js
index 65a1eaa..3e5fa2c 100644
--- a/www/js/channel/cpanel.js
+++ b/www/js/channel/cpanel.js
@@ -25,7 +25,7 @@ class cPanel{
this.poppedPanels = [];
//ClickDragger Objects
- this.activePanelDragger = new canopyUXUtils.clickDragger("#cpanel-active-drag-handle", "#cpanel-active-div", false);
+ this.activePanelDragger = new canopyUXUtils.clickDragger("#cpanel-active-drag-handle", "#cpanel-active-div", false, null, false);
this.pinnedPanelDragger = new canopyUXUtils.clickDragger("#cpanel-pinned-drag-handle", "#cpanel-pinned-div", false, this.client.chatBox.clickDragger);
//Element Nodes
@@ -150,6 +150,7 @@ class panelObj{
this.name = name;
this.pageURL = pageURL;
this.panelDocument = panelDocument;
+ this.ownerDoc = this.panelDocument.ownerDocument == null ? this.panelDocument : this.panelDocument.ownerDocument;
this.client = client;
}
@@ -162,6 +163,8 @@ class panelObj{
}
docSwitch(){
+ //Set owner doc
+ this.ownerDoc = this.panelDocument.ownerDocument == null ? this.panelDocument : this.panelDocument.ownerDocument;
}
closer(){
diff --git a/www/js/channel/panels/queuePanel.js b/www/js/channel/panels/queuePanel.js
index faf4ec6..2a3a9fa 100644
--- a/www/js/channel/panels/queuePanel.js
+++ b/www/js/channel/panels/queuePanel.js
@@ -8,11 +8,16 @@ class queuePanel extends panelObj{
//Create variable to hold rescale timer
this.rescaleTimer = null;
+ this.autoscroll = true;
+
//Define non-input event listeners
this.defineListeners();
}
docSwitch(){
+ //Call derived doc switch function
+ super.docSwitch();
+
//Get queue div
this.queue = this.panelDocument.querySelector('#queue');
//Get queue container
@@ -24,6 +29,21 @@ class queuePanel extends panelObj{
//Re-acquire time marker
this.timeMarker = this.panelDocument.querySelector('#time-marker');
+ //Get main control buttons
+ this.addMediaButton = this.panelDocument.querySelector('#queue-add-media');
+ this.scrollLockButton = this.panelDocument.querySelector('#queue-scroll-lock');
+
+ //Get control divs
+ //Add Media
+ this.addMediaDiv = this.panelDocument.querySelector('#queue-media-prompts');
+
+ //Get control div elements
+ //Add Media
+ this.addMediaLinkPrompt = this.panelDocument.querySelector('#media-link-input');
+ this.addMediaNamePrompt = this.panelDocument.querySelector('#media-name-input');
+ this.queueLastButton = this.panelDocument.querySelector('#queue-last-button');
+ this.queueAtButton = this.panelDocument.querySelector('#queue-at-button');
+
//Render out the queue
this.fullRender();
@@ -38,13 +58,48 @@ class queuePanel extends panelObj{
defineListeners(){
- this.client.socket.on("queue", (data) => {
- this.renderQueue();
- })
+ //Render queue when we receive a new copy of the queue data from the server
+ this.client.socket.on("clientMetadata", (data) => {this.renderQueue();})
+ this.client.socket.on("queue", (data) => {this.renderQueue();})
}
setupInput(){
+ //Re-render queue and time-marker on window resize as time-relative absolute positioning will be absolutely thrown
+ this.ownerDoc.defaultView.addEventListener('resize', this.resizeRender.bind(this));
+
+ //queue
this.queue.addEventListener('wheel', this.scaleScroll.bind(this));
+
+ //control bar controls
+ this.addMediaButton.addEventListener('click', this.toggleAddMedia.bind(this));
+ this.scrollLockButton.addEventListener('click', this.lockScroll.bind(this));
+
+ //control bar divs
+ this.queueLastButton.addEventListener('click', this.queueLast.bind(this))
+ }
+
+ toggleAddMedia(event){
+ if(this.addMediaDiv.style.display == 'none'){
+ this.addMediaDiv.style.display = '';
+ }else{
+ this.addMediaDiv.style.display = 'none';
+ }
+ }
+
+ queueLast(event){
+ //Send off the request
+ this.client.socket.emit("queue",{url:this.addMediaLinkPrompt.value, title:this.addMediaNamePrompt.value});
+ this.addMediaLinkPrompt.value = '';
+ this.addMediaNamePrompt.value = '';
+ }
+
+ lockScroll(event){
+ //Enable scroll lock
+ this.autoscroll = true;
+ //Light the indicator
+ this.scrollLockButton.classList.add('positive-button');
+ //Render the marker early to insta-jump
+ this.renderTimeMarker();
}
scaleScroll(event){
@@ -85,7 +140,6 @@ class queuePanel extends panelObj{
//Clear out the queue UI
this.clearQueue();
-
//Calculate new scale
const newScale = this.scale + (scaleFactor * scrollDirection);
@@ -107,6 +161,12 @@ class queuePanel extends panelObj{
clearTimeout(this.rescaleTimer);
//Set timeout to re-render after input stops
this.rescaleTimer = setTimeout(this.fullRender.bind(this), 500);
+ //Otherwise, if we're just scrolling
+ }else{
+ //Disable scroll lock
+ this.autoscroll = false;
+ //Unlight the indicator
+ this.scrollLockButton.classList.remove('positive-button');
}
}
@@ -167,12 +227,26 @@ class queuePanel extends panelObj{
return timeStrings.join(', ');
}
+ resizeRender(event){
+ const date = new Date();
+ this.renderQueue(date);
+ this.renderTimeMarker(date);
+ }
+
clearQueue(){
//Clear out queue container
- this.queueContainer.innerHTML = '';
+ this.queueContainer.innerHTML = '';;
//Clear out queue marker container
this.queueMarkerContainer.innerHTML = '';
+
+ //Grab all related tooltips
+ const tooltips = this.ownerDoc.body.querySelectorAll('.media-tooltip');
+ //clear them out since we're clearing out the elements with the event to remove them
+ //These should clear out on their own on mousemove but this makes things look a *little* prettier :)
+ for(let tooltip of tooltips){
+ tooltip.parentNode.remove();
+ }
//Clear any marker timers
clearTimeout(this.timeMarkerTimer);
@@ -206,13 +280,16 @@ class queuePanel extends panelObj{
}
//render the time marker
- this.renderTimeMarker(date);
+ this.renderTimeMarker(date, true);
//render out the queue
this.renderQueue(date);
}
renderQueue(date = new Date()){
+ //Clear out queue container
+ this.queueContainer.innerHTML = '';
+
//for every entry in the queue
for(let entry of this.client.queue){
//Create entry div
@@ -224,9 +301,64 @@ class queuePanel extends panelObj{
entryDiv.style.bottom = `${this.offsetByDate(new Date(entry[1].startTime + (entry[1].duration * 1000)), true)}px`;
//Create entry title
- const entryTitle = document.createElement('a');
+ const entryTitle = document.createElement('p');
entryTitle.textContent = entry[1].title;
- entryTitle.href = entry[1].url;
+
+ //Tooltip generation
+ //tooltip div
+ const tooltipDiv = document.createElement('div');
+ tooltipDiv.classList.add('media-tooltip');
+
+ //tooltip title
+ const tooltipTitle = document.createElement('p');
+ tooltipTitle.textContent = `Title: ${entry[1].title}`;
+
+ //tooltip filename
+ const tooltipFilename = document.createElement('p');
+ tooltipFilename.textContent = `File Name: ${entry[1].fileName}`;
+
+ //tooltip source
+ const tooltipSource = document.createElement('p');
+ tooltipSource.textContent = `Source: ${entry[1].type}`;
+
+ //tooltip duration
+ const tooltipDuration = document.createElement('p');
+ tooltipDuration.textContent = `Duration: ${entry[1].duration}`;
+
+ //tooltip start
+ const tooltipStart = document.createElement('p');
+ tooltipStart.textContent = `Start Time: ${new Date(entry[1].startTime).toLocaleString()}`;
+
+ //tooltip end
+ const tooltipEnd = document.createElement('p');
+ tooltipEnd.textContent = `Start Time: ${new Date(entry[1].startTime + (entry[1].duration * 1000)).toLocaleString()}`;
+
+ //append components
+ for(let component of [
+ tooltipTitle,
+ tooltipFilename,
+ tooltipSource,
+ tooltipDuration,
+ tooltipStart,
+ tooltipEnd
+ ]){
+ tooltipDiv.append(component);
+ }
+
+ //Setup media tooltip
+ entryDiv.addEventListener('mouseenter',(event)=>{utils.ux.displayTooltip(event, tooltipDiv, false, null, true, this.ownerDoc);});
+
+ //context menu
+ const menuMap = new Map([
+ ["Play now", ()=>{this.client.socket.emit('move', {uuid: entry[1].uuid})}],
+ ["Move To...", ()=>{}],
+ ["Delete", ()=>{this.client.socket.emit('delete', {uuid: entry[1].uuid})}],
+ ["Open in New Tab", ()=>{window.open(entry[1].url, '_blank').focus();}],
+ ["Copy URL", ()=>{navigator.clipboard.writeText(entry[1].url);}],
+ ]);
+
+ //Setup context menu
+ entryDiv.addEventListener('contextmenu', (event)=>{utils.ux.displayContextMenu(event, '', menuMap, this.ownerDoc);});
//Append entry elements
entryDiv.append(entryTitle);
@@ -236,7 +368,7 @@ class queuePanel extends panelObj{
}
}
- renderTimeMarker(date = new Date()){
+ renderTimeMarker(date = new Date(), forceScroll = false){
//Calculate marker position by date
const markerPosition = Math.round(this.offsetByDate(date));
@@ -260,12 +392,9 @@ class queuePanel extends panelObj{
this.timeMarker.style.top = `${markerPosition}px`;
//If the panel document isn't null (we're not actively switching panels)
- if(this.panelDocument != null){
- //Grab the current owner document object
- const ownerDoc = this.panelDocument.ownerDocument == null ? this.panelDocument : this.panelDocument.ownerDocument;
-
+ if(this.panelDocument != null && (this.autoscroll || forceScroll)){
//Get height difference between window and queue layout controller
- const docDifference = ownerDoc.defaultView.innerHeight - this.queueLayoutController.getBoundingClientRect().height;
+ const docDifference = this.ownerDoc.defaultView.innerHeight - this.queueLayoutController.getBoundingClientRect().height;
//Calculate scroll target by body difference and marker position
const scrollTarget = (markerPosition - (this.queueLayoutController.getBoundingClientRect().height - docDifference) / 2) + docDifference;
//Calculate scroll behavior by distance
@@ -292,6 +421,9 @@ class queuePanel extends panelObj{
}
renderQueueScale(inputDate = new Date()){
+ //Clear out queue marker container
+ this.queueMarkerContainer.innerHTML = '';
+
//Make sure we don't modify the date we're handed
const date = structuredClone(inputDate);
@@ -350,8 +482,8 @@ class queuePanel extends panelObj{
}
offsetByDate(date = new Date(), bottomOffset = false){
- //Pull start of day epoch from given date
- const dayEpoch = structuredClone(date).setHours(0,0,0,0);
+ //Pull start of day epoch from given date, make sure to use a new date object so we don't fuck up any date objects passed to us
+ const dayEpoch = new Date(date).setHours(0,0,0,0);
//Get difference between now and day epoch to get time since the start of the current day in milliseconds
const curTime = date.getTime() - dayEpoch;
//Devide by amount of milliseconds in a day to convert time over to a floating point number between 0 and 1
diff --git a/www/js/channel/player.js b/www/js/channel/player.js
index adeb85e..ec9b74f 100644
--- a/www/js/channel/player.js
+++ b/www/js/channel/player.js
@@ -229,7 +229,7 @@ class player{
this.showVideoIcon.style.display = "flex";
this.client.chatBox.hideChatIcon.style.display = "none";
//Need to clear the width from the split, or else it doesn't display properly
- this.client.chatBox.chatPanel.style.width = "100%";
+ this.client.chatBox.chatPanel.style.flexBasis = "100%";
}
}
diff --git a/www/js/utils.js b/www/js/utils.js
index bb415d5..d899d2d 100644
--- a/www/js/utils.js
+++ b/www/js/utils.js
@@ -71,17 +71,21 @@ class canopyUXUtils{
}
}
- displayTooltip(event, content, ajaxTooltip, cb, soft = false){
+ displayTooltip(event, content, ajaxTooltip, cb, soft = false, doc = document){
//Create the tooltip
const tooltip = new canopyUXUtils.tooltip(content, ajaxTooltip, ()=>{
- //Call mouse move again after ajax load to re-calculate position within context of the new content
- tooltip.moveToMouse(event);
+ //If this is an ajax tooltip
+ if(ajaxTooltip){
+ //Call mouse move again after ajax load to re-calculate position within context of the new content
+ tooltip.moveToMouse(event);
+ }
+
//If we have a callback function
if(typeof cb == "function"){
//Call async callback
cb();
}
- });
+ }, doc);
//Move the tooltip with the mouse
event.target.addEventListener('mousemove', tooltip.moveToMouse.bind(tooltip));
@@ -92,18 +96,29 @@ class canopyUXUtils{
//remove the tooltip on mouseleave
event.target.addEventListener('mouseleave', tooltip.remove.bind(tooltip));
+ //Kill tooltip with parent
+ doc.body.addEventListener('mousemove', killWithParent);
+
if(soft){
//remove the tooltip on context menu open
event.target.addEventListener('click', tooltip.remove.bind(tooltip));
event.target.addEventListener('contextmenu', tooltip.remove.bind(tooltip));
}
+
+ function killWithParent(){
+ //If the tooltip parent no longer exists
+ if(!event.target.isConnected){
+ //Kill the whole family :D
+ tooltip.remove();
+ }
+ }
}
- displayContextMenu(event, title, menuMap){
+ displayContextMenu(event, title, menuMap, doc){
event.preventDefault();
//Create context menu
- const contextMenu = new canopyUXUtils.contextMenu(title, menuMap);
+ const contextMenu = new canopyUXUtils.contextMenu(title, menuMap, doc);
//Move context menu to mouse
contextMenu.moveToMouse(event);
@@ -167,12 +182,13 @@ class canopyUXUtils{
}
static tooltip = class{
- constructor(content, ajaxTooltip = false, cb){
+ constructor(content, ajaxTooltip = false, cb, doc = document){
//Define non-tooltip node values
this.content = content;
this.ajaxPopup = ajaxTooltip;
this.cb = cb;
this.id = Math.random();
+ this.doc = doc;
//create and append tooltip
this.tooltip = document.createElement('div');
@@ -190,7 +206,15 @@ class canopyUXUtils{
this.tooltip.textContent = "Loading tooltip..."
this.tooltip.innerHTML = await utils.ajax.getTooltip(this.content);
}else{
- this.tooltip.innerHTML = this.content;
+ //If the content we received is a string
+ if(typeof this.content == "string"){
+ //Use it
+ this.tooltip.innerHTML = this.content;
+ //Otherwise
+ }else{
+ //Append it as a node
+ this.tooltip.appendChild(this.content);
+ }
}
if(this.cb){
@@ -200,14 +224,14 @@ class canopyUXUtils{
}
displayTooltip(){
- document.body.appendChild(this.tooltip);
+ this.doc.body.appendChild(this.tooltip);
}
moveToPos(x,y){
//If the distance between the left edge of the window - the window width is more than the width of our tooltip
- if((window.innerWidth - (window.innerWidth - x)) > this.tooltip.getBoundingClientRect().width){
+ if((this.doc.defaultView.innerWidth - (this.doc.defaultView.innerWidth - x)) > this.tooltip.getBoundingClientRect().width){
//Push it to the right edge of the cursor, where the hard edge typically is
- this.tooltip.style.right = `${window.innerWidth - x}px`;
+ this.tooltip.style.right = `${this.doc.defaultView.innerWidth - x}px`;
this.tooltip.style.left = '';
//otherwise, if we're close to the edge
}else{
@@ -218,9 +242,9 @@ class canopyUXUtils{
//If the distance between the top edge of the window - the window height is more than the heigt of our tooltip
- if((window.innerHeight - (window.innerHeight - y)) > this.tooltip.getBoundingClientRect().height){
+ if((this.doc.defaultView.innerHeight - (this.doc.defaultView.innerHeight - y)) > this.tooltip.getBoundingClientRect().height){
//Push it above the mouse
- this.tooltip.style.bottom = `${window.innerHeight - y}px`;
+ this.tooltip.style.bottom = `${this.doc.defaultView.innerHeight - y}px`;
this.tooltip.style.top = '';
//otherwise if we're close to the edge
}else{
@@ -243,9 +267,9 @@ class canopyUXUtils{
}
static contextMenu = class extends this.tooltip{
- constructor(title, menuMap){
+ constructor(title, menuMap, doc = document){
//Call inherited tooltip constructor
- super('Loading Menu...');
+ super('Loading Menu...', false, null, doc);
//Set tooltip class
this.tooltip.classList.add('context-menu');
@@ -281,8 +305,8 @@ class canopyUXUtils{
//Create event listener to remove tooltip whenever anything is clicked, inside or out of the menu
//Little hacky but we have to do it a bit later to prevent the opening event from closing the menu
- setTimeout(()=>{document.body.addEventListener('click', this.remove.bind(this));}, 1);
- setTimeout(()=>{document.body.addEventListener('contextmenu', this.remove.bind(this));}, 1);
+ setTimeout(()=>{this.doc.body.addEventListener('click', this.remove.bind(this));}, 1);
+ setTimeout(()=>{this.doc.body.addEventListener('contextmenu', this.remove.bind(this));}, 1);
}
}
@@ -390,7 +414,7 @@ class canopyUXUtils{
}
static clickDragger = class{
- constructor(handle, element, leftHandle = true, parent){
+ constructor(handle, element, leftHandle = true, parent, flex = true){
//Pull needed nodes
this.handle = document.querySelector(handle);
this.element = document.querySelector(element);
@@ -407,6 +431,9 @@ class canopyUXUtils{
//we put a click dragger in yo click dragger so you could click and drag while you click and drag
this.parent = parent;
+ //Whether or not click dragger is in a flexbox
+ this.flex = flex;
+
//Setup our event listeners
this.setupInput();
}
@@ -453,7 +480,13 @@ class canopyUXUtils{
//if we're not breaking the page, or we're moving left
if((!this.breakingScreen && pageBreak <= 0) || event.clientX < this.handle.getBoundingClientRect().left){
//Apply difference to width
+ if(this.flex){
+ this.element.style.flexBasis = `${this.calcWidth(difference)}vw`;
+ }
+ //I know it's kludgy to apply this along-side flex basis but it fixes some nasty bugs with nested draggers
+ //Don't @ me, it's not like i'm an actual web developer anyways :P
this.element.style.width = `${this.calcWidth(difference)}vw`;
+
//If we let go here, the width isn't breaking anything so there's nothing to fix.
this.breakingScreen = false;
}else{
@@ -477,6 +510,9 @@ class canopyUXUtils{
fixCutoff(standalone = true, pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width){
//Fix the page width
+ if(this.flex){
+ this.element.style.flexBasis = `${this.calcWidth(this.element.getBoundingClientRect().width + pageBreak)}vw`;
+ }
this.element.style.width = `${this.calcWidth(this.element.getBoundingClientRect().width + pageBreak)}vw`;
//If we're calling this outside of drag() (regardless of draglock unless set otherwise)