Theseus: bundle Pithos 0.3.14
This commit is contained in:
parent
828100e2ce
commit
39ebd87d9d
4 changed files with 87 additions and 26 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "pithos",
|
"id": "pithos",
|
||||||
"name": "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.",
|
"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",
|
"author": "Silent Mode",
|
||||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,12 @@ module.exports = {
|
||||||
// Theseus asks the user before restarting. Older Theseus builds lack
|
// Theseus asks the user before restarting. Older Theseus builds lack
|
||||||
// these hooks; the panel then points to Settings > Extensions.
|
// these hooks; the panel then points to Settings > Extensions.
|
||||||
api.onMessage("update", async ({ step = "stage" } = {}) => {
|
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 (step === "apply") {
|
||||||
if (typeof api.restartApp !== "function") return { fallback: "settings" };
|
if (typeof api.restartApp !== "function") return { fallback: "settings" };
|
||||||
return await api.restartApp();
|
return await api.restartApp();
|
||||||
|
|
@ -138,7 +144,7 @@ module.exports = {
|
||||||
if (typeof api.checkAndStageSelfUpdate !== "function") return { fallback: "settings" };
|
if (typeof api.checkAndStageSelfUpdate !== "function") return { fallback: "settings" };
|
||||||
const r = await api.checkAndStageSelfUpdate();
|
const r = await api.checkAndStageSelfUpdate();
|
||||||
const staged = r?.status === "staged" || r?.status === "already-staged";
|
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 }; });
|
api.onMessage("open-settings", ({ section = "" } = {}) => { api.openSettings?.(String(section)); return { ok: true }; });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
background: var(--surface); color: var(--text); font: inherit; font-size: 16px; cursor: pointer; }
|
||||||
.bar-btn:hover { border-color: var(--accent); color: var(--accent); }
|
.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); }
|
||||||
|
|
|
||||||
|
|
@ -91,12 +91,15 @@ function modal(title, body, buttons = []) {
|
||||||
const onKey = (e) => { if (e.key === 'Escape') close(undefined); };
|
const onKey = (e) => { if (e.key === 'Escape') close(undefined); };
|
||||||
const foot = h('div', { class: 'foot' }, buttons.map((b) => h('button', {
|
const foot = h('div', { class: 'foot' }, buttons.map((b) => h('button', {
|
||||||
class: `btn ${b.kind || ''}`, type: b.submit ? 'submit' : '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)));
|
}, b.label)));
|
||||||
const form = h('form', { class: 'modal', onsubmit: async (e) => {
|
const form = h('form', { class: 'modal', onsubmit: async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const b = buttons.find((x) => x.submit);
|
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);
|
if (v !== false) close(v);
|
||||||
} }, h('h2', {}, title), body, foot);
|
} }, h('h2', {}, title), body, foot);
|
||||||
const root = h('div', { class: 'modal-backdrop', onmousedown: (e) => { if (e.target === root) close(undefined); } }, form);
|
const root = h('div', { class: 'modal-backdrop', onmousedown: (e) => { if (e.target === root) close(undefined); } }, form);
|
||||||
|
|
@ -718,11 +721,9 @@ function account(main, rest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Earlier Sia accounts set aside on this computer, each one click away.
|
// Earlier Sia accounts set aside on this computer, each one click away.
|
||||||
function otherAccounts(after) {
|
const archiveName = (a) => a.label || (a.indexerUrl ? new URL(a.indexerUrl).host : a.name);
|
||||||
const list = accountInfo?.archives || [];
|
async function switchToArchive(a, after) {
|
||||||
if (!list.length) return null;
|
const name = archiveName(a);
|
||||||
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');
|
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;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -734,6 +735,36 @@ function otherAccounts(after) {
|
||||||
refreshNavAccount(true);
|
refreshNavAccount(true);
|
||||||
after();
|
after();
|
||||||
} catch (e) { fail(e); }
|
} 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) {
|
||||||
|
return switchToArchive(a, after);
|
||||||
}
|
}
|
||||||
return h('div', {},
|
return h('div', {},
|
||||||
h('h3', {}, 'Other accounts on this computer'),
|
h('h3', {}, 'Other accounts on this computer'),
|
||||||
|
|
@ -747,9 +778,11 @@ function otherAccounts(after) {
|
||||||
|
|
||||||
function setup(main) {
|
function setup(main) {
|
||||||
const body = h('div', { class: 'card' });
|
const body = h('div', { class: 'card' });
|
||||||
|
const switcher = h('div', { hidden: true, style: 'margin-bottom:16px' });
|
||||||
main.append(
|
main.append(
|
||||||
h('div', { class: 'page-head' }, h('div', {}, h('h1', {}, 'Account'),
|
h('div', { class: 'page-head' }, h('div', {}, h('h1', {}, 'Accounts'),
|
||||||
h('p', { class: 'muted' }, 'The Sia account that stores, and pays for, everything on this s3d.'))),
|
h('p', { class: 'muted' }, 'Your Sia accounts on this computer. The one in use stores, and pays for, everything on this s3d.'))),
|
||||||
|
switcher,
|
||||||
body,
|
body,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -764,7 +797,10 @@ function setup(main) {
|
||||||
|
|
||||||
if (l.state === 'done' || l.state === 'already') {
|
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 });
|
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,
|
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('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', { 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.'),
|
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); } }),
|
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' },
|
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'),
|
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('a', { class: 'btn', href: '#/users' }, 'Users & keys')));
|
||||||
h('button', { class: 'btn', onclick: switchAccount }, 'Add another account…')),
|
|
||||||
archivesBlock());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
switcher.hidden = true;
|
||||||
|
|
||||||
if (l.state === 'confirm') {
|
if (l.state === 'confirm') {
|
||||||
const words = (l.generatedPhrase || '').split(/\s+/);
|
const words = (l.generatedPhrase || '').split(/\s+/);
|
||||||
|
|
@ -1162,16 +1197,19 @@ function itemMenu(items) {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
document.querySelectorAll('.menu').forEach((m) => m.remove());
|
document.querySelectorAll('.menu').forEach((m) => m.remove());
|
||||||
const menu = h('div', { class: 'menu', role: 'menu' }, items.filter(Boolean).map(([label, run, kind]) =>
|
// Picking an item must also drop the listeners, or a stale Escape handler
|
||||||
h('button', { type: 'button', role: 'menuitem', class: kind || '', onclick: (ev) => { ev.stopPropagation(); menu.remove(); run(); } }, label)));
|
// 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);
|
document.body.append(menu);
|
||||||
const r = btn.getBoundingClientRect();
|
const r = btn.getBoundingClientRect();
|
||||||
const w = menu.offsetWidth, ht = menu.offsetHeight;
|
const w = menu.offsetWidth, ht = menu.offsetHeight;
|
||||||
menu.style.left = Math.max(8, Math.min(r.right - w, innerWidth - w - 8)) + 'px';
|
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';
|
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); };
|
function away(ev) { if (!menu.contains(ev.target)) done(); }
|
||||||
const away = (ev) => { if (!menu.contains(ev.target)) done(); };
|
function esc(ev) { if (ev.key === 'Escape') { ev.stopPropagation(); done(); btn.focus(); } }
|
||||||
const esc = (ev) => { if (ev.key === 'Escape') { ev.stopPropagation(); done(); btn.focus(); } };
|
|
||||||
setTimeout(() => { document.addEventListener('mousedown', away, true); document.addEventListener('keydown', esc, true); }, 0);
|
setTimeout(() => { document.addEventListener('mousedown', away, true); document.addEventListener('keydown', esc, true); }, 0);
|
||||||
menu.querySelector('button')?.focus();
|
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 || '');
|
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}` : '';
|
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 () => {
|
showChip(`↻ Restart to apply${next}`, '', `Pithos${next} is ready. Theseus restarts to apply it and reopens your tabs.`, async () => {
|
||||||
try {
|
try {
|
||||||
const a = await bridge.invoke('update', { step: 'apply' });
|
const a = await bridge.invoke('update', { step: 'apply' });
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue