additional nav bar items, lyricsearch.jsx changes/cleanup + bugfix for autocomplete scrolling & change to primereact theme (bootstrap4-dark-blue)

This commit is contained in:
2025-07-15 14:34:44 -04:00
parent b19b8dc22e
commit 289411c8eb
4 changed files with 174 additions and 135 deletions

View File

@ -62,6 +62,7 @@ initialize = () => {
// server issue/not playing // server issue/not playing
return fail("hard"); return fail("hard");
} }
canPlay = true;
if (currentUUID == data.uuid) { if (currentUUID == data.uuid) {
currentTime = data.elapsed; currentTime = data.elapsed;
currentDuration = data.duration; currentDuration = data.duration;
@ -72,7 +73,6 @@ initialize = () => {
author_text = data.artist; author_text = data.artist;
$(author).text(author_text); $(author).text(author_text);
if (data.genre && data.genre !== "N/A") { if (data.genre && data.genre !== "N/A") {
canPlay = true;
$(genre).text(data.genre); $(genre).text(data.genre);
if (! $(genre).is(':visible')) { if (! $(genre).is(':visible')) {
$(genre).show(); $(genre).show();

View File

@ -1,5 +1,5 @@
@import "tailwindcss"; @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] *)); @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
@plugin "@tailwindcss/typography"; @plugin "@tailwindcss/typography";
@ -227,3 +227,14 @@ Custom
white-space: pre-wrap; white-space: pre-wrap;
color: 'inherit'; color: 'inherit';
} }
.lyrics-card-copyButton {
float: right;
padding-bottom: 3%;
}
.p-autocomplete-items {
max-height: 200px !important;
overflow-y: auto !important;
overscroll-behavior: contain;
}

View File

@ -11,6 +11,7 @@ import Alert from '@mui/joy/Alert';
import Box from '@mui/joy/Box'; import Box from '@mui/joy/Box';
import Button from "@mui/joy/Button"; import Button from "@mui/joy/Button";
import Checkbox from "@mui/joy/Checkbox"; import Checkbox from "@mui/joy/Checkbox";
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import jQuery from "jquery"; import jQuery from "jquery";
import { AutoComplete } from 'primereact/autocomplete'; import { AutoComplete } from 'primereact/autocomplete';
import { api as API_URL } from '../config'; import { api as API_URL } from '../config';
@ -64,136 +65,159 @@ export function LyricSearchInputField(opts = {}) {
const [showAlert, setShowAlert] = useState(false); const [showAlert, setShowAlert] = useState(false);
const autoCompleteRef = useRef(null); const autoCompleteRef = useRef(null);
var search_toast = null; // Ensure the dropdown panel is scrollable after it shows
var ret_artist = null; const handlePanelShow = () => {
var ret_song = null; setTimeout(() => {
var ret_lyrics = null; const panel = document.querySelector(".p-autocomplete-panel");
var start_time = null; const items = panel?.querySelector(".p-autocomplete-items");
var end_time = null;
useEffect(() => {}, []); if (!items) return;
async function handleSearch() { 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) { if (autoCompleteRef.current) {
autoCompleteRef.current.hide(); 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) { if (!validSearch) {
setShowAlert(true); setShowAlert(true);
setTimeout(() => { setShowAlert(false); }, 5000); setTimeout(() => setShowAlert(false), 5000);
$("#alert").removeClass("hidden");
return; return;
} }
if (!$("#alert").hasClass("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;
}
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); setShowAlert(false);
$("#alert").addClass("hidden"); $("#alert").addClass("hidden");
} spinner.removeClass("hidden");
$('#spinner').removeClass("hidden"); box.addClass("hidden");
$(box).addClass("hidden");
//setTimeout(() => { $("#spinner").addClass("hidden"); alert('Not yet implemented.')}, 1000); const start_time = Date.now();
search_toast = toast.info("Searching...", {style: { color: '#000000', backgroundColor: 'rgba(217, 242, 255, 0.8)'}}); const search_toast = toast.info("Searching...", {
start_time = new Date().getTime() style: { color: '#000', backgroundColor: 'rgba(217, 242, 255, 0.8)' }
});
$.ajax({ $.ajax({
url: API_URL+'/lyric/search', url: `${API_URL}/lyric/search`,
method: 'POST', method: 'POST',
contentType: 'application/json; charset=utf-8', contentType: 'application/json; charset=utf-8',
data: JSON.stringify({ data: JSON.stringify({
a: search_artist, a: search_artist,
s: search_song, s: search_song,
excluded_sources: excluded_sources, excluded_sources,
src: 'Web', src: 'Web',
extra: true, extra: true,
}) })
}).done((data, txtStatus, xhr) => { }).done((data) => {
spinner.addClass("hidden");
if (data.err || !data.lyrics) { if (data.err || !data.lyrics) {
$(spinner).addClass("hidden");
return toast.update(search_toast, { return toast.update(search_toast, {
type: "",
render: `🙁 ${data.errorText}`, render: `🙁 ${data.errorText}`,
style: { backgroundColor: "rgba(255, 0, 0, 0.5)", color: 'inherit' }, type: "",
style: { backgroundColor: "rgba(255, 0, 0, 0.5)" },
hideProgressBar: true, hideProgressBar: true,
autoClose: 5000, 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(`<span id='lyrics-info'>${ret_artist} - ${ret_song}</span>${ret_lyrics}`);
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, {
const duration = ((Date.now() - start_time) / 1000).toFixed(1);
lyrics_content.html(`<span id='lyrics-info'>${data.artist} - ${data.song}</span>${data.lyrics}`);
box.removeClass("hidden");
toast.update(search_toast, {
render: `🦄 Found! (Took ${duration}s)`,
type: "", type: "",
render: render_text, style: { backgroundColor: "rgba(46, 186, 106, 1)" },
style: { backgroundColor: "rgba(255, 0, 0, 0.5)", color: 'inherit' }, 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, hideProgressBar: true,
autoClose: 5000, autoClose: 5000,
}) });
}) });
} };
const handleKeyDown = (e) => { const handleKeyDown = (e) => {
if (e.key !== "Enter") return; if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
handleSearch(); 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 ( return (
<div> <div>
<div id="alert">
{showAlert && ( {showAlert && (
<Alert <Alert
color="danger" color="danger"
@ -205,29 +229,24 @@ export function LyricSearchInputField(opts = {}) {
Format: Artist - Song Format: Artist - Song
</Alert> </Alert>
)} )}
</div>
<AutoComplete <AutoComplete
theme="nano"
size="40"
scrollHeight="200px"
autoFocus={true}
virtualScrollerOptions={false}
value={value}
id={opts.id} id={opts.id}
ref={autoCompleteRef} ref={autoCompleteRef}
value={value}
size={40}
suggestions={suggestions} suggestions={suggestions}
completeMethod={typeahead_search} completeMethod={typeahead_search}
placeholder={opts.placeholder} onChange={(e) => setValue(e.target.value)}
onChange={(e) => { setValue(e.target.value) }}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
/> onShow={handlePanelShow}
<Button id="lyric-search-btn" onClick={handleSearch} className={"btn"}> placeholder={opts.placeholder}
Search autoFocus />
</Button> <Button onClick={handleSearch} className="btn">Search</Button>
</div> </div>
); );
} }
export const UICheckbox = forwardRef(function UICheckbox(opts = {}, ref) { export const UICheckbox = forwardRef(function UICheckbox(opts = {}, ref) {
const [checked, setChecked] = useState(false); const [checked, setChecked] = useState(false);
const [showAlert, setShowAlert] = useState(false); const [showAlert, setShowAlert] = useState(false);
@ -271,6 +290,11 @@ export const UICheckbox = forwardRef(function UICheckbox(opts = {}, ref) {
export function LyricResultBox(opts={}) { export function LyricResultBox(opts={}) {
return ( return (
<Box className={`lyrics-card lyrics-card-${theme} hidden`} sx={{ p: 2 }}></Box> <div>
<Box className={`lyrics-card lyrics-card-${theme} hidden`} sx={{ p: 2 }}>
<div className='lyrics-content'></div>
{/* <ContentCopyIcon className='lyrics-card-copyButton' size='lg' /> */}
</Box>
</div>
) )
} }

View File

@ -6,10 +6,14 @@ import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule';
const navItems = { const navItems = {
"/": { name: "Home", className: "", icon: null }, "/": { name: "Home", className: "", icon: null },
"": { name: "", className: "", icon: HorizontalRuleIcon }, "divider-1": { name: "", className: "", icon: HorizontalRuleIcon },
"/radio": { name: "Radio", className: "", icon: null }, "/radio": { name: "Radio", className: "", icon: null },
"‡": { name: "", className: "", icon: HorizontalRuleIcon }, "divider-2": { name: "", className: "", icon: HorizontalRuleIcon },
"https://status.boatson.boats": { name: "Status", className: "", icon: ExitToApp } "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 },
}; };
--- ---