Feat: adding romaji convert feature on lyric
This commit is contained in:
parent
b3437dc99a
commit
df2b77eb7d
6 changed files with 1209 additions and 668 deletions
10
index.html
10
index.html
|
|
@ -609,6 +609,16 @@
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="info">
|
||||||
|
<span class="label">Romaji Lyrics</span>
|
||||||
|
<span class="description">Convert Japanese lyrics to Romaji (Latin characters)</span>
|
||||||
|
</div>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="romaji-lyrics-toggle">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<span class="label">Filename Template</span>
|
<span class="label">Filename Template</span>
|
||||||
|
|
|
||||||
580
js/lyrics.js
580
js/lyrics.js
|
|
@ -1,6 +1,12 @@
|
||||||
//js/lyrics.js
|
//js/lyrics.js
|
||||||
import { getTrackTitle, getTrackArtists, buildTrackFilename, SVG_DOWNLOAD, SVG_CLOSE } from './utils.js';
|
import {
|
||||||
import { sidePanelManager } from './side-panel.js';
|
getTrackTitle,
|
||||||
|
getTrackArtists,
|
||||||
|
buildTrackFilename,
|
||||||
|
SVG_DOWNLOAD,
|
||||||
|
SVG_CLOSE,
|
||||||
|
} from "./utils.js";
|
||||||
|
import { sidePanelManager } from "./side-panel.js";
|
||||||
|
|
||||||
export class LyricsManager {
|
export class LyricsManager {
|
||||||
constructor(api) {
|
constructor(api) {
|
||||||
|
|
@ -11,24 +17,169 @@ export class LyricsManager {
|
||||||
this.componentLoaded = false;
|
this.componentLoaded = false;
|
||||||
this.amLyricsElement = null;
|
this.amLyricsElement = null;
|
||||||
this.animationFrameId = 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
async ensureComponentLoaded() {
|
||||||
if (this.componentLoaded) return;
|
if (this.componentLoaded) return;
|
||||||
|
|
||||||
if (typeof customElements !== 'undefined' && customElements.get('am-lyrics')) {
|
if (
|
||||||
|
typeof customElements !== "undefined" &&
|
||||||
|
customElements.get("am-lyrics")
|
||||||
|
) {
|
||||||
this.componentLoaded = true;
|
this.componentLoaded = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement("script");
|
||||||
script.type = 'module';
|
script.type = "module";
|
||||||
script.src = 'https://cdn.jsdelivr.net/npm/@uimaxbai/am-lyrics@0.6.2/dist/src/am-lyrics.min.js';
|
script.src =
|
||||||
|
"https://cdn.jsdelivr.net/npm/@uimaxbai/am-lyrics@0.6.2/dist/src/am-lyrics.min.js";
|
||||||
|
|
||||||
script.onload = () => {
|
script.onload = () => {
|
||||||
if (typeof customElements !== 'undefined') {
|
if (typeof customElements !== "undefined") {
|
||||||
customElements.whenDefined('am-lyrics')
|
customElements
|
||||||
|
.whenDefined("am-lyrics")
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.componentLoaded = true;
|
this.componentLoaded = true;
|
||||||
resolve();
|
resolve();
|
||||||
|
|
@ -39,13 +190,13 @@ export class LyricsManager {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
script.onerror = () => reject(new Error('Failed to load lyrics component'));
|
script.onerror = () =>
|
||||||
|
reject(new Error("Failed to load lyrics component"));
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchLyrics(trackId, track = null) {
|
async fetchLyrics(trackId, track = null) {
|
||||||
// LRCLIB
|
|
||||||
if (track) {
|
if (track) {
|
||||||
if (this.lyricsCache.has(trackId)) {
|
if (this.lyricsCache.has(trackId)) {
|
||||||
return this.lyricsCache.get(trackId);
|
return this.lyricsCache.get(trackId);
|
||||||
|
|
@ -53,26 +204,28 @@ export class LyricsManager {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const artist = Array.isArray(track.artists)
|
const artist = Array.isArray(track.artists)
|
||||||
? track.artists.map(a => a.name || a).join(', ')
|
? track.artists.map((a) => a.name || a).join(", ")
|
||||||
: track.artist?.name || '';
|
: track.artist?.name || "";
|
||||||
const title = track.title || '';
|
const title = track.title || "";
|
||||||
const album = track.album?.title || '';
|
const album = track.album?.title || "";
|
||||||
const duration = track.duration ? Math.round(track.duration) : null;
|
const duration = track.duration ? Math.round(track.duration) : null;
|
||||||
|
|
||||||
if (!title || !artist) {
|
if (!title || !artist) {
|
||||||
console.warn('Missing required fields for LRCLIB');
|
console.warn("Missing required fields for LRCLIB");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
track_name: title,
|
track_name: title,
|
||||||
artist_name: artist
|
artist_name: artist,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (album) params.append('album_name', album);
|
if (album) params.append("album_name", album);
|
||||||
if (duration) params.append('duration', duration.toString());
|
if (duration) params.append("duration", duration.toString());
|
||||||
|
|
||||||
const response = await fetch(`https://lrclib.net/api/get?${params.toString()}`);
|
const response = await fetch(
|
||||||
|
`https://lrclib.net/api/get?${params.toString()}`,
|
||||||
|
);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -80,7 +233,7 @@ export class LyricsManager {
|
||||||
if (data.syncedLyrics) {
|
if (data.syncedLyrics) {
|
||||||
const lyricsData = {
|
const lyricsData = {
|
||||||
subtitles: data.syncedLyrics,
|
subtitles: data.syncedLyrics,
|
||||||
lyricsProvider: 'LRCLIB'
|
lyricsProvider: "LRCLIB",
|
||||||
};
|
};
|
||||||
|
|
||||||
this.lyricsCache.set(trackId, lyricsData);
|
this.lyricsCache.set(trackId, lyricsData);
|
||||||
|
|
@ -88,7 +241,7 @@ export class LyricsManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('LRCLIB fetch failed:', error);
|
console.warn("LRCLIB fetch failed:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,16 +250,21 @@ export class LyricsManager {
|
||||||
|
|
||||||
parseSyncedLyrics(subtitles) {
|
parseSyncedLyrics(subtitles) {
|
||||||
if (!subtitles) return [];
|
if (!subtitles) return [];
|
||||||
const lines = subtitles.split('\n').filter(line => line.trim());
|
const lines = subtitles.split("\n").filter((line) => line.trim());
|
||||||
return lines.map(line => {
|
return lines
|
||||||
|
.map((line) => {
|
||||||
const match = line.match(/\[(\d+):(\d+)\.(\d+)\]\s*(.+)/);
|
const match = line.match(/\[(\d+):(\d+)\.(\d+)\]\s*(.+)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const [, minutes, seconds, centiseconds, text] = match;
|
const [, minutes, seconds, centiseconds, text] = match;
|
||||||
const timeInSeconds = parseInt(minutes) * 60 + parseInt(seconds) + parseInt(centiseconds) / 100;
|
const timeInSeconds =
|
||||||
|
parseInt(minutes) * 60 +
|
||||||
|
parseInt(seconds) +
|
||||||
|
parseInt(centiseconds) / 100;
|
||||||
return { time: timeInSeconds, text: text.trim() };
|
return { time: timeInSeconds, text: text.trim() };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}).filter(Boolean);
|
})
|
||||||
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateLRCContent(lyricsData, track) {
|
generateLRCContent(lyricsData, track) {
|
||||||
|
|
@ -117,9 +275,9 @@ export class LyricsManager {
|
||||||
|
|
||||||
let lrc = `[ti:${trackTitle}]\n`;
|
let lrc = `[ti:${trackTitle}]\n`;
|
||||||
lrc += `[ar:${trackArtist}]\n`;
|
lrc += `[ar:${trackArtist}]\n`;
|
||||||
lrc += `[al:${track.album?.title || 'Unknown Album'}]\n`;
|
lrc += `[al:${track.album?.title || "Unknown Album"}]\n`;
|
||||||
lrc += `[by:${lyricsData.lyricsProvider || 'Unknown'}]\n`;
|
lrc += `[by:${lyricsData.lyricsProvider || "Unknown"}]\n`;
|
||||||
lrc += '\n';
|
lrc += "\n";
|
||||||
lrc += lyricsData.subtitles;
|
lrc += lyricsData.subtitles;
|
||||||
|
|
||||||
return lrc;
|
return lrc;
|
||||||
|
|
@ -128,15 +286,18 @@ export class LyricsManager {
|
||||||
downloadLRC(lyricsData, track) {
|
downloadLRC(lyricsData, track) {
|
||||||
const lrcContent = this.generateLRCContent(lyricsData, track);
|
const lrcContent = this.generateLRCContent(lyricsData, track);
|
||||||
if (!lrcContent) {
|
if (!lrcContent) {
|
||||||
alert('No synced lyrics available for this track');
|
alert("No synced lyrics available for this track");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = new Blob([lrcContent], { type: 'application/octet-stream' });
|
const blob = new Blob([lrcContent], { type: "application/octet-stream" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = buildTrackFilename(track, 'LOSSLESS').replace(/\.flac$/, '.lrc');
|
a.download = buildTrackFilename(track, "LOSSLESS").replace(
|
||||||
|
/\.flac$/,
|
||||||
|
".lrc",
|
||||||
|
);
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
|
|
@ -155,74 +316,348 @@ export class LyricsManager {
|
||||||
}
|
}
|
||||||
return currentIndex;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the root to traverse - check for shadow DOM first
|
||||||
|
const rootToTraverse = amLyricsElement.shadowRoot || amLyricsElement;
|
||||||
|
|
||||||
|
// Make sure Kuroshiro is ready
|
||||||
|
if (!this.kuroshiroLoaded) {
|
||||||
|
const success = await this.loadKuroshiro();
|
||||||
|
if (!success) {
|
||||||
|
console.warn("Cannot convert lyrics - Kuroshiro load failed");
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.isRomajiMode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openLyricsPanel(track, audioPlayer, lyricsManager) {
|
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();
|
||||||
|
|
||||||
|
// 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 renderControls = (container) => {
|
||||||
|
const isRomajiMode = manager.getRomajiMode();
|
||||||
|
manager.isRomajiMode = isRomajiMode;
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<button id="close-side-panel-btn" class="btn-icon" title="Close">
|
<button id="close-side-panel-btn" class="btn-icon" title="Close">
|
||||||
${SVG_CLOSE}
|
${SVG_CLOSE}
|
||||||
</button>
|
</button>
|
||||||
|
<button id="romaji-toggle-btn" class="btn-icon" title="Toggle Romaji (Japanese to Latin)" data-enabled="${isRomajiMode}" style="color: ${isRomajiMode ? "var(--primary)" : ""}">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
container.querySelector('#close-side-panel-btn').addEventListener('click', () => {
|
container
|
||||||
|
.querySelector("#close-side-panel-btn")
|
||||||
|
.addEventListener("click", () => {
|
||||||
sidePanelManager.close();
|
sidePanelManager.close();
|
||||||
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
|
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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) => {
|
const renderContent = async (container) => {
|
||||||
// Clean up any previous sync (though sidePanelManager might handle cleanup, we ensure it here)
|
|
||||||
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
|
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
|
||||||
|
|
||||||
await renderLyricsComponent(container, track, audioPlayer, manager);
|
await renderLyricsComponent(container, track, audioPlayer, manager);
|
||||||
};
|
};
|
||||||
|
|
||||||
sidePanelManager.open('lyrics', 'Lyrics', renderControls, renderContent);
|
sidePanelManager.open("lyrics", "Lyrics", renderControls, renderContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderLyricsComponent(container, track, audioPlayer, lyricsManager) {
|
async function renderLyricsComponent(
|
||||||
|
container,
|
||||||
|
track,
|
||||||
|
audioPlayer,
|
||||||
|
lyricsManager,
|
||||||
|
) {
|
||||||
container.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>';
|
container.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await lyricsManager.ensureComponentLoaded();
|
await lyricsManager.ensureComponentLoaded();
|
||||||
|
|
||||||
|
// Set initial Romaji mode
|
||||||
|
lyricsManager.isRomajiMode = lyricsManager.getRomajiMode();
|
||||||
|
lyricsManager.currentTrackId = track.id;
|
||||||
|
|
||||||
const title = track.title;
|
const title = track.title;
|
||||||
const artist = getTrackArtists(track);
|
const artist = getTrackArtists(track);
|
||||||
const album = track.album?.title;
|
const album = track.album?.title;
|
||||||
const durationMs = track.duration ? Math.round(track.duration * 1000) : undefined;
|
const durationMs = track.duration
|
||||||
const isrc = track.isrc || '';
|
? Math.round(track.duration * 1000)
|
||||||
|
: undefined;
|
||||||
|
const isrc = track.isrc || "";
|
||||||
|
|
||||||
container.innerHTML = '';
|
container.innerHTML = "";
|
||||||
const amLyrics = document.createElement('am-lyrics');
|
const amLyrics = document.createElement("am-lyrics");
|
||||||
amLyrics.setAttribute('song-title', title);
|
amLyrics.setAttribute("song-title", title);
|
||||||
amLyrics.setAttribute('song-artist', artist);
|
amLyrics.setAttribute("song-artist", artist);
|
||||||
if (album) amLyrics.setAttribute('song-album', album);
|
if (album) amLyrics.setAttribute("song-album", album);
|
||||||
if (durationMs) amLyrics.setAttribute('song-duration', durationMs);
|
if (durationMs) amLyrics.setAttribute("song-duration", durationMs);
|
||||||
amLyrics.setAttribute('query', `${title} ${artist}`.trim());
|
amLyrics.setAttribute("query", `${title} ${artist}`.trim());
|
||||||
if (isrc) amLyrics.setAttribute('isrc', isrc);
|
if (isrc) amLyrics.setAttribute("isrc", isrc);
|
||||||
|
|
||||||
amLyrics.setAttribute('highlight-color', '#93c5fd');
|
amLyrics.setAttribute("highlight-color", "#93c5fd");
|
||||||
amLyrics.setAttribute('hover-background-color', 'rgba(59, 130, 246, 0.14)');
|
amLyrics.setAttribute("hover-background-color", "rgba(59, 130, 246, 0.14)");
|
||||||
amLyrics.setAttribute('autoscroll', '');
|
amLyrics.setAttribute("autoscroll", "");
|
||||||
amLyrics.setAttribute('interpolate', '');
|
amLyrics.setAttribute("interpolate", "");
|
||||||
amLyrics.style.height = '100%';
|
amLyrics.style.height = "100%";
|
||||||
amLyrics.style.width = '100%';
|
amLyrics.style.width = "100%";
|
||||||
|
|
||||||
container.appendChild(amLyrics);
|
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);
|
const cleanup = setupSync(track, audioPlayer, amLyrics);
|
||||||
|
|
||||||
// Attach cleanup to container for easy access
|
// Attach cleanup to container for easy access
|
||||||
container.lyricsCleanup = cleanup;
|
container.lyricsCleanup = cleanup;
|
||||||
|
container.lyricsManager = lyricsManager;
|
||||||
|
|
||||||
return amLyrics;
|
return amLyrics;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load lyrics:', error);
|
console.error("Failed to load lyrics:", error);
|
||||||
container.innerHTML = '<div class="lyrics-error">Failed to load lyrics</div>';
|
container.innerHTML =
|
||||||
|
'<div class="lyrics-error">Failed to load lyrics</div>';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -269,11 +704,11 @@ function setupSync(track, audioPlayer, amLyrics) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
audioPlayer.addEventListener('timeupdate', updateTime);
|
audioPlayer.addEventListener("timeupdate", updateTime);
|
||||||
audioPlayer.addEventListener('play', onPlay);
|
audioPlayer.addEventListener("play", onPlay);
|
||||||
audioPlayer.addEventListener('pause', onPause);
|
audioPlayer.addEventListener("pause", onPause);
|
||||||
audioPlayer.addEventListener('seeked', updateTime);
|
audioPlayer.addEventListener("seeked", updateTime);
|
||||||
amLyrics.addEventListener('line-click', onLineClick);
|
amLyrics.addEventListener("line-click", onLineClick);
|
||||||
|
|
||||||
if (!audioPlayer.paused) {
|
if (!audioPlayer.paused) {
|
||||||
tick();
|
tick();
|
||||||
|
|
@ -283,15 +718,20 @@ function setupSync(track, audioPlayer, amLyrics) {
|
||||||
if (animationFrameId) {
|
if (animationFrameId) {
|
||||||
cancelAnimationFrame(animationFrameId);
|
cancelAnimationFrame(animationFrameId);
|
||||||
}
|
}
|
||||||
audioPlayer.removeEventListener('timeupdate', updateTime);
|
audioPlayer.removeEventListener("timeupdate", updateTime);
|
||||||
audioPlayer.removeEventListener('play', onPlay);
|
audioPlayer.removeEventListener("play", onPlay);
|
||||||
audioPlayer.removeEventListener('pause', onPause);
|
audioPlayer.removeEventListener("pause", onPause);
|
||||||
audioPlayer.removeEventListener('seeked', updateTime);
|
audioPlayer.removeEventListener("seeked", updateTime);
|
||||||
amLyrics.removeEventListener('line-click', onLineClick);
|
amLyrics.removeEventListener("line-click", onLineClick);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderLyricsInFullscreen(track, audioPlayer, lyricsManager, container) {
|
export async function renderLyricsInFullscreen(
|
||||||
|
track,
|
||||||
|
audioPlayer,
|
||||||
|
lyricsManager,
|
||||||
|
container,
|
||||||
|
) {
|
||||||
return renderLyricsComponent(container, track, audioPlayer, lyricsManager);
|
return renderLyricsComponent(container, track, audioPlayer, lyricsManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,6 +740,9 @@ export function clearFullscreenLyricsSync(container) {
|
||||||
container.lyricsCleanup();
|
container.lyricsCleanup();
|
||||||
container.lyricsCleanup = null;
|
container.lyricsCleanup = null;
|
||||||
}
|
}
|
||||||
|
if (container && container.lyricsManager) {
|
||||||
|
container.lyricsManager.stopLyricsObserver();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearLyricsPanelSync(audioPlayer, panel) {
|
export function clearLyricsPanelSync(audioPlayer, panel) {
|
||||||
|
|
@ -307,4 +750,7 @@ export function clearLyricsPanelSync(audioPlayer, panel) {
|
||||||
panel.lyricsCleanup();
|
panel.lyricsCleanup();
|
||||||
panel.lyricsCleanup = null;
|
panel.lyricsCleanup = null;
|
||||||
}
|
}
|
||||||
|
if (panel && panel.lyricsManager) {
|
||||||
|
panel.lyricsManager.stopLyricsObserver();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
358
js/settings.js
358
js/settings.js
|
|
@ -1,9 +1,17 @@
|
||||||
//js/settings
|
//js/settings
|
||||||
import { themeManager, lastFMStorage, nowPlayingSettings, lyricsSettings, backgroundSettings, trackListSettings, cardSettings } from './storage.js';
|
import {
|
||||||
import { db } from './db.js';
|
themeManager,
|
||||||
import { authManager } from './firebase/auth.js';
|
lastFMStorage,
|
||||||
import { syncManager } from './firebase/sync.js';
|
nowPlayingSettings,
|
||||||
import { initializeFirebaseSettingsUI } from './firebase/config.js';
|
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) {
|
export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
// Initialize Firebase UI & Settings
|
// Initialize Firebase UI & Settings
|
||||||
|
|
@ -11,43 +19,45 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
initializeFirebaseSettingsUI();
|
initializeFirebaseSettingsUI();
|
||||||
|
|
||||||
// Email Auth UI Logic
|
// Email Auth UI Logic
|
||||||
const toggleEmailBtn = document.getElementById('toggle-email-auth-btn');
|
const toggleEmailBtn = document.getElementById("toggle-email-auth-btn");
|
||||||
const cancelEmailBtn = document.getElementById('cancel-email-auth-btn');
|
const cancelEmailBtn = document.getElementById("cancel-email-auth-btn");
|
||||||
const authContainer = document.getElementById('email-auth-container');
|
const authContainer = document.getElementById("email-auth-container");
|
||||||
const authButtonsContainer = document.getElementById('auth-buttons-container');
|
const authButtonsContainer = document.getElementById(
|
||||||
const emailInput = document.getElementById('auth-email');
|
"auth-buttons-container",
|
||||||
const passwordInput = document.getElementById('auth-password');
|
);
|
||||||
const signInBtn = document.getElementById('email-signin-btn');
|
const emailInput = document.getElementById("auth-email");
|
||||||
const signUpBtn = document.getElementById('email-signup-btn');
|
const passwordInput = document.getElementById("auth-password");
|
||||||
|
const signInBtn = document.getElementById("email-signin-btn");
|
||||||
|
const signUpBtn = document.getElementById("email-signup-btn");
|
||||||
|
|
||||||
if (toggleEmailBtn && authContainer && authButtonsContainer) {
|
if (toggleEmailBtn && authContainer && authButtonsContainer) {
|
||||||
toggleEmailBtn.addEventListener('click', () => {
|
toggleEmailBtn.addEventListener("click", () => {
|
||||||
authContainer.style.display = 'flex';
|
authContainer.style.display = "flex";
|
||||||
authButtonsContainer.style.display = 'none';
|
authButtonsContainer.style.display = "none";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancelEmailBtn && authContainer && authButtonsContainer) {
|
if (cancelEmailBtn && authContainer && authButtonsContainer) {
|
||||||
cancelEmailBtn.addEventListener('click', () => {
|
cancelEmailBtn.addEventListener("click", () => {
|
||||||
authContainer.style.display = 'none';
|
authContainer.style.display = "none";
|
||||||
authButtonsContainer.style.display = 'flex';
|
authButtonsContainer.style.display = "flex";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signInBtn) {
|
if (signInBtn) {
|
||||||
signInBtn.addEventListener('click', async () => {
|
signInBtn.addEventListener("click", async () => {
|
||||||
const email = emailInput.value;
|
const email = emailInput.value;
|
||||||
const password = passwordInput.value;
|
const password = passwordInput.value;
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
alert('Please enter both email and password.');
|
alert("Please enter both email and password.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await authManager.signInWithEmail(email, password);
|
await authManager.signInWithEmail(email, password);
|
||||||
authContainer.style.display = 'none';
|
authContainer.style.display = "none";
|
||||||
authButtonsContainer.style.display = 'flex';
|
authButtonsContainer.style.display = "flex";
|
||||||
emailInput.value = '';
|
emailInput.value = "";
|
||||||
passwordInput.value = '';
|
passwordInput.value = "";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Error handled in authManager
|
// Error handled in authManager
|
||||||
}
|
}
|
||||||
|
|
@ -55,64 +65,65 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signUpBtn) {
|
if (signUpBtn) {
|
||||||
signUpBtn.addEventListener('click', async () => {
|
signUpBtn.addEventListener("click", async () => {
|
||||||
const email = emailInput.value;
|
const email = emailInput.value;
|
||||||
const password = passwordInput.value;
|
const password = passwordInput.value;
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
alert('Please enter both email and password.');
|
alert("Please enter both email and password.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await authManager.signUpWithEmail(email, password);
|
await authManager.signUpWithEmail(email, password);
|
||||||
authContainer.style.display = 'none';
|
authContainer.style.display = "none";
|
||||||
authButtonsContainer.style.display = 'flex';
|
authButtonsContainer.style.display = "flex";
|
||||||
emailInput.value = '';
|
emailInput.value = "";
|
||||||
passwordInput.value = '';
|
passwordInput.value = "";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Error handled in authManager
|
// Error handled in authManager
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastfmConnectBtn = document.getElementById('lastfm-connect-btn');
|
const lastfmConnectBtn = document.getElementById("lastfm-connect-btn");
|
||||||
const lastfmStatus = document.getElementById('lastfm-status');
|
const lastfmStatus = document.getElementById("lastfm-status");
|
||||||
const lastfmToggle = document.getElementById('lastfm-toggle');
|
const lastfmToggle = document.getElementById("lastfm-toggle");
|
||||||
const lastfmToggleSetting = document.getElementById('lastfm-toggle-setting');
|
const lastfmToggleSetting = document.getElementById("lastfm-toggle-setting");
|
||||||
const lastfmLoveToggle = document.getElementById('lastfm-love-toggle');
|
const lastfmLoveToggle = document.getElementById("lastfm-love-toggle");
|
||||||
const lastfmLoveSetting = document.getElementById('lastfm-love-setting');
|
const lastfmLoveSetting = document.getElementById("lastfm-love-setting");
|
||||||
|
|
||||||
function updateLastFMUI() {
|
function updateLastFMUI() {
|
||||||
if (scrobbler.isAuthenticated()) {
|
if (scrobbler.isAuthenticated()) {
|
||||||
lastfmStatus.textContent = `Connected as ${scrobbler.username}`;
|
lastfmStatus.textContent = `Connected as ${scrobbler.username}`;
|
||||||
lastfmConnectBtn.textContent = 'Disconnect';
|
lastfmConnectBtn.textContent = "Disconnect";
|
||||||
lastfmConnectBtn.classList.add('danger');
|
lastfmConnectBtn.classList.add("danger");
|
||||||
lastfmToggleSetting.style.display = 'flex';
|
lastfmToggleSetting.style.display = "flex";
|
||||||
lastfmLoveSetting.style.display = 'flex';
|
lastfmLoveSetting.style.display = "flex";
|
||||||
lastfmToggle.checked = lastFMStorage.isEnabled();
|
lastfmToggle.checked = lastFMStorage.isEnabled();
|
||||||
lastfmLoveToggle.checked = lastFMStorage.shouldLoveOnLike();
|
lastfmLoveToggle.checked = lastFMStorage.shouldLoveOnLike();
|
||||||
} else {
|
} else {
|
||||||
lastfmStatus.textContent = 'Connect your Last.fm account to scrobble tracks';
|
lastfmStatus.textContent =
|
||||||
lastfmConnectBtn.textContent = 'Connect Last.fm';
|
"Connect your Last.fm account to scrobble tracks";
|
||||||
lastfmConnectBtn.classList.remove('danger');
|
lastfmConnectBtn.textContent = "Connect Last.fm";
|
||||||
lastfmToggleSetting.style.display = 'none';
|
lastfmConnectBtn.classList.remove("danger");
|
||||||
lastfmLoveSetting.style.display = 'none';
|
lastfmToggleSetting.style.display = "none";
|
||||||
|
lastfmLoveSetting.style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLastFMUI();
|
updateLastFMUI();
|
||||||
|
|
||||||
lastfmConnectBtn?.addEventListener('click', async () => {
|
lastfmConnectBtn?.addEventListener("click", async () => {
|
||||||
if (scrobbler.isAuthenticated()) {
|
if (scrobbler.isAuthenticated()) {
|
||||||
if (confirm('Disconnect from Last.fm?')) {
|
if (confirm("Disconnect from Last.fm?")) {
|
||||||
scrobbler.disconnect();
|
scrobbler.disconnect();
|
||||||
updateLastFMUI();
|
updateLastFMUI();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authWindow = window.open('', '_blank');
|
const authWindow = window.open("", "_blank");
|
||||||
lastfmConnectBtn.disabled = true;
|
lastfmConnectBtn.disabled = true;
|
||||||
lastfmConnectBtn.textContent = 'Opening Last.fm...';
|
lastfmConnectBtn.textContent = "Opening Last.fm...";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { token, url } = await scrobbler.getAuthUrl();
|
const { token, url } = await scrobbler.getAuthUrl();
|
||||||
|
|
@ -120,13 +131,13 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
if (authWindow) {
|
if (authWindow) {
|
||||||
authWindow.location.href = url;
|
authWindow.location.href = url;
|
||||||
} else {
|
} else {
|
||||||
alert('Popup blocked! Please allow popups.');
|
alert("Popup blocked! Please allow popups.");
|
||||||
lastfmConnectBtn.textContent = 'Connect Last.fm';
|
lastfmConnectBtn.textContent = "Connect Last.fm";
|
||||||
lastfmConnectBtn.disabled = false;
|
lastfmConnectBtn.disabled = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastfmConnectBtn.textContent = 'Waiting for authorization...';
|
lastfmConnectBtn.textContent = "Waiting for authorization...";
|
||||||
|
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
const maxAttempts = 30;
|
const maxAttempts = 30;
|
||||||
|
|
@ -136,10 +147,10 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
|
|
||||||
if (attempts > maxAttempts) {
|
if (attempts > maxAttempts) {
|
||||||
clearInterval(checkAuth);
|
clearInterval(checkAuth);
|
||||||
lastfmConnectBtn.textContent = 'Connect Last.fm';
|
lastfmConnectBtn.textContent = "Connect Last.fm";
|
||||||
lastfmConnectBtn.disabled = false;
|
lastfmConnectBtn.disabled = false;
|
||||||
if (authWindow && !authWindow.closed) authWindow.close();
|
if (authWindow && !authWindow.closed) authWindow.close();
|
||||||
alert('Authorization timed out. Please try again.');
|
alert("Authorization timed out. Please try again.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,185 +170,221 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
// Still waiting
|
// Still waiting
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Last.fm connection failed:', error);
|
console.error("Last.fm connection failed:", error);
|
||||||
alert('Failed to connect to Last.fm: ' + error.message);
|
alert("Failed to connect to Last.fm: " + error.message);
|
||||||
lastfmConnectBtn.textContent = 'Connect Last.fm';
|
lastfmConnectBtn.textContent = "Connect Last.fm";
|
||||||
lastfmConnectBtn.disabled = false;
|
lastfmConnectBtn.disabled = false;
|
||||||
if (authWindow && !authWindow.closed) authWindow.close();
|
if (authWindow && !authWindow.closed) authWindow.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
lastfmToggle?.addEventListener('change', (e) => {
|
lastfmToggle?.addEventListener("change", (e) => {
|
||||||
lastFMStorage.setEnabled(e.target.checked);
|
lastFMStorage.setEnabled(e.target.checked);
|
||||||
});
|
});
|
||||||
|
|
||||||
lastfmLoveToggle?.addEventListener('change', (e) => {
|
lastfmLoveToggle?.addEventListener("change", (e) => {
|
||||||
lastFMStorage.setLoveOnLike(e.target.checked);
|
lastFMStorage.setLoveOnLike(e.target.checked);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Theme picker
|
// Theme picker
|
||||||
const themePicker = document.getElementById('theme-picker');
|
const themePicker = document.getElementById("theme-picker");
|
||||||
const currentTheme = themeManager.getTheme();
|
const currentTheme = themeManager.getTheme();
|
||||||
|
|
||||||
themePicker.querySelectorAll('.theme-option').forEach(option => {
|
themePicker.querySelectorAll(".theme-option").forEach((option) => {
|
||||||
if (option.dataset.theme === currentTheme) {
|
if (option.dataset.theme === currentTheme) {
|
||||||
option.classList.add('active');
|
option.classList.add("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
option.addEventListener('click', () => {
|
option.addEventListener("click", () => {
|
||||||
const theme = option.dataset.theme;
|
const theme = option.dataset.theme;
|
||||||
|
|
||||||
themePicker.querySelectorAll('.theme-option').forEach(opt => opt.classList.remove('active'));
|
themePicker
|
||||||
option.classList.add('active');
|
.querySelectorAll(".theme-option")
|
||||||
|
.forEach((opt) => opt.classList.remove("active"));
|
||||||
|
option.classList.add("active");
|
||||||
|
|
||||||
if (theme === 'custom') {
|
if (theme === "custom") {
|
||||||
document.getElementById('custom-theme-editor').classList.add('show');
|
document.getElementById("custom-theme-editor").classList.add("show");
|
||||||
renderCustomThemeEditor();
|
renderCustomThemeEditor();
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('custom-theme-editor').classList.remove('show');
|
document.getElementById("custom-theme-editor").classList.remove("show");
|
||||||
themeManager.setTheme(theme);
|
themeManager.setTheme(theme);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderCustomThemeEditor() {
|
function renderCustomThemeEditor() {
|
||||||
const grid = document.getElementById('theme-color-grid');
|
const grid = document.getElementById("theme-color-grid");
|
||||||
const customTheme = themeManager.getCustomTheme() || {
|
const customTheme = themeManager.getCustomTheme() || {
|
||||||
background: '#000000',
|
background: "#000000",
|
||||||
foreground: '#fafafa',
|
foreground: "#fafafa",
|
||||||
primary: '#ffffff',
|
primary: "#ffffff",
|
||||||
secondary: '#27272a',
|
secondary: "#27272a",
|
||||||
muted: '#27272a',
|
muted: "#27272a",
|
||||||
border: '#27272a',
|
border: "#27272a",
|
||||||
highlight: '#ffffff'
|
highlight: "#ffffff",
|
||||||
};
|
};
|
||||||
|
|
||||||
grid.innerHTML = Object.entries(customTheme).map(([key, value]) => `
|
grid.innerHTML = Object.entries(customTheme)
|
||||||
|
.map(
|
||||||
|
([key, value]) => `
|
||||||
<div class="theme-color-input">
|
<div class="theme-color-input">
|
||||||
<label>${key}</label>
|
<label>${key}</label>
|
||||||
<input type="color" data-color="${key}" value="${value}">
|
<input type="color" data-color="${key}" value="${value}">
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('apply-custom-theme')?.addEventListener('click', () => {
|
document
|
||||||
|
.getElementById("apply-custom-theme")
|
||||||
|
?.addEventListener("click", () => {
|
||||||
const colors = {};
|
const colors = {};
|
||||||
document.querySelectorAll('#theme-color-grid input[type="color"]').forEach(input => {
|
document
|
||||||
|
.querySelectorAll('#theme-color-grid input[type="color"]')
|
||||||
|
.forEach((input) => {
|
||||||
colors[input.dataset.color] = input.value;
|
colors[input.dataset.color] = input.value;
|
||||||
});
|
});
|
||||||
themeManager.setCustomTheme(colors);
|
themeManager.setCustomTheme(colors);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('reset-custom-theme')?.addEventListener('click', () => {
|
document
|
||||||
|
.getElementById("reset-custom-theme")
|
||||||
|
?.addEventListener("click", () => {
|
||||||
renderCustomThemeEditor();
|
renderCustomThemeEditor();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quality setting
|
// Quality setting
|
||||||
const qualitySetting = document.getElementById('quality-setting');
|
const qualitySetting = document.getElementById("quality-setting");
|
||||||
if (qualitySetting) {
|
if (qualitySetting) {
|
||||||
const savedQuality = localStorage.getItem('playback-quality') || 'LOSSLESS';
|
const savedQuality = localStorage.getItem("playback-quality") || "LOSSLESS";
|
||||||
qualitySetting.value = savedQuality;
|
qualitySetting.value = savedQuality;
|
||||||
player.setQuality(savedQuality);
|
player.setQuality(savedQuality);
|
||||||
|
|
||||||
qualitySetting.addEventListener('change', (e) => {
|
qualitySetting.addEventListener("change", (e) => {
|
||||||
const newQuality = e.target.value;
|
const newQuality = e.target.value;
|
||||||
player.setQuality(newQuality);
|
player.setQuality(newQuality);
|
||||||
localStorage.setItem('playback-quality', newQuality);
|
localStorage.setItem("playback-quality", newQuality);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now Playing Mode
|
// Now Playing Mode
|
||||||
const nowPlayingMode = document.getElementById('now-playing-mode');
|
const nowPlayingMode = document.getElementById("now-playing-mode");
|
||||||
if (nowPlayingMode) {
|
if (nowPlayingMode) {
|
||||||
nowPlayingMode.value = nowPlayingSettings.getMode();
|
nowPlayingMode.value = nowPlayingSettings.getMode();
|
||||||
nowPlayingMode.addEventListener('change', (e) => {
|
nowPlayingMode.addEventListener("change", (e) => {
|
||||||
nowPlayingSettings.setMode(e.target.value);
|
nowPlayingSettings.setMode(e.target.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track List Actions Mode
|
// Track List Actions Mode
|
||||||
const trackListActionsMode = document.getElementById('track-list-actions-mode');
|
const trackListActionsMode = document.getElementById(
|
||||||
|
"track-list-actions-mode",
|
||||||
|
);
|
||||||
if (trackListActionsMode) {
|
if (trackListActionsMode) {
|
||||||
trackListActionsMode.value = trackListSettings.getMode();
|
trackListActionsMode.value = trackListSettings.getMode();
|
||||||
trackListActionsMode.addEventListener('change', (e) => {
|
trackListActionsMode.addEventListener("change", (e) => {
|
||||||
trackListSettings.setMode(e.target.value);
|
trackListSettings.setMode(e.target.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compact Artist Toggle
|
// Compact Artist Toggle
|
||||||
const compactArtistToggle = document.getElementById('compact-artist-toggle');
|
const compactArtistToggle = document.getElementById("compact-artist-toggle");
|
||||||
if (compactArtistToggle) {
|
if (compactArtistToggle) {
|
||||||
compactArtistToggle.checked = cardSettings.isCompactArtist();
|
compactArtistToggle.checked = cardSettings.isCompactArtist();
|
||||||
compactArtistToggle.addEventListener('change', (e) => {
|
compactArtistToggle.addEventListener("change", (e) => {
|
||||||
cardSettings.setCompactArtist(e.target.checked);
|
cardSettings.setCompactArtist(e.target.checked);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compact Album Toggle
|
// Compact Album Toggle
|
||||||
const compactAlbumToggle = document.getElementById('compact-album-toggle');
|
const compactAlbumToggle = document.getElementById("compact-album-toggle");
|
||||||
if (compactAlbumToggle) {
|
if (compactAlbumToggle) {
|
||||||
compactAlbumToggle.checked = cardSettings.isCompactAlbum();
|
compactAlbumToggle.checked = cardSettings.isCompactAlbum();
|
||||||
compactAlbumToggle.addEventListener('change', (e) => {
|
compactAlbumToggle.addEventListener("change", (e) => {
|
||||||
cardSettings.setCompactAlbum(e.target.checked);
|
cardSettings.setCompactAlbum(e.target.checked);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download Lyrics Toggle
|
// Download Lyrics Toggle
|
||||||
const downloadLyricsToggle = document.getElementById('download-lyrics-toggle');
|
const downloadLyricsToggle = document.getElementById(
|
||||||
|
"download-lyrics-toggle",
|
||||||
|
);
|
||||||
if (downloadLyricsToggle) {
|
if (downloadLyricsToggle) {
|
||||||
downloadLyricsToggle.checked = lyricsSettings.shouldDownloadLyrics();
|
downloadLyricsToggle.checked = lyricsSettings.shouldDownloadLyrics();
|
||||||
downloadLyricsToggle.addEventListener('change', (e) => {
|
downloadLyricsToggle.addEventListener("change", (e) => {
|
||||||
lyricsSettings.setDownloadLyrics(e.target.checked);
|
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
|
// Album Background Toggle
|
||||||
const albumBackgroundToggle = document.getElementById('album-background-toggle');
|
const albumBackgroundToggle = document.getElementById(
|
||||||
|
"album-background-toggle",
|
||||||
|
);
|
||||||
if (albumBackgroundToggle) {
|
if (albumBackgroundToggle) {
|
||||||
albumBackgroundToggle.checked = backgroundSettings.isEnabled();
|
albumBackgroundToggle.checked = backgroundSettings.isEnabled();
|
||||||
albumBackgroundToggle.addEventListener('change', (e) => {
|
albumBackgroundToggle.addEventListener("change", (e) => {
|
||||||
backgroundSettings.setEnabled(e.target.checked);
|
backgroundSettings.setEnabled(e.target.checked);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filename template setting
|
// Filename template setting
|
||||||
const filenameTemplate = document.getElementById('filename-template');
|
const filenameTemplate = document.getElementById("filename-template");
|
||||||
if (filenameTemplate) {
|
if (filenameTemplate) {
|
||||||
filenameTemplate.value = localStorage.getItem('filename-template') || '{trackNumber} - {artist} - {title}';
|
filenameTemplate.value =
|
||||||
filenameTemplate.addEventListener('change', (e) => {
|
localStorage.getItem("filename-template") ||
|
||||||
localStorage.setItem('filename-template', e.target.value);
|
"{trackNumber} - {artist} - {title}";
|
||||||
|
filenameTemplate.addEventListener("change", (e) => {
|
||||||
|
localStorage.setItem("filename-template", e.target.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ZIP folder template
|
// ZIP folder template
|
||||||
const zipFolderTemplate = document.getElementById('zip-folder-template');
|
const zipFolderTemplate = document.getElementById("zip-folder-template");
|
||||||
if (zipFolderTemplate) {
|
if (zipFolderTemplate) {
|
||||||
zipFolderTemplate.value = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist}';
|
zipFolderTemplate.value =
|
||||||
zipFolderTemplate.addEventListener('change', (e) => {
|
localStorage.getItem("zip-folder-template") ||
|
||||||
localStorage.setItem('zip-folder-template', e.target.value);
|
"{albumTitle} - {albumArtist}";
|
||||||
|
zipFolderTemplate.addEventListener("change", (e) => {
|
||||||
|
localStorage.setItem("zip-folder-template", e.target.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// API settings
|
// API settings
|
||||||
document.getElementById('refresh-speed-test-btn')?.addEventListener('click', async () => {
|
document
|
||||||
const btn = document.getElementById('refresh-speed-test-btn');
|
.getElementById("refresh-speed-test-btn")
|
||||||
|
?.addEventListener("click", async () => {
|
||||||
|
const btn = document.getElementById("refresh-speed-test-btn");
|
||||||
const originalText = btn.textContent;
|
const originalText = btn.textContent;
|
||||||
btn.textContent = 'Testing...';
|
btn.textContent = "Testing...";
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.settings.refreshSpeedTests();
|
await api.settings.refreshSpeedTests();
|
||||||
ui.renderApiSettings();
|
ui.renderApiSettings();
|
||||||
btn.textContent = 'Done!';
|
btn.textContent = "Done!";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
btn.textContent = originalText;
|
btn.textContent = originalText;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to refresh speed tests:', error);
|
console.error("Failed to refresh speed tests:", error);
|
||||||
btn.textContent = 'Error';
|
btn.textContent = "Error";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
btn.textContent = originalText;
|
btn.textContent = originalText;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
|
|
@ -345,45 +392,58 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('api-instance-list')?.addEventListener('click', async (e) => {
|
document
|
||||||
const button = e.target.closest('button');
|
.getElementById("api-instance-list")
|
||||||
|
?.addEventListener("click", async (e) => {
|
||||||
|
const button = e.target.closest("button");
|
||||||
if (!button) return;
|
if (!button) return;
|
||||||
|
|
||||||
const li = button.closest('li');
|
const li = button.closest("li");
|
||||||
const index = parseInt(li.dataset.index, 10);
|
const index = parseInt(li.dataset.index, 10);
|
||||||
const type = li.dataset.type || 'api'; // Default to api if not present
|
const type = li.dataset.type || "api"; // Default to api if not present
|
||||||
|
|
||||||
const instances = await api.settings.getInstances(type);
|
const instances = await api.settings.getInstances(type);
|
||||||
|
|
||||||
if (button.classList.contains('move-up') && index > 0) {
|
if (button.classList.contains("move-up") && index > 0) {
|
||||||
[instances[index], instances[index - 1]] = [instances[index - 1], instances[index]];
|
[instances[index], instances[index - 1]] = [
|
||||||
} else if (button.classList.contains('move-down') && index < instances.length - 1) {
|
instances[index - 1],
|
||||||
[instances[index], instances[index + 1]] = [instances[index + 1], instances[index]];
|
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);
|
api.settings.saveInstances(instances, type);
|
||||||
ui.renderApiSettings();
|
ui.renderApiSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('clear-cache-btn')?.addEventListener('click', async () => {
|
document
|
||||||
const btn = document.getElementById('clear-cache-btn');
|
.getElementById("clear-cache-btn")
|
||||||
|
?.addEventListener("click", async () => {
|
||||||
|
const btn = document.getElementById("clear-cache-btn");
|
||||||
const originalText = btn.textContent;
|
const originalText = btn.textContent;
|
||||||
btn.textContent = 'Clearing...';
|
btn.textContent = "Clearing...";
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.clearCache();
|
await api.clearCache();
|
||||||
btn.textContent = 'Cleared!';
|
btn.textContent = "Cleared!";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
btn.textContent = originalText;
|
btn.textContent = originalText;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
if (window.location.hash.includes('settings')) {
|
if (window.location.hash.includes("settings")) {
|
||||||
ui.renderApiSettings();
|
ui.renderApiSettings();
|
||||||
}
|
}
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to clear cache:', error);
|
console.error("Failed to clear cache:", error);
|
||||||
btn.textContent = 'Error';
|
btn.textContent = "Error";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
btn.textContent = originalText;
|
btn.textContent = originalText;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
|
|
@ -391,37 +451,49 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('firebase-clear-cloud-btn')?.addEventListener('click', async () => {
|
document
|
||||||
if (confirm('Are you sure you want to delete ALL your data from the cloud? This cannot be undone.')) {
|
.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 {
|
try {
|
||||||
await syncManager.clearCloudData();
|
await syncManager.clearCloudData();
|
||||||
alert('Cloud data cleared successfully.');
|
alert("Cloud data cleared successfully.");
|
||||||
authManager.signOut();
|
authManager.signOut();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to clear cloud data:', error);
|
console.error("Failed to clear cloud data:", error);
|
||||||
alert('Failed to clear cloud data: ' + error.message);
|
alert("Failed to clear cloud data: " + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Backup & Restore
|
// Backup & Restore
|
||||||
document.getElementById('export-library-btn')?.addEventListener('click', async () => {
|
document
|
||||||
|
.getElementById("export-library-btn")
|
||||||
|
?.addEventListener("click", async () => {
|
||||||
const data = await db.exportData();
|
const data = await db.exportData();
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(data, null, 2)], {
|
||||||
|
type: "application/json",
|
||||||
|
});
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `monochrome-library-${new Date().toISOString().split('T')[0]}.json`;
|
a.download = `monochrome-library-${new Date().toISOString().split("T")[0]}.json`;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
const importInput = document.getElementById('import-library-input');
|
const importInput = document.getElementById("import-library-input");
|
||||||
document.getElementById('import-library-btn')?.addEventListener('click', () => {
|
document
|
||||||
|
.getElementById("import-library-btn")
|
||||||
|
?.addEventListener("click", () => {
|
||||||
importInput.click();
|
importInput.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
importInput?.addEventListener('change', async (e) => {
|
importInput?.addEventListener("change", async (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
|
|
@ -430,11 +502,11 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.target.result);
|
const data = JSON.parse(event.target.result);
|
||||||
await db.importData(data);
|
await db.importData(data);
|
||||||
alert('Library imported successfully!');
|
alert("Library imported successfully!");
|
||||||
window.location.reload(); // Simple way to refresh all state
|
window.location.reload(); // Simple way to refresh all state
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Import failed:', err);
|
console.error("Import failed:", err);
|
||||||
alert('Failed to import library. Please check the file format.');
|
alert("Failed to import library. Please check the file format.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
|
|
|
||||||
12
package-lock.json
generated
12
package-lock.json
generated
|
|
@ -10,7 +10,8 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^7.3.0",
|
"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": {
|
"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": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"homepage": "https://github.com/SamidyFR/monochrome#readme",
|
"homepage": "https://github.com/SamidyFR/monochrome#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^7.3.0",
|
"vite": "^7.3.0",
|
||||||
"vite-plugin-pwa": "^1.2.0"
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
|
"wanakana": "^5.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from "vite";
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: './',
|
base: "./",
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: "dist",
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: 'prompt',
|
registerType: "prompt",
|
||||||
workbox: {
|
workbox: {
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'],
|
globPatterns: ["**/*.{js,css,html,ico,png,svg,json}"],
|
||||||
cleanupOutdatedCaches: true,
|
cleanupOutdatedCaches: true,
|
||||||
// Define runtime caching strategies
|
// Define runtime caching strategies
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern: ({ request }) => request.destination === 'image',
|
urlPattern: ({ request }) => request.destination === "image",
|
||||||
handler: 'CacheFirst',
|
handler: "CacheFirst",
|
||||||
options: {
|
options: {
|
||||||
cacheName: 'images',
|
cacheName: "images",
|
||||||
expiration: {
|
expiration: {
|
||||||
maxEntries: 100,
|
maxEntries: 100,
|
||||||
maxAgeSeconds: 60 * 24 * 60 * 60, // 60 Days
|
maxAgeSeconds: 60 * 24 * 60 * 60, // 60 Days
|
||||||
|
|
@ -27,21 +27,23 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
urlPattern: ({ request }) => request.destination === 'audio' || request.destination === 'video',
|
urlPattern: ({ request }) =>
|
||||||
handler: 'CacheFirst',
|
request.destination === "audio" ||
|
||||||
|
request.destination === "video",
|
||||||
|
handler: "CacheFirst",
|
||||||
options: {
|
options: {
|
||||||
cacheName: 'media',
|
cacheName: "media",
|
||||||
expiration: {
|
expiration: {
|
||||||
maxEntries: 50,
|
maxEntries: 50,
|
||||||
maxAgeSeconds: 60 * 24 * 60 * 60, // 60 Days
|
maxAgeSeconds: 60 * 24 * 60 * 60, // 60 Days
|
||||||
},
|
},
|
||||||
rangeRequests: true, // Support scrubbing
|
rangeRequests: true, // Support scrubbing
|
||||||
},
|
},
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
includeAssets: ['instances.json', 'discord.html'],
|
],
|
||||||
manifest: false // Use existing public/manifest.json
|
},
|
||||||
})
|
includeAssets: ["instances.json", "discord.html"],
|
||||||
]
|
manifest: false, // Use existing public/manifest.json
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
Loading…
Reference in a new issue