diff --git a/index.html b/index.html
index 38a1f7c..400a4a6 100644
--- a/index.html
+++ b/index.html
@@ -3644,6 +3644,27 @@
+
Backup & Restore
diff --git a/js/app.js b/js/app.js
index 431b66f..d399923 100644
--- a/js/app.js
+++ b/js/app.js
@@ -1,6 +1,13 @@
//js/app.js
import { LosslessAPI } from './api.js';
-import { apiSettings, themeManager, nowPlayingSettings, downloadQualitySettings, sidebarSettings } from './storage.js';
+import {
+ apiSettings,
+ themeManager,
+ nowPlayingSettings,
+ downloadQualitySettings,
+ sidebarSettings,
+ pwaUpdateSettings,
+} from './storage.js';
import { UIRenderer } from './ui.js';
import { Player } from './player.js';
import { MultiScrobbler } from './multi-scrobbler.js';
@@ -1471,7 +1478,13 @@ document.addEventListener('DOMContentLoaded', async () => {
} else {
const updateSW = registerSW({
onNeedRefresh() {
- showUpdateNotification(() => updateSW(true));
+ if (pwaUpdateSettings.isAutoUpdateEnabled()) {
+ // Auto-update: immediately activate the new service worker
+ updateSW(true);
+ } else {
+ // Show notification with Update button and dismiss option
+ showUpdateNotification(() => updateSW(true));
+ }
},
onOfflineReady() {
console.log('App ready to work offline');
@@ -1558,6 +1571,12 @@ document.addEventListener('DOMContentLoaded', async () => {
});
function showUpdateNotification(updateCallback) {
+ // Remove any existing update notification
+ const existingNotification = document.querySelector('.update-notification');
+ if (existingNotification) {
+ existingNotification.remove();
+ }
+
const notification = document.createElement('div');
notification.className = 'update-notification';
notification.innerHTML = `
@@ -1565,7 +1584,15 @@ function showUpdateNotification(updateCallback) {
Update Available
A new version of Monochrome is available.
-
+
+
+
+
`;
document.body.appendChild(notification);
@@ -1578,6 +1605,10 @@ function showUpdateNotification(updateCallback) {
window.location.reload();
}
});
+
+ document.getElementById('dismiss-update-btn').addEventListener('click', () => {
+ notification.remove();
+ });
}
function showMissingTracksNotification(missingTracks) {
diff --git a/js/settings.js b/js/settings.js
index 491a20c..39bf616 100644
--- a/js/settings.js
+++ b/js/settings.js
@@ -27,6 +27,7 @@ import {
monoAudioSettings,
exponentialVolumeSettings,
audioEffectsSettings,
+ pwaUpdateSettings,
} from './storage.js';
import { audioContextManager, EQ_PRESETS } from './audio-context.js';
import { getButterchurnPresets } from './visualizers/butterchurn.js';
@@ -1777,6 +1778,73 @@ export function initializeSettings(scrobbler, player, api, ui) {
});
}
+ // PWA Auto-Update Toggle
+ const pwaAutoUpdateToggle = document.getElementById('pwa-auto-update-toggle');
+ if (pwaAutoUpdateToggle) {
+ pwaAutoUpdateToggle.checked = pwaUpdateSettings.isAutoUpdateEnabled();
+ pwaAutoUpdateToggle.addEventListener('change', (e) => {
+ pwaUpdateSettings.setAutoUpdateEnabled(e.target.checked);
+ });
+ }
+
+ // Reset Local Data Button
+ const resetLocalDataBtn = document.getElementById('reset-local-data-btn');
+ if (resetLocalDataBtn) {
+ resetLocalDataBtn.addEventListener('click', async () => {
+ if (
+ confirm(
+ 'WARNING: This will clear all local data including settings, cache, and library.\n\nAre you sure you want to continue?\n\n(Cloud-synced data will not be affected)'
+ )
+ ) {
+ try {
+ // Clear all localStorage
+ const keysToPreserve = [];
+ // Optionally preserve certain keys if needed
+
+ // Get all keys
+ const allKeys = Object.keys(localStorage);
+
+ // Clear each key except preserved ones
+ allKeys.forEach((key) => {
+ if (!keysToPreserve.includes(key)) {
+ localStorage.removeItem(key);
+ }
+ });
+
+ // Clear IndexedDB - try to clear individual stores, fallback to deleting database
+ try {
+ const stores = ['tracks', 'albums', 'artists', 'playlists', 'settings', 'history'];
+ for (const storeName of stores) {
+ try {
+ await db.performTransaction(storeName, 'readwrite', (store) => store.clear());
+ } catch (e) {
+ // Store might not exist, continue
+ }
+ }
+ } catch (dbError) {
+ console.log('Could not clear IndexedDB stores:', dbError);
+ // Try to delete the entire database as fallback
+ try {
+ const deleteRequest = indexedDB.deleteDatabase('monochrome-music');
+ await new Promise((resolve, reject) => {
+ deleteRequest.onsuccess = resolve;
+ deleteRequest.onerror = reject;
+ });
+ } catch (deleteError) {
+ console.log('Could not delete IndexedDB:', deleteError);
+ }
+ }
+
+ alert('All local data has been cleared. The app will now reload.');
+ window.location.reload();
+ } catch (error) {
+ console.error('Failed to reset local data:', error);
+ alert('Failed to reset local data: ' + error.message);
+ }
+ }
+ });
+ }
+
// Font Settings
initializeFontSettings();
diff --git a/js/storage.js b/js/storage.js
index c9bc7e3..9953e16 100644
--- a/js/storage.js
+++ b/js/storage.js
@@ -1703,3 +1703,20 @@ export const fontSettings = {
}));
},
};
+
+export const pwaUpdateSettings = {
+ STORAGE_KEY: 'pwa-auto-update-enabled',
+
+ isAutoUpdateEnabled() {
+ try {
+ // Default to true (auto-update) if not set
+ return localStorage.getItem(this.STORAGE_KEY) !== 'false';
+ } catch {
+ return true;
+ }
+ },
+
+ setAutoUpdateEnabled(enabled) {
+ localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false');
+ },
+};
diff --git a/styles.css b/styles.css
index d11761d..9487a92 100644
--- a/styles.css
+++ b/styles.css
@@ -3430,6 +3430,34 @@ input:checked + .slider::before {
align-items: stretch;
}
+.update-notification-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 0.75rem;
+}
+
+.update-notification-actions .btn-icon {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--muted-foreground);
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius);
+ cursor: pointer;
+ transition: all var(--transition);
+ flex-shrink: 0;
+}
+
+.update-notification-actions .btn-icon:hover {
+ background: var(--secondary);
+ color: var(--foreground);
+ border-color: var(--foreground);
+}
+
.close-shortcuts {
background: transparent;
border: none;