Feat: adding romaji convert feature on lyric

This commit is contained in:
Aji Priyo Wibowo 2026-01-08 15:49:54 +07:00
parent b3437dc99a
commit df2b77eb7d
6 changed files with 1209 additions and 668 deletions

View file

@ -30,7 +30,7 @@
<li data-action="download">Download</li> <li data-action="download">Download</li>
</ul> </ul>
</div> </div>
<div id="side-panel" class="side-panel"> <div id="side-panel" class="side-panel">
<div class="panel-header"> <div class="panel-header">
<h3 id="side-panel-title">Panel</h3> <h3 id="side-panel-title">Panel</h3>
@ -315,7 +315,7 @@
</div> </div>
</header> </header>
<div class="track-list" id="album-detail-tracklist"></div> <div class="track-list" id="album-detail-tracklist"></div>
<section class="content-section" id="album-section-more-albums" style="display: none; margin-top: 3rem;"> <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> <h2 class="section-title" id="album-title-more-albums">More from Artist</h2>
<div class="card-grid" id="album-detail-more-albums"></div> <div class="card-grid" id="album-detail-more-albums"></div>
@ -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>
@ -669,7 +679,7 @@
<h2 class="section-title">About Monochrome</h2> <h2 class="section-title">About Monochrome</h2>
<div class="about-content"> <div class="about-content">
<p class="about-description"> <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. Built with modern web technologies, it provides a clean, distraction-free listening experience.
<br> <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). <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"> <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> <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> <button id="donate-btn" class="btn-secondary">Donate To Monochrome</button>
</main> </main>
<footer class="now-playing-bar"> <footer class="now-playing-bar">
<div class="track-info"> <div class="track-info">
<img src="./assets/appicon.png" alt="Current Track Cover" class="cover"> <img src="./assets/appicon.png" alt="Current Track Cover" class="cover">

File diff suppressed because it is too large Load diff

View file

@ -1,442 +1,514 @@
//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
authManager.updateUI(authManager.user); authManager.updateUI(authManager.user);
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) {
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) { const authWindow = window.open("", "_blank");
cancelEmailBtn.addEventListener('click', () => { lastfmConnectBtn.disabled = true;
authContainer.style.display = 'none'; lastfmConnectBtn.textContent = "Opening Last.fm...";
authButtonsContainer.style.display = 'flex';
});
}
if (signInBtn) { try {
signInBtn.addEventListener('click', async () => { const { token, url } = await scrobbler.getAuthUrl();
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) { if (authWindow) {
signUpBtn.addEventListener('click', async () => { authWindow.location.href = url;
const email = emailInput.value; } else {
const password = passwordInput.value; alert("Popup blocked! Please allow popups.");
if (!email || !password) { lastfmConnectBtn.textContent = "Connect Last.fm";
alert('Please enter both email and password.'); lastfmConnectBtn.disabled = false;
return; 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'); lastfmConnectBtn.textContent = "Waiting for authorization...";
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() { let attempts = 0;
if (scrobbler.isAuthenticated()) { const maxAttempts = 30;
lastfmStatus.textContent = `Connected as ${scrobbler.username}`;
lastfmConnectBtn.textContent = 'Disconnect'; const checkAuth = setInterval(async () => {
lastfmConnectBtn.classList.add('danger'); attempts++;
lastfmToggleSetting.style.display = 'flex';
lastfmLoveSetting.style.display = 'flex'; if (attempts > maxAttempts) {
lastfmToggle.checked = lastFMStorage.isEnabled(); clearInterval(checkAuth);
lastfmLoveToggle.checked = lastFMStorage.shouldLoveOnLike(); lastfmConnectBtn.textContent = "Connect Last.fm";
} else { lastfmConnectBtn.disabled = false;
lastfmStatus.textContent = 'Connect your Last.fm account to scrobble tracks'; if (authWindow && !authWindow.closed) authWindow.close();
lastfmConnectBtn.textContent = 'Connect Last.fm'; alert("Authorization timed out. Please try again.");
lastfmConnectBtn.classList.remove('danger'); return;
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;
}
const authWindow = window.open('', '_blank');
lastfmConnectBtn.disabled = true;
lastfmConnectBtn.textContent = 'Opening Last.fm...';
try { try {
const { token, url } = await scrobbler.getAuthUrl(); const result = await scrobbler.completeAuthentication(token);
if (authWindow) { if (result.success) {
authWindow.location.href = url; clearInterval(checkAuth);
} 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 (authWindow && !authWindow.closed) authWindow.close(); 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) => { function renderCustomThemeEditor() {
lastFMStorage.setEnabled(e.target.checked); 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) => { grid.innerHTML = Object.entries(customTheme)
lastFMStorage.setLoveOnLike(e.target.checked); .map(
}); ([key, value]) => `
// 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]) => `
<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
const colors = {}; .getElementById("apply-custom-theme")
document.querySelectorAll('#theme-color-grid input[type="color"]').forEach(input => { ?.addEventListener("click", () => {
colors[input.dataset.color] = input.value; 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', () => { document
renderCustomThemeEditor(); .getElementById("reset-custom-theme")
?.addEventListener("click", () => {
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
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);
}
}); });
}
document.getElementById('api-instance-list')?.addEventListener('click', async (e) => { // Now Playing Mode
const button = e.target.closest('button'); const nowPlayingMode = document.getElementById("now-playing-mode");
if (!button) return; if (nowPlayingMode) {
nowPlayingMode.value = nowPlayingSettings.getMode();
nowPlayingMode.addEventListener("change", (e) => {
nowPlayingSettings.setMode(e.target.value);
});
}
const li = button.closest('li'); // Track List Actions Mode
const index = parseInt(li.dataset.index, 10); const trackListActionsMode = document.getElementById(
const type = li.dataset.type || 'api'; // Default to api if not present "track-list-actions-mode",
);
const instances = await api.settings.getInstances(type); if (trackListActionsMode) {
trackListActionsMode.value = trackListSettings.getMode();
trackListActionsMode.addEventListener("change", (e) => {
trackListSettings.setMode(e.target.value);
});
}
if (button.classList.contains('move-up') && index > 0) { // Compact Artist Toggle
[instances[index], instances[index - 1]] = [instances[index - 1], instances[index]]; const compactArtistToggle = document.getElementById("compact-artist-toggle");
} else if (button.classList.contains('move-down') && index < instances.length - 1) { if (compactArtistToggle) {
[instances[index], instances[index + 1]] = [instances[index + 1], instances[index]]; 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(); 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 () => { document
const btn = document.getElementById('clear-cache-btn'); .getElementById("api-instance-list")
const originalText = btn.textContent; ?.addEventListener("click", async (e) => {
btn.textContent = 'Clearing...'; const button = e.target.closest("button");
btn.disabled = true; 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 { try {
await api.clearCache(); await syncManager.clearCloudData();
btn.textContent = 'Cleared!'; alert("Cloud data cleared successfully.");
setTimeout(() => { authManager.signOut();
btn.textContent = originalText;
btn.disabled = false;
if (window.location.hash.includes('settings')) {
ui.renderApiSettings();
}
}, 1500);
} catch (error) { } catch (error) {
console.error('Failed to clear cache:', error); console.error("Failed to clear cloud data:", error);
btn.textContent = 'Error'; alert("Failed to clear cloud data: " + error.message);
setTimeout(() => {
btn.textContent = originalText;
btn.disabled = false;
}, 1500);
} }
}
}); });
document.getElementById('firebase-clear-cloud-btn')?.addEventListener('click', async () => { // Backup & Restore
if (confirm('Are you sure you want to delete ALL your data from the cloud? This cannot be undone.')) { document
try { .getElementById("export-library-btn")
await syncManager.clearCloudData(); ?.addEventListener("click", async () => {
alert('Cloud data cleared successfully.'); const data = await db.exportData();
authManager.signOut(); const blob = new Blob([JSON.stringify(data, null, 2)], {
} catch (error) { type: "application/json",
console.error('Failed to clear cloud data:', error); });
alert('Failed to clear cloud data: ' + error.message); 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 const importInput = document.getElementById("import-library-input");
document.getElementById('export-library-btn')?.addEventListener('click', async () => { document
const data = await db.exportData(); .getElementById("import-library-btn")
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); ?.addEventListener("click", () => {
const url = URL.createObjectURL(blob); importInput.click();
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'); importInput?.addEventListener("change", async (e) => {
document.getElementById('import-library-btn')?.addEventListener('click', () => { const file = e.target.files[0];
importInput.click(); if (!file) return;
});
importInput?.addEventListener('change', async (e) => { const reader = new FileReader();
const file = e.target.files[0]; reader.onload = async (event) => {
if (!file) return; try {
const data = JSON.parse(event.target.result);
const reader = new FileReader(); await db.importData(data);
reader.onload = async (event) => { alert("Library imported successfully!");
try { window.location.reload(); // Simple way to refresh all state
const data = JSON.parse(event.target.result); } catch (err) {
await db.importData(data); console.error("Import failed:", err);
alert('Library imported successfully!'); alert("Failed to import library. Please check the file format.");
window.location.reload(); // Simple way to refresh all state }
} catch (err) { };
console.error('Import failed:', err); reader.readAsText(file);
alert('Failed to import library. Please check the file format.'); });
}
};
reader.readAsText(file);
});
} }

12
package-lock.json generated
View file

@ -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",

View file

@ -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"
} }
} }

View file

@ -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'], includeAssets: ["instances.json", "discord.html"],
manifest: false // Use existing public/manifest.json manifest: false, // Use existing public/manifest.json
}) }),
] ],
}); });