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 ContentCopyIcon from '@mui/icons-material/ContentCopy';
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 (
);
}
export function LyricSearchInputField(opts = {}) {
const [value, setValue] = useState("");
const [suggestions, setSuggestions] = useState([]);
const [showAlert, setShowAlert] = useState(false);
const autoCompleteRef = useRef(null);
// 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();
}
const validSearch = value.includes(" - ");
if (!validSearch) {
setShowAlert(true);
setTimeout(() => setShowAlert(false), 5000);
return;
}
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);
$("#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`,
method: 'POST',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify({
a: search_artist,
s: search_song,
excluded_sources,
src: 'Web',
extra: true,
})
}).done((data) => {
spinner.addClass("hidden");
if (data.err || !data.lyrics) {
return toast.update(search_toast, {
render: `🙁 ${data.errorText}`,
type: "",
style: { backgroundColor: "rgba(255, 0, 0, 0.5)" },
hideProgressBar: true,
autoClose: 5000,
});
}
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, {
render: `🦄 Found! (Took ${duration}s)`,
type: "",
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") {
e.preventDefault();
handleSearch();
}
};
return (
{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 />
Search
);
}
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),
checked,
}));
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 (
{
setChecked(e.target.checked);
verifyExclusions();
}}
/>
);
});
export function LyricResultBox(opts={}) {
return (
)
}