From 289411c8eb6fff1f4d38fa0ef0f0e24f95b1e1b3 Mon Sep 17 00:00:00 2001 From: codey Date: Tue, 15 Jul 2025 14:34:44 -0400 Subject: [PATCH] additional nav bar items, lyricsearch.jsx changes/cleanup + bugfix for autocomplete scrolling & change to primereact theme (bootstrap4-dark-blue) --- public/scripts/player.js | 2 +- src/assets/styles/global.css | 13 +- src/components/LyricSearch.jsx | 284 ++++++++++++++++++--------------- src/components/Nav.astro | 10 +- 4 files changed, 174 insertions(+), 135 deletions(-) diff --git a/public/scripts/player.js b/public/scripts/player.js index a1a1a79..fe58ecf 100644 --- a/public/scripts/player.js +++ b/public/scripts/player.js @@ -62,6 +62,7 @@ initialize = () => { // server issue/not playing return fail("hard"); } + canPlay = true; if (currentUUID == data.uuid) { currentTime = data.elapsed; currentDuration = data.duration; @@ -72,7 +73,6 @@ initialize = () => { author_text = data.artist; $(author).text(author_text); if (data.genre && data.genre !== "N/A") { - canPlay = true; $(genre).text(data.genre); if (! $(genre).is(':visible')) { $(genre).show(); diff --git a/src/assets/styles/global.css b/src/assets/styles/global.css index 412c724..e6db57f 100644 --- a/src/assets/styles/global.css +++ b/src/assets/styles/global.css @@ -1,5 +1,5 @@ @import "tailwindcss"; -@import "primereact/resources/themes/nano/theme.css"; +@import "primereact/resources/themes/bootstrap4-dark-blue/theme.css"; @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); @plugin "@tailwindcss/typography"; @@ -227,3 +227,14 @@ Custom white-space: pre-wrap; color: 'inherit'; } + +.lyrics-card-copyButton { + float: right; + padding-bottom: 3%; +} + +.p-autocomplete-items { + max-height: 200px !important; + overflow-y: auto !important; + overscroll-behavior: contain; +} \ No newline at end of file diff --git a/src/components/LyricSearch.jsx b/src/components/LyricSearch.jsx index 0e04be7..8768a6c 100644 --- a/src/components/LyricSearch.jsx +++ b/src/components/LyricSearch.jsx @@ -11,6 +11,7 @@ import Alert from '@mui/joy/Alert'; import Box from '@mui/joy/Box'; import Button from "@mui/joy/Button"; import Checkbox from "@mui/joy/Checkbox"; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import jQuery from "jquery"; import { AutoComplete } from 'primereact/autocomplete'; import { api as API_URL } from '../config'; @@ -63,171 +64,189 @@ export function LyricSearchInputField(opts = {}) { const [suggestions, setSuggestions] = useState([]); const [showAlert, setShowAlert] = useState(false); const autoCompleteRef = useRef(null); - - var search_toast = null; - var ret_artist = null; - var ret_song = null; - var ret_lyrics = null; - var start_time = null; - var end_time = null; - - useEffect(() => {}, []); - - async function handleSearch() { + + // Ensure the dropdown panel is scrollable after it shows + const handlePanelShow = () => { + setTimeout(() => { + const panel = document.querySelector(".p-autocomplete-panel"); + const items = panel?.querySelector(".p-autocomplete-items"); + + if (!items) return; + + items.style.maxHeight = "200px"; + items.style.overflowY = "auto"; + items.style.overscrollBehavior = "contain"; + + // ✅ Attach wheel scroll manually + const wheelHandler = (e) => { + const delta = e.deltaY; + const atTop = items.scrollTop === 0; + const atBottom = items.scrollTop + items.clientHeight >= items.scrollHeight; + + if ((delta < 0 && atTop) || (delta > 0 && atBottom)) { + e.preventDefault(); // prevent outer scroll + } else { + e.stopPropagation(); // prevent parent scroll + } + }; + + // Clean up first, then re-add + items.removeEventListener('wheel', wheelHandler); + items.addEventListener('wheel', wheelHandler, { passive: false }); + + // Cleanup on hide + const observer = new MutationObserver(() => { + if (!document.body.contains(items)) { + items.removeEventListener('wheel', wheelHandler); + observer.disconnect(); + } + }); + observer.observe(document.body, { childList: true, subtree: true }); + }, 0); + }; + + + + const typeahead_search = (event) => { + const query = event.query; + $.ajax({ + url: `${API_URL}/typeahead/lyrics`, + method: 'POST', + contentType: 'application/json; charset=utf-8', + data: JSON.stringify({ query }), + dataType: 'json', + success: setSuggestions + }); + }; + + const handleSearch = () => { if (autoCompleteRef.current) { autoCompleteRef.current.hide(); } - let validSearch = (value.trim() && (value.split(" - ").length > 1)); - let box = document.querySelector("[class*='lyrics-card-']") - let spinner = $('#spinner'); - let excluded_sources = []; - - $("#exclude-checkboxes").find("input:checkbox").each(function () { - if (this.checked) { - let src = this.id.replace("excl-", "").toLowerCase(); - excluded_sources.push(src); - } - }); - - if (validSearch) { - // define artist, song + additional checks - let a_s_split = value.split(" - ", 2) - var [search_artist, search_song] = a_s_split; - search_artist = search_artist.trim(); - search_song = search_song.trim(); - if (! search_artist || ! search_song) { - validSearch = false; // artist and song could not be derived - } - } - + + const validSearch = value.includes(" - "); if (!validSearch) { setShowAlert(true); - setTimeout(() => { setShowAlert(false); }, 5000); - $("#alert").removeClass("hidden"); + setTimeout(() => setShowAlert(false), 5000); return; } - if (!$("#alert").hasClass("hidden")) { - setShowAlert(false); - $("#alert").addClass("hidden"); + + const [rawArtist, rawSong] = value.split(" - ", 2); + const search_artist = rawArtist?.trim(); + const search_song = rawSong?.trim(); + + if (!search_artist || !search_song) { + setShowAlert(true); + setTimeout(() => setShowAlert(false), 5000); + return; } - $('#spinner').removeClass("hidden"); - $(box).addClass("hidden"); - //setTimeout(() => { $("#spinner").addClass("hidden"); alert('Not yet implemented.')}, 1000); - search_toast = toast.info("Searching...", {style: { color: '#000000', backgroundColor: 'rgba(217, 242, 255, 0.8)'}}); - start_time = new Date().getTime() + + const box = $("[class*='lyrics-card-']"); + const lyrics_content = $(".lyrics-content"); + const spinner = $("#spinner"); + const excluded_sources = []; + + $("#exclude-checkboxes input:checked").each(function () { + excluded_sources.push(this.id.replace("excl-", "").toLowerCase()); + }); + + setShowAlert(false); + $("#alert").addClass("hidden"); + spinner.removeClass("hidden"); + box.addClass("hidden"); + + const start_time = Date.now(); + const search_toast = toast.info("Searching...", { + style: { color: '#000', backgroundColor: 'rgba(217, 242, 255, 0.8)' } + }); + $.ajax({ - url: API_URL+'/lyric/search', + url: `${API_URL}/lyric/search`, method: 'POST', contentType: 'application/json; charset=utf-8', data: JSON.stringify({ a: search_artist, s: search_song, - excluded_sources: excluded_sources, + excluded_sources, src: 'Web', extra: true, }) - }).done((data, txtStatus, xhr) => { + }).done((data) => { + spinner.addClass("hidden"); + if (data.err || !data.lyrics) { - $(spinner).addClass("hidden"); return toast.update(search_toast, { - type: "", render: `🙁 ${data.errorText}`, - style: { backgroundColor: "rgba(255, 0, 0, 0.5)", color: 'inherit' }, + type: "", + style: { backgroundColor: "rgba(255, 0, 0, 0.5)" }, hideProgressBar: true, autoClose: 5000, - }) + }); } - end_time = new Date().getTime(); - let duration = (end_time - start_time) / 1000; - ret_artist = data.artist; - ret_song = data.song; - ret_lyrics = data.lyrics; - $(box).removeClass("hidden"); - $(spinner).addClass("hidden"); - $(box).html(`${ret_artist} - ${ret_song}${ret_lyrics}`); + + const duration = ((Date.now() - start_time) / 1000).toFixed(1); + lyrics_content.html(`${data.artist} - ${data.song}${data.lyrics}`); + box.removeClass("hidden"); + toast.update(search_toast, { - type: "", - style: { backgroundColor: "rgba(46, 186, 106, 1)", color: 'inherit' }, render: `🦄 Found! (Took ${duration}s)`, - autoClose: 2000, - hideProgressBar: true - }); - }).fail((jqXHR, textStatus, error) => { - $(spinner).addClass("hidden"); - let render_text = `😕 Failed to reach search endpoint (${jqXHR.status})`; - if (typeof jqXHR.responseJSON.detail !== "undefined") { - render_text += `\n${jqXHR.responseJSON.detail}`; - } - return toast.update(search_toast, { type: "", - render: render_text, - style: { backgroundColor: "rgba(255, 0, 0, 0.5)", color: 'inherit' }, + style: { backgroundColor: "rgba(46, 186, 106, 1)" }, + autoClose: 2000, + hideProgressBar: true, + }); + }).fail((jqXHR) => { + spinner.addClass("hidden"); + const msg = `😕 Failed to reach search endpoint (${jqXHR.status})` + + (jqXHR.responseJSON?.detail ? `\n${jqXHR.responseJSON.detail}` : ""); + toast.update(search_toast, { + render: msg, + type: "", + style: { backgroundColor: "rgba(255, 0, 0, 0.5)" }, hideProgressBar: true, autoClose: 5000, - }) - }) - } - + }); + }); + }; + const handleKeyDown = (e) => { - if (e.key !== "Enter") return; - e.preventDefault(); - handleSearch(); + if (e.key === "Enter") { + e.preventDefault(); + handleSearch(); + } }; - - const typeahead_search = (event) => { - let query = event.query; - $.ajax({ - url: API_URL+'/typeahead/lyrics', - method: 'POST', - contentType: 'application/json; charset=utf-8', - data: JSON.stringify({ - query: query - }), - dataType: 'json', - success: function (json) { - return setSuggestions(json); - } - }) - }; - - + return (
-
- {showAlert && ( - setShowAlert(false)} - > - You must specify both an artist and song to search. -
- Format: Artist - Song -
- )} -
- { setValue(e.target.value) }} - onKeyDown={handleKeyDown} - /> - + {showAlert && ( + setShowAlert(false)} + > + You must specify both an artist and song to search. +
+ Format: Artist - Song +
+ )} + setValue(e.target.value)} + onKeyDown={handleKeyDown} + onShow={handlePanelShow} + placeholder={opts.placeholder} + autoFocus /> +
); } + export const UICheckbox = forwardRef(function UICheckbox(opts = {}, ref) { const [checked, setChecked] = useState(false); const [showAlert, setShowAlert] = useState(false); @@ -271,6 +290,11 @@ export const UICheckbox = forwardRef(function UICheckbox(opts = {}, ref) { export function LyricResultBox(opts={}) { return ( - +
+ +
+ {/* */} +
+
) } \ No newline at end of file diff --git a/src/components/Nav.astro b/src/components/Nav.astro index efdab75..4325d2e 100644 --- a/src/components/Nav.astro +++ b/src/components/Nav.astro @@ -6,10 +6,14 @@ import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule'; const navItems = { "/": { name: "Home", className: "", icon: null }, - "": { name: "", className: "", icon: HorizontalRuleIcon }, + "divider-1": { name: "", className: "", icon: HorizontalRuleIcon }, "/radio": { name: "Radio", className: "", icon: null }, - "‡": { name: "", className: "", icon: HorizontalRuleIcon }, - "https://status.boatson.boats": { name: "Status", className: "", icon: ExitToApp } + "divider-2": { name: "", className: "", icon: HorizontalRuleIcon }, + "https://status.boatson.boats": { name: "Status", className: "", icon: ExitToApp }, + "divider-3": { name: "", className: "", icon: HorizontalRuleIcon }, + "https://kode.boatson.boats": { name: "Git", className: "", icon: ExitToApp }, + "divider-4": { name: "", className: "", icon: HorizontalRuleIcon }, + "https://old.codey.lol": { name: "Old Site", className: "", icon: ExitToApp }, }; ---