diff --git a/bundled-addons/pithos/addon.json b/bundled-addons/pithos/addon.json index 1685801b..33eae35d 100644 --- a/bundled-addons/pithos/addon.json +++ b/bundled-addons/pithos/addon.json @@ -1,7 +1,7 @@ { "id": "pithos", "name": "Pithos", - "version": "0.3.7", + "version": "0.3.8", "description": "Run s3d, the Sia S3 gateway, from the Theseus sidebar: connect it to a Sia indexer, create S3 users and access keys, browse and share buckets, and watch uploads reach Sia.", "author": "Silent Mode", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=", diff --git a/bundled-addons/pithos/core/server.js b/bundled-addons/pithos/core/server.js index 67590ea3..25fba25f 100644 --- a/bundled-addons/pithos/core/server.js +++ b/bundled-addons/pithos/core/server.js @@ -65,6 +65,13 @@ export async function createPithos(opts = {}) { const tickets = new Map(); const subscribers = new Set(); let versionCache = null; + // Pithos's own version: addon.json in the Theseus add-on, package.json elsewhere. + const pithosVersion = (() => { + for (const f of ['../addon.json', '../package.json']) { + try { const v = JSON.parse(fs.readFileSync(new URL(f, import.meta.url), 'utf8')).version; if (v) return v; } catch {} + } + return null; + })(); const broadcast = (event, data) => { const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; @@ -144,6 +151,7 @@ export async function createPithos(opts = {}) { const v = await version(); return { host: hostName, + pithos: pithosVersion, daemon: daemon.status(), external: await probeExternal(c), s3d: { ...v, pinned: S3D_VERSION, installable: !!assetForPlatform(), outdated: !!(v?.version && cmpVersion(v.version, S3D_VERSION) < 0) }, diff --git a/bundled-addons/pithos/index.js b/bundled-addons/pithos/index.js index 0531eaa8..a9238c1f 100644 --- a/bundled-addons/pithos/index.js +++ b/bundled-addons/pithos/index.js @@ -117,6 +117,22 @@ module.exports = { return { url: p.viewUrl(apiPath) }; }); + // Footer updater: stage the newest signed Pithos from the extension + // channel, then (second click) ask Theseus to restart and apply it. + // Theseus asks the user before restarting. Older Theseus builds lack + // these hooks; the panel then points to Settings > Extensions. + api.onMessage("update", async ({ step = "stage" } = {}) => { + if (step === "apply") { + if (typeof api.restartApp !== "function") return { fallback: "settings" }; + return await api.restartApp(); + } + if (typeof api.checkAndStageSelfUpdate !== "function") return { fallback: "settings" }; + const r = await api.checkAndStageSelfUpdate(); + const staged = r?.status === "staged" || r?.status === "already-staged"; + return { staged, status: r?.status || "unknown", detail: r?.detail || null, current: r?.current || null, next: r?.next || null }; + }); + api.onMessage("open-settings", ({ section = "" } = {}) => { api.openSettings?.(String(section)); return { ok: true }; }); + // A full browser-tab view, for anyone who prefers it to the sidebar. api.onMessage("open-in-tab", async () => { const p = await ensureServer(); diff --git a/bundled-addons/pithos/ui/app.css b/bundled-addons/pithos/ui/app.css index 75fd5b95..5aa2c246 100644 --- a/bundled-addons/pithos/ui/app.css +++ b/bundled-addons/pithos/ui/app.css @@ -351,3 +351,27 @@ body.has-player .toasts { bottom: 84px; } .pl-btn { width: 28px; height: 28px; font-size: 14px; } .pl-main { width: 34px; height: 34px; } } + +/* Footer: pithos.sia, the version and the updater (like Aegis's) */ +.brandfoot { position: fixed; left: 0; right: 0; bottom: 0; z-index: 14; height: 28px; display: flex; align-items: center; + justify-content: space-between; padding: 0 12px; background: var(--surface); border-top: 1px solid var(--border); + font-size: 11.5px; color: var(--muted); } +@media (min-width: 761px) { .brandfoot { left: 220px; } } +.brandlink { display: inline-flex; align-items: center; gap: 5px; color: var(--muted); text-decoration: none; padding: 2px 4px; border-radius: 4px; } +.brandlink:hover { color: var(--accent); } +.brandfoot-right { display: inline-flex; align-items: center; gap: 8px; } +.brandver { font-variant-numeric: tabular-nums; letter-spacing: .3px; } +.brandcheck { appearance: none; background: none; border: 0; color: var(--muted); cursor: pointer; padding: 0 2px; font: inherit; + font-size: 13px; line-height: 1; opacity: .75; } +.brandcheck:hover { color: var(--accent); opacity: 1; } +.brandcheck.spin { animation: spin 1s linear infinite; } +.brandupd { appearance: none; border: 0; font: inherit; font-size: 11.5px; font-weight: 600; padding: 2px 9px; border-radius: 999px; + background: var(--accent-soft); color: var(--accent); cursor: pointer; white-space: nowrap; } +.brandupd:disabled { cursor: default; } +.brandupd.ok, .brandupd.wait { background: var(--surface-2); color: var(--muted); font-weight: 500; } +.brandupd.err { background: var(--danger-soft); color: var(--danger); font-weight: 500; } +body.has-foot main { padding-bottom: 64px; } +body.has-foot .player { bottom: 28px; } +body.has-foot .toasts { bottom: 44px; } +body.has-foot.has-player main { padding-bottom: 124px; } +body.has-foot.has-player .toasts { bottom: 112px; } diff --git a/bundled-addons/pithos/ui/app.js b/bundled-addons/pithos/ui/app.js index 8a03d863..68068d52 100644 --- a/bundled-addons/pithos/ui/app.js +++ b/bundled-addons/pithos/ui/app.js @@ -1752,6 +1752,87 @@ $('#unlock-btn').addEventListener('click', async () => { } }); +// ---- Footer: pithos.sia, the version, and (in Theseus) the updater ----------- +// Like Aegis: checks the extension channel when Pithos opens and every few +// hours; a newer release replaces the version with "↑ vX". One click +// downloads and verifies it, the next restarts Theseus to apply it. +const UPDATES_URL = 'https://navigate.st/bns/theseus.x/extensions/pithos/updates.json'; +function cmpSemver(a, b) { + const pa = String(a || '0').split('.').map((n) => Number(n) || 0); + const pb = String(b || '0').split('.').map((n) => Number(n) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { const d = (pa[i] || 0) - (pb[i] || 0); if (d) return d < 0 ? -1 : 1; } + return 0; +} +function setupFooter() { + const foot = $('#brandfoot'); + const ver = $('#brandver'); + const chip = $('#brandupd'); + const check = $('#brandcheck'); + const current = state.status?.pithos || null; + ver.textContent = current ? `v${current}` : ''; + foot.hidden = false; + document.body.classList.add('has-foot'); + if (bridge) { + $('#brandlink').addEventListener('click', (e) => { e.preventDefault(); bridge.invoke('open-external', { url: 'https://pithos.sia/' }).catch(fail); }); + } + if (!bridge || !current) return; // only the Theseus add-on updates itself + + let latest = null; + let busy = false; + const showChip = (text, cls, title, onclick) => { + chip.hidden = false; ver.hidden = true; + chip.className = `brandupd${cls ? ' ' + cls : ''}`; + chip.textContent = text; chip.title = title || ''; + chip.disabled = !onclick; chip.onclick = onclick || null; + }; + const rest = () => { chip.hidden = true; ver.hidden = false; }; + const flash = (text, cls, title) => { showChip(text, cls, title, null); setTimeout(() => { if (!busy && chip.classList.contains(cls)) paint(); }, 2500); }; + function paint() { + if (latest && cmpSemver(latest, current) > 0) showChip(`↑ v${latest}`, '', `Pithos v${latest} is available. Click to install it.`, install); + else rest(); + } + async function scan(manual) { + check.classList.add('spin'); + try { + const r = await fetch(`${UPDATES_URL}?t=${Math.floor(Date.now() / 60000)}`, { cache: 'no-store' }); + if (!r.ok) throw new Error(`HTTP ${r.status}`); + const list = (await r.json())?.addons || []; + latest = list.reduce((best, e) => (!best || cmpSemver(e.version, best) > 0 ? e.version : best), null); + if (busy) return; + if (manual && !(latest && cmpSemver(latest, current) > 0)) flash('✓ Up to date', 'ok', `Pithos v${current} is the latest release`); + else paint(); + } catch (e) { + if (manual) flash('⚠ Check failed', 'err', e.message); + } finally { check.classList.remove('spin'); } + } + async function install() { + busy = true; + showChip('Downloading…', 'wait', 'Downloading and verifying the signed release', null); + try { + const r = await bridge.invoke('update', { step: 'stage' }); + if (r?.fallback) { + busy = false; + return showChip('Update in Settings', '', 'This Theseus updates extensions in Settings › Extensions', () => bridge.invoke('open-settings', { section: 'addons' }).catch(fail)); + } + if (!r?.staged) { + busy = false; + return r?.status === 'up-to-date' ? flash('✓ Up to date', 'ok', '') : flash('⚠ Update failed', 'err', r?.detail || r?.status || ''); + } + const next = r.next ? ` v${r.next}` : ''; + showChip(`↻ Restart to apply${next}`, '', `Pithos${next} is ready. Theseus restarts to apply it and reopens your tabs.`, async () => { + try { + const a = await bridge.invoke('update', { step: 'apply' }); + if (a?.deferred) showChip(`Applies on next start${next}`, 'ok', 'Pithos updates the next time Theseus starts', null); + } catch (e) { flash('⚠ Restart failed', 'err', e.message); } + }); + } catch (e) { busy = false; flash('⚠ Update failed', 'err', e.message); } + } + check.hidden = false; + check.addEventListener('click', () => scan(true)); + setTimeout(() => scan(false), 1500); + setInterval(() => scan(false), 6 * 3600 * 1000); +} + // The full-page tab opened from the Theseus panel: one button back. function addBackToSidebar() { const row = h('div', { class: 'host-controls' }, @@ -1789,6 +1870,7 @@ async function boot() { state.logSeq = state.logs.at(-1)?.seq || 0; } catch {} await refreshStatus(); + setupFooter(); if (!bridge && state.status?.host === 'theseus') addBackToSidebar(); connectEvents(); window.addEventListener('hashchange', route); diff --git a/bundled-addons/pithos/ui/index.html b/bundled-addons/pithos/ui/index.html index ffa9cb6e..71da02c1 100644 --- a/bundled-addons/pithos/ui/index.html +++ b/bundled-addons/pithos/ui/index.html @@ -46,6 +46,14 @@ +