Finished up with Persistent queue storage in database.
This commit is contained in:
parent
d60182ceae
commit
8a273d8055
|
|
@ -159,13 +159,13 @@ module.exports = class{
|
|||
//If we don't have a valid UUID
|
||||
if(!validator.isUUID(data.uuid)){
|
||||
//Bitch, moan, complain...
|
||||
loggerUtils.socketErrorHandler(socket, "Bad UUID!", "validation");
|
||||
loggerUtils.socketErrorHandler(socket, "Bad UUID!", "queue");
|
||||
//and ignore it!
|
||||
return;
|
||||
}
|
||||
|
||||
//Remove media by UUID
|
||||
this.removeMedia(data.uuid, socket);
|
||||
await this.removeMedia(data.uuid, socket);
|
||||
}catch(err){
|
||||
return loggerUtils.socketExceptionHandler(socket, err);
|
||||
}
|
||||
|
|
@ -181,7 +181,7 @@ module.exports = class{
|
|||
//If we don't have a valid UUID
|
||||
if(!validator.isUUID(data.uuid)){
|
||||
//Bitch, moan, complain...
|
||||
loggerUtils.socketErrorHandler(socket, "Bad UUID!", "validation");
|
||||
loggerUtils.socketErrorHandler(socket, "Bad UUID!", "queue");
|
||||
//and ignore it!
|
||||
return;
|
||||
}
|
||||
|
|
@ -246,7 +246,7 @@ module.exports = class{
|
|||
this.scheduleMedia(mediaObj, socket);
|
||||
}
|
||||
|
||||
refreshNextTimer(){
|
||||
refreshNextTimer(volatile = false){
|
||||
//Grab the next item
|
||||
const nextItem = this.getNextItem();
|
||||
|
||||
|
|
@ -258,7 +258,7 @@ module.exports = class{
|
|||
//If we have a current item and it isn't currently playing
|
||||
if(currentItem != null && (this.nowPlaying == null || currentItem.uuid != this.nowPlaying.uuid)){
|
||||
//Start the found item at w/ a pre-calculated time stamp to reflect the given start time
|
||||
this.start(currentItem, Math.round((new Date().getTime() - currentItem.startTime) / 1000) + currentItem.startTimeStamp);
|
||||
this.start(currentItem, Math.round((new Date().getTime() - currentItem.startTime) / 1000) + currentItem.startTimeStamp, volatile);
|
||||
}
|
||||
//otherwise if we have an item
|
||||
}else{
|
||||
|
|
@ -268,22 +268,44 @@ module.exports = class{
|
|||
//Clear out any item that might be up next
|
||||
clearTimeout(this.nextTimer);
|
||||
//Set the next timer
|
||||
this.nextTimer = setTimeout(()=>{this.start(nextItem, nextItem.startTimeStamp)}, startsIn);
|
||||
this.nextTimer = setTimeout(()=>{this.start(nextItem, nextItem.startTimeStamp, volatile)}, startsIn);
|
||||
}
|
||||
}
|
||||
|
||||
removeRange(start = new Date().getTime() - 60 * 1000, end = new Date().getTime(), socket){
|
||||
async removeRange(start = new Date().getTime() - 60 * 1000, end = new Date().getTime(), socket){
|
||||
//Find items within given range
|
||||
const foundItems = this.getItemsBetweenEpochs(start, end);
|
||||
|
||||
//For each item
|
||||
for(let item of foundItems){
|
||||
//Remove media
|
||||
this.removeMedia(item.uuid, socket);
|
||||
try{
|
||||
//DO everything ourselves since we don't have a fance end() function to do it
|
||||
const chanDB = await channelModel.findOne({name:this.channel.name});
|
||||
|
||||
//If we couldn't find the channel
|
||||
if(chanDB == null){
|
||||
//FUCK
|
||||
throw new Error(`Unable to find channel document ${this.channel.name} while queue item!`);
|
||||
}
|
||||
|
||||
//For each item
|
||||
for(let item of foundItems){
|
||||
//Remove media, passing down chanDB so we're not looking again and again
|
||||
await this.removeMedia(item.uuid, socket, chanDB);
|
||||
}
|
||||
|
||||
}catch(err){
|
||||
//If this was originated by someone
|
||||
if(socket != null){
|
||||
//Bitch at them
|
||||
loggerUtils.socketExceptionHandler(socket, err);
|
||||
//If not
|
||||
}else{
|
||||
//Bitch to the console
|
||||
loggerUtils.localExceptionHandler(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rescheduleMedia(uuid, start = new Date().getTime() + 5, socket){
|
||||
async rescheduleMedia(uuid, start = new Date().getTime() + 5, socket){
|
||||
//Find our media, don't remove it yet since we want to do some more testing first
|
||||
const media = this.getItemByUUID(uuid);
|
||||
|
||||
|
|
@ -320,7 +342,7 @@ module.exports = class{
|
|||
}
|
||||
|
||||
//Remove the media from the schedule
|
||||
this.removeMedia(uuid);
|
||||
await this.removeMedia(uuid);
|
||||
|
||||
//Grab the old start time for safe keeping
|
||||
const oldStart = media.startTime;
|
||||
|
|
@ -333,7 +355,7 @@ module.exports = class{
|
|||
|
||||
//Attempt to schedule media at given time
|
||||
//Otherwise, if it returns false for fuckup
|
||||
if(!this.scheduleMedia(media, socket)){
|
||||
if(!(await this.scheduleMedia(media, socket))){
|
||||
//Reset start time
|
||||
media.startTime = oldStart;
|
||||
|
||||
|
|
@ -341,39 +363,112 @@ module.exports = class{
|
|||
media.startTimeStamp = 0;
|
||||
|
||||
//Schedule in old slot
|
||||
this.scheduleMedia(media, socket, true);
|
||||
this.scheduleMedia(media, socket, null, true);
|
||||
}
|
||||
}
|
||||
|
||||
removeMedia(uuid, socket){
|
||||
async removeMedia(uuid, socket, chanDB){
|
||||
//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");
|
||||
try{
|
||||
//DO everything ourselves since we don't have a fance end() function to do it
|
||||
chanDB = await channelModel.findOne({name:this.channel.name});
|
||||
|
||||
//If we couldn't find the channel
|
||||
if(chanDB == null){
|
||||
//FUCK
|
||||
throw new Error(`Unable to find channel document ${this.channel.name} while queue item!`);
|
||||
}
|
||||
|
||||
//Keep a copy of the archive that hasn't been changed
|
||||
const preArchive = chanDB.media.archived;
|
||||
|
||||
//Filter out the requested item from the archive
|
||||
chanDB.media.archived = chanDB.media.archived.filter((record)=>{
|
||||
return record.uuid.toString() != uuid;
|
||||
})
|
||||
|
||||
//If nothing changed in the archive
|
||||
if(preArchive.length == chanDB.media.archived.length){
|
||||
//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");
|
||||
}
|
||||
//Otherwise
|
||||
}else{
|
||||
//Broadcast changes
|
||||
this.broadcastQueue(chanDB);
|
||||
|
||||
//Save changes to the DB
|
||||
await chanDB.save();
|
||||
}
|
||||
}catch(err){
|
||||
//If this was originated by someone
|
||||
if(socket != null){
|
||||
//Bitch at them
|
||||
loggerUtils.socketExceptionHandler(socket, err);
|
||||
//If not
|
||||
}else{
|
||||
//Bitch to the console
|
||||
loggerUtils.localExceptionHandler(err);
|
||||
}
|
||||
}
|
||||
|
||||
//Ignore it
|
||||
return false;
|
||||
}
|
||||
|
||||
//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
|
||||
//Take the item out of the schedule map
|
||||
this.schedule.delete(media.startTime);
|
||||
|
||||
//Refresh next timer
|
||||
this.refreshNextTimer();
|
||||
|
||||
//Broadcast the channel queue
|
||||
this.broadcastQueue();
|
||||
//If we're currently playing the requested item.
|
||||
if(this.nowPlaying != null && this.nowPlaying.uuid == uuid){
|
||||
//End playback
|
||||
this.end(false, true);
|
||||
//otherwise
|
||||
}else{
|
||||
try{
|
||||
//DO everything ourselves since we don't have a fance end() function to do it
|
||||
chanDB = await channelModel.findOne({name:this.channel.name});
|
||||
|
||||
//If we couldn't find the channel
|
||||
if(chanDB == null){
|
||||
//FUCK
|
||||
throw new Error(`Unable to find channel document ${this.channel.name} while queue item!`);
|
||||
}
|
||||
|
||||
//Filter media out by UUID
|
||||
chanDB.media.scheduled = chanDB.media.scheduled.filter((record) => {
|
||||
return record.uuid != uuid;
|
||||
});
|
||||
|
||||
await chanDB.save();
|
||||
|
||||
//Broadcast the channel
|
||||
this.broadcastQueue(chanDB);
|
||||
|
||||
}catch(err){
|
||||
//Broadcast the channel
|
||||
this.broadcastQueue();
|
||||
|
||||
//If this was originated by someone
|
||||
if(socket != null){
|
||||
//Bitch at them
|
||||
loggerUtils.socketExceptionHandler(socket, err);
|
||||
//If not
|
||||
}else{
|
||||
//Bitch to the console
|
||||
loggerUtils.localExceptionHandler(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//return found media in-case our calling function needs it :P
|
||||
return media;
|
||||
|
|
@ -400,12 +495,9 @@ module.exports = class{
|
|||
|
||||
//End the media
|
||||
this.end();
|
||||
|
||||
//Broadcast the channel queue
|
||||
this.broadcastQueue();
|
||||
}
|
||||
|
||||
scheduleMedia(mediaObj, socket, force = false){
|
||||
async scheduleMedia(mediaObj, socket, chanDB, force = false, volatile = false, startVolatile = false){
|
||||
/* 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
|
||||
I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass.
|
||||
|
|
@ -490,17 +582,54 @@ module.exports = class{
|
|||
this.broadcastQueue();
|
||||
|
||||
//Refresh the next timer to ensure whatever comes on next is right
|
||||
this.refreshNextTimer();
|
||||
this.refreshNextTimer(startVolatile);
|
||||
|
||||
//If media has more than a minute before starting and DB transactions are enabled
|
||||
if(mediaObj.startTime - new Date().getTime() > 1000 && !volatile){
|
||||
//fuck you yoda you fucking nerd
|
||||
try{
|
||||
//If we didn't get handed a freebie
|
||||
if(chanDB == null){
|
||||
//Go out and get it done ourselves
|
||||
chanDB = await channelModel.findOne({name:this.channel.name});
|
||||
}
|
||||
|
||||
//If we couldn't find the channel
|
||||
if(chanDB == null){
|
||||
//FUCK
|
||||
throw new Error(`Unable to find channel document ${this.channel.name} while saving item to queue!`);
|
||||
}
|
||||
|
||||
//Add media to the persistant schedule
|
||||
chanDB.media.scheduled.push(mediaObj);
|
||||
|
||||
//Save the database
|
||||
chanDB.save();
|
||||
|
||||
//If something fucked up
|
||||
}catch(err){
|
||||
//If this was originated by someone
|
||||
if(socket != null){
|
||||
//Bitch at them
|
||||
loggerUtils.socketExceptionHandler(socket, err);
|
||||
//If not
|
||||
}else{
|
||||
//Bitch to the console
|
||||
loggerUtils.localExceptionHandler(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//return media object for use
|
||||
return mediaObj;
|
||||
}
|
||||
|
||||
async start(mediaObj, timestamp = mediaObj.startTimeStamp){
|
||||
async start(mediaObj, timestamp = mediaObj.startTimeStamp, volatile = false){
|
||||
//If something is already playing
|
||||
if(this.nowPlaying != null){
|
||||
//Silently end the media
|
||||
this.end(true);
|
||||
//Silently end the media in RAM so the database isn't stepping on itself up ahead
|
||||
//Alternatively we could've used await, but then we'd be doubling up on DB transactions :P
|
||||
this.end(true, true, true);
|
||||
}
|
||||
|
||||
//reset current timestamp
|
||||
|
|
@ -509,17 +638,31 @@ module.exports = class{
|
|||
//Set current playing media
|
||||
this.nowPlaying = mediaObj;
|
||||
|
||||
try{
|
||||
//Get our channel
|
||||
const chanDB = await channelModel.findOne({name: this.channel.name});
|
||||
//if DB transactions are enabled
|
||||
if(!volatile){
|
||||
try{
|
||||
//Get our channel
|
||||
const chanDB = await channelModel.findOne({name: this.channel.name});
|
||||
|
||||
//Set the now playing queued media document
|
||||
chanDB.media.nowPlaying = mediaObj;
|
||||
//If nowPlaying isn't null
|
||||
if(chanDB.media.nowPlaying != null){
|
||||
//Archive whats already in there since we're about to clobber the fuck out of it
|
||||
chanDB.media.archived.push(chanDB.media.nowPlaying);
|
||||
}
|
||||
|
||||
//Save the channel
|
||||
await chanDB.save();
|
||||
}catch(err){
|
||||
loggerUtils.localExceptionHandler(err);
|
||||
//Set the now playing queued media document
|
||||
chanDB.media.nowPlaying = mediaObj;
|
||||
|
||||
//Filter media out of schedule by UUID
|
||||
chanDB.media.scheduled = chanDB.media.scheduled.filter((record) => {
|
||||
return record.uuid != mediaObj.uuid;
|
||||
});
|
||||
|
||||
//Save the channel
|
||||
await chanDB.save();
|
||||
}catch(err){
|
||||
loggerUtils.localExceptionHandler(err);
|
||||
}
|
||||
}
|
||||
|
||||
//Send play signal out to the channel
|
||||
|
|
@ -555,7 +698,7 @@ module.exports = class{
|
|||
}
|
||||
}
|
||||
|
||||
async end(quiet = false){
|
||||
async end(quiet = false, noArchive = false, volatile = false, chanDB){
|
||||
try{
|
||||
//Call off any existing sync timer
|
||||
clearTimeout(this.syncTimer);
|
||||
|
|
@ -579,30 +722,41 @@ module.exports = class{
|
|||
this.server.io.in(this.channel.name).emit('end', {});
|
||||
}
|
||||
|
||||
//Now that everything is clean, we can take our time with the DB :P
|
||||
const chanDB = await channelModel.findOne({name:this.channel.name});
|
||||
if(!volatile){
|
||||
//Now that everything is clean, we can take our time with the DB :P
|
||||
chanDB = await channelModel.findOne({name:this.channel.name});
|
||||
|
||||
//If we couldn't find the channel
|
||||
if(chanDB == null){
|
||||
//FUCK
|
||||
throw new Error(`Unable to find channel document ${this.channel.name} while ending queue item!`);
|
||||
//If we couldn't find the channel
|
||||
if(chanDB == null){
|
||||
//FUCK
|
||||
throw new Error(`Unable to find channel document ${this.channel.name} while ending queue item!`);
|
||||
}
|
||||
|
||||
//If we haven't changed 'nowPlaying' in the play list
|
||||
if(chanDB.media.nowPlaying.uuid == wasPlaying.uuid){
|
||||
//Take it out
|
||||
await chanDB.media.nowPlaying.deleteOne();
|
||||
}
|
||||
|
||||
//Take it out of the active schedule
|
||||
this.schedule.delete(wasPlaying.startTime);
|
||||
|
||||
if(!noArchive){
|
||||
//Add the item to the channel archive
|
||||
chanDB.media.archived.push(wasPlaying);
|
||||
}
|
||||
|
||||
//broadcast queue using unsaved archive
|
||||
this.broadcastQueue(chanDB);
|
||||
|
||||
//Save our changes to the DB
|
||||
await chanDB.save();
|
||||
}else{
|
||||
//broadcast queue using unsaved archive
|
||||
this.broadcastQueue(chanDB);
|
||||
}
|
||||
|
||||
//If we haven't changed 'nowPlaying' in the play list
|
||||
if(chanDB.media.nowPlaying.uuid == wasPlaying.uuid){
|
||||
//Take it out
|
||||
await chanDB.media.nowPlaying.deleteOne();
|
||||
}
|
||||
|
||||
//Take it out of the active schedule
|
||||
this.schedule.delete(wasPlaying.startTime);
|
||||
|
||||
//Add the item to the channel archive
|
||||
chanDB.media.archived.push(wasPlaying);
|
||||
|
||||
//Save our changes to the DB
|
||||
await chanDB.save();
|
||||
}catch(err){
|
||||
this.broadcastQueue();
|
||||
loggerUtils.localExceptionHandler(err);
|
||||
}
|
||||
}
|
||||
|
|
@ -704,7 +858,7 @@ module.exports = class{
|
|||
}
|
||||
|
||||
async broadcastQueue(chanDB){
|
||||
this.server.io.in(this.channel.name).emit('queue',{queue: await this.prepQueue()});
|
||||
this.server.io.in(this.channel.name).emit('queue',{queue: await this.prepQueue(chanDB)});
|
||||
}
|
||||
|
||||
async prepQueue(chanDB){
|
||||
|
|
@ -727,11 +881,10 @@ module.exports = class{
|
|||
//Get yestedays epoch
|
||||
const yesterday = new Date().setDate(new Date().getDate() - 1);
|
||||
|
||||
|
||||
//Iterate through the channel archive backwards to save time
|
||||
for(let mediaIndex = chanDB.media.archived.length - 1; mediaIndex >= 0; mediaIndex--){
|
||||
//Grab the current media record
|
||||
const media = chanDB.media.archived[mediaIndex];
|
||||
let media = chanDB.media.archived[mediaIndex].rehydrate();
|
||||
|
||||
//If the media started within the last 24 hours
|
||||
if(media.startTime > yesterday){
|
||||
|
|
@ -770,15 +923,67 @@ module.exports = class{
|
|||
throw new Error(`Unable to find channel document ${this.channel.name} while rehydrating queue!`);
|
||||
}
|
||||
|
||||
const now = new Date().getTime();
|
||||
|
||||
//Next: Update this function to handle ended items
|
||||
|
||||
//If something was playing
|
||||
if(chanDB.media.nowPlaying != null){
|
||||
//Rehydrate the currently playing item
|
||||
//Rehydrate the currently playing item int oa queued media object
|
||||
const wasPlaying = chanDB.media.nowPlaying.rehydrate();
|
||||
|
||||
//Schedule it
|
||||
this.scheduleMedia(wasPlaying, null, true);
|
||||
//If the media hasn't ended yet
|
||||
if(wasPlaying.getEndTime() > now){
|
||||
//Re-Schedule it in RAM
|
||||
await this.scheduleMedia(wasPlaying, null, chanDB, true, true);
|
||||
//Otherwise, if it has
|
||||
}else{
|
||||
//Null out nowPlaying
|
||||
chanDB.media.nowPlaying = null;
|
||||
|
||||
//Archive the bitch
|
||||
chanDB.media.archived.push(wasPlaying);
|
||||
}
|
||||
}
|
||||
|
||||
//Create a new array to hold the new schedule so we only have to write to the DB once.
|
||||
let newSched = [];
|
||||
|
||||
//For every saved scheduled item
|
||||
for(let record of chanDB.media.scheduled){
|
||||
//Rehydrate the current record into a queued media object
|
||||
const mediaObj = record.rehydrate();
|
||||
|
||||
//If the item hasn't started
|
||||
if(mediaObj.startTime > now){
|
||||
//Add record to new schedule
|
||||
newSched.push(record);
|
||||
|
||||
|
||||
//Re-Schedule it in RAM
|
||||
await this.scheduleMedia(mediaObj, null, chanDB, true, true, false);
|
||||
}else{
|
||||
//If the media should be playing now
|
||||
if(mediaObj.getEndTime() > now){
|
||||
//Save record to nowPlaying in the DB
|
||||
chanDB.media.nowPlaying = record;
|
||||
|
||||
//Re-Schedule it in RAM
|
||||
await this.scheduleMedia(mediaObj, null, chanDB, true, true, true);
|
||||
//If it's been ended
|
||||
}else{
|
||||
//Archive ended media
|
||||
chanDB.media.archived.push(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Update schedule to only contain what hasn't been played yet
|
||||
chanDB.media.scheduled = newSched;
|
||||
|
||||
//Save the DB
|
||||
await chanDB.save();
|
||||
|
||||
//if something fucked up
|
||||
}catch(err){
|
||||
//bitch about it in the server console
|
||||
|
|
|
|||
|
|
@ -535,6 +535,10 @@ div.now-playing{
|
|||
text-shadow: var(--focus-glow0);
|
||||
}
|
||||
|
||||
div.archived{
|
||||
color: var(--bg2-alt1);
|
||||
}
|
||||
|
||||
/* altcha theming*/
|
||||
div.altcha{
|
||||
box-shadow: 4px 4px 1px var(--bg1-alt0) inset;
|
||||
|
|
|
|||
|
|
@ -566,7 +566,7 @@ class queuePanel extends panelObj{
|
|||
`Source: ${entry[1].type}`,
|
||||
`Duration: ${entry[1].duration}`,
|
||||
`Start Time: ${new Date(entry[1].startTime).toLocaleString()}${entry[1].startTimeStamp == 0 ? '' : ' (Started Late)'}`,
|
||||
`End Time: ${new Date(this.getMediaEnd(entry[1])).toLocaleString()}`
|
||||
`End Time: ${new Date(this.getMediaEnd(entry[1])).toLocaleString()}${entry[1].earlyEnd == null ? '' : ' (Ended Early)'}`
|
||||
]){
|
||||
//Create a 'p' node
|
||||
const component = document.createElement('p');
|
||||
|
|
@ -587,19 +587,23 @@ class queuePanel extends panelObj{
|
|||
|
||||
//Create context menu map
|
||||
const menuMap = new Map();
|
||||
const now = new Date();
|
||||
|
||||
//If the item hasn't started yet
|
||||
if(entry[1].startTime > new Date().getTime()){
|
||||
if(entry[1].startTime > now.getTime()){
|
||||
//Add 'Play' option to context menu
|
||||
menuMap.set("Play now", ()=>{this.client.socket.emit('move', {uuid: entry[1].uuid})});
|
||||
//Add 'Move To...' option to context menu
|
||||
menuMap.set("Move To...", (event)=>{new reschedulePopup(event, this.client, entry[1], null, this.ownerDoc)});
|
||||
//Otherwise, if the item is currently playing
|
||||
}else if(this.getMediaEnd(entry[1]) > new Date().getTime()){
|
||||
}else if(this.getMediaEnd(entry[1]) > now.getTime()){
|
||||
//Add 'Stop' option to context menu
|
||||
menuMap.set("Stop", ()=>{this.client.socket.emit('stop', {uuid: entry[1].uuid})});
|
||||
//Add the Now Playing glow, not the prettiest place to add this, but why let a good conditional go to waste?
|
||||
entryDiv.classList.add('now-playing');
|
||||
//Otherwise, if the item has been archived
|
||||
}else{
|
||||
entryDiv.classList.add('archived');
|
||||
}
|
||||
|
||||
//Add 'Delete' option to context menu
|
||||
|
|
@ -609,8 +613,11 @@ class queuePanel extends panelObj{
|
|||
//Add 'Copy URL' option to context menu
|
||||
menuMap.set("Copy URL", ()=>{navigator.clipboard.writeText(entry[1].url);})
|
||||
|
||||
//Setup drag n drop
|
||||
entryDiv.addEventListener('mousedown', clickEntry.bind(this));
|
||||
//If the item hasn't yet ended
|
||||
if(this.getMediaEnd(entry[1]) > now.getTime()){
|
||||
//Setup drag n drop
|
||||
entryDiv.addEventListener('mousedown', clickEntry.bind(this));
|
||||
}
|
||||
|
||||
//Setup context menu
|
||||
entryDiv.addEventListener('contextmenu', (event)=>{
|
||||
|
|
|
|||
Loading…
Reference in a new issue