diff --git a/extension/background/background.js b/extension/background/background.js index 5b93be1..e3e3bc7 100644 --- a/extension/background/background.js +++ b/extension/background/background.js @@ -31,6 +31,69 @@ browser.runtime.onInstalled.addListener(() => ensureInitialized()); browser.runtime.onStartup.addListener(() => ensureInitialized()); ensureInitialized().catch(e => console.error('init failed:', e)); +// ---- Extension self-update check (#1489) ---- +// Installed per-instance from the operator's FC host, so Firefox's static +// update_url can't apply (each instance has a different host). Instead ask the +// configured backend for the latest published version and nudge the operator to +// reinstall the freshly-signed XPI — surfaced as a popup banner (on demand) and +// a toolbar badge (daily). /api/extension/manifest is public and returns +// {version, latest_url, sha256}; the XPI is served from the web root (not /api). + +function versionIsNewer(candidate, current) { + // Dotted numeric compare so 1.0.10 > 1.0.9 (a plain string compare wouldn't). + const a = String(candidate).split('.').map(n => parseInt(n, 10) || 0); + const b = String(current).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(a.length, b.length); i++) { + if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) > (b[i] || 0); + } + return false; +} + +async function checkForUpdateInfo() { + await ensureInitialized(); + if (!api.isConfigured()) return { updateAvailable: false, configured: false }; + let info; + try { + info = await api.getExtensionManifest(); + } catch (e) { + return { updateAvailable: false, error: e.message }; + } + const currentVersion = browser.runtime.getManifest().version; + const latestVersion = info && info.version ? info.version : null; + // latest_url is served from the web root; strip the /api suffix off baseUrl + // (same transform as OPEN_ARTIST_PAGE). + const base = (api.baseUrl || '').replace(/\/+$/, '').replace(/\/api$/, ''); + return { + updateAvailable: !!latestVersion && versionIsNewer(latestVersion, currentVersion), + currentVersion, + latestVersion, + xpiUrl: info && info.latest_url ? `${base}${info.latest_url}` : null, + }; +} + +async function refreshUpdateBadge() { + let r; + try { r = await checkForUpdateInfo(); } catch { return; } + try { + await browser.action.setBadgeText({ text: r.updateAvailable ? '↑' : '' }); + if (r.updateAvailable) { + await browser.action.setBadgeBackgroundColor({ color: '#F4BA7A' }); + await browser.action.setTitle({ title: `FabledCurator — update available (v${r.latestVersion})` }); + } else { + await browser.action.setTitle({ title: 'FabledCurator' }); + } + } catch { /* action API unavailable — non-fatal */ } +} + +// Daily proactive check (needs the "alarms" permission). create() is idempotent +// by name, so re-running it on each event-page load is safe. +browser.alarms.create('fc-update-check', { periodInMinutes: 24 * 60, delayInMinutes: 1 }); +browser.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === 'fc-update-check') refreshUpdateBadge(); +}); +browser.runtime.onStartup.addListener(() => refreshUpdateBadge()); +browser.runtime.onInstalled.addListener(() => refreshUpdateBadge()); + // ---- Discord token capture via webRequest ---- browser.webRequest.onBeforeSendHeaders.addListener( @@ -298,6 +361,9 @@ browser.runtime.onMessage.addListener(async (msg) => { } } + case 'CHECK_UPDATE': + return await checkForUpdateInfo(); + default: return { error: `Unknown message type: ${msg.type}` }; } diff --git a/extension/lib/api.js b/extension/lib/api.js index 37b2c74..2554a97 100644 --- a/extension/lib/api.js +++ b/extension/lib/api.js @@ -89,6 +89,12 @@ class FabledCuratorAPI { const qs = new URLSearchParams({ url }).toString(); return this.request('GET', `/extension/probe?${qs}`); } + // Latest published extension version on this instance — drives the in-app + // update prompt. Public endpoint (no key needed, but request() sends it + // harmlessly). Returns {version, xpi_url, latest_url, sha256}. + getExtensionManifest() { + return this.request('GET', '/extension/manifest'); + } // Connection test = the cheapest read with auth. testConnection() { diff --git a/extension/manifest.json b/extension/manifest.json index c5f12c9..4348ba1 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "FabledCurator", - "version": "1.0.8", + "version": "1.0.9", "description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.", "browser_specific_settings": { @@ -22,7 +22,8 @@ "tabs", "activeTab", "webRequest", - "webRequestBlocking" + "webRequestBlocking", + "alarms" ], "host_permissions": [ diff --git a/extension/package.json b/extension/package.json index 0f96df1..16a9858 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1,6 +1,6 @@ { "name": "fabledcurator-extension", - "version": "1.0.8", + "version": "1.0.9", "private": true, "description": "Firefox extension for FabledCurator", "scripts": { diff --git a/extension/popup/popup.css b/extension/popup/popup.css index 7f796f8..c3c942e 100644 --- a/extension/popup/popup.css +++ b/extension/popup/popup.css @@ -72,6 +72,17 @@ body { .btn.block { display: block; width: 100%; margin-top: 8px; } .btn.link { background: none; color: var(--on-surface-variant); padding: 4px; } .btn.link:hover { color: var(--accent); } +.btn.small { padding: 6px 12px; font-size: 13px; } + +/* In-app update prompt (accent-tinted so it reads as an actionable notice). */ +.update-banner { + display: flex; align-items: center; gap: 10px; + margin: 10px 10px 0; padding: 10px 12px; + background: rgba(244, 186, 122, 0.12); + border: 1px solid rgba(244, 186, 122, 0.4); + border-radius: 6px; +} +#update-text { flex: 1; font-size: 13px; } .source-row .play { background: none; border: none; color: var(--on-surface-variant); diff --git a/extension/popup/popup.html b/extension/popup/popup.html index d9d9ddf..e41615b 100644 --- a/extension/popup/popup.html +++ b/extension/popup/popup.html @@ -20,6 +20,11 @@