Theseus: bundle Pithos 0.3.14

This commit is contained in:
Local Dev 2026-10-04 15:56:01 +02:00
parent 828100e2ce
commit 39ebd87d9d
4 changed files with 87 additions and 26 deletions

View file

@ -1,7 +1,7 @@
{
"id": "pithos",
"name": "Pithos",
"version": "0.3.13",
"version": "0.3.14",
"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=",

View file

@ -131,6 +131,12 @@ module.exports = {
// 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" } = {}) => {
// In place, no restart (Theseus 0.3.77+): Theseus swaps the new version in,
// restarts the add-on and reloads this panel.
if (step === "apply-now") {
if (typeof api.applySelfUpdate !== "function") return { ok: false, reason: "needs a restart" };
return await api.applySelfUpdate();
}
if (step === "apply") {
if (typeof api.restartApp !== "function") return { fallback: "settings" };
return await api.restartApp();
@ -138,7 +144,7 @@ module.exports = {
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 };
return { staged, status: r?.status || "unknown", detail: r?.detail || null, current: r?.current || null, next: r?.next || null, inPlace: typeof api.applySelfUpdate === "function" };
});
api.onMessage("open-settings", ({ section = "" } = {}) => { api.openSettings?.(String(section)); return { ok: true }; });

View file

@ -442,3 +442,14 @@ nav a.nav-account.active .na-label { color: var(--accent); }
background: var(--surface); color: var(--text); font: inherit; font-size: 16px; cursor: pointer; }
.bar-btn:hover { border-color: var(--accent); color: var(--accent); }
}
/* Account switcher on the Accounts page */
.acct-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr)); gap: 10px; }
.acct-tile { display: flex; flex-direction: column; gap: 4px; padding: 12px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2); min-width: 0; align-items: flex-start; }
.acct-tile .row.spread { width: 100%; }
.acct-tile.current { border-color: var(--accent); background: var(--accent-soft); }
.acct-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.acct-tile .btn { margin-top: auto; }
.acct-tile.add { appearance: none; font: inherit; color: var(--text); cursor: pointer; border-style: dashed; background: transparent; text-align: left; justify-content: center; }
.acct-tile.add:hover { border-color: var(--accent); }
.acct-plus { font-size: 20px; line-height: 1; color: var(--accent); }

View file

@ -91,12 +91,15 @@ function modal(title, body, buttons = []) {
const onKey = (e) => { if (e.key === 'Escape') close(undefined); };
const foot = h('div', { class: 'foot' }, buttons.map((b) => h('button', {
class: `btn ${b.kind || ''}`, type: b.submit ? 'submit' : 'button',
onclick: b.submit ? null : async () => { const v = b.value ? await b.value() : b.result; if (v !== false) close(v); },
// A fixed result (Cancel: false/null) always closes; only a value()
// action that returns false keeps the dialog open (e.g. Copy link).
onclick: b.submit ? null : async () => { if (!b.value) return close(b.result); const v = await b.value(); if (v !== false) close(v); },
}, b.label)));
const form = h('form', { class: 'modal', onsubmit: async (e) => {
e.preventDefault();
const b = buttons.find((x) => x.submit);
const v = b.value ? await b.value() : b.result;
if (!b.value) return close(b.result);
const v = await b.value();
if (v !== false) close(v);
} }, h('h2', {}, title), body, foot);
const root = h('div', { class: 'modal-backdrop', onmousedown: (e) => { if (e.target === root) close(undefined); } }, form);
@ -718,22 +721,50 @@ function account(main, rest) {
}
// Earlier Sia accounts set aside on this computer, each one click away.
const archiveName = (a) => a.label || (a.indexerUrl ? new URL(a.indexerUrl).host : a.name);
async function switchToArchive(a, after) {
const name = archiveName(a);
const ok = await confirmModal(`Switch to ${name}?`, 's3d stops and switches back to that Sia account with its users, keys and drives. The current account is set aside and you can switch back the same way.', 'Switch', 'primary');
if (!ok) return;
try {
await api('POST', '/api/account/restore', { name: a.name });
await api('POST', '/api/daemon/start').catch(() => {});
toast(`Switched to ${name}`);
await refreshStatus();
await loadAccount();
refreshNavAccount(true);
after();
} catch (e) { fail(e); }
}
// The account switcher: every Sia account on this computer side by side, the
// one in use marked, the others one click away, and Add another account.
function accountSwitcher(onAdd, after) {
const list = accountInfo?.archives || [];
const cur = h('div', { class: 'acct-tile current' },
h('div', { class: 'row spread' }, h('span', { class: 'badge ok' }, 'in use'), h('span', { class: 'small muted' }, accountInfo?.indexerUrl ? new URL(accountInfo.indexerUrl).host : '')),
h('strong', { class: 'acct-name' }, accountName(accountInfo)),
h('span', { class: 'small muted', id: 'acct-space' }, ' '));
api('GET', '/api/space').then((sp) => { if (sp?.available) put(cur.querySelector('#acct-space'), `${fmtBytes(sp.remainingStorage)} free of ${fmtBytes(sp.maxPinnedData)}`); }).catch(() => {});
return h('div', { class: 'card acct-switcher' },
h('div', { class: 'row spread', style: 'margin-bottom:10px' }, h('h2', { style: 'margin:0' }, 'Your Sia accounts'),
list.length ? h('span', { class: 'small muted' }, `${list.length + 1} on this computer`) : null),
h('div', { class: 'acct-tiles' },
cur,
list.map((a) => h('div', { class: 'acct-tile' },
h('span', { class: 'small muted' }, a.archivedAt ? `set aside ${fmtDate(a.archivedAt)}` : 'set aside'),
h('strong', { class: 'acct-name' }, archiveName(a)),
h('span', { class: 'small muted' }, [a.fingerprint && `key ${a.fingerprint}`, a.users.length && `users: ${a.users.join(', ')}`].filter(Boolean).join(' · ')),
a.movedTo ? h('span', { class: 'small muted' }, `Drives moved to ${a.movedTo}; kept as a backup.`)
: h('button', { class: 'btn small', disabled: !a.hasData, onclick: () => switchToArchive(a, after) }, 'Switch to'))),
h('button', { type: 'button', class: 'acct-tile add', onclick: onAdd }, h('span', { class: 'acct-plus' }, '+'), h('strong', {}, 'Add another account'), h('span', { class: 'small muted' }, 'This one is set aside, ready to switch back to'))));
}
function otherAccounts(after) {
const list = accountInfo?.archives || [];
if (!list.length) return null;
async function restore(a) {
const name = a.label || (a.indexerUrl ? new URL(a.indexerUrl).host : a.name);
const ok = await confirmModal(`Switch to ${name}?`, 's3d stops and switches back to that Sia account with its users, keys and drives. The current account is set aside and you can switch back the same way.', 'Switch', 'primary');
if (!ok) return;
try {
await api('POST', '/api/account/restore', { name: a.name });
await api('POST', '/api/daemon/start').catch(() => {});
toast(`Switched to ${name}`);
await refreshStatus();
await loadAccount();
refreshNavAccount(true);
after();
} catch (e) { fail(e); }
return switchToArchive(a, after);
}
return h('div', {},
h('h3', {}, 'Other accounts on this computer'),
@ -747,9 +778,11 @@ function otherAccounts(after) {
function setup(main) {
const body = h('div', { class: 'card' });
const switcher = h('div', { hidden: true, style: 'margin-bottom:16px' });
main.append(
h('div', { class: 'page-head' }, h('div', {}, h('h1', {}, 'Account'),
h('p', { class: 'muted' }, 'The Sia account that stores, and pays for, everything on this s3d.'))),
h('div', { class: 'page-head' }, h('div', {}, h('h1', {}, 'Accounts'),
h('p', { class: 'muted' }, 'Your Sia accounts on this computer. The one in use stores, and pays for, everything on this s3d.'))),
switcher,
body,
);
@ -764,7 +797,10 @@ function setup(main) {
if (l.state === 'done' || l.state === 'already') {
const label = h('input', { type: 'text', placeholder: 'e.g. the email you sign in with', value: accountInfo?.label || '', maxlength: 120 });
put(switcher, accountSwitcher(switchAccount, draw));
switcher.hidden = false;
put(body,
h('div', { class: 'small muted', style: 'text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px' }, 'Account in use'),
h('h2', {}, accountInfo?.label || (l.indexerUrl ? new URL(l.indexerUrl).host : 'Connected to Sia')),
h('p', { class: 'row small', style: 'margin-top:-6px' }, h('span', { class: 'badge ok' }, 'connected'), accountInfo?.label && l.indexerUrl && h('span', { class: 'muted' }, new URL(l.indexerUrl).host)),
h('p', {}, l.indexerUrl ? ['This s3d uses a Sia account on ', h('code', {}, l.indexerUrl), '. All S3 users, drives and files on it are stored, and paid for, through that account.'] : 'This s3d is registered with an indexer.'),
@ -775,11 +811,10 @@ function setup(main) {
siaAccountBlock(accountInfo, async () => { try { accountInfo = await api('GET', '/api/account?fresh=1'); draw(); } catch (e) { fail(e); } }),
h('div', { class: 'row', style: 'margin-top:16px' },
daemonState() !== 'running' && h('button', { class: 'btn primary', onclick: async () => { await daemonAction(daemonState() === 'crashed' ? 'restart' : 'start'); navTo('#/overview'); } }, 'Start s3d'),
h('a', { class: 'btn', href: '#/users' }, 'Users & keys'),
h('button', { class: 'btn', onclick: switchAccount }, 'Add another account…')),
archivesBlock());
h('a', { class: 'btn', href: '#/users' }, 'Users & keys')));
return;
}
switcher.hidden = true;
if (l.state === 'confirm') {
const words = (l.generatedPhrase || '').split(/\s+/);
@ -1162,16 +1197,19 @@ function itemMenu(items) {
btn.addEventListener('click', (e) => {
e.stopPropagation();
document.querySelectorAll('.menu').forEach((m) => m.remove());
const menu = h('div', { class: 'menu', role: 'menu' }, items.filter(Boolean).map(([label, run, kind]) =>
h('button', { type: 'button', role: 'menuitem', class: kind || '', onclick: (ev) => { ev.stopPropagation(); menu.remove(); run(); } }, label)));
// Picking an item must also drop the listeners, or a stale Escape handler
// swallows Escape for every dialog opened afterwards.
let menu;
function done() { menu?.remove(); document.removeEventListener('mousedown', away, true); document.removeEventListener('keydown', esc, true); }
menu = h('div', { class: 'menu', role: 'menu' }, items.filter(Boolean).map(([label, run, kind]) =>
h('button', { type: 'button', role: 'menuitem', class: kind || '', onclick: (ev) => { ev.stopPropagation(); done(); run(); } }, label)));
document.body.append(menu);
const r = btn.getBoundingClientRect();
const w = menu.offsetWidth, ht = menu.offsetHeight;
menu.style.left = Math.max(8, Math.min(r.right - w, innerWidth - w - 8)) + 'px';
menu.style.top = (r.bottom + 4 + ht > innerHeight - 8 ? Math.max(8, r.top - ht - 4) : r.bottom + 4) + 'px';
const done = () => { menu.remove(); document.removeEventListener('mousedown', away, true); document.removeEventListener('keydown', esc, true); };
const away = (ev) => { if (!menu.contains(ev.target)) done(); };
const esc = (ev) => { if (ev.key === 'Escape') { ev.stopPropagation(); done(); btn.focus(); } };
function away(ev) { if (!menu.contains(ev.target)) done(); }
function esc(ev) { if (ev.key === 'Escape') { ev.stopPropagation(); done(); btn.focus(); } }
setTimeout(() => { document.addEventListener('mousedown', away, true); document.addEventListener('keydown', esc, true); }, 0);
menu.querySelector('button')?.focus();
});
@ -2476,6 +2514,12 @@ function setupFooter() {
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}` : '';
// Newer Theseus applies it in place: the panel reloads on the new version.
if (r.inPlace) {
showChip(`Updating to${next}…`, 'wait', 'Pithos reloads on the new version in a moment', null);
const a = await bridge.invoke('update', { step: 'apply-now' }).catch(() => null);
if (a?.ok) return;
}
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' });