diff --git a/js/lyrics.js b/js/lyrics.js
index aab86c5..0a30a45 100644
--- a/js/lyrics.js
+++ b/js/lyrics.js
@@ -1,310 +1,756 @@
//js/lyrics.js
-import { getTrackTitle, getTrackArtists, buildTrackFilename, SVG_DOWNLOAD, SVG_CLOSE } from './utils.js';
-import { sidePanelManager } from './side-panel.js';
+import {
+ getTrackTitle,
+ getTrackArtists,
+ buildTrackFilename,
+ SVG_DOWNLOAD,
+ SVG_CLOSE,
+} from "./utils.js";
+import { sidePanelManager } from "./side-panel.js";
export class LyricsManager {
- constructor(api) {
- this.api = api;
- this.currentLyrics = null;
- this.syncedLyrics = [];
- this.lyricsCache = new Map();
- this.componentLoaded = false;
- this.amLyricsElement = null;
- this.animationFrameId = null;
+ constructor(api) {
+ this.api = api;
+ this.currentLyrics = null;
+ this.syncedLyrics = [];
+ this.lyricsCache = new Map();
+ this.componentLoaded = false;
+ this.amLyricsElement = null;
+ this.animationFrameId = null;
+ this.currentTrackId = null;
+ this.mutationObserver = null;
+ this.romajiObserver = null;
+ this.isRomajiMode = false;
+ this.originalLyricsData = null;
+ this.kuroshiroLoaded = false;
+ this.kuroshiroLoading = false;
+ this.convertedNodes = new WeakSet(); // Track already converted nodes
+ }
+
+ // Load Kuroshiro from CDN
+ async loadKuroshiro() {
+ if (this.kuroshiroLoaded) return true;
+ if (this.kuroshiroLoading) {
+ // Wait for existing load to complete
+ return new Promise((resolve) => {
+ const checkLoad = setInterval(() => {
+ if (!this.kuroshiroLoading) {
+ clearInterval(checkLoad);
+ resolve(this.kuroshiroLoaded);
+ }
+ }, 100);
+ });
}
- async ensureComponentLoaded() {
- if (this.componentLoaded) return;
-
- if (typeof customElements !== 'undefined' && customElements.get('am-lyrics')) {
- this.componentLoaded = true;
- return;
+ this.kuroshiroLoading = true;
+ try {
+ console.log("Loading Kuroshiro for Kanji to Romaji conversion...");
+
+ // Load Kuroshiro from CDN
+ if (!window.Kuroshiro) {
+ await this.loadScript(
+ "https://unpkg.com/kuroshiro@1.2.0/dist/kuroshiro.min.js",
+ );
+ }
+
+ // Load Kuromoji analyzer from CDN with proper dictionary path
+ if (!window.KuromojiAnalyzer) {
+ await this.loadScript(
+ "https://unpkg.com/kuroshiro-analyzer-kuromoji@1.1.0/dist/kuroshiro-analyzer-kuromoji.min.js",
+ );
+ }
+
+ // Initialize Kuroshiro (CDN version exports as .default)
+ const Kuroshiro = window.Kuroshiro.default || window.Kuroshiro;
+ const KuromojiAnalyzer =
+ window.KuromojiAnalyzer.default || window.KuromojiAnalyzer;
+
+ this.kuroshiro = new Kuroshiro();
+
+ // Initialize with custom dictionary path from unpkg
+ await this.kuroshiro.init(
+ new KuromojiAnalyzer({
+ dictPath: "https://unpkg.com/kuromoji@0.1.2/dict/",
+ }),
+ );
+
+ this.kuroshiroLoaded = true;
+ this.kuroshiroLoading = false;
+ console.log("✓ Kuroshiro loaded and initialized successfully");
+ return true;
+ } catch (error) {
+ console.error("✗ Failed to load Kuroshiro:", error);
+ this.kuroshiroLoaded = false;
+ this.kuroshiroLoading = false;
+ return false;
+ }
+ }
+
+ // Helper to load external scripts
+ loadScript(src) {
+ return new Promise((resolve, reject) => {
+ const script = document.createElement("script");
+ script.src = src;
+ script.onload = resolve;
+ script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
+ document.head.appendChild(script);
+ });
+ }
+
+ // Check if text contains Japanese characters
+ containsJapanese(text) {
+ if (!text) return false;
+ // Match any Japanese character (Hiragana, Katakana, Kanji)
+ return /[\u3040-\u30FF\u31F0-\u9FFF]/.test(text);
+ }
+
+ // Convert Japanese text to Romaji (including Kanji)
+ async convertToRomaji(text) {
+ if (!text) return text;
+
+ // Make sure Kuroshiro is loaded
+ if (!this.kuroshiroLoaded) {
+ const success = await this.loadKuroshiro();
+ if (!success) {
+ console.warn("Kuroshiro not available, skipping conversion");
+ return text;
+ }
+ }
+
+ if (!this.kuroshiro) {
+ console.warn("Kuroshiro not available, skipping conversion");
+ return text;
+ }
+
+ try {
+ // Convert to Romaji using Kuroshiro (handles Kanji, Hiragana, Katakana)
+ const result = await this.kuroshiro.convert(text, {
+ to: "romaji",
+ mode: "spaced",
+ romajiSystem: "hepburn",
+ });
+ return result;
+ } catch (error) {
+ console.warn(
+ "Romaji conversion failed for text:",
+ text.substring(0, 30),
+ error,
+ );
+ return text;
+ }
+ }
+
+ // Set Romaji mode and save preference
+ setRomajiMode(enabled) {
+ this.isRomajiMode = enabled;
+ try {
+ localStorage.setItem("lyricsRomajiMode", enabled ? "true" : "false");
+ } catch (e) {
+ console.warn("Failed to save Romaji mode preference:", e);
+ }
+ }
+
+ // Get saved Romaji mode preference
+ getRomajiMode() {
+ try {
+ return localStorage.getItem("lyricsRomajiMode") === "true";
+ } catch (e) {
+ return false;
+ }
+ }
+
+ async ensureComponentLoaded() {
+ if (this.componentLoaded) return;
+
+ if (
+ typeof customElements !== "undefined" &&
+ customElements.get("am-lyrics")
+ ) {
+ this.componentLoaded = true;
+ return;
+ }
+
+ return new Promise((resolve, reject) => {
+ const script = document.createElement("script");
+ script.type = "module";
+ script.src =
+ "https://cdn.jsdelivr.net/npm/@uimaxbai/am-lyrics@0.6.2/dist/src/am-lyrics.min.js";
+
+ script.onload = () => {
+ if (typeof customElements !== "undefined") {
+ customElements
+ .whenDefined("am-lyrics")
+ .then(() => {
+ this.componentLoaded = true;
+ resolve();
+ })
+ .catch(reject);
+ } else {
+ resolve();
+ }
+ };
+
+ script.onerror = () =>
+ reject(new Error("Failed to load lyrics component"));
+ document.head.appendChild(script);
+ });
+ }
+
+ async fetchLyrics(trackId, track = null) {
+ if (track) {
+ if (this.lyricsCache.has(trackId)) {
+ return this.lyricsCache.get(trackId);
+ }
+
+ try {
+ const artist = Array.isArray(track.artists)
+ ? track.artists.map((a) => a.name || a).join(", ")
+ : track.artist?.name || "";
+ const title = track.title || "";
+ const album = track.album?.title || "";
+ const duration = track.duration ? Math.round(track.duration) : null;
+
+ if (!title || !artist) {
+ console.warn("Missing required fields for LRCLIB");
+ return null;
}
- return new Promise((resolve, reject) => {
- const script = document.createElement('script');
- script.type = 'module';
- script.src = 'https://cdn.jsdelivr.net/npm/@uimaxbai/am-lyrics@0.6.2/dist/src/am-lyrics.min.js';
-
- script.onload = () => {
- if (typeof customElements !== 'undefined') {
- customElements.whenDefined('am-lyrics')
- .then(() => {
- this.componentLoaded = true;
- resolve();
- })
- .catch(reject);
- } else {
- resolve();
- }
- };
-
- script.onerror = () => reject(new Error('Failed to load lyrics component'));
- document.head.appendChild(script);
+ const params = new URLSearchParams({
+ track_name: title,
+ artist_name: artist,
});
+
+ if (album) params.append("album_name", album);
+ if (duration) params.append("duration", duration.toString());
+
+ const response = await fetch(
+ `https://lrclib.net/api/get?${params.toString()}`,
+ );
+
+ if (response.ok) {
+ const data = await response.json();
+
+ if (data.syncedLyrics) {
+ const lyricsData = {
+ subtitles: data.syncedLyrics,
+ lyricsProvider: "LRCLIB",
+ };
+
+ this.lyricsCache.set(trackId, lyricsData);
+ return lyricsData;
+ }
+ }
+ } catch (error) {
+ console.warn("LRCLIB fetch failed:", error);
+ }
}
- async fetchLyrics(trackId, track = null) {
- // LRCLIB
- if (track) {
- if (this.lyricsCache.has(trackId)) {
- return this.lyricsCache.get(trackId);
- }
+ return null;
+ }
- try {
- const artist = Array.isArray(track.artists)
- ? track.artists.map(a => a.name || a).join(', ')
- : track.artist?.name || '';
- const title = track.title || '';
- const album = track.album?.title || '';
- const duration = track.duration ? Math.round(track.duration) : null;
-
- if (!title || !artist) {
- console.warn('Missing required fields for LRCLIB');
- return null;
- }
-
- const params = new URLSearchParams({
- track_name: title,
- artist_name: artist
- });
-
- if (album) params.append('album_name', album);
- if (duration) params.append('duration', duration.toString());
-
- const response = await fetch(`https://lrclib.net/api/get?${params.toString()}`);
-
- if (response.ok) {
- const data = await response.json();
-
- if (data.syncedLyrics) {
- const lyricsData = {
- subtitles: data.syncedLyrics,
- lyricsProvider: 'LRCLIB'
- };
-
- this.lyricsCache.set(trackId, lyricsData);
- return lyricsData;
- }
- }
- } catch (error) {
- console.warn('LRCLIB fetch failed:', error);
- }
+ parseSyncedLyrics(subtitles) {
+ if (!subtitles) return [];
+ const lines = subtitles.split("\n").filter((line) => line.trim());
+ return lines
+ .map((line) => {
+ const match = line.match(/\[(\d+):(\d+)\.(\d+)\]\s*(.+)/);
+ if (match) {
+ const [, minutes, seconds, centiseconds, text] = match;
+ const timeInSeconds =
+ parseInt(minutes) * 60 +
+ parseInt(seconds) +
+ parseInt(centiseconds) / 100;
+ return { time: timeInSeconds, text: text.trim() };
}
-
return null;
+ })
+ .filter(Boolean);
+ }
+
+ generateLRCContent(lyricsData, track) {
+ if (!lyricsData || !lyricsData.subtitles) return null;
+
+ const trackTitle = getTrackTitle(track);
+ const trackArtist = getTrackArtists(track);
+
+ let lrc = `[ti:${trackTitle}]\n`;
+ lrc += `[ar:${trackArtist}]\n`;
+ lrc += `[al:${track.album?.title || "Unknown Album"}]\n`;
+ lrc += `[by:${lyricsData.lyricsProvider || "Unknown"}]\n`;
+ lrc += "\n";
+ lrc += lyricsData.subtitles;
+
+ return lrc;
+ }
+
+ downloadLRC(lyricsData, track) {
+ const lrcContent = this.generateLRCContent(lyricsData, track);
+ if (!lrcContent) {
+ alert("No synced lyrics available for this track");
+ return;
}
- parseSyncedLyrics(subtitles) {
- if (!subtitles) return [];
- const lines = subtitles.split('\n').filter(line => line.trim());
- return lines.map(line => {
- const match = line.match(/\[(\d+):(\d+)\.(\d+)\]\s*(.+)/);
- if (match) {
- const [, minutes, seconds, centiseconds, text] = match;
- const timeInSeconds = parseInt(minutes) * 60 + parseInt(seconds) + parseInt(centiseconds) / 100;
- return { time: timeInSeconds, text: text.trim() };
- }
- return null;
- }).filter(Boolean);
+ const blob = new Blob([lrcContent], { type: "application/octet-stream" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = buildTrackFilename(track, "LOSSLESS").replace(
+ /\.flac$/,
+ ".lrc",
+ );
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }
+
+ getCurrentLine(currentTime) {
+ if (!this.syncedLyrics || this.syncedLyrics.length === 0) return -1;
+ let currentIndex = -1;
+ for (let i = 0; i < this.syncedLyrics.length; i++) {
+ if (currentTime >= this.syncedLyrics[i].time) {
+ currentIndex = i;
+ } else {
+ break;
+ }
+ }
+ return currentIndex;
+ }
+
+ // Setup MutationObserver to convert lyrics in am-lyrics component
+ setupLyricsObserver(amLyricsElement) {
+ this.stopLyricsObserver();
+
+ if (!amLyricsElement) return;
+
+ // Check for shadow DOM
+ const observeRoot = amLyricsElement.shadowRoot || amLyricsElement;
+
+ this.romajiObserver = new MutationObserver((mutations) => {
+ // Only process if new text content was added (ignore attribute changes like highlight)
+ const hasNewContent = mutations.some(
+ (mutation) =>
+ mutation.type === "childList" && mutation.addedNodes.length > 0,
+ );
+
+ if (!hasNewContent) {
+ // Ignore highlight changes and other attribute mutations
+ return;
+ }
+
+ // Debounce mutations
+ if (this.observerTimeout) {
+ clearTimeout(this.observerTimeout);
+ }
+ this.observerTimeout = setTimeout(async () => {
+ await this.convertLyricsContent(amLyricsElement);
+ }, 100);
+ });
+
+ // Observe all child nodes for changes (in shadow DOM if it exists)
+ // Only watch for new nodes, not attribute changes (to avoid highlight spam)
+ this.romajiObserver.observe(observeRoot, {
+ childList: true,
+ subtree: true,
+ characterData: false, // Don't watch text changes, only new nodes
+ attributes: false, // Don't watch attribute changes (highlight, etc)
+ });
+
+ // Initial conversion if Romaji mode is enabled
+ if (this.isRomajiMode) {
+ // Try immediately and after delays to catch lyrics when they load
+ this.convertLyricsContent(amLyricsElement);
+ setTimeout(async () => {
+ await this.convertLyricsContent(amLyricsElement);
+ }, 500);
+ setTimeout(async () => {
+ await this.convertLyricsContent(amLyricsElement);
+ }, 1500);
+ setTimeout(async () => {
+ await this.convertLyricsContent(amLyricsElement);
+ }, 3000);
+ }
+ }
+
+ // Convert lyrics content to Romaji
+ async convertLyricsContent(amLyricsElement) {
+ if (!amLyricsElement || !this.isRomajiMode) {
+ return;
}
- generateLRCContent(lyricsData, track) {
- if (!lyricsData || !lyricsData.subtitles) return null;
+ // Find the root to traverse - check for shadow DOM first
+ const rootToTraverse = amLyricsElement.shadowRoot || amLyricsElement;
- const trackTitle = getTrackTitle(track);
- const trackArtist = getTrackArtists(track);
-
- let lrc = `[ti:${trackTitle}]\n`;
- lrc += `[ar:${trackArtist}]\n`;
- lrc += `[al:${track.album?.title || 'Unknown Album'}]\n`;
- lrc += `[by:${lyricsData.lyricsProvider || 'Unknown'}]\n`;
- lrc += '\n';
- lrc += lyricsData.subtitles;
-
- return lrc;
+ // Make sure Kuroshiro is ready
+ if (!this.kuroshiroLoaded) {
+ const success = await this.loadKuroshiro();
+ if (!success) {
+ console.warn("Cannot convert lyrics - Kuroshiro load failed");
+ return;
+ }
}
- downloadLRC(lyricsData, track) {
- const lrcContent = this.generateLRCContent(lyricsData, track);
- if (!lrcContent) {
- alert('No synced lyrics available for this track');
- return;
+ // Find all text nodes in the component
+ const textNodes = [];
+ const walker = document.createTreeWalker(
+ rootToTraverse,
+ NodeFilter.SHOW_TEXT,
+ null,
+ false,
+ );
+
+ let node;
+ while ((node = walker.nextNode())) {
+ textNodes.push(node);
+ }
+
+ // Convert Japanese text to Romaji (using async/await for Kuroshiro)
+ let convertedCount = 0;
+
+ for (const textNode of textNodes) {
+ // Skip if already converted
+ if (this.convertedNodes.has(textNode)) {
+ continue;
+ }
+
+ if (!textNode.parentElement) {
+ continue;
+ }
+
+ const parentTag = textNode.parentElement.tagName?.toLowerCase();
+ const parentClass = String(textNode.parentElement.className || "");
+
+ // Skip elements that shouldn't be converted
+ const skipTags = ["script", "style", "code", "input", "textarea", "time"];
+ if (skipTags.includes(parentTag)) {
+ continue;
+ }
+
+ const originalText = textNode.textContent;
+
+ // Skip progress indicators and timestamps (but NOT progress-text which is the actual lyrics!)
+ if (
+ (parentClass.includes("progress") &&
+ !parentClass.includes("progress-text")) ||
+ (parentClass.includes("time") &&
+ !parentClass.includes("progress-text")) ||
+ parentClass.includes("timestamp")
+ ) {
+ continue;
+ }
+
+ if (!originalText || originalText.trim().length === 0) {
+ continue;
+ }
+
+ // Check if contains Japanese
+ if (this.containsJapanese(originalText)) {
+ const romajiText = await this.convertToRomaji(originalText);
+
+ if (romajiText && romajiText !== originalText) {
+ textNode.textContent = romajiText;
+ // Mark as converted
+ this.convertedNodes.add(textNode);
+ convertedCount++;
}
+ }
+ }
+ }
- const blob = new Blob([lrcContent], { type: 'application/octet-stream' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = buildTrackFilename(track, 'LOSSLESS').replace(/\.flac$/, '.lrc');
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
+ // Stop the observer
+ stopLyricsObserver() {
+ if (this.romajiObserver) {
+ this.romajiObserver.disconnect();
+ this.romajiObserver = null;
+ }
+ if (this.observerTimeout) {
+ clearTimeout(this.observerTimeout);
+ this.observerTimeout = null;
+ }
+ // Clear converted nodes tracking when stopping
+ this.convertedNodes = new WeakSet();
+ }
+
+ // Toggle Romaji mode
+ async toggleRomajiMode(amLyricsElement) {
+ this.isRomajiMode = !this.isRomajiMode;
+ this.setRomajiMode(this.isRomajiMode);
+
+ if (amLyricsElement) {
+ if (this.isRomajiMode) {
+ // Turning ON: Setup observer and convert immediately
+ this.setupLyricsObserver(amLyricsElement);
+ // Also try immediate conversion (don't wait for timeout)
+ await this.convertLyricsContent(amLyricsElement);
+ } else {
+ // Turning OFF: Stop observer (original lyrics should remain)
+ // Note: To restore original Japanese, we'd need to reload the component
+ // For now, the converted text stays until lyrics are reloaded
+ this.stopLyricsObserver();
+ }
}
- getCurrentLine(currentTime) {
- if (!this.syncedLyrics || this.syncedLyrics.length === 0) return -1;
- let currentIndex = -1;
- for (let i = 0; i < this.syncedLyrics.length; i++) {
- if (currentTime >= this.syncedLyrics[i].time) {
- currentIndex = i;
- } else {
- break;
- }
- }
- return currentIndex;
- }
+ return this.isRomajiMode;
+ }
}
export async function openLyricsPanel(track, audioPlayer, lyricsManager) {
- // If no manager provided, create a temp one
- const manager = lyricsManager || new LyricsManager();
+ const manager = lyricsManager || new LyricsManager();
- const renderControls = (container) => {
- container.innerHTML = `
+ // Load Kuroshiro early for Kanji conversion (blocking if Romaji mode is enabled)
+ if (!manager.kuroshiroLoaded && !manager.kuroshiroLoading) {
+ if (manager.getRomajiMode()) {
+ // If Romaji mode is enabled, wait for Kuroshiro to load before continuing
+ await manager.loadKuroshiro();
+ } else {
+ // Otherwise, load in background
+ manager.loadKuroshiro().catch((err) => {
+ console.warn("Failed to load Kuroshiro for Romaji conversion:", err);
+ });
+ }
+ }
+
+ const renderControls = (container) => {
+ const isRomajiMode = manager.getRomajiMode();
+ manager.isRomajiMode = isRomajiMode;
+
+ container.innerHTML = `
${SVG_CLOSE}
+
+
+
+
+
+
`;
- container.querySelector('#close-side-panel-btn').addEventListener('click', () => {
- sidePanelManager.close();
- clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
- });
- };
-
- const renderContent = async (container) => {
- // Clean up any previous sync (though sidePanelManager might handle cleanup, we ensure it here)
+ container
+ .querySelector("#close-side-panel-btn")
+ .addEventListener("click", () => {
+ sidePanelManager.close();
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
-
- await renderLyricsComponent(container, track, audioPlayer, manager);
- };
+ });
- sidePanelManager.open('lyrics', 'Lyrics', renderControls, renderContent);
+ // Romaji toggle button handler
+ const romajiBtn = container.querySelector("#romaji-toggle-btn");
+ if (romajiBtn) {
+ const updateRomajiBtn = () => {
+ const enabled = manager.isRomajiMode;
+ romajiBtn.setAttribute("data-enabled", enabled);
+ romajiBtn.style.color = enabled ? "var(--primary)" : "";
+ };
+ updateRomajiBtn();
+
+ romajiBtn.addEventListener("click", async () => {
+ const amLyrics = sidePanelManager.panel.querySelector("am-lyrics");
+ if (amLyrics) {
+ const newMode = await manager.toggleRomajiMode(amLyrics);
+ updateRomajiBtn();
+ }
+ });
+ }
+ };
+
+ const renderContent = async (container) => {
+ clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
+ await renderLyricsComponent(container, track, audioPlayer, manager);
+ };
+
+ sidePanelManager.open("lyrics", "Lyrics", renderControls, renderContent);
}
-async function renderLyricsComponent(container, track, audioPlayer, lyricsManager) {
- container.innerHTML = '
Loading lyrics...
';
+async function renderLyricsComponent(
+ container,
+ track,
+ audioPlayer,
+ lyricsManager,
+) {
+ container.innerHTML = '
Loading lyrics...
';
- try {
- await lyricsManager.ensureComponentLoaded();
+ try {
+ await lyricsManager.ensureComponentLoaded();
- const title = track.title;
- const artist = getTrackArtists(track);
- const album = track.album?.title;
- const durationMs = track.duration ? Math.round(track.duration * 1000) : undefined;
- const isrc = track.isrc || '';
+ // Set initial Romaji mode
+ lyricsManager.isRomajiMode = lyricsManager.getRomajiMode();
+ lyricsManager.currentTrackId = track.id;
- container.innerHTML = '';
- const amLyrics = document.createElement('am-lyrics');
- amLyrics.setAttribute('song-title', title);
- amLyrics.setAttribute('song-artist', artist);
- if (album) amLyrics.setAttribute('song-album', album);
- if (durationMs) amLyrics.setAttribute('song-duration', durationMs);
- amLyrics.setAttribute('query', `${title} ${artist}`.trim());
- if (isrc) amLyrics.setAttribute('isrc', isrc);
+ const title = track.title;
+ const artist = getTrackArtists(track);
+ const album = track.album?.title;
+ const durationMs = track.duration
+ ? Math.round(track.duration * 1000)
+ : undefined;
+ const isrc = track.isrc || "";
- amLyrics.setAttribute('highlight-color', '#93c5fd');
- amLyrics.setAttribute('hover-background-color', 'rgba(59, 130, 246, 0.14)');
- amLyrics.setAttribute('autoscroll', '');
- amLyrics.setAttribute('interpolate', '');
- amLyrics.style.height = '100%';
- amLyrics.style.width = '100%';
+ container.innerHTML = "";
+ const amLyrics = document.createElement("am-lyrics");
+ amLyrics.setAttribute("song-title", title);
+ amLyrics.setAttribute("song-artist", artist);
+ if (album) amLyrics.setAttribute("song-album", album);
+ if (durationMs) amLyrics.setAttribute("song-duration", durationMs);
+ amLyrics.setAttribute("query", `${title} ${artist}`.trim());
+ if (isrc) amLyrics.setAttribute("isrc", isrc);
- container.appendChild(amLyrics);
-
- const cleanup = setupSync(track, audioPlayer, amLyrics);
-
- // Attach cleanup to container for easy access
- container.lyricsCleanup = cleanup;
-
- return amLyrics;
- } catch (error) {
- console.error('Failed to load lyrics:', error);
- container.innerHTML = '
Failed to load lyrics
';
- return null;
+ amLyrics.setAttribute("highlight-color", "#93c5fd");
+ amLyrics.setAttribute("hover-background-color", "rgba(59, 130, 246, 0.14)");
+ amLyrics.setAttribute("autoscroll", "");
+ amLyrics.setAttribute("interpolate", "");
+ amLyrics.style.height = "100%";
+ amLyrics.style.width = "100%";
+
+ container.appendChild(amLyrics);
+
+ // Wait for lyrics to load in the component, then setup observer
+ const waitForLyrics = () => {
+ return new Promise((resolve) => {
+ // Check if lyrics are already loaded
+ const hasLyrics =
+ amLyrics.querySelector(".lyric-line, [class*='lyric']") ||
+ (amLyrics.textContent && amLyrics.textContent.length > 50);
+
+ if (hasLyrics) {
+ resolve();
+ return;
+ }
+
+ // Wait up to 10 seconds for lyrics to load
+ let attempts = 0;
+ const maxAttempts = 20;
+ const interval = setInterval(() => {
+ attempts++;
+ const hasContent =
+ amLyrics.querySelector(".lyric-line, [class*='lyric']") ||
+ (amLyrics.textContent && amLyrics.textContent.length > 50);
+ if (hasContent || attempts >= maxAttempts) {
+ clearInterval(interval);
+ resolve();
+ }
+ }, 500);
+ });
+ };
+
+ await waitForLyrics();
+
+ // Setup observer to convert lyrics to Romaji
+ lyricsManager.setupLyricsObserver(amLyrics);
+
+ // If Romaji mode is already enabled, convert after shadow DOM is ready
+ if (lyricsManager.isRomajiMode) {
+ // Ensure Kuroshiro is loaded before converting
+ if (!lyricsManager.kuroshiroLoaded) {
+ await lyricsManager.loadKuroshiro();
+ }
+ // Add small delay to ensure shadow DOM is fully populated
+ await new Promise((resolve) => setTimeout(resolve, 200));
+ await lyricsManager.convertLyricsContent(amLyrics);
}
+
+ const cleanup = setupSync(track, audioPlayer, amLyrics);
+
+ // Attach cleanup to container for easy access
+ container.lyricsCleanup = cleanup;
+ container.lyricsManager = lyricsManager;
+
+ return amLyrics;
+ } catch (error) {
+ console.error("Failed to load lyrics:", error);
+ container.innerHTML =
+ '
Failed to load lyrics
';
+ return null;
+ }
}
function setupSync(track, audioPlayer, amLyrics) {
- let baseTimeMs = 0;
- let lastTimestamp = performance.now();
- let animationFrameId = null;
-
- const updateTime = () => {
- const currentMs = audioPlayer.currentTime * 1000;
- baseTimeMs = currentMs;
- lastTimestamp = performance.now();
- amLyrics.currentTime = currentMs;
- };
+ let baseTimeMs = 0;
+ let lastTimestamp = performance.now();
+ let animationFrameId = null;
- const tick = () => {
- if (!audioPlayer.paused) {
- const now = performance.now();
- const elapsed = now - lastTimestamp;
- const nextMs = baseTimeMs + elapsed;
- amLyrics.currentTime = nextMs;
- animationFrameId = requestAnimationFrame(tick);
- }
- };
+ const updateTime = () => {
+ const currentMs = audioPlayer.currentTime * 1000;
+ baseTimeMs = currentMs;
+ lastTimestamp = performance.now();
+ amLyrics.currentTime = currentMs;
+ };
- const onPlay = () => {
- baseTimeMs = audioPlayer.currentTime * 1000;
- lastTimestamp = performance.now();
- tick();
- };
-
- const onPause = () => {
- if (animationFrameId) {
- cancelAnimationFrame(animationFrameId);
- animationFrameId = null;
- }
- };
-
- const onLineClick = (e) => {
- if (e.detail && e.detail.timestamp) {
- audioPlayer.currentTime = e.detail.timestamp / 1000;
- audioPlayer.play();
- }
- };
-
- audioPlayer.addEventListener('timeupdate', updateTime);
- audioPlayer.addEventListener('play', onPlay);
- audioPlayer.addEventListener('pause', onPause);
- audioPlayer.addEventListener('seeked', updateTime);
- amLyrics.addEventListener('line-click', onLineClick);
-
+ const tick = () => {
if (!audioPlayer.paused) {
- tick();
+ const now = performance.now();
+ const elapsed = now - lastTimestamp;
+ const nextMs = baseTimeMs + elapsed;
+ amLyrics.currentTime = nextMs;
+ animationFrameId = requestAnimationFrame(tick);
}
-
- return () => {
- if (animationFrameId) {
- cancelAnimationFrame(animationFrameId);
- }
- audioPlayer.removeEventListener('timeupdate', updateTime);
- audioPlayer.removeEventListener('play', onPlay);
- audioPlayer.removeEventListener('pause', onPause);
- audioPlayer.removeEventListener('seeked', updateTime);
- amLyrics.removeEventListener('line-click', onLineClick);
- };
+ };
+
+ const onPlay = () => {
+ baseTimeMs = audioPlayer.currentTime * 1000;
+ lastTimestamp = performance.now();
+ tick();
+ };
+
+ const onPause = () => {
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ animationFrameId = null;
+ }
+ };
+
+ const onLineClick = (e) => {
+ if (e.detail && e.detail.timestamp) {
+ audioPlayer.currentTime = e.detail.timestamp / 1000;
+ audioPlayer.play();
+ }
+ };
+
+ audioPlayer.addEventListener("timeupdate", updateTime);
+ audioPlayer.addEventListener("play", onPlay);
+ audioPlayer.addEventListener("pause", onPause);
+ audioPlayer.addEventListener("seeked", updateTime);
+ amLyrics.addEventListener("line-click", onLineClick);
+
+ if (!audioPlayer.paused) {
+ tick();
+ }
+
+ return () => {
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ }
+ audioPlayer.removeEventListener("timeupdate", updateTime);
+ audioPlayer.removeEventListener("play", onPlay);
+ audioPlayer.removeEventListener("pause", onPause);
+ audioPlayer.removeEventListener("seeked", updateTime);
+ amLyrics.removeEventListener("line-click", onLineClick);
+ };
}
-export async function renderLyricsInFullscreen(track, audioPlayer, lyricsManager, container) {
- return renderLyricsComponent(container, track, audioPlayer, lyricsManager);
+export async function renderLyricsInFullscreen(
+ track,
+ audioPlayer,
+ lyricsManager,
+ container,
+) {
+ return renderLyricsComponent(container, track, audioPlayer, lyricsManager);
}
export function clearFullscreenLyricsSync(container) {
- if (container && container.lyricsCleanup) {
- container.lyricsCleanup();
- container.lyricsCleanup = null;
- }
+ if (container && container.lyricsCleanup) {
+ container.lyricsCleanup();
+ container.lyricsCleanup = null;
+ }
+ if (container && container.lyricsManager) {
+ container.lyricsManager.stopLyricsObserver();
+ }
}
export function clearLyricsPanelSync(audioPlayer, panel) {
- if (panel && panel.lyricsCleanup) {
- panel.lyricsCleanup();
- panel.lyricsCleanup = null;
- }
+ if (panel && panel.lyricsCleanup) {
+ panel.lyricsCleanup();
+ panel.lyricsCleanup = null;
+ }
+ if (panel && panel.lyricsManager) {
+ panel.lyricsManager.stopLyricsObserver();
+ }
}
diff --git a/js/settings.js b/js/settings.js
index 0079f6a..1974462 100644
--- a/js/settings.js
+++ b/js/settings.js
@@ -1,442 +1,514 @@
//js/settings
-import { themeManager, lastFMStorage, nowPlayingSettings, lyricsSettings, backgroundSettings, trackListSettings, cardSettings } from './storage.js';
-import { db } from './db.js';
-import { authManager } from './firebase/auth.js';
-import { syncManager } from './firebase/sync.js';
-import { initializeFirebaseSettingsUI } from './firebase/config.js';
+import {
+ themeManager,
+ lastFMStorage,
+ nowPlayingSettings,
+ lyricsSettings,
+ backgroundSettings,
+ trackListSettings,
+ cardSettings,
+} from "./storage.js";
+import { db } from "./db.js";
+import { authManager } from "./firebase/auth.js";
+import { syncManager } from "./firebase/sync.js";
+import { initializeFirebaseSettingsUI } from "./firebase/config.js";
export function initializeSettings(scrobbler, player, api, ui) {
- // Initialize Firebase UI & Settings
- authManager.updateUI(authManager.user);
- initializeFirebaseSettingsUI();
+ // Initialize Firebase UI & Settings
+ authManager.updateUI(authManager.user);
+ initializeFirebaseSettingsUI();
- // Email Auth UI Logic
- const toggleEmailBtn = document.getElementById('toggle-email-auth-btn');
- const cancelEmailBtn = document.getElementById('cancel-email-auth-btn');
- const authContainer = document.getElementById('email-auth-container');
- const authButtonsContainer = document.getElementById('auth-buttons-container');
- const emailInput = document.getElementById('auth-email');
- const passwordInput = document.getElementById('auth-password');
- const signInBtn = document.getElementById('email-signin-btn');
- const signUpBtn = document.getElementById('email-signup-btn');
+ // Email Auth UI Logic
+ const toggleEmailBtn = document.getElementById("toggle-email-auth-btn");
+ const cancelEmailBtn = document.getElementById("cancel-email-auth-btn");
+ const authContainer = document.getElementById("email-auth-container");
+ const authButtonsContainer = document.getElementById(
+ "auth-buttons-container",
+ );
+ const emailInput = document.getElementById("auth-email");
+ const passwordInput = document.getElementById("auth-password");
+ const signInBtn = document.getElementById("email-signin-btn");
+ const signUpBtn = document.getElementById("email-signup-btn");
- if (toggleEmailBtn && authContainer && authButtonsContainer) {
- toggleEmailBtn.addEventListener('click', () => {
- authContainer.style.display = 'flex';
- authButtonsContainer.style.display = 'none';
- });
+ if (toggleEmailBtn && authContainer && authButtonsContainer) {
+ toggleEmailBtn.addEventListener("click", () => {
+ authContainer.style.display = "flex";
+ authButtonsContainer.style.display = "none";
+ });
+ }
+
+ if (cancelEmailBtn && authContainer && authButtonsContainer) {
+ cancelEmailBtn.addEventListener("click", () => {
+ authContainer.style.display = "none";
+ authButtonsContainer.style.display = "flex";
+ });
+ }
+
+ if (signInBtn) {
+ signInBtn.addEventListener("click", async () => {
+ const email = emailInput.value;
+ const password = passwordInput.value;
+ if (!email || !password) {
+ alert("Please enter both email and password.");
+ return;
+ }
+ try {
+ await authManager.signInWithEmail(email, password);
+ authContainer.style.display = "none";
+ authButtonsContainer.style.display = "flex";
+ emailInput.value = "";
+ passwordInput.value = "";
+ } catch (e) {
+ // Error handled in authManager
+ }
+ });
+ }
+
+ if (signUpBtn) {
+ signUpBtn.addEventListener("click", async () => {
+ const email = emailInput.value;
+ const password = passwordInput.value;
+ if (!email || !password) {
+ alert("Please enter both email and password.");
+ return;
+ }
+ try {
+ await authManager.signUpWithEmail(email, password);
+ authContainer.style.display = "none";
+ authButtonsContainer.style.display = "flex";
+ emailInput.value = "";
+ passwordInput.value = "";
+ } catch (e) {
+ // Error handled in authManager
+ }
+ });
+ }
+
+ const lastfmConnectBtn = document.getElementById("lastfm-connect-btn");
+ const lastfmStatus = document.getElementById("lastfm-status");
+ const lastfmToggle = document.getElementById("lastfm-toggle");
+ const lastfmToggleSetting = document.getElementById("lastfm-toggle-setting");
+ const lastfmLoveToggle = document.getElementById("lastfm-love-toggle");
+ const lastfmLoveSetting = document.getElementById("lastfm-love-setting");
+
+ function updateLastFMUI() {
+ if (scrobbler.isAuthenticated()) {
+ lastfmStatus.textContent = `Connected as ${scrobbler.username}`;
+ lastfmConnectBtn.textContent = "Disconnect";
+ lastfmConnectBtn.classList.add("danger");
+ lastfmToggleSetting.style.display = "flex";
+ lastfmLoveSetting.style.display = "flex";
+ lastfmToggle.checked = lastFMStorage.isEnabled();
+ lastfmLoveToggle.checked = lastFMStorage.shouldLoveOnLike();
+ } else {
+ lastfmStatus.textContent =
+ "Connect your Last.fm account to scrobble tracks";
+ lastfmConnectBtn.textContent = "Connect Last.fm";
+ lastfmConnectBtn.classList.remove("danger");
+ lastfmToggleSetting.style.display = "none";
+ lastfmLoveSetting.style.display = "none";
+ }
+ }
+
+ updateLastFMUI();
+
+ lastfmConnectBtn?.addEventListener("click", async () => {
+ if (scrobbler.isAuthenticated()) {
+ if (confirm("Disconnect from Last.fm?")) {
+ scrobbler.disconnect();
+ updateLastFMUI();
+ }
+ return;
}
- if (cancelEmailBtn && authContainer && authButtonsContainer) {
- cancelEmailBtn.addEventListener('click', () => {
- authContainer.style.display = 'none';
- authButtonsContainer.style.display = 'flex';
- });
- }
+ const authWindow = window.open("", "_blank");
+ lastfmConnectBtn.disabled = true;
+ lastfmConnectBtn.textContent = "Opening Last.fm...";
- if (signInBtn) {
- signInBtn.addEventListener('click', async () => {
- const email = emailInput.value;
- const password = passwordInput.value;
- if (!email || !password) {
- alert('Please enter both email and password.');
- return;
- }
- try {
- await authManager.signInWithEmail(email, password);
- authContainer.style.display = 'none';
- authButtonsContainer.style.display = 'flex';
- emailInput.value = '';
- passwordInput.value = '';
- } catch (e) {
- // Error handled in authManager
- }
- });
- }
+ try {
+ const { token, url } = await scrobbler.getAuthUrl();
- if (signUpBtn) {
- signUpBtn.addEventListener('click', async () => {
- const email = emailInput.value;
- const password = passwordInput.value;
- if (!email || !password) {
- alert('Please enter both email and password.');
- return;
- }
- try {
- await authManager.signUpWithEmail(email, password);
- authContainer.style.display = 'none';
- authButtonsContainer.style.display = 'flex';
- emailInput.value = '';
- passwordInput.value = '';
- } catch (e) {
- // Error handled in authManager
- }
- });
- }
+ if (authWindow) {
+ authWindow.location.href = url;
+ } else {
+ alert("Popup blocked! Please allow popups.");
+ lastfmConnectBtn.textContent = "Connect Last.fm";
+ lastfmConnectBtn.disabled = false;
+ return;
+ }
- const lastfmConnectBtn = document.getElementById('lastfm-connect-btn');
- const lastfmStatus = document.getElementById('lastfm-status');
- const lastfmToggle = document.getElementById('lastfm-toggle');
- const lastfmToggleSetting = document.getElementById('lastfm-toggle-setting');
- const lastfmLoveToggle = document.getElementById('lastfm-love-toggle');
- const lastfmLoveSetting = document.getElementById('lastfm-love-setting');
+ lastfmConnectBtn.textContent = "Waiting for authorization...";
- function updateLastFMUI() {
- if (scrobbler.isAuthenticated()) {
- lastfmStatus.textContent = `Connected as ${scrobbler.username}`;
- lastfmConnectBtn.textContent = 'Disconnect';
- lastfmConnectBtn.classList.add('danger');
- lastfmToggleSetting.style.display = 'flex';
- lastfmLoveSetting.style.display = 'flex';
- lastfmToggle.checked = lastFMStorage.isEnabled();
- lastfmLoveToggle.checked = lastFMStorage.shouldLoveOnLike();
- } else {
- lastfmStatus.textContent = 'Connect your Last.fm account to scrobble tracks';
- lastfmConnectBtn.textContent = 'Connect Last.fm';
- lastfmConnectBtn.classList.remove('danger');
- lastfmToggleSetting.style.display = 'none';
- lastfmLoveSetting.style.display = 'none';
+ let attempts = 0;
+ const maxAttempts = 30;
+
+ const checkAuth = setInterval(async () => {
+ attempts++;
+
+ if (attempts > maxAttempts) {
+ clearInterval(checkAuth);
+ lastfmConnectBtn.textContent = "Connect Last.fm";
+ lastfmConnectBtn.disabled = false;
+ if (authWindow && !authWindow.closed) authWindow.close();
+ alert("Authorization timed out. Please try again.");
+ return;
}
- }
-
- updateLastFMUI();
-
- lastfmConnectBtn?.addEventListener('click', async () => {
- if (scrobbler.isAuthenticated()) {
- if (confirm('Disconnect from Last.fm?')) {
- scrobbler.disconnect();
- updateLastFMUI();
- }
- return;
- }
-
- const authWindow = window.open('', '_blank');
- lastfmConnectBtn.disabled = true;
- lastfmConnectBtn.textContent = 'Opening Last.fm...';
try {
- const { token, url } = await scrobbler.getAuthUrl();
+ const result = await scrobbler.completeAuthentication(token);
- if (authWindow) {
- authWindow.location.href = url;
- } else {
- alert('Popup blocked! Please allow popups.');
- lastfmConnectBtn.textContent = 'Connect Last.fm';
- lastfmConnectBtn.disabled = false;
- return;
- }
-
- lastfmConnectBtn.textContent = 'Waiting for authorization...';
-
- let attempts = 0;
- const maxAttempts = 30;
-
- const checkAuth = setInterval(async () => {
- attempts++;
-
- if (attempts > maxAttempts) {
- clearInterval(checkAuth);
- lastfmConnectBtn.textContent = 'Connect Last.fm';
- lastfmConnectBtn.disabled = false;
- if (authWindow && !authWindow.closed) authWindow.close();
- alert('Authorization timed out. Please try again.');
- return;
- }
-
- try {
- const result = await scrobbler.completeAuthentication(token);
-
- if (result.success) {
- clearInterval(checkAuth);
- if (authWindow && !authWindow.closed) authWindow.close();
- updateLastFMUI();
- lastfmConnectBtn.disabled = false;
- lastFMStorage.setEnabled(true);
- lastfmToggle.checked = true;
- alert(`Successfully connected to Last.fm as ${result.username}!`);
- }
- } catch (e) {
- // Still waiting
- }
- }, 2000);
-
- } catch (error) {
- console.error('Last.fm connection failed:', error);
- alert('Failed to connect to Last.fm: ' + error.message);
- lastfmConnectBtn.textContent = 'Connect Last.fm';
- lastfmConnectBtn.disabled = false;
+ if (result.success) {
+ clearInterval(checkAuth);
if (authWindow && !authWindow.closed) authWindow.close();
+ updateLastFMUI();
+ lastfmConnectBtn.disabled = false;
+ lastFMStorage.setEnabled(true);
+ lastfmToggle.checked = true;
+ alert(`Successfully connected to Last.fm as ${result.username}!`);
+ }
+ } catch (e) {
+ // Still waiting
}
+ }, 2000);
+ } catch (error) {
+ console.error("Last.fm connection failed:", error);
+ alert("Failed to connect to Last.fm: " + error.message);
+ lastfmConnectBtn.textContent = "Connect Last.fm";
+ lastfmConnectBtn.disabled = false;
+ if (authWindow && !authWindow.closed) authWindow.close();
+ }
+ });
+
+ lastfmToggle?.addEventListener("change", (e) => {
+ lastFMStorage.setEnabled(e.target.checked);
+ });
+
+ lastfmLoveToggle?.addEventListener("change", (e) => {
+ lastFMStorage.setLoveOnLike(e.target.checked);
+ });
+
+ // Theme picker
+ const themePicker = document.getElementById("theme-picker");
+ const currentTheme = themeManager.getTheme();
+
+ themePicker.querySelectorAll(".theme-option").forEach((option) => {
+ if (option.dataset.theme === currentTheme) {
+ option.classList.add("active");
+ }
+
+ option.addEventListener("click", () => {
+ const theme = option.dataset.theme;
+
+ themePicker
+ .querySelectorAll(".theme-option")
+ .forEach((opt) => opt.classList.remove("active"));
+ option.classList.add("active");
+
+ if (theme === "custom") {
+ document.getElementById("custom-theme-editor").classList.add("show");
+ renderCustomThemeEditor();
+ } else {
+ document.getElementById("custom-theme-editor").classList.remove("show");
+ themeManager.setTheme(theme);
+ }
});
+ });
- lastfmToggle?.addEventListener('change', (e) => {
- lastFMStorage.setEnabled(e.target.checked);
- });
+ function renderCustomThemeEditor() {
+ const grid = document.getElementById("theme-color-grid");
+ const customTheme = themeManager.getCustomTheme() || {
+ background: "#000000",
+ foreground: "#fafafa",
+ primary: "#ffffff",
+ secondary: "#27272a",
+ muted: "#27272a",
+ border: "#27272a",
+ highlight: "#ffffff",
+ };
- lastfmLoveToggle?.addEventListener('change', (e) => {
- lastFMStorage.setLoveOnLike(e.target.checked);
- });
-
- // Theme picker
- const themePicker = document.getElementById('theme-picker');
- const currentTheme = themeManager.getTheme();
-
- themePicker.querySelectorAll('.theme-option').forEach(option => {
- if (option.dataset.theme === currentTheme) {
- option.classList.add('active');
- }
-
- option.addEventListener('click', () => {
- const theme = option.dataset.theme;
-
- themePicker.querySelectorAll('.theme-option').forEach(opt => opt.classList.remove('active'));
- option.classList.add('active');
-
- if (theme === 'custom') {
- document.getElementById('custom-theme-editor').classList.add('show');
- renderCustomThemeEditor();
- } else {
- document.getElementById('custom-theme-editor').classList.remove('show');
- themeManager.setTheme(theme);
- }
- });
- });
-
- function renderCustomThemeEditor() {
- const grid = document.getElementById('theme-color-grid');
- const customTheme = themeManager.getCustomTheme() || {
- background: '#000000',
- foreground: '#fafafa',
- primary: '#ffffff',
- secondary: '#27272a',
- muted: '#27272a',
- border: '#27272a',
- highlight: '#ffffff'
- };
-
- grid.innerHTML = Object.entries(customTheme).map(([key, value]) => `
+ grid.innerHTML = Object.entries(customTheme)
+ .map(
+ ([key, value]) => `
${key}
- `).join('');
- }
+ `,
+ )
+ .join("");
+ }
- document.getElementById('apply-custom-theme')?.addEventListener('click', () => {
- const colors = {};
- document.querySelectorAll('#theme-color-grid input[type="color"]').forEach(input => {
- colors[input.dataset.color] = input.value;
+ document
+ .getElementById("apply-custom-theme")
+ ?.addEventListener("click", () => {
+ const colors = {};
+ document
+ .querySelectorAll('#theme-color-grid input[type="color"]')
+ .forEach((input) => {
+ colors[input.dataset.color] = input.value;
});
- themeManager.setCustomTheme(colors);
+ themeManager.setCustomTheme(colors);
});
- document.getElementById('reset-custom-theme')?.addEventListener('click', () => {
- renderCustomThemeEditor();
+ document
+ .getElementById("reset-custom-theme")
+ ?.addEventListener("click", () => {
+ renderCustomThemeEditor();
});
- // Quality setting
- const qualitySetting = document.getElementById('quality-setting');
- if (qualitySetting) {
- const savedQuality = localStorage.getItem('playback-quality') || 'LOSSLESS';
- qualitySetting.value = savedQuality;
- player.setQuality(savedQuality);
+ // Quality setting
+ const qualitySetting = document.getElementById("quality-setting");
+ if (qualitySetting) {
+ const savedQuality = localStorage.getItem("playback-quality") || "LOSSLESS";
+ qualitySetting.value = savedQuality;
+ player.setQuality(savedQuality);
- qualitySetting.addEventListener('change', (e) => {
- const newQuality = e.target.value;
- player.setQuality(newQuality);
- localStorage.setItem('playback-quality', newQuality);
- });
- }
-
- // Now Playing Mode
- const nowPlayingMode = document.getElementById('now-playing-mode');
- if (nowPlayingMode) {
- nowPlayingMode.value = nowPlayingSettings.getMode();
- nowPlayingMode.addEventListener('change', (e) => {
- nowPlayingSettings.setMode(e.target.value);
- });
- }
-
- // Track List Actions Mode
- const trackListActionsMode = document.getElementById('track-list-actions-mode');
- if (trackListActionsMode) {
- trackListActionsMode.value = trackListSettings.getMode();
- trackListActionsMode.addEventListener('change', (e) => {
- trackListSettings.setMode(e.target.value);
- });
- }
-
- // Compact Artist Toggle
- const compactArtistToggle = document.getElementById('compact-artist-toggle');
- if (compactArtistToggle) {
- compactArtistToggle.checked = cardSettings.isCompactArtist();
- compactArtistToggle.addEventListener('change', (e) => {
- cardSettings.setCompactArtist(e.target.checked);
- });
- }
-
- // Compact Album Toggle
- const compactAlbumToggle = document.getElementById('compact-album-toggle');
- if (compactAlbumToggle) {
- compactAlbumToggle.checked = cardSettings.isCompactAlbum();
- compactAlbumToggle.addEventListener('change', (e) => {
- cardSettings.setCompactAlbum(e.target.checked);
- });
- }
-
- // Download Lyrics Toggle
- const downloadLyricsToggle = document.getElementById('download-lyrics-toggle');
- if (downloadLyricsToggle) {
- downloadLyricsToggle.checked = lyricsSettings.shouldDownloadLyrics();
- downloadLyricsToggle.addEventListener('change', (e) => {
- lyricsSettings.setDownloadLyrics(e.target.checked);
- });
- }
-
- // Album Background Toggle
- const albumBackgroundToggle = document.getElementById('album-background-toggle');
- if (albumBackgroundToggle) {
- albumBackgroundToggle.checked = backgroundSettings.isEnabled();
- albumBackgroundToggle.addEventListener('change', (e) => {
- backgroundSettings.setEnabled(e.target.checked);
- });
- }
-
- // Filename template setting
- const filenameTemplate = document.getElementById('filename-template');
- if (filenameTemplate) {
- filenameTemplate.value = localStorage.getItem('filename-template') || '{trackNumber} - {artist} - {title}';
- filenameTemplate.addEventListener('change', (e) => {
- localStorage.setItem('filename-template', e.target.value);
- });
- }
-
- // ZIP folder template
- const zipFolderTemplate = document.getElementById('zip-folder-template');
- if (zipFolderTemplate) {
- zipFolderTemplate.value = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist}';
- zipFolderTemplate.addEventListener('change', (e) => {
- localStorage.setItem('zip-folder-template', e.target.value);
- });
- }
-
- // API settings
- document.getElementById('refresh-speed-test-btn')?.addEventListener('click', async () => {
- const btn = document.getElementById('refresh-speed-test-btn');
- const originalText = btn.textContent;
- btn.textContent = 'Testing...';
- btn.disabled = true;
-
- try {
- await api.settings.refreshSpeedTests();
- ui.renderApiSettings();
- btn.textContent = 'Done!';
- setTimeout(() => {
- btn.textContent = originalText;
- btn.disabled = false;
- }, 1500);
- } catch (error) {
- console.error('Failed to refresh speed tests:', error);
- btn.textContent = 'Error';
- setTimeout(() => {
- btn.textContent = originalText;
- btn.disabled = false;
- }, 1500);
- }
+ qualitySetting.addEventListener("change", (e) => {
+ const newQuality = e.target.value;
+ player.setQuality(newQuality);
+ localStorage.setItem("playback-quality", newQuality);
});
+ }
- document.getElementById('api-instance-list')?.addEventListener('click', async (e) => {
- const button = e.target.closest('button');
- if (!button) return;
+ // Now Playing Mode
+ const nowPlayingMode = document.getElementById("now-playing-mode");
+ if (nowPlayingMode) {
+ nowPlayingMode.value = nowPlayingSettings.getMode();
+ nowPlayingMode.addEventListener("change", (e) => {
+ nowPlayingSettings.setMode(e.target.value);
+ });
+ }
- const li = button.closest('li');
- const index = parseInt(li.dataset.index, 10);
- const type = li.dataset.type || 'api'; // Default to api if not present
-
- const instances = await api.settings.getInstances(type);
+ // Track List Actions Mode
+ const trackListActionsMode = document.getElementById(
+ "track-list-actions-mode",
+ );
+ if (trackListActionsMode) {
+ trackListActionsMode.value = trackListSettings.getMode();
+ trackListActionsMode.addEventListener("change", (e) => {
+ trackListSettings.setMode(e.target.value);
+ });
+ }
- if (button.classList.contains('move-up') && index > 0) {
- [instances[index], instances[index - 1]] = [instances[index - 1], instances[index]];
- } else if (button.classList.contains('move-down') && index < instances.length - 1) {
- [instances[index], instances[index + 1]] = [instances[index + 1], instances[index]];
- }
+ // Compact Artist Toggle
+ const compactArtistToggle = document.getElementById("compact-artist-toggle");
+ if (compactArtistToggle) {
+ compactArtistToggle.checked = cardSettings.isCompactArtist();
+ compactArtistToggle.addEventListener("change", (e) => {
+ cardSettings.setCompactArtist(e.target.checked);
+ });
+ }
- api.settings.saveInstances(instances, type);
+ // Compact Album Toggle
+ const compactAlbumToggle = document.getElementById("compact-album-toggle");
+ if (compactAlbumToggle) {
+ compactAlbumToggle.checked = cardSettings.isCompactAlbum();
+ compactAlbumToggle.addEventListener("change", (e) => {
+ cardSettings.setCompactAlbum(e.target.checked);
+ });
+ }
+
+ // Download Lyrics Toggle
+ const downloadLyricsToggle = document.getElementById(
+ "download-lyrics-toggle",
+ );
+ if (downloadLyricsToggle) {
+ downloadLyricsToggle.checked = lyricsSettings.shouldDownloadLyrics();
+ downloadLyricsToggle.addEventListener("change", (e) => {
+ lyricsSettings.setDownloadLyrics(e.target.checked);
+ });
+ }
+
+ // Romaji Lyrics Toggle
+ const romajiLyricsToggle = document.getElementById("romaji-lyrics-toggle");
+ if (romajiLyricsToggle) {
+ romajiLyricsToggle.checked =
+ localStorage.getItem("lyricsRomajiMode") === "true";
+ romajiLyricsToggle.addEventListener("change", (e) => {
+ localStorage.setItem(
+ "lyricsRomajiMode",
+ e.target.checked ? "true" : "false",
+ );
+ });
+ }
+
+ // Album Background Toggle
+ const albumBackgroundToggle = document.getElementById(
+ "album-background-toggle",
+ );
+ if (albumBackgroundToggle) {
+ albumBackgroundToggle.checked = backgroundSettings.isEnabled();
+ albumBackgroundToggle.addEventListener("change", (e) => {
+ backgroundSettings.setEnabled(e.target.checked);
+ });
+ }
+
+ // Filename template setting
+ const filenameTemplate = document.getElementById("filename-template");
+ if (filenameTemplate) {
+ filenameTemplate.value =
+ localStorage.getItem("filename-template") ||
+ "{trackNumber} - {artist} - {title}";
+ filenameTemplate.addEventListener("change", (e) => {
+ localStorage.setItem("filename-template", e.target.value);
+ });
+ }
+
+ // ZIP folder template
+ const zipFolderTemplate = document.getElementById("zip-folder-template");
+ if (zipFolderTemplate) {
+ zipFolderTemplate.value =
+ localStorage.getItem("zip-folder-template") ||
+ "{albumTitle} - {albumArtist}";
+ zipFolderTemplate.addEventListener("change", (e) => {
+ localStorage.setItem("zip-folder-template", e.target.value);
+ });
+ }
+
+ // API settings
+ document
+ .getElementById("refresh-speed-test-btn")
+ ?.addEventListener("click", async () => {
+ const btn = document.getElementById("refresh-speed-test-btn");
+ const originalText = btn.textContent;
+ btn.textContent = "Testing...";
+ btn.disabled = true;
+
+ try {
+ await api.settings.refreshSpeedTests();
ui.renderApiSettings();
+ btn.textContent = "Done!";
+ setTimeout(() => {
+ btn.textContent = originalText;
+ btn.disabled = false;
+ }, 1500);
+ } catch (error) {
+ console.error("Failed to refresh speed tests:", error);
+ btn.textContent = "Error";
+ setTimeout(() => {
+ btn.textContent = originalText;
+ btn.disabled = false;
+ }, 1500);
+ }
});
- document.getElementById('clear-cache-btn')?.addEventListener('click', async () => {
- const btn = document.getElementById('clear-cache-btn');
- const originalText = btn.textContent;
- btn.textContent = 'Clearing...';
- btn.disabled = true;
+ document
+ .getElementById("api-instance-list")
+ ?.addEventListener("click", async (e) => {
+ const button = e.target.closest("button");
+ if (!button) return;
+ const li = button.closest("li");
+ const index = parseInt(li.dataset.index, 10);
+ const type = li.dataset.type || "api"; // Default to api if not present
+
+ const instances = await api.settings.getInstances(type);
+
+ if (button.classList.contains("move-up") && index > 0) {
+ [instances[index], instances[index - 1]] = [
+ instances[index - 1],
+ instances[index],
+ ];
+ } else if (
+ button.classList.contains("move-down") &&
+ index < instances.length - 1
+ ) {
+ [instances[index], instances[index + 1]] = [
+ instances[index + 1],
+ instances[index],
+ ];
+ }
+
+ api.settings.saveInstances(instances, type);
+ ui.renderApiSettings();
+ });
+
+ document
+ .getElementById("clear-cache-btn")
+ ?.addEventListener("click", async () => {
+ const btn = document.getElementById("clear-cache-btn");
+ const originalText = btn.textContent;
+ btn.textContent = "Clearing...";
+ btn.disabled = true;
+
+ try {
+ await api.clearCache();
+ btn.textContent = "Cleared!";
+ setTimeout(() => {
+ btn.textContent = originalText;
+ btn.disabled = false;
+ if (window.location.hash.includes("settings")) {
+ ui.renderApiSettings();
+ }
+ }, 1500);
+ } catch (error) {
+ console.error("Failed to clear cache:", error);
+ btn.textContent = "Error";
+ setTimeout(() => {
+ btn.textContent = originalText;
+ btn.disabled = false;
+ }, 1500);
+ }
+ });
+
+ document
+ .getElementById("firebase-clear-cloud-btn")
+ ?.addEventListener("click", async () => {
+ if (
+ confirm(
+ "Are you sure you want to delete ALL your data from the cloud? This cannot be undone.",
+ )
+ ) {
try {
- await api.clearCache();
- btn.textContent = 'Cleared!';
- setTimeout(() => {
- btn.textContent = originalText;
- btn.disabled = false;
- if (window.location.hash.includes('settings')) {
- ui.renderApiSettings();
- }
- }, 1500);
+ await syncManager.clearCloudData();
+ alert("Cloud data cleared successfully.");
+ authManager.signOut();
} catch (error) {
- console.error('Failed to clear cache:', error);
- btn.textContent = 'Error';
- setTimeout(() => {
- btn.textContent = originalText;
- btn.disabled = false;
- }, 1500);
+ console.error("Failed to clear cloud data:", error);
+ alert("Failed to clear cloud data: " + error.message);
}
+ }
});
- document.getElementById('firebase-clear-cloud-btn')?.addEventListener('click', async () => {
- if (confirm('Are you sure you want to delete ALL your data from the cloud? This cannot be undone.')) {
- try {
- await syncManager.clearCloudData();
- alert('Cloud data cleared successfully.');
- authManager.signOut();
- } catch (error) {
- console.error('Failed to clear cloud data:', error);
- alert('Failed to clear cloud data: ' + error.message);
- }
- }
+ // Backup & Restore
+ document
+ .getElementById("export-library-btn")
+ ?.addEventListener("click", async () => {
+ const data = await db.exportData();
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `monochrome-library-${new Date().toISOString().split("T")[0]}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
});
- // Backup & Restore
- document.getElementById('export-library-btn')?.addEventListener('click', async () => {
- const data = await db.exportData();
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `monochrome-library-${new Date().toISOString().split('T')[0]}.json`;
- a.click();
- URL.revokeObjectURL(url);
+ const importInput = document.getElementById("import-library-input");
+ document
+ .getElementById("import-library-btn")
+ ?.addEventListener("click", () => {
+ importInput.click();
});
- const importInput = document.getElementById('import-library-input');
- document.getElementById('import-library-btn')?.addEventListener('click', () => {
- importInput.click();
- });
+ importInput?.addEventListener("change", async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
- importInput?.addEventListener('change', async (e) => {
- const file = e.target.files[0];
- if (!file) return;
-
- const reader = new FileReader();
- reader.onload = async (event) => {
- try {
- const data = JSON.parse(event.target.result);
- await db.importData(data);
- alert('Library imported successfully!');
- window.location.reload(); // Simple way to refresh all state
- } catch (err) {
- console.error('Import failed:', err);
- alert('Failed to import library. Please check the file format.');
- }
- };
- reader.readAsText(file);
- });
+ const reader = new FileReader();
+ reader.onload = async (event) => {
+ try {
+ const data = JSON.parse(event.target.result);
+ await db.importData(data);
+ alert("Library imported successfully!");
+ window.location.reload(); // Simple way to refresh all state
+ } catch (err) {
+ console.error("Import failed:", err);
+ alert("Failed to import library. Please check the file format.");
+ }
+ };
+ reader.readAsText(file);
+ });
}
diff --git a/package-lock.json b/package-lock.json
index 2026095..1d4e47b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,8 @@
"license": "ISC",
"devDependencies": {
"vite": "^7.3.0",
- "vite-plugin-pwa": "^1.2.0"
+ "vite-plugin-pwa": "^1.2.0",
+ "wanakana": "^5.3.1"
}
},
"node_modules/@apideck/better-ajv-errors": {
@@ -5749,6 +5750,15 @@
}
}
},
+ "node_modules/wanakana": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/wanakana/-/wanakana-5.3.1.tgz",
+ "integrity": "sha512-OSDqupzTlzl2LGyqTdhcXcl6ezMiFhcUwLBP8YKaBIbMYW1wAwDvupw2T9G9oVaKT9RmaSpyTXjxddFPUcFFIw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
diff --git a/package.json b/package.json
index 992aaed..f01543f 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"homepage": "https://github.com/SamidyFR/monochrome#readme",
"devDependencies": {
"vite": "^7.3.0",
- "vite-plugin-pwa": "^1.2.0"
+ "vite-plugin-pwa": "^1.2.0",
+ "wanakana": "^5.3.1"
}
}
diff --git a/vite.config.js b/vite.config.js
index a084aed..d0b1c95 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,25 +1,25 @@
-import { defineConfig } from 'vite';
-import { VitePWA } from 'vite-plugin-pwa';
+import { defineConfig } from "vite";
+import { VitePWA } from "vite-plugin-pwa";
export default defineConfig({
- base: './',
+ base: "./",
build: {
- outDir: 'dist',
+ outDir: "dist",
emptyOutDir: true,
},
plugins: [
VitePWA({
- registerType: 'prompt',
+ registerType: "prompt",
workbox: {
- globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'],
+ globPatterns: ["**/*.{js,css,html,ico,png,svg,json}"],
cleanupOutdatedCaches: true,
// Define runtime caching strategies
runtimeCaching: [
{
- urlPattern: ({ request }) => request.destination === 'image',
- handler: 'CacheFirst',
+ urlPattern: ({ request }) => request.destination === "image",
+ handler: "CacheFirst",
options: {
- cacheName: 'images',
+ cacheName: "images",
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 24 * 60 * 60, // 60 Days
@@ -27,21 +27,23 @@ export default defineConfig({
},
},
{
- urlPattern: ({ request }) => request.destination === 'audio' || request.destination === 'video',
- handler: 'CacheFirst',
+ urlPattern: ({ request }) =>
+ request.destination === "audio" ||
+ request.destination === "video",
+ handler: "CacheFirst",
options: {
- cacheName: 'media',
+ cacheName: "media",
expiration: {
maxEntries: 50,
maxAgeSeconds: 60 * 24 * 60 * 60, // 60 Days
},
rangeRequests: true, // Support scrubbing
},
- }
- ]
+ },
+ ],
},
- includeAssets: ['instances.json', 'discord.html'],
- manifest: false // Use existing public/manifest.json
- })
- ]
-});
\ No newline at end of file
+ includeAssets: ["instances.json", "discord.html"],
+ manifest: false, // Use existing public/manifest.json
+ }),
+ ],
+});