feat: Update AudioPlayer and LyricSearch components for improved functionality and user experience

This commit is contained in:
2025-11-25 13:05:37 -05:00
parent 8500cd6e67
commit ee25ad243c
5 changed files with 133 additions and 122 deletions

View File

@@ -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);
})();

View File

@@ -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;

View File

@@ -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"
/>
<span
@@ -343,14 +360,18 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) {
{statusTitle}
</span>
</div>
<Button onClick={() => handleSearch()} className="btn">
<Button
onClick={() => handleSearch()}
className="btn"
ref={searchButtonRef}
>
Search
</Button>
<div className="mt-4">
Exclude:<br />
<div id="exclude-checkboxes">
<UICheckbox id="excl-Genius" label="Genius" onToggle={toggleExclusion} />
<UICheckbox id="excl-LRCLib" label="LRCLib" onToggle={toggleExclusion} />
<UICheckbox id="excl-LRCLib-Cache" label="LRCLib-Cache" onToggle={toggleExclusion} />
<UICheckbox id="excl-Cache" label="Cache" onToggle={toggleExclusion} />
</div>
</div>
@@ -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
</button>
@@ -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
</button>
@@ -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}
>

View File

@@ -46,20 +46,8 @@ export default function RandomMsg() {
className="inline-block"
>
<path
d="M21 12a9 9 0 1 1-2.64-6.36"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<polyline
points="21 3 21 9 15 9"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
d="M17.65 6.35a7.95 7.95 0 0 0-5.65-2.35 8 8 0 1 0 7.75 9.94h-2.08a6 6 0 1 1-5.67-7.94 5.94 5.94 0 0 1 4.22 1.78L13 11h7V4z"
fill="currentColor"
/>
</svg>

View File

@@ -33,93 +33,7 @@ const currentPath = Astro.url.pathname;
---
<script is:inline define:vars={{ API_URL }}>
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 = "/";
}
};
// Mobile menu toggle
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;
}
// Remove existing listeners by cloning (prevents duplicate listeners)
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';
}
});
// Close menu when clicking a link
const mobileLinks = mobileMenu.querySelectorAll('a');
mobileLinks.forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('open');
menuIcon.style.display = 'block';
closeIcon.style.display = 'none';
});
});
// Close menu when clicking outside
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';
}
}
};
// Remove old handler if exists
document.removeEventListener('click', closeHandler);
document.addEventListener('click', closeHandler);
}
// Initialize on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMobileMenu);
} else {
initMobileMenu();
}
// Re-initialize after view transitions
document.addEventListener('astro:page-load', initMobileMenu);
</script>
<script src="/scripts/nav-controls.js" defer data-api-url={API_URL}></script>
<nav class="w-full px-4 sm:px-6 py-4 bg-transparent sticky top-0 z-50 backdrop-blur-sm bg-white/80 dark:bg-[#121212]/80 border-b border-neutral-200/50 dark:border-neutral-800/50">
<div class="max-w-7xl mx-auto">