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 (

Lyric Search


Exclude:
); } 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 />
); } 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 (
{/* */}
) }