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({
|
const hls = new Hls({
|
||||||
lowLatencyMode: true,
|
lowLatencyMode: false,
|
||||||
abrEnabled: 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
|
liveMaxLatencyDuration: 3.0, // max allowed latency before catchup
|
||||||
liveCatchUpPlaybackRate: 1.02,
|
liveCatchUpPlaybackRate: 1.02,
|
||||||
maxBufferLength: 30,
|
// maxBufferLength: 30,
|
||||||
maxMaxBufferLength: 60,
|
// maxMaxBufferLength: 60,
|
||||||
maxBufferHole: 0.5,
|
// maxBufferHole: 2.0,
|
||||||
manifestLoadingTimeOut: 4000,
|
// manifestLoadingTimeOut: 5000,
|
||||||
fragLoadingTimeOut: 10000, // playback speed when catching up
|
// fragLoadingTimeOut: 10000, // playback speed when catching up
|
||||||
});
|
});
|
||||||
|
|
||||||
hlsInstance.current = hls;
|
hlsInstance.current = hls;
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) {
|
|||||||
const [inputStatus, setInputStatus] = useState("hint");
|
const [inputStatus, setInputStatus] = useState("hint");
|
||||||
const searchToastRef = useRef(null);
|
const searchToastRef = useRef(null);
|
||||||
const autoCompleteRef = 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 [theme, setTheme] = useState(document.documentElement.getAttribute("data-theme") || "light");
|
||||||
const statusLabels = {
|
const statusLabels = {
|
||||||
hint: "Format: Artist - Song",
|
hint: "Format: Artist - Song",
|
||||||
@@ -196,6 +198,7 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) {
|
|||||||
if (autoCompleteRef.current) {
|
if (autoCompleteRef.current) {
|
||||||
autoCompleteRef.current.hide();
|
autoCompleteRef.current.hide();
|
||||||
}
|
}
|
||||||
|
autoCompleteInputRef.current?.blur(); // blur early so the suggestion panel closes immediately
|
||||||
|
|
||||||
const evaluation = evaluateSearchValue(searchValue);
|
const evaluation = evaluateSearchValue(searchValue);
|
||||||
if (!evaluation?.valid) {
|
if (!evaluation?.valid) {
|
||||||
@@ -209,13 +212,19 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) {
|
|||||||
setLyricsResult(null);
|
setLyricsResult(null);
|
||||||
setShowLyrics(false);
|
setShowLyrics(false);
|
||||||
|
|
||||||
if (!toast.isActive('lyrics-searching-toast')) {
|
const toastId = "lyrics-searching-toast";
|
||||||
searchToastRef.current = toast.info("Searching...", {
|
toast.dismiss(toastId);
|
||||||
toastId: "lyrics-searching-toast"
|
searchToastRef.current = toast.info("Searching...", {
|
||||||
});
|
toastId,
|
||||||
}
|
});
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
const dismissSearchToast = () => {
|
||||||
|
if (searchToastRef.current) {
|
||||||
|
toast.dismiss(searchToastRef.current);
|
||||||
|
searchToastRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/lyric/search`, {
|
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();
|
const data = await res.json();
|
||||||
if (!res.ok || !data.lyrics) {
|
if (!res.ok || !data.lyrics) {
|
||||||
throw new Error(data.errorText || "Unknown error.");
|
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)}`;
|
const hash = `#${encodeURIComponent(data.artist)}/${encodeURIComponent(data.song)}`;
|
||||||
window.history.pushState(null, '', hash);
|
window.history.pushState(null, '', hash);
|
||||||
|
|
||||||
toast.update(searchToastRef.current, {
|
dismissSearchToast();
|
||||||
type: "success",
|
toast.success(`Found! (Took ${duration}s)`, {
|
||||||
render: `Found! (Took ${duration}s)`,
|
|
||||||
className: "Toastify__toast--success",
|
|
||||||
autoClose: 2500,
|
autoClose: 2500,
|
||||||
|
toastId: `lyrics-success-${Date.now()}`,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.update(searchToastRef.current, {
|
dismissSearchToast();
|
||||||
type: "error",
|
toast.error(error.message, {
|
||||||
render: error.message,
|
|
||||||
icon: () => "😕",
|
icon: () => "😕",
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
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" : ""}`}
|
className={`lyric-search-input ${inputStatus === "error" ? "has-error" : ""} ${inputStatus === "ready" ? "has-ready" : ""}`}
|
||||||
aria-invalid={inputStatus === "error"}
|
aria-invalid={inputStatus === "error"}
|
||||||
aria-label={`Lyric search input. ${statusTitle}`}
|
aria-label={`Lyric search input. ${statusTitle}`}
|
||||||
|
inputRef={(el) => {
|
||||||
|
autoCompleteInputRef.current = el;
|
||||||
|
}}
|
||||||
aria-controls="lyric-search-input"
|
aria-controls="lyric-search-input"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -343,14 +360,18 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) {
|
|||||||
{statusTitle}
|
{statusTitle}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => handleSearch()} className="btn">
|
<Button
|
||||||
|
onClick={() => handleSearch()}
|
||||||
|
className="btn"
|
||||||
|
ref={searchButtonRef}
|
||||||
|
>
|
||||||
Search
|
Search
|
||||||
</Button>
|
</Button>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
Exclude:<br />
|
Exclude:<br />
|
||||||
<div id="exclude-checkboxes">
|
<div id="exclude-checkboxes">
|
||||||
<UICheckbox id="excl-Genius" label="Genius" onToggle={toggleExclusion} />
|
<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} />
|
<UICheckbox id="excl-Cache" label="Cache" onToggle={toggleExclusion} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -373,6 +394,7 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) {
|
|||||||
type="button"
|
type="button"
|
||||||
className={`text-size-btn text-size-large ${textSize === "large" ? "active" : ""}`}
|
className={`text-size-btn text-size-large ${textSize === "large" ? "active" : ""}`}
|
||||||
onClick={() => setTextSize("large")}
|
onClick={() => setTextSize("large")}
|
||||||
|
title="Larger text"
|
||||||
>
|
>
|
||||||
Aa
|
Aa
|
||||||
</button>
|
</button>
|
||||||
@@ -380,6 +402,7 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) {
|
|||||||
type="button"
|
type="button"
|
||||||
className={`text-size-btn ${textSize === "normal" ? "active" : ""}`}
|
className={`text-size-btn ${textSize === "normal" ? "active" : ""}`}
|
||||||
onClick={() => setTextSize("normal")}
|
onClick={() => setTextSize("normal")}
|
||||||
|
title="Default text"
|
||||||
>
|
>
|
||||||
Aa
|
Aa
|
||||||
</button>
|
</button>
|
||||||
@@ -389,6 +412,7 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) {
|
|||||||
variant="plain"
|
variant="plain"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
aria-label="Copy lyrics"
|
aria-label="Copy lyrics"
|
||||||
|
title="Copy lyrics"
|
||||||
className="lyrics-action-button"
|
className="lyrics-action-button"
|
||||||
onClick={handleCopyLyrics}
|
onClick={handleCopyLyrics}
|
||||||
>
|
>
|
||||||
@@ -399,6 +423,7 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) {
|
|||||||
variant="plain"
|
variant="plain"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
aria-label="Copy lyric link"
|
aria-label="Copy lyric link"
|
||||||
|
title="Copy shareable link"
|
||||||
className="lyrics-action-button"
|
className="lyrics-action-button"
|
||||||
onClick={handleCopyLink}
|
onClick={handleCopyLink}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -46,20 +46,8 @@ export default function RandomMsg() {
|
|||||||
className="inline-block"
|
className="inline-block"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M21 12a9 9 0 1 1-2.64-6.36"
|
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="none"
|
fill="currentColor"
|
||||||
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"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
|||||||
@@ -33,93 +33,7 @@ const currentPath = Astro.url.pathname;
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<script is:inline define:vars={{ API_URL }}>
|
<script src="/scripts/nav-controls.js" defer data-api-url={API_URL}></script>
|
||||||
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>
|
|
||||||
|
|
||||||
<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">
|
<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">
|
<div class="max-w-7xl mx-auto">
|
||||||
|
|||||||
Reference in New Issue
Block a user