diff --git a/manifest.firefox.json b/manifest.firefox.json index 31e9417..b20038a 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -12,7 +12,8 @@ "permissions": [ "storage", "activeTab", - "scripting" + "scripting", + "notifications" ], "host_permissions": [ "https://claude.ai/*", diff --git a/manifest.json b/manifest.json index f16e63c..d73335f 100644 --- a/manifest.json +++ b/manifest.json @@ -6,7 +6,8 @@ "permissions": [ "storage", "activeTab", - "scripting" + "scripting", + "notifications" ], "host_permissions": [ "https://claude.ai/*", diff --git a/src/background/service-worker.js b/src/background/service-worker.js index 7f04898..a93cb58 100644 --- a/src/background/service-worker.js +++ b/src/background/service-worker.js @@ -146,6 +146,13 @@ const messageHandlers = { sendResponse({ count: tabCounts.get(tabId) || 0 }); }, + async 'sync:notification-seen'() { + // Options page opened — clear the sync badge and pending notification flag + await api.storage.local.remove('ss_sync_notification'); + api.action.setBadgeText({ text: '' }); + api.action.setBadgeBackgroundColor({ color: '#6b7280' }); + }, + async 'update:settings'(message) { await Storage.saveSettings(message.settings); @@ -309,6 +316,33 @@ api.storage.onChanged.addListener(async (changes, areaName) => { } } + // When a sync operation applied new data, show badge + notification + if (areaName === 'local' && changes.ss_sync_notification?.newValue) { + const notif = changes.ss_sync_notification.newValue; + const sourceLabel = { + 'file': 'sync folder', + 'browser-sync': 'browser account sync', + 'code': 'sync code import', + }[notif.source] || 'sync'; + + // Purple badge — persists until Options is opened + api.action.setBadgeText({ text: 'SYN' }); + api.action.setBadgeBackgroundColor({ color: '#7c3aed' }); + + // Desktop notification + try { + api.notifications.create('ss-sync-applied', { + type: 'basic', + iconUrl: 'icons/icon48.svg', + title: 'Silent Send — Settings Synced', + message: `Settings updated via ${sourceLabel}. Open Options to review.`, + priority: 1, + }); + } catch (e) { + // Notifications permission not granted — badge is still visible + } + } + // When sync storage changes (another device pushed new data), pull it into local if (areaName === 'sync' && (changes.ss_sync_meta || Object.keys(changes).some(k => k.startsWith('ss_sync_chunk_')))) { const settings = await Storage.getSettings(); @@ -318,6 +352,14 @@ api.storage.onChanged.addListener(async (changes, areaName) => { } }); +// Clicking a sync notification opens the Options page +api.notifications.onClicked.addListener((notificationId) => { + if (notificationId === 'ss-sync-applied') { + api.runtime.openOptionsPage(); + api.notifications.clear(notificationId); + } +}); + // --- Set initial state --- api.runtime.onInstalled.addListener(async () => { api.action.setBadgeBackgroundColor({ color: '#6b7280' }); @@ -329,6 +371,14 @@ api.runtime.onInstalled.addListener(async () => { (async () => { const settings = await Storage.getSettings(); await updateIcon(settings); + + // Restore the SYN badge if the user hasn't opened Options since the last sync + const stored = await api.storage.local.get('ss_sync_notification'); + if (stored.ss_sync_notification) { + api.action.setBadgeText({ text: 'SYN' }); + api.action.setBadgeBackgroundColor({ color: '#7c3aed' }); + } + if (settings.browserSync) { await SilentSendSync.pullFromSyncStorage(); } diff --git a/src/lib/sync.js b/src/lib/sync.js index e2a1540..6a6cfd6 100644 --- a/src/lib/sync.js +++ b/src/lib/sync.js @@ -65,7 +65,7 @@ const SilentSendSync = { } } - await this._applyData(data); + await this._applyData(data, 'code'); return { success: true, importTime: new Date(data.lastModified).toLocaleString() }; } catch (e) { return { success: false, reason: e.message }; @@ -131,7 +131,7 @@ const SilentSendSync = { const json = chunkKeys.map(k => chunkResult[k] || '').join(''); const data = JSON.parse(json); - await this._applyData(data); + await this._applyData(data, 'browser-sync'); return { imported: true, time: new Date(data.lastModified).toLocaleString() }; } catch (e) { console.warn('[Silent Send] pullFromSyncStorage failed:', e); @@ -154,8 +154,12 @@ const SilentSendSync = { }; }, - async _applyData(data) { - const toSet = { ss_lastModified: data.lastModified }; + async _applyData(data, source = 'unknown') { + const toSet = { + ss_lastModified: data.lastModified, + // Signal the service worker to show a badge/notification + ss_sync_notification: { source, time: Date.now() }, + }; if (data.identity !== undefined) toSet.ss_identity = data.identity; if (data.mappings !== undefined) toSet.ss_mappings = data.mappings; if (data.settings !== undefined) toSet.ss_settings = data.settings; diff --git a/src/options/options.html b/src/options/options.html index e283434..0634b24 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -147,8 +147,9 @@ Pick the same folder in each browser once. Changes are written to silent-send-sync.json automatically whenever settings change, and read back whenever this page is opened or regains focus. - Use a shared folder (Dropbox, OneDrive, iCloud Drive, or any network share) - for cross-computer sync too. + Works with any cloud storage that has a desktop sync client — + Dropbox, OneDrive, Google Drive, iCloud Drive, Box, pCloud, Nextcloud, + Synology Drive, or any network share. Just pick the cloud-synced folder.

diff --git a/src/options/options.js b/src/options/options.js index 4e29c99..20d5646 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -96,6 +96,10 @@ document.addEventListener('DOMContentLoaded', async () => { }); // --- File-based auto-sync --- + // Tell the service worker the user has seen any pending sync notification + api.runtime.sendMessage({ type: 'sync:notification-seen' }).catch(() => {}); + await api.storage.local.remove('ss_sync_notification'); + await initFileSync(); $('#btnPickSyncFolder').addEventListener('click', pickSyncFolder); @@ -559,7 +563,7 @@ async function checkFileSyncUpdate() { const local = await SilentSendSync._getAllData(); if (data.lastModified > (local.lastModified || 0)) { - await SilentSendSync._applyData(data); + await SilentSendSync._applyData(data, 'file'); mappings = await Storage.getMappings(); settings = await Storage.getSettings(); $('#browserSync').checked = settings.browserSync === true;