Made new ui the standard
This commit is contained in:
parent
eb2f3e8fc5
commit
6236e533a7
3 changed files with 826 additions and 826 deletions
|
|
@ -1,352 +1,524 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Spotify Queue</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #121212;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #1f1f1f;
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
background: #1f1f1f;
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
input[type=text],
|
||||
button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
flex: 1;
|
||||
background: #2a2a2a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #1db954;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #555;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.song {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
background: #1a1a1a;
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.song.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.song img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.song div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.song .title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.song .artist {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.song .user {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
background: #2a2a2a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tabs button.active {
|
||||
background: #1db954;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 300px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
<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>
|
||||
<div id="loginScreen" class="login">
|
||||
<h2>Enter your name</h2>
|
||||
<input id="usernameInput" type="text" placeholder="Username" />
|
||||
<button onclick="login()">Join</button>
|
||||
<!-- 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>
|
||||
|
||||
<div id="app" class="hidden">
|
||||
<header>
|
||||
<h1>Spotify Queue</h1>
|
||||
<button onclick="logout()">Logout</button>
|
||||
</header>
|
||||
<!-- 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="tabs">
|
||||
<button id="tabMy" class="active" onclick="switchTab('my')">My Queue</button>
|
||||
<button id="tabMerged" onclick="switchTab('merged')">Merged Queue</button>
|
||||
<div class="navbar-center w-1/2">
|
||||
<form class="join flex-grow" style="anchor-name: --search-nav;" id="search-nav-form">
|
||||
<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..." id="search-nav-input">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary join-item">Search</button>
|
||||
</form>
|
||||
<ul class="dropdown list rounded-box bg-base-100 shadow-sm overflow-y-scroll max-h-[40vh]" popover
|
||||
id="search-nav-dropdown" style="position-anchor:--search-nav; width: anchor-size(width);">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<a class="btn btn-neutral main-logout" id="nav-logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div id="myView">
|
||||
<div style="display:flex; gap:0.5rem; margin-bottom:1rem;">
|
||||
<input id="searchInput" type="text" placeholder="Search for songs…" />
|
||||
<button onclick="search()">Search</button>
|
||||
</div>
|
||||
<div id="searchResults"></div>
|
||||
<h3 style="margin-top:1rem;">My Queue</h3>
|
||||
<div id="myQueue"></div>
|
||||
</div>
|
||||
|
||||
<div id="mergedView" class="hidden">
|
||||
<h3>Merged Queue</h3>
|
||||
<div id="mergedQueue"></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>
|
||||
</main>
|
||||
<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>
|
||||
|
||||
<!-- <footer> -->
|
||||
<!-- <input id="searchFooter" type="text" placeholder="Search…" /> -->
|
||||
<!-- <button onclick="searchFooter()">Search</button> -->
|
||||
<!-- </footer> -->
|
||||
|
||||
<!-- 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 id="search-dock-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>
|
||||
|
||||
<script>
|
||||
let ws;
|
||||
let username = '';
|
||||
let myQueue = [];
|
||||
let mergedQueue = [];
|
||||
let searchResults = [];
|
||||
let dragged = null;
|
||||
<!-- Custom Components -->
|
||||
<div class="hidden">
|
||||
<!-- template song personal -->
|
||||
<template id="template-song-personal">
|
||||
<li class="list-row song">
|
||||
<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>
|
||||
|
||||
<!-- template song search -->
|
||||
<template id="template-song-search">
|
||||
<li class="list-row song">
|
||||
<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>
|
||||
<button class="btn btn-square btn-soft btn-success">
|
||||
+
|
||||
</button>
|
||||
</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>
|
||||
|
||||
|
||||
<dialog id="search-dock-modal" class="modal modal-bottom sm:modal-middle">
|
||||
<div class="modal-box flex flex-col sm:h-auto max-h-[70vh]">
|
||||
<ul id="search-dock-songs" class="list overflow-y-auto flex-grow">
|
||||
</ul>
|
||||
|
||||
<div class="modal-action mt-4">
|
||||
<div class="join flex-grow">
|
||||
<form class="contents" style="anchor-name: --search-nav;" id="search-dock-form">
|
||||
<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..." id="search-dock-input">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary join-item">Search</button>
|
||||
</form>
|
||||
<form method="dialog">
|
||||
<button class="btn btn-neutral join-item text-2xl">×</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
|
||||
<script type="module">
|
||||
import Sortable from 'https://cdn.jsdelivr.net/npm/@shopify/draggable/build/esm/Sortable/Sortable.mjs';
|
||||
|
||||
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");
|
||||
const DOMTemplateSongSearch = $("#template-song-search");
|
||||
|
||||
const DOMSearchNavForm = $("#search-nav-form")
|
||||
const DOMSearchNavDropown = $("#search-nav-dropdown")
|
||||
const DOMSearchNavInput = $("#search-nav-input")
|
||||
|
||||
const DOMSearchDockSongs = $("#search-dock-songs")
|
||||
const DOMSearchDockModal = $("#search-dock-modal")
|
||||
const DOMSearchDockForm = $("#search-dock-form")
|
||||
const DOMSearchDockInput = $("#search-dock-input")
|
||||
const DOMSearchDockButton = $("#search-dock-button")
|
||||
|
||||
|
||||
// VARIABLES
|
||||
let ws;
|
||||
let username = (window.localStorage.username ?? "").trim();
|
||||
let sessionOvertaken = false;
|
||||
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);
|
||||
|
||||
})
|
||||
},
|
||||
|
||||
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((data, index) => {
|
||||
const {user, song} = data;
|
||||
|
||||
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 = user + " - " + 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.send(JSON.stringify({c: 'getqueue'}));
|
||||
};
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
const msg = JSON.parse(e.data);
|
||||
if (msg.c === 'ping') {ws.send(JSON.stringify({c: 'pong'})); return;}
|
||||
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.merged || [];
|
||||
}
|
||||
if (msg.c === 'getqueue') {
|
||||
myQueue = msg.d.queue || [];
|
||||
renderMyQueue();
|
||||
else if (msg.c === "logout") {
|
||||
mpAlert("Session takeover", "You've been logged in on another device. Please reload this page to reconnect.")
|
||||
sessionOvertaken = true;
|
||||
}
|
||||
if (msg.c === 'updatemergedqueue') {
|
||||
console.log(msg)
|
||||
mergedQueue = msg.d.queue || [];
|
||||
if (document.getElementById('tabMerged').classList.contains('active')) renderMerged();
|
||||
else if (msg.c === "status") {
|
||||
connected.value = msg.d.status ? 0 : 1;
|
||||
}
|
||||
if (msg.c === "logout") {
|
||||
location.reload();
|
||||
else if (msg.c === "search") {
|
||||
for (let song of msg.d.songs) {
|
||||
const el = document.importNode(DOMTemplateSongSearch.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";
|
||||
|
||||
const el1 = el.cloneNode(true)
|
||||
|
||||
DOMSearchNavDropown.append(el);
|
||||
DOMSearchDockSongs.append(el1);
|
||||
|
||||
const handler = () => ws?.send(JSON.stringify({
|
||||
c: "queuesong",
|
||||
d: {song}
|
||||
}))
|
||||
const childrenD = DOMSearchNavDropown.children;
|
||||
const childrenS = DOMSearchDockSongs.children;
|
||||
childrenD[childrenD.length - 1]?.querySelector("button")?.addEventListener("click", handler);
|
||||
childrenS[childrenS.length - 1]?.querySelector("button")?.addEventListener("click", handler);
|
||||
}
|
||||
|
||||
DOMSearchDockSongs.scrollTo({top: 0})
|
||||
}
|
||||
};
|
||||
ws.onclose = () => setTimeout(connect, 3000);
|
||||
ws.onclose = () => {
|
||||
connected.value = 2;
|
||||
|
||||
if (sessionOvertaken) return;
|
||||
setTimeout(connect, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function login() {
|
||||
const name = document.getElementById('usernameInput').value.trim();
|
||||
if (!name) return;
|
||||
function login(e, auto) {
|
||||
e?.preventDefault();
|
||||
username = (username === "" ? DOMLoginInput.value : username).trim();
|
||||
|
||||
username = name;
|
||||
document.getElementById('loginScreen').style.display = 'none';
|
||||
document.getElementById('app').classList.remove('hidden');
|
||||
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();
|
||||
|
||||
function logout() {
|
||||
username = '';
|
||||
if (ws) ws.close();
|
||||
document.getElementById('app').classList.add('hidden');
|
||||
document.getElementById('loginScreen').style.display = 'flex';
|
||||
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 navSearch(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const query = DOMSearchNavInput.value.trim();
|
||||
if (query === "") return
|
||||
|
||||
ws?.send(JSON.stringify({
|
||||
c: "search",
|
||||
d: {query}
|
||||
}))
|
||||
|
||||
DOMSearchNavDropown.innerHTML = "";
|
||||
DOMSearchNavDropown.showPopover();
|
||||
}
|
||||
function dockSearch(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const query = DOMSearchDockInput.value.trim();
|
||||
if (query === "") return
|
||||
|
||||
ws?.send(JSON.stringify({
|
||||
c: "search",
|
||||
d: {query}
|
||||
}))
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
document.getElementById('tabMy').classList.toggle('active', tab === 'my');
|
||||
document.getElementById('tabMerged').classList.toggle('active', tab === 'merged');
|
||||
document.getElementById('myView').classList.toggle('hidden', tab !== 'my');
|
||||
document.getElementById('mergedView').classList.toggle('hidden', tab !== 'merged');
|
||||
if (tab === 'merged') renderMerged();
|
||||
}
|
||||
Array.prototype.move = function (from, to) {
|
||||
this.splice(to, 0, this.splice(from, 1)[0]);
|
||||
};
|
||||
let sortable = new Sortable(DOMMainSongPersonal, {
|
||||
draggable: '.song',
|
||||
handle: '.drag-handle',
|
||||
mirror: {
|
||||
constrainDimensions: true,
|
||||
}
|
||||
});
|
||||
sortable.on('sortable:stop', (evt) => {
|
||||
setTimeout(() => {
|
||||
const {oldIndex, newIndex} = evt.data;
|
||||
queues.personal.move(oldIndex, newIndex);
|
||||
|
||||
function search() {
|
||||
const q = document.getElementById('searchInput').value.trim();
|
||||
if (!q) return;
|
||||
ws.send(JSON.stringify({c: 'search', d: {query: q}}));
|
||||
}
|
||||
function searchFooter() {
|
||||
const q = document.getElementById('searchFooter').value.trim();
|
||||
if (!q) return;
|
||||
document.getElementById('searchInput').value = q;
|
||||
search();
|
||||
switchTab('my');
|
||||
}
|
||||
if (queues.personal.length > 0) {
|
||||
ws?.send(JSON.stringify({c: 'setqueue', d: {queue: queues.personal}}));
|
||||
}
|
||||
}, 0)
|
||||
});
|
||||
|
||||
function addSong(song) {
|
||||
myQueue.push(song);
|
||||
ws.send(JSON.stringify({c: 'queuesong', d: {song}}));
|
||||
ws.send(JSON.stringify({c: 'getqueue'}));
|
||||
}
|
||||
DOMLoginForm.addEventListener("submit", e => login(e, false));
|
||||
[DOMMainNavLogout, DOMMainDockLogout].forEach(v => v.addEventListener("click", logout))
|
||||
document.body.onload = () => login(null, true);
|
||||
|
||||
function removeSong(idx) {
|
||||
myQueue.splice(idx, 1);
|
||||
ws.send(JSON.stringify({c: 'setqueue', d: {queue: myQueue}}));
|
||||
renderMyQueue();
|
||||
}
|
||||
DOMSearchNavForm.addEventListener("submit", navSearch)
|
||||
DOMSearchDockForm.addEventListener("submit", dockSearch)
|
||||
DOMSearchDockButton.addEventListener("click", () => {
|
||||
DOMSearchDockSongs.innerHTML = "";
|
||||
DOMSearchDockModal.showModal()
|
||||
})
|
||||
|
||||
function renderSearch() {
|
||||
const el = document.getElementById('searchResults');
|
||||
el.innerHTML = '';
|
||||
searchResults.forEach(s => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'song';
|
||||
div.innerHTML = `
|
||||
<img src="${s.album.coverUrl || 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'}" alt="">
|
||||
<div>
|
||||
<div class="title">${s.name}</div>
|
||||
<div class="artist">${s.artists.join(', ')}</div>
|
||||
</div>
|
||||
<button onclick='addSong(${JSON.stringify(s)})'>+</button>
|
||||
`;
|
||||
el.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMyQueue() {
|
||||
const el = document.getElementById('myQueue');
|
||||
el.innerHTML = '';
|
||||
myQueue.forEach((s, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'song';
|
||||
div.draggable = true;
|
||||
div.dataset.idx = i;
|
||||
div.innerHTML = `
|
||||
<img src="${s.album.coverUrl || 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'}" alt="">
|
||||
<div>
|
||||
<div class="title">${s.name}</div>
|
||||
<div class="artist">${s.artists.join(', ')}</div>
|
||||
</div>
|
||||
<button onclick="removeSong(${i})">✕</button>
|
||||
`;
|
||||
div.ondragstart = e => {dragged = +e.target.dataset.idx; e.target.classList.add('dragging');};
|
||||
div.ondragend = e => e.target.classList.remove('dragging');
|
||||
div.ondragover = e => e.preventDefault();
|
||||
div.ondrop = e => {
|
||||
e.preventDefault();
|
||||
const to = +e.currentTarget.dataset.idx;
|
||||
if (dragged === to) return;
|
||||
const temp = myQueue[dragged];
|
||||
myQueue.splice(dragged, 1);
|
||||
myQueue.splice(to, 0, temp);
|
||||
ws.send(JSON.stringify({c: 'setqueue', d: {queue: myQueue}}));
|
||||
renderMyQueue();
|
||||
};
|
||||
el.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMerged() {
|
||||
const el = document.getElementById('mergedQueue');
|
||||
el.innerHTML = '';
|
||||
mergedQueue.forEach(item => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'song';
|
||||
div.innerHTML = `
|
||||
<img src="${item.song.album.coverUrl || 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'}" alt="">
|
||||
<div>
|
||||
<div class="title">${item.song.name}</div>
|
||||
<div class="artist">${item.song.artists.join(', ')}</div>
|
||||
<div class="user">Added by ${item.user}</div>
|
||||
</div>
|
||||
`;
|
||||
el.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function escSong(s) {
|
||||
return encodeURIComponent(JSON.stringify(s).replace(/'/g, "\\'"));
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
524
static/new.html
524
static/new.html
|
|
@ -1,524 +0,0 @@
|
|||
<!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" style="anchor-name: --search-nav;" id="search-nav-form">
|
||||
<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..." id="search-nav-input">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary join-item">Search</button>
|
||||
</form>
|
||||
<ul class="dropdown list rounded-box bg-base-100 shadow-sm overflow-y-scroll max-h-[40vh]" popover
|
||||
id="search-nav-dropdown" style="position-anchor:--search-nav; width: anchor-size(width);">
|
||||
</ul>
|
||||
</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 id="search-dock-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">
|
||||
<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>
|
||||
|
||||
<!-- template song search -->
|
||||
<template id="template-song-search">
|
||||
<li class="list-row song">
|
||||
<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>
|
||||
<button class="btn btn-square btn-soft btn-success">
|
||||
+
|
||||
</button>
|
||||
</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>
|
||||
|
||||
|
||||
<dialog id="search-dock-modal" class="modal modal-bottom sm:modal-middle">
|
||||
<div class="modal-box flex flex-col sm:h-auto max-h-[70vh]">
|
||||
<ul id="search-dock-songs" class="list overflow-y-auto flex-grow">
|
||||
</ul>
|
||||
|
||||
<div class="modal-action mt-4">
|
||||
<div class="join flex-grow">
|
||||
<form class="contents" style="anchor-name: --search-nav;" id="search-dock-form">
|
||||
<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..." id="search-dock-input">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary join-item">Search</button>
|
||||
</form>
|
||||
<form method="dialog">
|
||||
<button class="btn btn-neutral join-item text-2xl">×</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
|
||||
<script type="module">
|
||||
import Sortable from 'https://cdn.jsdelivr.net/npm/@shopify/draggable/build/esm/Sortable/Sortable.mjs';
|
||||
|
||||
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");
|
||||
const DOMTemplateSongSearch = $("#template-song-search");
|
||||
|
||||
const DOMSearchNavForm = $("#search-nav-form")
|
||||
const DOMSearchNavDropown = $("#search-nav-dropdown")
|
||||
const DOMSearchNavInput = $("#search-nav-input")
|
||||
|
||||
const DOMSearchDockSongs = $("#search-dock-songs")
|
||||
const DOMSearchDockModal = $("#search-dock-modal")
|
||||
const DOMSearchDockForm = $("#search-dock-form")
|
||||
const DOMSearchDockInput = $("#search-dock-input")
|
||||
const DOMSearchDockButton = $("#search-dock-button")
|
||||
|
||||
|
||||
// VARIABLES
|
||||
let ws;
|
||||
let username = (window.localStorage.username ?? "").trim();
|
||||
let sessionOvertaken = false;
|
||||
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);
|
||||
|
||||
})
|
||||
},
|
||||
|
||||
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((data, index) => {
|
||||
const {user, song} = data;
|
||||
|
||||
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 = user + " - " + 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 === 'getqueue') {
|
||||
console.log(msg.d)
|
||||
queues.personal = msg.d.personal || [];
|
||||
queues.merged = msg.d.merged || [];
|
||||
}
|
||||
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;
|
||||
}
|
||||
else if (msg.c === "search") {
|
||||
for (let song of msg.d.songs) {
|
||||
const el = document.importNode(DOMTemplateSongSearch.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";
|
||||
|
||||
const el1 = el.cloneNode(true)
|
||||
|
||||
DOMSearchNavDropown.append(el);
|
||||
DOMSearchDockSongs.append(el1);
|
||||
|
||||
const handler = () => ws?.send(JSON.stringify({
|
||||
c: "queuesong",
|
||||
d: {song}
|
||||
}))
|
||||
const childrenD = DOMSearchNavDropown.children;
|
||||
const childrenS = DOMSearchDockSongs.children;
|
||||
childrenD[childrenD.length - 1]?.querySelector("button")?.addEventListener("click", handler);
|
||||
childrenS[childrenS.length - 1]?.querySelector("button")?.addEventListener("click", handler);
|
||||
}
|
||||
|
||||
DOMSearchDockSongs.scrollTo({top: 0})
|
||||
}
|
||||
};
|
||||
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 navSearch(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const query = DOMSearchNavInput.value.trim();
|
||||
if (query === "") return
|
||||
|
||||
ws?.send(JSON.stringify({
|
||||
c: "search",
|
||||
d: {query}
|
||||
}))
|
||||
|
||||
DOMSearchNavDropown.innerHTML = "";
|
||||
DOMSearchNavDropown.showPopover();
|
||||
}
|
||||
function dockSearch(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const query = DOMSearchDockInput.value.trim();
|
||||
if (query === "") return
|
||||
|
||||
ws?.send(JSON.stringify({
|
||||
c: "search",
|
||||
d: {query}
|
||||
}))
|
||||
}
|
||||
|
||||
Array.prototype.move = function (from, to) {
|
||||
this.splice(to, 0, this.splice(from, 1)[0]);
|
||||
};
|
||||
let sortable = new Sortable(DOMMainSongPersonal, {
|
||||
draggable: '.song',
|
||||
handle: '.drag-handle',
|
||||
mirror: {
|
||||
constrainDimensions: true,
|
||||
}
|
||||
});
|
||||
sortable.on('sortable:stop', (evt) => {
|
||||
setTimeout(() => {
|
||||
const {oldIndex, newIndex} = evt.data;
|
||||
queues.personal.move(oldIndex, newIndex);
|
||||
|
||||
if (queues.personal.length > 0) {
|
||||
ws?.send(JSON.stringify({c: 'setqueue', d: {queue: queues.personal}}));
|
||||
}
|
||||
}, 0)
|
||||
});
|
||||
|
||||
DOMLoginForm.addEventListener("submit", e => login(e, false));
|
||||
[DOMMainNavLogout, DOMMainDockLogout].forEach(v => v.addEventListener("click", logout))
|
||||
document.body.onload = () => login(null, true);
|
||||
|
||||
DOMSearchNavForm.addEventListener("submit", navSearch)
|
||||
DOMSearchDockForm.addEventListener("submit", dockSearch)
|
||||
DOMSearchDockButton.addEventListener("click", () => {
|
||||
DOMSearchDockSongs.innerHTML = "";
|
||||
DOMSearchDockModal.openModal()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
352
static/old.html
Normal file
352
static/old.html
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Spotify Queue</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #121212;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #1f1f1f;
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
background: #1f1f1f;
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
input[type=text],
|
||||
button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
flex: 1;
|
||||
background: #2a2a2a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #1db954;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #555;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.song {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
background: #1a1a1a;
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.song.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.song img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.song div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.song .title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.song .artist {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.song .user {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
background: #2a2a2a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tabs button.active {
|
||||
background: #1db954;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 300px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="loginScreen" class="login">
|
||||
<h2>Enter your name</h2>
|
||||
<input id="usernameInput" type="text" placeholder="Username" />
|
||||
<button onclick="login()">Join</button>
|
||||
</div>
|
||||
|
||||
<div id="app" class="hidden">
|
||||
<header>
|
||||
<h1>Spotify Queue</h1>
|
||||
<button onclick="logout()">Logout</button>
|
||||
</header>
|
||||
|
||||
<div class="tabs">
|
||||
<button id="tabMy" class="active" onclick="switchTab('my')">My Queue</button>
|
||||
<button id="tabMerged" onclick="switchTab('merged')">Merged Queue</button>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div id="myView">
|
||||
<div style="display:flex; gap:0.5rem; margin-bottom:1rem;">
|
||||
<input id="searchInput" type="text" placeholder="Search for songs…" />
|
||||
<button onclick="search()">Search</button>
|
||||
</div>
|
||||
<div id="searchResults"></div>
|
||||
<h3 style="margin-top:1rem;">My Queue</h3>
|
||||
<div id="myQueue"></div>
|
||||
</div>
|
||||
|
||||
<div id="mergedView" class="hidden">
|
||||
<h3>Merged Queue</h3>
|
||||
<div id="mergedQueue"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- <footer> -->
|
||||
<!-- <input id="searchFooter" type="text" placeholder="Search…" /> -->
|
||||
<!-- <button onclick="searchFooter()">Search</button> -->
|
||||
<!-- </footer> -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws;
|
||||
let username = '';
|
||||
let myQueue = [];
|
||||
let mergedQueue = [];
|
||||
let searchResults = [];
|
||||
let dragged = null;
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(`ws://${location.host}/ws/user`);
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({c: 'login', d: {name: username}}));
|
||||
ws.send(JSON.stringify({c: 'getqueue'}));
|
||||
};
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
const msg = JSON.parse(e.data);
|
||||
if (msg.c === 'ping') {ws.send(JSON.stringify({c: 'pong'})); return;}
|
||||
if (msg.c === 'search') {
|
||||
searchResults = msg.d.songs || [];
|
||||
renderSearch();
|
||||
}
|
||||
if (msg.c === 'getqueue') {
|
||||
myQueue = msg.d.queue || [];
|
||||
renderMyQueue();
|
||||
}
|
||||
if (msg.c === 'updatemergedqueue') {
|
||||
console.log(msg)
|
||||
mergedQueue = msg.d.queue || [];
|
||||
if (document.getElementById('tabMerged').classList.contains('active')) renderMerged();
|
||||
}
|
||||
if (msg.c === "logout") {
|
||||
location.reload();
|
||||
}
|
||||
};
|
||||
ws.onclose = () => setTimeout(connect, 3000);
|
||||
}
|
||||
|
||||
function login() {
|
||||
const name = document.getElementById('usernameInput').value.trim();
|
||||
if (!name) return;
|
||||
|
||||
username = name;
|
||||
document.getElementById('loginScreen').style.display = 'none';
|
||||
document.getElementById('app').classList.remove('hidden');
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
function logout() {
|
||||
username = '';
|
||||
if (ws) ws.close();
|
||||
document.getElementById('app').classList.add('hidden');
|
||||
document.getElementById('loginScreen').style.display = 'flex';
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
document.getElementById('tabMy').classList.toggle('active', tab === 'my');
|
||||
document.getElementById('tabMerged').classList.toggle('active', tab === 'merged');
|
||||
document.getElementById('myView').classList.toggle('hidden', tab !== 'my');
|
||||
document.getElementById('mergedView').classList.toggle('hidden', tab !== 'merged');
|
||||
if (tab === 'merged') renderMerged();
|
||||
}
|
||||
|
||||
function search() {
|
||||
const q = document.getElementById('searchInput').value.trim();
|
||||
if (!q) return;
|
||||
ws.send(JSON.stringify({c: 'search', d: {query: q}}));
|
||||
}
|
||||
function searchFooter() {
|
||||
const q = document.getElementById('searchFooter').value.trim();
|
||||
if (!q) return;
|
||||
document.getElementById('searchInput').value = q;
|
||||
search();
|
||||
switchTab('my');
|
||||
}
|
||||
|
||||
function addSong(song) {
|
||||
myQueue.push(song);
|
||||
ws.send(JSON.stringify({c: 'queuesong', d: {song}}));
|
||||
ws.send(JSON.stringify({c: 'getqueue'}));
|
||||
}
|
||||
|
||||
function removeSong(idx) {
|
||||
myQueue.splice(idx, 1);
|
||||
ws.send(JSON.stringify({c: 'setqueue', d: {queue: myQueue}}));
|
||||
renderMyQueue();
|
||||
}
|
||||
|
||||
function renderSearch() {
|
||||
const el = document.getElementById('searchResults');
|
||||
el.innerHTML = '';
|
||||
searchResults.forEach(s => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'song';
|
||||
div.innerHTML = `
|
||||
<img src="${s.album.coverUrl || 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'}" alt="">
|
||||
<div>
|
||||
<div class="title">${s.name}</div>
|
||||
<div class="artist">${s.artists.join(', ')}</div>
|
||||
</div>
|
||||
<button onclick='addSong(${JSON.stringify(s)})'>+</button>
|
||||
`;
|
||||
el.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMyQueue() {
|
||||
const el = document.getElementById('myQueue');
|
||||
el.innerHTML = '';
|
||||
myQueue.forEach((s, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'song';
|
||||
div.draggable = true;
|
||||
div.dataset.idx = i;
|
||||
div.innerHTML = `
|
||||
<img src="${s.album.coverUrl || 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'}" alt="">
|
||||
<div>
|
||||
<div class="title">${s.name}</div>
|
||||
<div class="artist">${s.artists.join(', ')}</div>
|
||||
</div>
|
||||
<button onclick="removeSong(${i})">✕</button>
|
||||
`;
|
||||
div.ondragstart = e => {dragged = +e.target.dataset.idx; e.target.classList.add('dragging');};
|
||||
div.ondragend = e => e.target.classList.remove('dragging');
|
||||
div.ondragover = e => e.preventDefault();
|
||||
div.ondrop = e => {
|
||||
e.preventDefault();
|
||||
const to = +e.currentTarget.dataset.idx;
|
||||
if (dragged === to) return;
|
||||
const temp = myQueue[dragged];
|
||||
myQueue.splice(dragged, 1);
|
||||
myQueue.splice(to, 0, temp);
|
||||
ws.send(JSON.stringify({c: 'setqueue', d: {queue: myQueue}}));
|
||||
renderMyQueue();
|
||||
};
|
||||
el.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMerged() {
|
||||
const el = document.getElementById('mergedQueue');
|
||||
el.innerHTML = '';
|
||||
mergedQueue.forEach(item => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'song';
|
||||
div.innerHTML = `
|
||||
<img src="${item.song.album.coverUrl || 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'}" alt="">
|
||||
<div>
|
||||
<div class="title">${item.song.name}</div>
|
||||
<div class="artist">${item.song.artists.join(', ')}</div>
|
||||
<div class="user">Added by ${item.user}</div>
|
||||
</div>
|
||||
`;
|
||||
el.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function escSong(s) {
|
||||
return encodeURIComponent(JSON.stringify(s).replace(/'/g, "\\'"));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue