Files
codey.lol/src/components/LyricSearch.jsx

276 lines
7.8 KiB
React
Raw Normal View History

2025-06-18 07:46:59 -04:00
import { CircularProgress } from "@mui/joy";
import React, {
forwardRef,
useImperativeHandle,
useEffect,
useRef,
useState,
} from "react";
import { default as $ } from "jquery";
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 jQuery from "jquery";
import { AutoComplete } from 'primereact/autocomplete';
import { api as API_URL } from '../config';
window.$ = window.jQuery = jQuery;
const theme = document.documentElement.getAttribute("data-theme")
document.addEventListener('set-theme', (e) => {
const box = document.querySelector("[class*='lyrics-card-']")
let removedClass = "lyrics-card-dark";
let newTheme = e.detail;
if (newTheme !== "light") {
removedClass = "lyrics-card-light";
}
$(box).removeClass(removedClass)
$(box).addClass(`lyrics-card-${newTheme}`);
});
export default function LyricSearch() {
return (
<div className="lyric-search">
<h2 className="title">
<span>Lyric Search</span>
</h2>
<div className="card-text my-4">
<label>Search:</label>
<LyricSearchInputField
id="lyric-search-input"
placeholder="Artist - Song" />
<br />
Exclude:<br />
<div id="exclude-checkboxes">
<UICheckbox id="excl-Genius" label="Genius" />
<UICheckbox id="excl-LRCLib" label="LRCLib" />
<UICheckbox id="excl-Cache" label="Cache" />
</div>
<div id="spinner" className="hidden">
<CircularProgress
variant="plain"
color="primary"
size="md"/></div>
</div>
<LyricResultBox/>
</div>
);
}
export function LyricSearchInputField(opts = {}) {
const [value, setValue] = useState("");
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() {
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
}
}
if (!validSearch) {
setShowAlert(true);
setTimeout(() => { setShowAlert(false); }, 5000);
$("#alert").removeClass("hidden");
return;
}
if (!$("#alert").hasClass("hidden")) {
setShowAlert(false);
$("#alert").addClass("hidden");
}
$('#spinner').removeClass("hidden");
$(box).addClass("hidden");
//setTimeout(() => { $("#spinner").addClass("hidden"); alert('Not yet implemented.')}, 1000);
2025-06-18 11:41:03 -04:00
search_toast = toast.info("Searching...", {style: { color: '#000000', backgroundColor: 'rgba(217, 242, 255, 0.8)'}});
2025-06-18 07:46:59 -04:00
start_time = new Date().getTime()
$.ajax({
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,
src: 'Web',
extra: true,
})
}).done((data, txtStatus, xhr) => {
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' },
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(`<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");
2025-07-01 11:38:20 -04:00
let render_text = `😕 Failed to reach search endpoint (${jqXHR.status})`;
if (typeof jqXHR.responseJSON.detail !== "undefined") {
render_text += `\n${jqXHR.responseJSON.detail}`;
}
2025-06-18 07:46:59 -04:00
return toast.update(search_toast, {
2025-07-01 11:38:20 -04:00
type: "",
render: render_text,
2025-06-18 07:46:59 -04:00
style: { backgroundColor: "rgba(255, 0, 0, 0.5)", color: 'inherit' },
hideProgressBar: true,
autoClose: 5000,
})
})
}
const handleKeyDown = (e) => {
if (e.key !== "Enter") return;
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 (
<div>
<div id="alert">
{showAlert && (
<Alert
color="danger"
variant="solid"
onClose={() => setShowAlert(false)}
>
You must specify both an artist and song to search.
<br />
Format: Artist - Song
</Alert>
)}
</div>
<AutoComplete
theme="nano"
size="40"
scrollHeight="200px"
autoFocus={true}
virtualScrollerOptions={false}
value={value}
id={opts.id}
ref={autoCompleteRef}
suggestions={suggestions}
completeMethod={typeahead_search}
placeholder={opts.placeholder}
onChange={(e) => { setValue(e.target.value) }}
onKeyDown={handleKeyDown}
/>
<Button id="lyric-search-btn" onClick={handleSearch} className={"btn"}>
Search
</Button>
</div>
);
}
export const UICheckbox = forwardRef(function UICheckbox(opts = {}, ref) {
const [checked, setChecked] = useState(false);
const [showAlert, setShowAlert] = useState(false);
let valid_exclusions = true;
useImperativeHandle(ref, () => ({
setChecked: (val) => setChecked(val),
2025-06-18 11:41:03 -04:00
checked,
2025-06-18 07:46:59 -04:00
}));
const verifyExclusions = (e) => {
let exclude_error = false;
if (($("#exclude-checkboxes").find("input:checkbox").filter(":checked").length == 3)){
$("#exclude-checkboxes").find("input:checkbox").each(function () {
exclude_error = true;
this.click();
});
if (exclude_error) {
toast.error("All sources were excluded; exclusions have been reset.",
{ style: { backgroundColor: "rgba(255, 0, 0, 0.5)", color: 'inherit' } },
);
}
}
};
return (
<div>
<Checkbox
id={opts.id}
key={opts.label}
checked={checked}
label={opts.label}
style={{ color: "inherit" }}
onChange={(e) => {
setChecked(e.target.checked);
verifyExclusions();
}}
/>
</div>
);
});
export function LyricResultBox(opts={}) {
return (
2025-06-18 11:41:03 -04:00
<Box className={`lyrics-card lyrics-card-${theme} hidden`} sx={{ p: 2 }}></Box>
2025-06-18 07:46:59 -04:00
)
}