| 
						
					 | 
				
			
			 | 
			 | 
			
				@@ -19,6 +19,7 @@ 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";
 | 
			
		
		
	
	
		
			
				
					
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@@ -31,6 +32,7 @@ document.addEventListener('set-theme', (e) => {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				});
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				export default function LyricSearch() {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				 const [showLyrics, setShowLyrics] = useState(false); 
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  return (
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    <div className="lyric-search">
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    <h2 className="title">
 | 
			
		
		
	
	
		
			
				
					
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@@ -40,7 +42,8 @@ export default function LyricSearch() {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    <label>Search:</label>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    <LyricSearchInputField
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              id="lyric-search-input"
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              placeholder="Artist - Song" />
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              placeholder="Artist - Song"
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              setShowLyrics={setShowLyrics} />
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    <br />
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    Exclude:<br />
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    <div id="exclude-checkboxes">
 | 
			
		
		
	
	
		
			
				
					
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@@ -54,159 +57,142 @@ export default function LyricSearch() {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    color="primary"
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    size="md"/></div>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    </div>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    <LyricResultBox/>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    </div>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  );
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				export function LyricSearchInputField(opts = {}) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				export function LyricSearchInputField({ id, placeholder, setShowLyrics }) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const [value, setValue] = useState("");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const [suggestions, setSuggestions] = useState([]);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const [showAlert, setShowAlert] = useState(false);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const [alertVisible, setAlertVisible] = useState(false);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const [isLoading, setIsLoading] = useState(false);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const [excludedSources, setExcludedSources] = useState([]);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const [lyricsResult, setLyricsResult] = useState(null);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const autoCompleteRef = useRef(null);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  // Ensure the dropdown panel is scrollable after it shows
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  // Typeahead: fetch suggestions
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const fetchSuggestions = async (event) => {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const query = event.query;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const res = await fetch(`${API_URL}/typeahead/lyrics`, {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      method: "POST",
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      headers: { "Content-Type": "application/json" },
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      body: JSON.stringify({ query }),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const json = await res.json();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    setSuggestions(json);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  };
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  // Toggle exclusion state for checkboxes
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const toggleExclusion = (source) => {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    setExcludedSources((prev) =>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      prev.includes(source)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        ? prev.filter((s) => s !== source)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        : [...prev, source]
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    );
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  };
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  // Show scrollable dropdown panel with mouse wheel handling
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const handlePanelShow = () => {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    setTimeout(() => {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      const panel = document.querySelector(".p-autocomplete-panel");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      const items = panel?.querySelector(".p-autocomplete-items");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      if (!items) return;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      if (items) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        items.style.maxHeight = "200px";
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        items.style.overflowY = "auto";
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        items.style.overscrollBehavior = "contain";
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      items.style.maxHeight = "200px";
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      items.style.overflowY = "auto";
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      items.style.overscrollBehavior = "contain";
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        const wheelHandler = (e) => {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          const delta = e.deltaY;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          const atTop = items.scrollTop === 0;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          const atBottom =
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            items.scrollTop + items.clientHeight >= items.scrollHeight;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      // ✅ 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();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          } else {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            e.stopPropagation();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        };
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        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 });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        items.removeEventListener("wheel", wheelHandler);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        items.addEventListener("wheel", wheelHandler, { passive: false });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    }, 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 = () => {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const handleSearch = async () => {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    if (autoCompleteRef.current) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      autoCompleteRef.current.hide();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const validSearch = value.includes(" - ");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    if (!validSearch) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      setShowAlert(true);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      setTimeout(() => setShowAlert(false), 5000);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    if (!value.includes(" - ")) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      setAlertVisible(true);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      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);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const [artist, song] = value.split(" - ", 2).map((v) => v.trim());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    if (!artist || !song) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      setAlertVisible(true);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      return;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const box = $("[class*='lyrics-card-']");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const lyrics_content = $(".lyrics-content");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const spinner = $("#spinner");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const excluded_sources = [];
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    setAlertVisible(false);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    setIsLoading(true);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    setLyricsResult(null);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    setShowLyrics(false);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    $("#exclude-checkboxes input:checked").each(function () {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      excluded_sources.push(this.id.replace("excl-", "").toLowerCase());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const toastId = toast.info("Searching...", {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      style: {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        color: "#000",
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        backgroundColor: "rgba(217, 242, 255, 0.8)",
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      },
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    setShowAlert(false);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    $("#alert").addClass("hidden");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    spinner.removeClass("hidden");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    box.addClass("hidden");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const startTime = Date.now();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const start_time = Date.now();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    const search_toast = toast.info("Searching...", {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      style: { color: '#000', backgroundColor: 'rgba(217, 242, 255, 0.8)' }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    try {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      const res = await fetch(`${API_URL}/lyric/search`, {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        method: "POST",
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        headers: { "Content-Type": "application/json" },
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        body: JSON.stringify({
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          a: artist,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          s: song,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          excluded_sources: excludedSources,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          src: "Web",
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          extra: true,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        }),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    $.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 data = await res.json();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      if (!res.ok || !data.lyrics) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        throw new Error(data.errorText || "Unknown error.");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      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");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      const duration = ((Date.now() - startTime) / 1000).toFixed(1);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      setLyricsResult({ artist: data.artist, song: data.song, lyrics: data.lyrics });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      setShowLyrics(true);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      toast.update(search_toast, {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      toast.update(toastId, {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        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,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    } catch (error) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      toast.update(toastId, {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        render: `😕 ${error.message}`,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        type: "",
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        style: { backgroundColor: "rgba(255, 0, 0, 0.5)" },
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        hideProgressBar: true,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        autoClose: 5000,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        hideProgressBar: true,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    } finally {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      setIsLoading(false);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  };
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  const handleKeyDown = (e) => {
 | 
			
		
		
	
	
		
			
				
					
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@@ -218,30 +204,52 @@ export function LyricSearchInputField(opts = {}) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  return (
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    <div>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      {showAlert && (
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      {alertVisible && (
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        <Alert
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          color="danger"
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          variant="solid"
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          onClose={() => setShowAlert(false)}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          onClose={() => setAlertVisible(false)}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          sx={{ mb: 2 }}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        >
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          You must specify both an artist and song to search.
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          <br />
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          Format: Artist - Song
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        </Alert>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      )}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      <AutoComplete
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        id={opts.id}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        id={id}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        ref={autoCompleteRef}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        value={value}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        size={40}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        suggestions={suggestions}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        completeMethod={typeahead_search}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        completeMethod={fetchSuggestions}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        onChange={(e) => setValue(e.target.value)}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        onKeyDown={handleKeyDown}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        onShow={handlePanelShow}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        placeholder={opts.placeholder}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        autoFocus      />
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      <Button onClick={handleSearch} className="btn">Search</Button>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        placeholder={placeholder}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        autoFocus
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        size={40}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      />
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      <Button onClick={handleSearch} className="btn">
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        Search
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      </Button>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      {isLoading && (
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        <div className="mt-3">
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          <CircularProgress variant="plain" color="primary" size="md" />
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        </div>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      )}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      {lyricsResult && (
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          <div className={`lyrics-card lyrics-card-${theme} mt-4 p-4 rounded-md shadow-md`}>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            <div className="lyrics-content">
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              <div style={{ textAlign: "center", fontWeight: "bold", marginBottom: "1rem" }}>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                {lyricsResult.artist} - {lyricsResult.song}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            </div>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            <div dangerouslySetInnerHTML={{ __html: lyricsResult.lyrics }} />
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          </div>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      </div>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      )}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    </div>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  );
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				}
 | 
			
		
		
	
	
		
			
				
					
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@@ -291,7 +299,7 @@ export const UICheckbox = forwardRef(function UICheckbox(opts = {}, ref) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				export function LyricResultBox(opts={}) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  return (
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    <div>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      <Box className={`lyrics-card lyrics-card-${theme} hidden`} sx={{ p: 2 }}>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      <Box className={`lyrics-card lyrics-card-${theme}`} sx={{ p: 2 }}>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        <div className='lyrics-content'></div>
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        {/* <ContentCopyIcon className='lyrics-card-copyButton' size='lg' /> */}
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      </Box>
 | 
			
		
		
	
	
		
			
				
					
					| 
						
					 | 
				
			
			 | 
			 | 
			
				 
 |