complete except for drag&drop queue reordering and sorting
This commit is contained in:
parent
63dec0ede2
commit
ccbb7fcd13
5 changed files with 532 additions and 17 deletions
|
|
@ -1,12 +1,11 @@
|
||||||
import { SpotifyWS } from "./spotify.ts"
|
import { SpotifyWS } from "./spotify.ts"
|
||||||
import { HeartbeatWS } from "./heartbeat.ts";
|
import { HeartbeatWS } from "./heartbeat.ts";
|
||||||
import { PlayerManager } from "./playerManager.ts";
|
import { PlayerManager } from "./playerManager.ts";
|
||||||
import { serveFile } from "@std/http/file-server";
|
import { serveDir } from "@std/http/file-server";
|
||||||
import { UserWS } from "./user.ts";
|
import { UserWS } from "./user.ts";
|
||||||
|
|
||||||
const USER_PATTERN = new URLPattern({pathname: "/ws/user"})
|
const USER_PATTERN = new URLPattern({pathname: "/ws/user"})
|
||||||
const SPOTIFY_PATTERN = new URLPattern({pathname: "/ws/spotify"})
|
const SPOTIFY_PATTERN = new URLPattern({pathname: "/ws/spotify"})
|
||||||
const song = "spotify:track:2VxJVGiTsK6UyrxPJJ2lR9";
|
|
||||||
|
|
||||||
function commonWs(req: Request) {
|
function commonWs(req: Request) {
|
||||||
const { socket, response } = Deno.upgradeWebSocket(req);
|
const { socket, response } = Deno.upgradeWebSocket(req);
|
||||||
|
|
@ -33,11 +32,11 @@ Deno.serve((req) => {
|
||||||
const spState = spotify?.ws.readyState;
|
const spState = spotify?.ws.readyState;
|
||||||
if (spState !== undefined && spState !== WebSocket.CLOSED) spotify?.ws.close();
|
if (spState !== undefined && spState !== WebSocket.CLOSED) spotify?.ws.close();
|
||||||
|
|
||||||
spotify = new SpotifyWS(socket, () => song);
|
new SpotifyWS(socket);
|
||||||
new HeartbeatWS(socket);
|
new HeartbeatWS(socket);
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
return serveFile(req, "./static/index.html")
|
return serveDir(req, {fsRoot: "./static", urlRoot: ""})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,11 @@ export class PlayerManager {
|
||||||
return song.song;
|
return song.song;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public broadcastStatus() {
|
||||||
|
for (const user of Object.values(this.userQueue)) {
|
||||||
|
user.userWS.updateStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
public updateMergedQueue() {
|
public updateMergedQueue() {
|
||||||
this.generateMergedQueue();
|
this.generateMergedQueue();
|
||||||
this.broadcastMergedQueue();
|
this.broadcastMergedQueue();
|
||||||
|
|
@ -69,7 +74,7 @@ export class PlayerManager {
|
||||||
}
|
}
|
||||||
private broadcastMergedQueue() {
|
private broadcastMergedQueue() {
|
||||||
for (const user of Object.values(this.userQueue)) {
|
for (const user of Object.values(this.userQueue)) {
|
||||||
user.userWS.broadcastQueue(this.mergedQueue);
|
user.userWS.updateQueue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,10 @@ export class SpotifyWS {
|
||||||
|
|
||||||
this.onMessage = this.onMessage.bind(this);
|
this.onMessage = this.onMessage.bind(this);
|
||||||
this.onClose = this.onClose.bind(this);
|
this.onClose = this.onClose.bind(this);
|
||||||
|
this.onOpen = this.onOpen.bind(this);
|
||||||
|
|
||||||
this.ws.addEventListener("message", this.onMessage);
|
this.ws.addEventListener("message", this.onMessage);
|
||||||
|
this.ws.addEventListener("open", this.onOpen);
|
||||||
this.ws.addEventListener("close", this.onClose);
|
this.ws.addEventListener("close", this.onClose);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,8 +56,13 @@ export class SpotifyWS {
|
||||||
this.ws.send(data);
|
this.ws.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onOpen() {
|
||||||
|
globalThis.spotify = this;
|
||||||
|
playerManager.broadcastStatus();
|
||||||
|
}
|
||||||
private onClose() {
|
private onClose() {
|
||||||
globalThis.spotify = undefined;
|
globalThis.spotify = undefined;
|
||||||
|
playerManager.broadcastStatus();
|
||||||
}
|
}
|
||||||
private onMessage(msg: MessageEvent<string>) {
|
private onMessage(msg: MessageEvent<string>) {
|
||||||
const text = msg.data;
|
const text = msg.data;
|
||||||
|
|
|
||||||
27
src/user.ts
27
src/user.ts
|
|
@ -23,24 +23,23 @@ export class UserWS {
|
||||||
this.ws.send(buildCommand("logout"))
|
this.ws.send(buildCommand("logout"))
|
||||||
this.ws.close();
|
this.ws.close();
|
||||||
}
|
}
|
||||||
public broadcastQueue(queue: MergedQueue) {
|
|
||||||
this.ws.send(buildCommand("updatemergedqueue", {queue}))
|
|
||||||
}
|
|
||||||
public updateQueue() {
|
public updateQueue() {
|
||||||
const queue = this.playerQueue?.queue;
|
const queue = this.playerQueue?.queue;
|
||||||
if (!queue) return;
|
if (!queue) return;
|
||||||
|
|
||||||
this.ws.send(buildCommand("getqueue", {queue}))
|
this.ws.send(buildCommand("getqueue", {personal: queue, merged: playerManager.mergedQueue }))
|
||||||
|
}
|
||||||
|
public updateStatus() {
|
||||||
|
this.ws.send(buildCommand("status", {status: spotify !== undefined}))
|
||||||
}
|
}
|
||||||
|
|
||||||
private spotifyConnected(): boolean {
|
|
||||||
return spotify === undefined;
|
|
||||||
}
|
|
||||||
private login(name: string) {
|
private login(name: string) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.loggedIn = true;
|
this.loggedIn = true;
|
||||||
|
|
||||||
this.playerQueue = playerManager.login(name, this);
|
this.playerQueue = playerManager.login(name, this);
|
||||||
|
this.updateQueue();
|
||||||
|
this.updateStatus();
|
||||||
}
|
}
|
||||||
private async onMessage(msg: MessageEvent<string>) {
|
private async onMessage(msg: MessageEvent<string>) {
|
||||||
const command = parseCommand(msg.data);
|
const command = parseCommand(msg.data);
|
||||||
|
|
@ -73,12 +72,11 @@ export class UserWS {
|
||||||
const cmd = command as Command<{query: string}>;
|
const cmd = command as Command<{query: string}>;
|
||||||
|
|
||||||
|
|
||||||
const songs = await spotify?.search(cmd.d.query)
|
let songs = await spotify?.search(cmd.d.query).catch(_ => console.log("Failed to search..."))
|
||||||
if (songs === undefined) {
|
if (songs === undefined) {
|
||||||
this.ws.send(buildCommand("search", {songs: [], connected: false}))
|
songs = []
|
||||||
} else {
|
}
|
||||||
this.ws.send(buildCommand("search", {songs, connected: true}))
|
this.ws.send(buildCommand("search", {songs, connected: true}))
|
||||||
}
|
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +102,11 @@ export class UserWS {
|
||||||
|
|
||||||
this.playerQueue.queue.push(cmd.d.song);
|
this.playerQueue.queue.push(cmd.d.song);
|
||||||
playerManager.updateMergedQueue();
|
playerManager.updateMergedQueue();
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "status": {
|
||||||
|
this.updateStatus();
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
500
static/new.html
Normal file
500
static/new.html
Normal file
|
|
@ -0,0 +1,500 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SpotiQueue</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- login page -->
|
||||||
|
<div id="m-login" class="w-full h-dvh flex items-center justify-center">
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">Login</h2>
|
||||||
|
|
||||||
|
<form class="card-actions" id="login-form">
|
||||||
|
<div class="input">
|
||||||
|
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2.5" fill="none" stroke="currentColor">
|
||||||
|
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
|
||||||
|
<circle cx="12" cy="7" r="4"></circle>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<input id="login-input" type="text" required placeholder="Username">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary btn-block" type="submit">Join</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main page -->
|
||||||
|
<div id="m-page" class="hidden w-full h-dvh flex flex-col p-2">
|
||||||
|
<!-- Desktop navbar -->
|
||||||
|
<div class="hidden sm:block pb-2 w-full flex-none">
|
||||||
|
<div class="navbar bg-base-300 shadow-sm rounded-md">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<a class="btn btn-ghost text-xl">SpotiQueue</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-center w-1/2">
|
||||||
|
<form class="join flex-grow">
|
||||||
|
<div class="input join-item w-full">
|
||||||
|
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2.5" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<path d="m21 21-4.3-4.3"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<input type="search" required placeholder="Search your favourite songs...">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary join-item">Search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-end">
|
||||||
|
<a class="btn btn-neutral main-logout" id="nav-logout">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<div class="flex-grow flex flex-col md:flex-row-reverse gap-4 m-2">
|
||||||
|
<div class="flex-3 bg-base-200 rounded-md shadow-md">
|
||||||
|
<ul class="list bg-base-100 rounded-box shadow-md m-1" id="songlist-merged">
|
||||||
|
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide">Merged Queue</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="flex-5 md:flex-4 lg:flex-5 bg-base-200 rounded-md shadow-md">
|
||||||
|
<ul class="list bg-base-100 rounded-box shadow-md m-1" id="songlist-personal">
|
||||||
|
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide">Personal Queue</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Mobile dock -->
|
||||||
|
<div class="dock sm:hidden justify-self-end z-0 static">
|
||||||
|
<button class="dock-active">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256" class="size-[1.2em]">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M32 64a8 8 0 0 1 8-8h176a8 8 0 0 1 0 16H40a8 8 0 0 1-8-8m104 56H40a8 8 0 0 0 0 16h96a8 8 0 0 0 0-16m0 64H40a8 8 0 0 0 0 16h96a8 8 0 0 0 0-16m112-24a8 8 0 0 1-3.76 6.78l-64 40A8 8 0 0 1 168 200v-80a8 8 0 0 1 12.24-6.78l64 40A8 8 0 0 1 248 160m-23.09 0L184 134.43v51.14Z" />
|
||||||
|
</svg>
|
||||||
|
<span class="dock-label">Queues</span>
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="size-[1.2em]">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l5.6 5.6q.275.275.275.7t-.275.7t-.7.275t-.7-.275l-5.6-5.6q-.75.6-1.725.95T9.5 16m0-2q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
|
||||||
|
</svg>
|
||||||
|
<span class="dock-label">Search</span>
|
||||||
|
</button>
|
||||||
|
<button id="dock-logout">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="size-[1.2em]">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h6q.425 0 .713.288T12 4t-.288.713T11 5H5v14h6q.425 0 .713.288T12 20t-.288.713T11 21zm12.175-8H10q-.425 0-.712-.288T9 12t.288-.712T10 11h7.175L15.3 9.125q-.275-.275-.275-.675t.275-.7t.7-.313t.725.288L20.3 11.3q.3.3.3.7t-.3.7l-3.575 3.575q-.3.3-.712.288t-.713-.313q-.275-.3-.262-.712t.287-.688z" />
|
||||||
|
</svg>
|
||||||
|
<span class="dock-label">Logout</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dialog id="pmodal" class="modal modal-bottom sm:modal-middle">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 id="pmodal-header" class="text-lg font-bold"></h3>
|
||||||
|
<p id="pmodal-text" class="py-4"></p>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Components -->
|
||||||
|
<div class="hidden">
|
||||||
|
<!-- template song personal -->
|
||||||
|
<template id="template-song-personal">
|
||||||
|
<li class="list-row song" draggable="true">
|
||||||
|
<div class="drag-handle cursor-move select-none touch-none text-2xl opacity-50 mr-2">⋮⋮</div>
|
||||||
|
<div><img class="size-10 rounded-box"
|
||||||
|
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="thumbnail"></div>
|
||||||
|
<div class="list-col-grow">
|
||||||
|
<div class="song-songname">Songname</div>
|
||||||
|
<div class="text-xs uppercase font-semibold opacity-60 song-artistname">Artistname</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-square btn-soft btn-error">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="size-[1.2em]">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M7 21q-.825 0-1.412-.587T5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413T17 21zm2-4h2V8H9zm4 0h2V8h-2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- template song merged -->
|
||||||
|
<template id="template-song-merged">
|
||||||
|
<li class="list-row song">
|
||||||
|
<div class="text-4xl font-thin opacity-30 tabular-nums">01</div>
|
||||||
|
<div>
|
||||||
|
<img class="size-10 rounded-box"
|
||||||
|
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="thumbnail">
|
||||||
|
</div>
|
||||||
|
<div class="list-col-grow">
|
||||||
|
<div class="song-songname">Songname</div>
|
||||||
|
<div class="text-xs uppercase font-semibold opacity-60 song-artistname">Artist name</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dialog id="modal" class="modal modal-bottom sm:modal-middle">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 id="modal-header" class="text-lg font-bold"></h3>
|
||||||
|
<p id="modal-text" class="py-4"></p>
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<form method="dialog">
|
||||||
|
<button class="btn">Close</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const $ = document.querySelector.bind(document);
|
||||||
|
const $all = document.querySelectorAll.bind(document);
|
||||||
|
|
||||||
|
// ELEMENTS
|
||||||
|
const DOMModal = $("#modal");
|
||||||
|
const DOMModalHeader = $("#modal-header");
|
||||||
|
const DOMModalText = $("#modal-text");
|
||||||
|
const DOMPModal = $("#pmodal");
|
||||||
|
const DOMPModalHeader = $("#pmodal-header");
|
||||||
|
const DOMPModalText = $("#pmodal-text");
|
||||||
|
|
||||||
|
const DOMLoginPage = $("#m-login");
|
||||||
|
const DOMLoginInput = $("#login-input");
|
||||||
|
const DOMLoginForm = $("#login-form");
|
||||||
|
|
||||||
|
const DOMMainPage = $("#m-page");
|
||||||
|
const DOMMainSongPersonal = $("#songlist-personal")
|
||||||
|
const DOMMainSongMerged = $("#songlist-merged")
|
||||||
|
const DOMMainNavLogout = $("#nav-logout");
|
||||||
|
const DOMMainDockLogout = $("#dock-logout");
|
||||||
|
|
||||||
|
const DOMTemplateSongPersonal = $("#template-song-personal");
|
||||||
|
const DOMTemplateSongMerged = $("#template-song-merged");
|
||||||
|
|
||||||
|
|
||||||
|
// VARIABLES
|
||||||
|
let ws;
|
||||||
|
let username = (window.localStorage.username ?? "").trim();
|
||||||
|
let sessionOvertaken = false;
|
||||||
|
let draggedItem = null;
|
||||||
|
let dragStartY = 0;
|
||||||
|
let dragOffset = 0;
|
||||||
|
let draggedItemOriginalIndex = -1;
|
||||||
|
let draggedItemOriginalY = 0;
|
||||||
|
let emptiQueue = {
|
||||||
|
trackName: "Loneliness",
|
||||||
|
authorName: "Empty Queue"
|
||||||
|
}
|
||||||
|
|
||||||
|
// REACTIVE VARIABLES
|
||||||
|
const queues = {
|
||||||
|
_personal: [],
|
||||||
|
_merged: [],
|
||||||
|
|
||||||
|
get personal() {
|
||||||
|
return this._personal
|
||||||
|
},
|
||||||
|
set personal(newV) {
|
||||||
|
this._personal = newV;
|
||||||
|
(DOMMainSongPersonal.querySelectorAll(".song") ?? []).forEach(v => {
|
||||||
|
DOMMainSongPersonal.removeChild(v);
|
||||||
|
})
|
||||||
|
|
||||||
|
if (newV.length === 0) {
|
||||||
|
const el = document.importNode(DOMTemplateSongPersonal.content, true);
|
||||||
|
el.querySelector(".song-songname").innerText = emptiQueue.trackName;
|
||||||
|
el.querySelector(".song-artistname").innerText = emptiQueue.authorName;
|
||||||
|
el.querySelector("img").src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||||
|
DOMMainSongPersonal.append(el);
|
||||||
|
|
||||||
|
DOMMainSongPersonal.querySelector("img")?.remove()
|
||||||
|
DOMMainSongPersonal.querySelector("button")?.remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newV.forEach((song, index) => {
|
||||||
|
const el = document.importNode(DOMTemplateSongPersonal.content, true);
|
||||||
|
|
||||||
|
el.querySelector(".song-songname").innerText = song.name;
|
||||||
|
el.querySelector(".song-artistname").innerText = song.artists.join(", ");
|
||||||
|
el.querySelector("img").src = song.album?.coverUrl ?? "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||||
|
el.querySelector("button").addEventListener("click", () => removeSongFromQueue(index))
|
||||||
|
|
||||||
|
DOMMainSongPersonal.append(el);
|
||||||
|
|
||||||
|
const children = DOMMainSongPersonal.children
|
||||||
|
children[children.length - 1].dataset.index = index;
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
get merged() {
|
||||||
|
return this._merged
|
||||||
|
},
|
||||||
|
set merged(newV) {
|
||||||
|
this._merged = newV;
|
||||||
|
(DOMMainSongMerged.querySelectorAll(".song") ?? []).forEach(v => {
|
||||||
|
DOMMainSongMerged.removeChild(v);
|
||||||
|
})
|
||||||
|
|
||||||
|
if (newV.length === 0) {
|
||||||
|
const el = document.importNode(DOMTemplateSongMerged.content, true);
|
||||||
|
el.querySelector(".song-songname").innerText = emptiQueue.trackName;
|
||||||
|
el.querySelector(".song-artistname").innerText = emptiQueue.authorName;
|
||||||
|
el.querySelector("img").src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||||
|
DOMMainSongMerged.append(el);
|
||||||
|
|
||||||
|
DOMMainSongMerged.querySelector("img")?.remove()
|
||||||
|
DOMMainSongMerged.querySelector("button")?.remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newV.forEach((song, index) => {
|
||||||
|
const el = document.importNode(DOMTemplateSongMerged.content, true);
|
||||||
|
el.querySelector(".tabular-nums").innerText = (index + 1).toString().padStart(2, "0");
|
||||||
|
el.querySelector(".song-songname").innerText = song.name;
|
||||||
|
el.querySelector(".song-artistname").innerText = song.artists.join(", ");
|
||||||
|
el.querySelector("img").src = song.album?.coverUrl ?? "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||||
|
DOMMainSongMerged.append(el);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const connected = { // 0 = connected, 1 = servererror, 2 = disconnected
|
||||||
|
_value: 2,
|
||||||
|
get value() {
|
||||||
|
return this._value
|
||||||
|
},
|
||||||
|
set value(newV) {
|
||||||
|
this._value = newV;
|
||||||
|
|
||||||
|
if (sessionOvertaken) return
|
||||||
|
switch (newV) {
|
||||||
|
case 0:
|
||||||
|
DOMPModal.open = false;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
mpAlert("Connection problem - Extension", "The SpotiQueue server lost connection to the host. Please reconnect the extension.");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
mpAlert("Connection Problem - Server", "Lost connection to the SpotiQueue server. Please try to reconnect.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// FUNCTIONS
|
||||||
|
function mAlert(header, text) {
|
||||||
|
DOMModalHeader.innerText = header;
|
||||||
|
DOMModalText.innerText = text;
|
||||||
|
DOMModal.showModal();
|
||||||
|
}
|
||||||
|
function mpAlert(header, text) {
|
||||||
|
DOMPModalHeader.innerText = header;
|
||||||
|
DOMPModalText.innerText = text;
|
||||||
|
DOMPModal.showModal();
|
||||||
|
}
|
||||||
|
function connect() {
|
||||||
|
ws = new WebSocket(`ws://${location.host}/ws/user`);
|
||||||
|
let willReconnect = true;
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
connected.value = 0;
|
||||||
|
ws.send(JSON.stringify({c: 'login', d: {name: username}}));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
const msg = JSON.parse(e.data);
|
||||||
|
if (msg.c === 'ping') {ws.send(JSON.stringify({c: 'pong'})); return;}
|
||||||
|
else if (msg.c === 'search') {
|
||||||
|
searchResults = msg.d.songs || [];
|
||||||
|
renderSearch();
|
||||||
|
}
|
||||||
|
else if (msg.c === 'getqueue') {
|
||||||
|
console.log(msg.d)
|
||||||
|
queues.personal = msg.d.personal || [];
|
||||||
|
queues.merged = msg.d.personal || [];
|
||||||
|
}
|
||||||
|
else if (msg.c === "logout") {
|
||||||
|
mpAlert("Session takeover", "You've been logged in on another device. Please reload this page to reconnect.")
|
||||||
|
sessionOvertaken = true;
|
||||||
|
}
|
||||||
|
else if (msg.c === "status") {
|
||||||
|
connected.value = msg.d.status ? 0 : 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
connected.value = 2;
|
||||||
|
|
||||||
|
if (sessionOvertaken) return;
|
||||||
|
setTimeout(connect, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function login(e, auto) {
|
||||||
|
e?.preventDefault();
|
||||||
|
username = (username === "" ? DOMLoginInput.value : username).trim();
|
||||||
|
|
||||||
|
if (username === "" && !auto) {
|
||||||
|
mAlert("Error", "You've entered an invalid nickname. Please try again.");
|
||||||
|
return;
|
||||||
|
} else if (username === "") return;
|
||||||
|
|
||||||
|
window.localStorage.username = username;
|
||||||
|
DOMLoginPage.style.display = "none";
|
||||||
|
DOMMainPage.style.display = "flex";
|
||||||
|
DOMLoginInput.value = ""
|
||||||
|
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
function logout(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
username = "";
|
||||||
|
window.localStorage.username = "";
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
function removeSongFromQueue(index) {
|
||||||
|
console.log(index)
|
||||||
|
queues.personal.splice(index, 1)
|
||||||
|
queues.personal = [...queues.personal];
|
||||||
|
|
||||||
|
ws?.send(JSON.stringify({c: 'setqueue', d: {queue: queues.personal}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(y) {
|
||||||
|
if (draggedItem == null) return;
|
||||||
|
|
||||||
|
const allSongs = [...DOMMainSongPersonal.querySelectorAll(".song:not(.opacity-0)")];
|
||||||
|
const otherSongs = allSongs.filter(el => el !== draggedItem);
|
||||||
|
|
||||||
|
let targetIndex = -1;
|
||||||
|
for (let i = 0; i < otherSongs.length; i++) {
|
||||||
|
const element = otherSongs[i];
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const elementMiddle = rect.top + rect.height / 2;
|
||||||
|
|
||||||
|
if (y < elementMiddle) {
|
||||||
|
targetIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetIndex === -1) {
|
||||||
|
DOMMainSongPersonal.appendChild(draggedItem);
|
||||||
|
} else {
|
||||||
|
const targetElement = otherSongs[targetIndex];
|
||||||
|
DOMMainSongPersonal.insertBefore(draggedItem, targetElement);
|
||||||
|
}
|
||||||
|
} function getDragAfterElement(container, y) {
|
||||||
|
const draggableElements = [...container.querySelectorAll(".song:not(.opacity-0):not(.opacity-0)")];
|
||||||
|
|
||||||
|
return draggableElements.reduce((closest, child) => {
|
||||||
|
const box = child.getBoundingClientRect();
|
||||||
|
const offset = y - box.top - box.height / 2;
|
||||||
|
|
||||||
|
if (offset < 0 && offset > closest.offset) {
|
||||||
|
return {offset: offset, element: child};
|
||||||
|
} else {
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
}, {offset: Number.NEGATIVE_INFINITY}).element;
|
||||||
|
}
|
||||||
|
function updateQueueOrder() {
|
||||||
|
const newOrder = [];
|
||||||
|
|
||||||
|
for (let element of DOMMainSongPersonal.querySelectorAll(".song")) {
|
||||||
|
let oldIndex = element.dataset.index;
|
||||||
|
if (oldIndex === undefined) return;
|
||||||
|
|
||||||
|
newOrder.push(queues.personal[+oldIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newOrder.length > 0) {
|
||||||
|
queues.personal = newOrder;
|
||||||
|
ws?.send(JSON.stringify({c: 'setqueue', d: {queue: queues.personal}}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DOMMainSongPersonal.addEventListener("dragstart", (e) => {
|
||||||
|
if (!e.target.classList.contains("song")) return
|
||||||
|
draggedItem = e.target;
|
||||||
|
setTimeout(() => e.target.classList.add("opacity-0"), 0);
|
||||||
|
|
||||||
|
});
|
||||||
|
DOMMainSongPersonal.addEventListener("dragend", (e) => {
|
||||||
|
if (!e.target.classList.contains("song")) return;
|
||||||
|
|
||||||
|
setTimeout(() => e.target.classList.remove("opacity-0"), 0);
|
||||||
|
draggedItem = null;
|
||||||
|
});
|
||||||
|
DOMMainSongPersonal.addEventListener("dragover", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleDragOver(e.clientY);
|
||||||
|
});
|
||||||
|
DOMMainSongPersonal.addEventListener("drop", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
updateQueueOrder();
|
||||||
|
});
|
||||||
|
DOMMainSongPersonal.addEventListener("touchstart", (e) => {
|
||||||
|
if (!e.target.closest(".drag-handle")) return
|
||||||
|
|
||||||
|
const songElement = e.target.closest(".song");
|
||||||
|
if (songElement) {
|
||||||
|
draggedItem = songElement;
|
||||||
|
dragStartY = e.touches[0].clientY;
|
||||||
|
dragOffset = 0;
|
||||||
|
|
||||||
|
const rect = songElement.getBoundingClientRect();
|
||||||
|
songElement.dataset.originalTop = rect.top;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}, {passive: false});
|
||||||
|
|
||||||
|
DOMMainSongPersonal.addEventListener("touchmove", (e) => {
|
||||||
|
if (!draggedItem) return
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const touchY = e.touches[0].clientY;
|
||||||
|
dragOffset = touchY - dragStartY;
|
||||||
|
draggedItem.style.transform = `translateY(${dragOffset}px)`;
|
||||||
|
handleDragOver(touchY);
|
||||||
|
}, {passive: false});
|
||||||
|
|
||||||
|
DOMMainSongPersonal.addEventListener("touchend", (e) => {
|
||||||
|
if (draggedItem === null) return;
|
||||||
|
draggedItem.style.transform = '';
|
||||||
|
updateQueueOrder();
|
||||||
|
|
||||||
|
draggedItem = null;
|
||||||
|
dragStartY = 0;
|
||||||
|
dragOffset = 0;
|
||||||
|
});
|
||||||
|
DOMLoginForm.addEventListener("submit", e => login(e, false));
|
||||||
|
[DOMMainNavLogout, DOMMainDockLogout].forEach(v => v.addEventListener("click", logout))
|
||||||
|
document.body.onload = () => login(null, true);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue