Merge pull request #56 from NexiaMoe/feat/lyric-romaji
Add Japanese to Romaji converter
This commit is contained in:
commit
2799cd8d85
5 changed files with 1243 additions and 667 deletions
20
index.html
20
index.html
|
|
@ -30,7 +30,7 @@
|
|||
<li data-action="download">Download</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="side-panel" class="side-panel">
|
||||
<div class="panel-header">
|
||||
<h3 id="side-panel-title">Panel</h3>
|
||||
|
|
@ -315,7 +315,7 @@
|
|||
</div>
|
||||
</header>
|
||||
<div class="track-list" id="album-detail-tracklist"></div>
|
||||
|
||||
|
||||
<section class="content-section" id="album-section-more-albums" style="display: none; margin-top: 3rem;">
|
||||
<h2 class="section-title" id="album-title-more-albums">More from Artist</h2>
|
||||
<div class="card-grid" id="album-detail-more-albums"></div>
|
||||
|
|
@ -609,6 +609,16 @@
|
|||
<span class="slider"></span>
|
||||
</label>
|
||||
</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="info">
|
||||
<span class="label">Filename Template</span>
|
||||
|
|
@ -669,7 +679,7 @@
|
|||
<h2 class="section-title">About Monochrome</h2>
|
||||
<div class="about-content">
|
||||
<p class="about-description">
|
||||
Monochrome is a lightweight, privacy-focused music streaming client designed for high-fidelity audio playback.
|
||||
Monochrome is a lightweight, privacy-focused music streaming client designed for high-fidelity audio playback.
|
||||
Built with modern web technologies, it provides a clean, distraction-free listening experience.
|
||||
<br>
|
||||
<strong>NOTE:</strong> The instance you are currently on (monochrome.samidy.com) is a Community Fork adding additional features (library/playlists, accounts, mix + Big UI Improvments & More).
|
||||
|
|
@ -765,9 +775,9 @@
|
|||
<div class="donate-content">
|
||||
<p style="text-align: center;" class="donate-description">If Monochrome has been useful To you and you're able to, consider giving a little donation. <br> It helps Pay for the domain, and you get To support us :)</p>
|
||||
<button id="donate-btn" class="btn-secondary">Donate To Monochrome</button>
|
||||
|
||||
|
||||
</main>
|
||||
|
||||
|
||||
<footer class="now-playing-bar">
|
||||
<div class="track-info">
|
||||
<img src="./assets/appicon.png" alt="Current Track Cover" class="cover">
|
||||
|
|
|
|||
|
|
@ -118,6 +118,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const scrobbler = new LastFMScrobbler();
|
||||
const lyricsManager = new LyricsManager(api);
|
||||
|
||||
// Pre-load Kuroshiro for romaji conversion in background (always load so it's ready instantly)
|
||||
lyricsManager.loadKuroshiro().catch(err => {
|
||||
console.warn('Failed to pre-load Kuroshiro:', err);
|
||||
});
|
||||
|
||||
const currentTheme = themeManager.getTheme();
|
||||
themeManager.setTheme(currentTheme);
|
||||
trackListSettings.getMode();
|
||||
|
|
|
|||
991
js/lyrics.js
991
js/lyrics.js
File diff suppressed because it is too large
Load diff
854
js/settings.js
854
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]) => `
|
||||
<div class="theme-color-input">
|
||||
<label>${key}</label>
|
||||
<input type="color" data-color="${key}" value="${value}">
|
||||
</div>
|
||||
`).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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
]
|
||||
});
|
||||
includeAssets: ["instances.json", "discord.html"],
|
||||
manifest: false, // Use existing public/manifest.json
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue