Merge pull request 'feat(extension): in-app update prompt + v1.0.9 (#1489)' (#231) from dev into main
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
extension / lint (push) Successful in 8s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 30s
Build images / sign-extension (push) Successful in 3m34s
Build images / build-web (push) Successful in 8s
CI / integration (push) Successful in 3m50s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
extension / lint (push) Successful in 8s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 30s
Build images / sign-extension (push) Successful in 3m34s
Build images / build-web (push) Successful in 8s
CI / integration (push) Successful in 3m50s
This commit was merged in pull request #231.
This commit is contained in:
@@ -31,6 +31,69 @@ browser.runtime.onInstalled.addListener(() => ensureInitialized());
|
|||||||
browser.runtime.onStartup.addListener(() => ensureInitialized());
|
browser.runtime.onStartup.addListener(() => ensureInitialized());
|
||||||
ensureInitialized().catch(e => console.error('init failed:', e));
|
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 ----
|
// ---- Discord token capture via webRequest ----
|
||||||
|
|
||||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||||
@@ -298,6 +361,9 @@ browser.runtime.onMessage.addListener(async (msg) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'CHECK_UPDATE':
|
||||||
|
return await checkForUpdateInfo();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { error: `Unknown message type: ${msg.type}` };
|
return { error: `Unknown message type: ${msg.type}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ class FabledCuratorAPI {
|
|||||||
const qs = new URLSearchParams({ url }).toString();
|
const qs = new URLSearchParams({ url }).toString();
|
||||||
return this.request('GET', `/extension/probe?${qs}`);
|
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.
|
// Connection test = the cheapest read with auth.
|
||||||
testConnection() {
|
testConnection() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "FabledCurator",
|
"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.",
|
"description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.",
|
||||||
|
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
"tabs",
|
"tabs",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"webRequest",
|
"webRequest",
|
||||||
"webRequestBlocking"
|
"webRequestBlocking",
|
||||||
|
"alarms"
|
||||||
],
|
],
|
||||||
|
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fabledcurator-extension",
|
"name": "fabledcurator-extension",
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Firefox extension for FabledCurator",
|
"description": "Firefox extension for FabledCurator",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -72,6 +72,17 @@ body {
|
|||||||
.btn.block { display: block; width: 100%; margin-top: 8px; }
|
.btn.block { display: block; width: 100%; margin-top: 8px; }
|
||||||
.btn.link { background: none; color: var(--on-surface-variant); padding: 4px; }
|
.btn.link { background: none; color: var(--on-surface-variant); padding: 4px; }
|
||||||
.btn.link:hover { color: var(--accent); }
|
.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 {
|
.source-row .play {
|
||||||
background: none; border: none; color: var(--on-surface-variant);
|
background: none; border: none; color: var(--on-surface-variant);
|
||||||
|
|||||||
@@ -20,6 +20,11 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="main-content" class="main hidden">
|
<section id="main-content" class="main hidden">
|
||||||
|
<div id="update-banner" class="update-banner hidden">
|
||||||
|
<span id="update-text"></span>
|
||||||
|
<button id="update-btn" class="btn primary small">Update</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<button class="tab active" data-tab="platforms">Platforms</button>
|
<button class="tab active" data-tab="platforms">Platforms</button>
|
||||||
<button class="tab" data-tab="sources">Sources</button>
|
<button class="tab" data-tab="sources">Sources</button>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ async function init() {
|
|||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
showPlatformsLoading();
|
showPlatformsLoading();
|
||||||
testConnectionIfNeeded();
|
testConnectionIfNeeded();
|
||||||
|
checkForUpdate();
|
||||||
loadPlatformStatus().catch(e => showError(`Failed to load platforms: ${e.message}`));
|
loadPlatformStatus().catch(e => showError(`Failed to load platforms: ${e.message}`));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showSetupRequired();
|
showSetupRequired();
|
||||||
@@ -63,6 +64,26 @@ function updateConnectionDot(connected) {
|
|||||||
d.title = connected ? 'Connected to FabledCurator' : 'Disconnected';
|
d.title = connected ? 'Connected to FabledCurator' : 'Disconnected';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nudge to reinstall when the configured instance publishes a newer signed XPI
|
||||||
|
// (the extension is self-hosted, so there's no Firefox auto-update). Never
|
||||||
|
// blocks the popup — a failed check just leaves the banner hidden.
|
||||||
|
async function checkForUpdate() {
|
||||||
|
try {
|
||||||
|
const r = await browser.runtime.sendMessage({ type: 'CHECK_UPDATE' });
|
||||||
|
if (r && r.updateAvailable && r.xpiUrl) showUpdateBanner(r);
|
||||||
|
} catch { /* non-fatal */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function showUpdateBanner(r) {
|
||||||
|
document.getElementById('update-text').textContent =
|
||||||
|
`Update available — v${r.latestVersion} (installed v${r.currentVersion})`;
|
||||||
|
// Opening the signed XPI triggers Firefox's native install prompt.
|
||||||
|
document.getElementById('update-btn').addEventListener('click', () => {
|
||||||
|
browser.tabs.create({ url: r.xpiUrl });
|
||||||
|
});
|
||||||
|
document.getElementById('update-banner').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
async function loadPlatformStatus() {
|
async function loadPlatformStatus() {
|
||||||
const status = await browser.runtime.sendMessage({ type: 'GET_PLATFORM_STATUS' });
|
const status = await browser.runtime.sendMessage({ type: 'GET_PLATFORM_STATUS' });
|
||||||
const c = document.getElementById('platforms-list');
|
const c = document.getElementById('platforms-list');
|
||||||
|
|||||||
Reference in New Issue
Block a user