Added search to new ui
This commit is contained in:
parent
ccbb7fcd13
commit
eb2f3e8fc5
2 changed files with 145 additions and 121 deletions
|
|
@ -76,7 +76,7 @@ export class UserWS {
|
||||||
if (songs === undefined) {
|
if (songs === undefined) {
|
||||||
songs = []
|
songs = []
|
||||||
}
|
}
|
||||||
this.ws.send(buildCommand("search", {songs, connected: true}))
|
this.ws.send(buildCommand("search", {songs}))
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
||||||
264
static/new.html
264
static/new.html
|
|
@ -42,7 +42,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-center w-1/2">
|
<div class="navbar-center w-1/2">
|
||||||
<form class="join flex-grow">
|
<form class="join flex-grow" style="anchor-name: --search-nav;" id="search-nav-form">
|
||||||
<div class="input join-item w-full">
|
<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">
|
<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">
|
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2.5" fill="none" stroke="currentColor">
|
||||||
|
|
@ -50,10 +50,13 @@
|
||||||
<path d="m21 21-4.3-4.3"></path>
|
<path d="m21 21-4.3-4.3"></path>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<input type="search" required placeholder="Search your favourite songs...">
|
<input type="search" required placeholder="Search your favourite songs..." id="search-nav-input">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary join-item">Search</button>
|
<button type="submit" class="btn btn-primary join-item">Search</button>
|
||||||
</form>
|
</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>
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
|
|
@ -87,7 +90,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
<span class="dock-label">Queues</span>
|
<span class="dock-label">Queues</span>
|
||||||
</button>
|
</button>
|
||||||
<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]">
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="size-[1.2em]">
|
||||||
<path fill="currentColor"
|
<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" />
|
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" />
|
||||||
|
|
@ -115,7 +118,7 @@
|
||||||
<div class="hidden">
|
<div class="hidden">
|
||||||
<!-- template song personal -->
|
<!-- template song personal -->
|
||||||
<template id="template-song-personal">
|
<template id="template-song-personal">
|
||||||
<li class="list-row song" draggable="true">
|
<li class="list-row song">
|
||||||
<div class="drag-handle cursor-move select-none touch-none text-2xl opacity-50 mr-2">⋮⋮</div>
|
<div class="drag-handle cursor-move select-none touch-none text-2xl opacity-50 mr-2">⋮⋮</div>
|
||||||
<div><img class="size-10 rounded-box"
|
<div><img class="size-10 rounded-box"
|
||||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="thumbnail"></div>
|
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="thumbnail"></div>
|
||||||
|
|
@ -146,6 +149,23 @@
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</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>
|
</div>
|
||||||
|
|
||||||
<dialog id="modal" class="modal modal-bottom sm:modal-middle">
|
<dialog id="modal" class="modal modal-bottom sm:modal-middle">
|
||||||
|
|
@ -164,7 +184,41 @@
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
|
||||||
|
<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 $ = document.querySelector.bind(document);
|
||||||
const $all = document.querySelectorAll.bind(document);
|
const $all = document.querySelectorAll.bind(document);
|
||||||
|
|
||||||
|
|
@ -188,17 +242,23 @@
|
||||||
|
|
||||||
const DOMTemplateSongPersonal = $("#template-song-personal");
|
const DOMTemplateSongPersonal = $("#template-song-personal");
|
||||||
const DOMTemplateSongMerged = $("#template-song-merged");
|
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
|
// VARIABLES
|
||||||
let ws;
|
let ws;
|
||||||
let username = (window.localStorage.username ?? "").trim();
|
let username = (window.localStorage.username ?? "").trim();
|
||||||
let sessionOvertaken = false;
|
let sessionOvertaken = false;
|
||||||
let draggedItem = null;
|
|
||||||
let dragStartY = 0;
|
|
||||||
let dragOffset = 0;
|
|
||||||
let draggedItemOriginalIndex = -1;
|
|
||||||
let draggedItemOriginalY = 0;
|
|
||||||
let emptiQueue = {
|
let emptiQueue = {
|
||||||
trackName: "Loneliness",
|
trackName: "Loneliness",
|
||||||
authorName: "Empty Queue"
|
authorName: "Empty Queue"
|
||||||
|
|
@ -240,10 +300,7 @@
|
||||||
|
|
||||||
DOMMainSongPersonal.append(el);
|
DOMMainSongPersonal.append(el);
|
||||||
|
|
||||||
const children = DOMMainSongPersonal.children
|
|
||||||
children[children.length - 1].dataset.index = index;
|
|
||||||
})
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
get merged() {
|
get merged() {
|
||||||
|
|
@ -267,11 +324,13 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newV.forEach((song, index) => {
|
newV.forEach((data, index) => {
|
||||||
|
const {user, song} = data;
|
||||||
|
|
||||||
const el = document.importNode(DOMTemplateSongMerged.content, true);
|
const el = document.importNode(DOMTemplateSongMerged.content, true);
|
||||||
el.querySelector(".tabular-nums").innerText = (index + 1).toString().padStart(2, "0");
|
el.querySelector(".tabular-nums").innerText = (index + 1).toString().padStart(2, "0");
|
||||||
el.querySelector(".song-songname").innerText = song.name;
|
el.querySelector(".song-songname").innerText = song.name;
|
||||||
el.querySelector(".song-artistname").innerText = song.artists.join(", ");
|
el.querySelector(".song-artistname").innerText = user + " - " + song.artists?.join(", ") ?? "";
|
||||||
el.querySelector("img").src = song.album?.coverUrl ?? "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
el.querySelector("img").src = song.album?.coverUrl ?? "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||||
DOMMainSongMerged.append(el);
|
DOMMainSongMerged.append(el);
|
||||||
})
|
})
|
||||||
|
|
@ -324,14 +383,10 @@
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
const msg = JSON.parse(e.data);
|
const msg = JSON.parse(e.data);
|
||||||
if (msg.c === 'ping') {ws.send(JSON.stringify({c: 'pong'})); return;}
|
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') {
|
else if (msg.c === 'getqueue') {
|
||||||
console.log(msg.d)
|
console.log(msg.d)
|
||||||
queues.personal = msg.d.personal || [];
|
queues.personal = msg.d.personal || [];
|
||||||
queues.merged = msg.d.personal || [];
|
queues.merged = msg.d.merged || [];
|
||||||
}
|
}
|
||||||
else if (msg.c === "logout") {
|
else if (msg.c === "logout") {
|
||||||
mpAlert("Session takeover", "You've been logged in on another device. Please reload this page to reconnect.")
|
mpAlert("Session takeover", "You've been logged in on another device. Please reload this page to reconnect.")
|
||||||
|
|
@ -340,6 +395,31 @@
|
||||||
else if (msg.c === "status") {
|
else if (msg.c === "status") {
|
||||||
connected.value = msg.d.status ? 0 : 1;
|
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 = () => {
|
ws.onclose = () => {
|
||||||
connected.value = 2;
|
connected.value = 2;
|
||||||
|
|
@ -379,120 +459,64 @@
|
||||||
|
|
||||||
ws?.send(JSON.stringify({c: 'setqueue', d: {queue: queues.personal}}));
|
ws?.send(JSON.stringify({c: 'setqueue', d: {queue: queues.personal}}));
|
||||||
}
|
}
|
||||||
|
function navSearch(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
function handleDragOver(y) {
|
const query = DOMSearchNavInput.value.trim();
|
||||||
if (draggedItem == null) return;
|
if (query === "") return
|
||||||
|
|
||||||
const allSongs = [...DOMMainSongPersonal.querySelectorAll(".song:not(.opacity-0)")];
|
ws?.send(JSON.stringify({
|
||||||
const otherSongs = allSongs.filter(el => el !== draggedItem);
|
c: "search",
|
||||||
|
d: {query}
|
||||||
|
}))
|
||||||
|
|
||||||
let targetIndex = -1;
|
DOMSearchNavDropown.innerHTML = "";
|
||||||
for (let i = 0; i < otherSongs.length; i++) {
|
DOMSearchNavDropown.showPopover();
|
||||||
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() {
|
function dockSearch(e) {
|
||||||
const newOrder = [];
|
e.preventDefault();
|
||||||
|
|
||||||
for (let element of DOMMainSongPersonal.querySelectorAll(".song")) {
|
const query = DOMSearchDockInput.value.trim();
|
||||||
let oldIndex = element.dataset.index;
|
if (query === "") return
|
||||||
if (oldIndex === undefined) return;
|
|
||||||
|
|
||||||
newOrder.push(queues.personal[+oldIndex]);
|
ws?.send(JSON.stringify({
|
||||||
}
|
c: "search",
|
||||||
|
d: {query}
|
||||||
if (newOrder.length > 0) {
|
}))
|
||||||
queues.personal = newOrder;
|
|
||||||
ws?.send(JSON.stringify({c: 'setqueue', d: {queue: queues.personal}}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DOMMainSongPersonal.addEventListener("dragstart", (e) => {
|
Array.prototype.move = function (from, to) {
|
||||||
if (!e.target.classList.contains("song")) return
|
this.splice(to, 0, this.splice(from, 1)[0]);
|
||||||
draggedItem = e.target;
|
};
|
||||||
setTimeout(() => e.target.classList.add("opacity-0"), 0);
|
let sortable = new Sortable(DOMMainSongPersonal, {
|
||||||
|
draggable: '.song',
|
||||||
});
|
handle: '.drag-handle',
|
||||||
DOMMainSongPersonal.addEventListener("dragend", (e) => {
|
mirror: {
|
||||||
if (!e.target.classList.contains("song")) return;
|
constrainDimensions: true,
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
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));
|
DOMLoginForm.addEventListener("submit", e => login(e, false));
|
||||||
[DOMMainNavLogout, DOMMainDockLogout].forEach(v => v.addEventListener("click", logout))
|
[DOMMainNavLogout, DOMMainDockLogout].forEach(v => v.addEventListener("click", logout))
|
||||||
document.body.onload = () => login(null, true);
|
document.body.onload = () => login(null, true);
|
||||||
|
|
||||||
|
DOMSearchNavForm.addEventListener("submit", navSearch)
|
||||||
|
DOMSearchDockForm.addEventListener("submit", dockSearch)
|
||||||
|
DOMSearchDockButton.addEventListener("click", () => {
|
||||||
|
DOMSearchDockSongs.innerHTML = "";
|
||||||
|
DOMSearchDockModal.openModal()
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue