additional nav bar items, lyricsearch.jsx changes/cleanup + bugfix for autocomplete scrolling & change to primereact theme (bootstrap4-dark-blue)
This commit is contained in:
@ -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();
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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,170 +65,188 @@ 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")) {
|
|
||||||
setShowAlert(false);
|
const [rawArtist, rawSong] = value.split(" - ", 2);
|
||||||
$("#alert").addClass("hidden");
|
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");
|
const box = $("[class*='lyrics-card-']");
|
||||||
//setTimeout(() => { $("#spinner").addClass("hidden"); alert('Not yet implemented.')}, 1000);
|
const lyrics_content = $(".lyrics-content");
|
||||||
search_toast = toast.info("Searching...", {style: { color: '#000000', backgroundColor: 'rgba(217, 242, 255, 0.8)'}});
|
const spinner = $("#spinner");
|
||||||
start_time = new Date().getTime()
|
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({
|
$.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;
|
const duration = ((Date.now() - start_time) / 1000).toFixed(1);
|
||||||
ret_artist = data.artist;
|
lyrics_content.html(`<span id='lyrics-info'>${data.artist} - ${data.song}</span>${data.lyrics}`);
|
||||||
ret_song = data.song;
|
box.removeClass("hidden");
|
||||||
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, {
|
toast.update(search_toast, {
|
||||||
type: "",
|
|
||||||
style: { backgroundColor: "rgba(46, 186, 106, 1)", color: 'inherit' },
|
|
||||||
render: `🦄 Found! (Took ${duration}s)`,
|
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: "",
|
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"
|
variant="solid"
|
||||||
variant="solid"
|
onClose={() => setShowAlert(false)}
|
||||||
onClose={() => setShowAlert(false)}
|
>
|
||||||
>
|
You must specify both an artist and song to search.
|
||||||
You must specify both an artist and song to search.
|
<br />
|
||||||
<br />
|
Format: Artist - Song
|
||||||
Format: Artist - Song
|
</Alert>
|
||||||
</Alert>
|
)}
|
||||||
)}
|
<AutoComplete
|
||||||
</div>
|
id={opts.id}
|
||||||
<AutoComplete
|
ref={autoCompleteRef}
|
||||||
theme="nano"
|
value={value}
|
||||||
size="40"
|
size={40}
|
||||||
scrollHeight="200px"
|
suggestions={suggestions}
|
||||||
autoFocus={true}
|
completeMethod={typeahead_search}
|
||||||
virtualScrollerOptions={false}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
value={value}
|
onKeyDown={handleKeyDown}
|
||||||
id={opts.id}
|
onShow={handlePanelShow}
|
||||||
ref={autoCompleteRef}
|
placeholder={opts.placeholder}
|
||||||
suggestions={suggestions}
|
autoFocus />
|
||||||
completeMethod={typeahead_search}
|
<Button onClick={handleSearch} className="btn">Search</Button>
|
||||||
placeholder={opts.placeholder}
|
|
||||||
onChange={(e) => { setValue(e.target.value) }}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
/>
|
|
||||||
<Button id="lyric-search-btn" 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -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 },
|
||||||
};
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user