initial commit
This commit is contained in:
280
src/components/LyricSearch.jsx
Normal file
280
src/components/LyricSearch.jsx
Normal file
@@ -0,0 +1,280 @@
|
||||
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);
|
||||
search_toast = toast.info("Searching...");
|
||||
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");
|
||||
return toast.update(search_toast, {
|
||||
render: `😕 Failed to reach search endpoint (${jqXHR.status})`,
|
||||
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);
|
||||
}
|
||||
})
|
||||
// Fetch data from your API using the event.query (user's input)
|
||||
// axios.get(`your-api-endpoint?query=${event.query}`)
|
||||
// .then(response => {
|
||||
// setSuggestions(response.data); // Update suggestions state with the fetched data
|
||||
// })
|
||||
// .catch(error => {
|
||||
// console.error('Error fetching suggestions:', error);
|
||||
// });
|
||||
};
|
||||
|
||||
|
||||
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),
|
||||
checked, // (optional) expose value for reading too
|
||||
}));
|
||||
|
||||
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 true;
|
||||
};
|
||||
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 (
|
||||
<Box className={`lyrics-card lyrics-card-${theme} hidden`} sx={{p: 2 }}></Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user