feat: Update AudioPlayer and LyricSearch components for improved functionality and user experience
This commit is contained in:
84
public/scripts/nav-controls.js
Normal file
84
public/scripts/nav-controls.js
Normal 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);
|
||||
})();
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user