diff --git a/public/scripts/nav-controls.js b/public/scripts/nav-controls.js new file mode 100644 index 0000000..26066c2 --- /dev/null +++ b/public/scripts/nav-controls.js @@ -0,0 +1,84 @@ +(function () { + const scriptEl = document.currentScript; + const API_URL = scriptEl?.dataset?.apiUrl; + + window.toggleTheme = () => { + const currentTheme = document.documentElement.getAttribute("data-theme"); + const newTheme = currentTheme === "dark" ? "light" : "dark"; + document.dispatchEvent(new CustomEvent("set-theme", { detail: newTheme })); + }; + + window.handleLogout = async () => { + try { + await fetch(`${API_URL}/auth/logout`, { + method: "POST", + credentials: "include", + }); + } catch (error) { + console.error("Logout failed", error); + } finally { + window.location.href = "/"; + } + }; + + function initMobileMenu() { + const menuBtn = document.getElementById("mobile-menu-btn"); + const mobileMenu = document.getElementById("mobile-menu"); + const menuIcon = document.getElementById("menu-icon"); + const closeIcon = document.getElementById("close-icon"); + + if (!menuBtn || !mobileMenu || !menuIcon || !closeIcon) { + return; + } + + const newMenuBtn = menuBtn.cloneNode(true); + menuBtn.parentNode.replaceChild(newMenuBtn, menuBtn); + + newMenuBtn.addEventListener("click", (e) => { + e.stopPropagation(); + const isOpen = mobileMenu.classList.contains("open"); + + if (isOpen) { + mobileMenu.classList.remove("open"); + menuIcon.style.display = "block"; + closeIcon.style.display = "none"; + } else { + mobileMenu.classList.add("open"); + menuIcon.style.display = "none"; + closeIcon.style.display = "block"; + } + }); + + const mobileLinks = mobileMenu.querySelectorAll("a"); + mobileLinks.forEach((link) => { + link.addEventListener("click", () => { + mobileMenu.classList.remove("open"); + menuIcon.style.display = "block"; + closeIcon.style.display = "none"; + }); + }); + + const closeHandler = (e) => { + if (!mobileMenu.contains(e.target) && !newMenuBtn.contains(e.target)) { + if (mobileMenu.classList.contains("open")) { + mobileMenu.classList.remove("open"); + menuIcon.style.display = "block"; + closeIcon.style.display = "none"; + } + } + }; + + document.removeEventListener("click", closeHandler); + document.addEventListener("click", closeHandler); + } + + const ready = () => initMobileMenu(); + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", ready, { once: true }); + } else { + ready(); + } + + document.addEventListener("astro:page-load", initMobileMenu); +})(); diff --git a/src/components/AudioPlayer.jsx b/src/components/AudioPlayer.jsx index e9744e1..7844f71 100644 --- a/src/components/AudioPlayer.jsx +++ b/src/components/AudioPlayer.jsx @@ -217,16 +217,16 @@ export default function Player({ user }) { } const hls = new Hls({ - lowLatencyMode: true, + lowLatencyMode: false, abrEnabled: false, - liveSyncDuration: 0.5, // seconds behind live edge target + liveSyncDuration: 0.6, // seconds behind live edge target liveMaxLatencyDuration: 3.0, // max allowed latency before catchup liveCatchUpPlaybackRate: 1.02, - maxBufferLength: 30, - maxMaxBufferLength: 60, - maxBufferHole: 0.5, - manifestLoadingTimeOut: 4000, - fragLoadingTimeOut: 10000, // playback speed when catching up + // maxBufferLength: 30, + // maxMaxBufferLength: 60, + // maxBufferHole: 2.0, + // manifestLoadingTimeOut: 5000, + // fragLoadingTimeOut: 10000, // playback speed when catching up }); hlsInstance.current = hls; diff --git a/src/components/LyricSearch.jsx b/src/components/LyricSearch.jsx index 7f8d781..1dd4ed9 100644 --- a/src/components/LyricSearch.jsx +++ b/src/components/LyricSearch.jsx @@ -55,6 +55,8 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) { const [inputStatus, setInputStatus] = useState("hint"); const searchToastRef = useRef(null); const autoCompleteRef = useRef(null); + const autoCompleteInputRef = useRef(null); + const searchButtonRef = useRef(null); const [theme, setTheme] = useState(document.documentElement.getAttribute("data-theme") || "light"); const statusLabels = { hint: "Format: Artist - Song", @@ -196,6 +198,7 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) { if (autoCompleteRef.current) { autoCompleteRef.current.hide(); } + autoCompleteInputRef.current?.blur(); // blur early so the suggestion panel closes immediately const evaluation = evaluateSearchValue(searchValue); if (!evaluation?.valid) { @@ -209,13 +212,19 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) { setLyricsResult(null); setShowLyrics(false); - if (!toast.isActive('lyrics-searching-toast')) { - searchToastRef.current = toast.info("Searching...", { - toastId: "lyrics-searching-toast" - }); - } + const toastId = "lyrics-searching-toast"; + toast.dismiss(toastId); + searchToastRef.current = toast.info("Searching...", { + toastId, + }); const startTime = Date.now(); + const dismissSearchToast = () => { + if (searchToastRef.current) { + toast.dismiss(searchToastRef.current); + searchToastRef.current = null; + } + }; try { const res = await fetch(`${API_URL}/lyric/search`, { @@ -230,6 +239,10 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) { }), }); + if (res.status === 429) { + throw new Error("Too many requests. Please wait a moment and try again."); + } + const data = await res.json(); if (!res.ok || !data.lyrics) { throw new Error(data.errorText || "Unknown error."); @@ -244,21 +257,22 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) { const hash = `#${encodeURIComponent(data.artist)}/${encodeURIComponent(data.song)}`; window.history.pushState(null, '', hash); - toast.update(searchToastRef.current, { - type: "success", - render: `Found! (Took ${duration}s)`, - className: "Toastify__toast--success", + dismissSearchToast(); + toast.success(`Found! (Took ${duration}s)`, { autoClose: 2500, + toastId: `lyrics-success-${Date.now()}`, }); } catch (error) { - toast.update(searchToastRef.current, { - type: "error", - render: error.message, + dismissSearchToast(); + toast.error(error.message, { icon: () => "😕", autoClose: 5000, }); } finally { setIsLoading(false); + autoCompleteInputRef.current?.blur(); + searchButtonRef.current?.blur(); + searchToastRef.current = null; } }; @@ -329,6 +343,9 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) { className={`lyric-search-input ${inputStatus === "error" ? "has-error" : ""} ${inputStatus === "ready" ? "has-ready" : ""}`} aria-invalid={inputStatus === "error"} aria-label={`Lyric search input. ${statusTitle}`} + inputRef={(el) => { + autoCompleteInputRef.current = el; + }} aria-controls="lyric-search-input" /> -
Exclude:
- +
@@ -373,6 +394,7 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) { type="button" className={`text-size-btn text-size-large ${textSize === "large" ? "active" : ""}`} onClick={() => setTextSize("large")} + title="Larger text" > Aa @@ -380,6 +402,7 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) { type="button" className={`text-size-btn ${textSize === "normal" ? "active" : ""}`} onClick={() => setTextSize("normal")} + title="Default text" > Aa @@ -389,6 +412,7 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) { variant="plain" color="neutral" aria-label="Copy lyrics" + title="Copy lyrics" className="lyrics-action-button" onClick={handleCopyLyrics} > @@ -399,6 +423,7 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) { variant="plain" color="neutral" aria-label="Copy lyric link" + title="Copy shareable link" className="lyrics-action-button" onClick={handleCopyLink} > diff --git a/src/components/RandomMsg.jsx b/src/components/RandomMsg.jsx index 9f2c241..e942ec1 100644 --- a/src/components/RandomMsg.jsx +++ b/src/components/RandomMsg.jsx @@ -46,20 +46,8 @@ export default function RandomMsg() { className="inline-block" > - diff --git a/src/layouts/Nav.astro b/src/layouts/Nav.astro index 5b21e09..3842447 100644 --- a/src/layouts/Nav.astro +++ b/src/layouts/Nav.astro @@ -33,93 +33,7 @@ const currentPath = Astro.url.pathname; --- - +