Pithos 0.3.17: Windows desktop app with update notices; accounts named after your Sia Storage email
This commit is contained in:
parent
19dc0649dd
commit
29ab1a5b9b
3 changed files with 92 additions and 5 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "pithos",
|
||||
"name": "Pithos",
|
||||
"version": "0.3.16",
|
||||
"version": "0.3.17",
|
||||
"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=",
|
||||
|
|
|
|||
|
|
@ -164,6 +164,8 @@ export async function createPithos(opts = {}) {
|
|||
const local = {
|
||||
host: hostName,
|
||||
pithos: pithosVersion,
|
||||
// electron-builder's portable exe sets this; updates then fetch the portable build.
|
||||
portable: hostName === 'desktop' && !!process.env.PORTABLE_EXECUTABLE_FILE,
|
||||
daemon: daemon.status(),
|
||||
external: await probeExternal(c),
|
||||
s3d: { ...v, pinned: S3D_VERSION, installable: !!assetForPlatform(), outdated: !!(v?.version && cmpVersion(v.version, S3D_VERSION) < 0) },
|
||||
|
|
@ -531,6 +533,20 @@ export async function createPithos(opts = {}) {
|
|||
// ---- free space on the Sia account ----------------------------------------
|
||||
// From the indexer (signed with s3d's app key). Cached for a minute.
|
||||
let spaceCache = { at: 0, value: null };
|
||||
// ---- desktop updates: the newest Pithos desktop build on dl.silentmode.st ----
|
||||
const RELEASES_URL = process.env.PITHOS_RELEASES_URL || 'https://dl.silentmode.st/releases-manifest.json';
|
||||
let releaseCache = null;
|
||||
route('GET', '/api/desktop-release', async (req, url) => {
|
||||
if (releaseCache && Date.now() - releaseCache.at < 10 * 60_000 && url.searchParams.get('fresh') !== '1') return releaseCache.body;
|
||||
const res = await fetch(`${RELEASES_URL}?t=${Math.floor(Date.now() / 60000)}`, { cache: 'no-store', signal: AbortSignal.timeout(15_000) }).catch((e) => { throw new HttpError(502, e.message); });
|
||||
if (!res.ok) throw new HttpError(502, `the release list answered ${res.status}`);
|
||||
const list = ((await res.json())?.releases || []).filter((e) => e.id === 'pithos-desktop' && /^\d+\.\d+\.\d+$/.test(e.version));
|
||||
const latest = list.reduce((best, e) => (!best || cmpVersion(e.version, best.version) > 0 ? e : best), null);
|
||||
const body = { latest: latest && { version: latest.version, date: latest.date || null, changes: String(latest.changes || '').slice(0, 600) } };
|
||||
releaseCache = { at: Date.now(), body };
|
||||
return body;
|
||||
});
|
||||
|
||||
route('GET', '/api/space', async () => {
|
||||
// On Silent Mode this call is forwarded: the server's s3d answers for its own folder.
|
||||
if (Date.now() - spaceCache.at < 60_000 && spaceCache.value) return spaceCache.value;
|
||||
|
|
@ -883,7 +899,10 @@ export async function createPithos(opts = {}) {
|
|||
login.cancel();
|
||||
for (const s of subscribers) s.end();
|
||||
await daemon.stop();
|
||||
await new Promise((r) => server.close(r));
|
||||
// Idle keep-alive sockets would hold close() for seconds; nobody is left to answer.
|
||||
const closed = new Promise((r) => server.close(r));
|
||||
server.closeAllConnections?.();
|
||||
await closed;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,7 +158,13 @@ const eventHandlers = {
|
|||
if (state.logs.length > 2000) state.logs.shift();
|
||||
logSink?.(l);
|
||||
},
|
||||
login(d) { state.login = d; emit(); },
|
||||
login(d) {
|
||||
const was = state.login?.state;
|
||||
state.login = d;
|
||||
emit();
|
||||
// the account card names the new account as soon as it is connected
|
||||
if (['done', 'already'].includes(d?.state) && d.state !== was) refreshNavAccount(true);
|
||||
},
|
||||
install(p) { if (p.phase === 'extract') toast('Verified checksum, installing…'); },
|
||||
move(p) { progressSink?.(p); },
|
||||
convert(p) { progressSink?.(p); },
|
||||
|
|
@ -432,6 +438,32 @@ function getStarted(main) {
|
|||
);
|
||||
|
||||
const go = (n) => { wiz.step = Math.max(0, Math.min(STEPS.length - 1, n)); draw(); };
|
||||
|
||||
// Sia Storage never tells an app who signed in, so the person types the
|
||||
// email while they are signing in there, and it becomes the account label.
|
||||
// One input lives across redraws, so typing survives the login events.
|
||||
function emailField() {
|
||||
if (!wiz.emailInput) {
|
||||
const inp = h('input', { type: 'email', placeholder: 'you@example.com', maxlength: 120, autocomplete: 'email' });
|
||||
let timer;
|
||||
const save = () => {
|
||||
clearTimeout(timer);
|
||||
const v = inp.value.trim();
|
||||
if (v === (wiz.savedEmail || '')) return;
|
||||
wiz.savedEmail = v;
|
||||
api('PUT', '/api/account/label', { label: v }).then(() => refreshNavAccount(true)).catch(() => {});
|
||||
};
|
||||
inp.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(save, 800); });
|
||||
inp.addEventListener('change', save);
|
||||
wiz.emailInput = inp;
|
||||
}
|
||||
const inp = wiz.emailInput;
|
||||
if (document.activeElement === inp) queueMicrotask(() => inp.focus());
|
||||
return h('label', { class: 'field', style: 'max-width:420px;margin-top:16px' },
|
||||
h('span', {}, 'Your Sia Storage email (optional)'),
|
||||
inp,
|
||||
h('small', {}, 'Sia Storage does not tell apps who you are, so Pithos names this account with the email you type here. You can change it later on Accounts.'));
|
||||
}
|
||||
const nav = (opts = {}) => h('div', { class: 'row wizard-nav' },
|
||||
wiz.step > 0 && wiz.step < STEPS.length - 1 && h('button', { class: 'btn', disabled: opts.backDisabled, onclick: () => go(wiz.step - 1) }, 'Back'),
|
||||
h('span', { style: 'flex:1' }),
|
||||
|
|
@ -661,6 +693,7 @@ function getStarted(main) {
|
|||
h('div', { class: 'row' },
|
||||
h('button', { class: wiz.autoOpened === false ? 'btn primary' : 'btn', onclick: () => openLink(l.approvalUrl) }, wiz.autoOpened === false ? 'Open the approval page ↗' : 'Open the approval page again ↗'),
|
||||
h('button', { class: 'btn small', onclick: () => copy(l.approvalUrl) }, 'Copy link')),
|
||||
emailField(),
|
||||
h('p', { class: 'muted row', style: 'margin-top:16px' }, h('span', { class: 'spinner' }), ' Waiting for your approval…'),
|
||||
nav({ next: false, extra: h('button', { class: 'btn danger', onclick: () => api('POST', '/api/login/cancel').then(() => go(3)).catch(fail) }, 'Cancel') }),
|
||||
];
|
||||
|
|
@ -807,7 +840,7 @@ function setup(main) {
|
|||
h('label', { class: 'field', style: 'max-width:420px' }, h('span', {}, 'Account label'),
|
||||
h('div', { class: 'row', style: 'flex-wrap:nowrap' }, label,
|
||||
h('button', { class: 'btn', onclick: async () => { try { await api('PUT', '/api/account/label', { label: label.value }); await loadAccount(); refreshNavAccount(true); toast('Label saved'); } catch (e) { fail(e); } } }, 'Save')),
|
||||
h('small', {}, 'Pithos cannot see the login email, so name it here: the email you approved the connection with is a good label.')),
|
||||
h('small', {}, 'Sia Storage does not tell apps your email, so name the account here: the email you approved the connection with is a good label.')),
|
||||
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'),
|
||||
|
|
@ -902,7 +935,7 @@ function setup(main) {
|
|||
await refreshStatus();
|
||||
await loadAccount();
|
||||
refreshNavAccount(true);
|
||||
Object.assign(wiz, { step: 1, accountReady: false, openedUrl: null, key: null });
|
||||
Object.assign(wiz, { step: 1, accountReady: false, openedUrl: null, key: null, emailInput: null, savedEmail: '' });
|
||||
navTo('#/start');
|
||||
} catch (e) { fail(e); }
|
||||
}
|
||||
|
|
@ -2498,6 +2531,40 @@ $('#unlock-btn').addEventListener('click', async () => {
|
|||
// 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';
|
||||
|
||||
// The desktop app: newer builds are listed in Silent Mode's release manifest
|
||||
// (read by the local server). The chip downloads the matching installer, or
|
||||
// the portable exe, in the browser.
|
||||
function desktopUpdates(current, { ver, chip, check }) {
|
||||
const file = state.status?.portable ? 'Pithos-portable.exe' : 'Pithos-Setup.exe';
|
||||
const rest = () => { chip.hidden = true; ver.hidden = false; };
|
||||
const flash = (text, cls, title) => {
|
||||
chip.hidden = false; ver.hidden = true; chip.className = `brandupd ${cls}`; chip.textContent = text; chip.title = title || ''; chip.disabled = true; chip.onclick = null;
|
||||
setTimeout(() => paint(), 2500);
|
||||
};
|
||||
let latest = null;
|
||||
function paint() {
|
||||
if (!latest || cmpSemver(latest.version, current) <= 0) return rest();
|
||||
chip.hidden = false; ver.hidden = true; chip.className = 'brandupd'; chip.disabled = false;
|
||||
chip.textContent = `↑ v${latest.version}`;
|
||||
chip.title = `Pithos v${latest.version} is available${latest.changes ? ': ' + latest.changes : ''}. Click to download it, then run it to update.`;
|
||||
chip.onclick = () => window.open(`https://dl.silentmode.st/latest/${file}`, '_blank', 'noopener');
|
||||
}
|
||||
async function scan(manual) {
|
||||
check.classList.add('spin');
|
||||
try {
|
||||
latest = (await api('GET', `/api/desktop-release${manual ? '?fresh=1' : ''}`)).latest;
|
||||
if (manual && !(latest && cmpSemver(latest.version, current) > 0)) flash('✓ Up to date', 'ok', `Pithos v${current} is the latest desktop release`);
|
||||
else paint();
|
||||
} catch (e) {
|
||||
if (manual) flash('⚠ Check failed', 'err', e.message);
|
||||
} finally { check.classList.remove('spin'); }
|
||||
}
|
||||
check.hidden = false;
|
||||
check.addEventListener('click', () => scan(true));
|
||||
setTimeout(() => scan(false), 1500);
|
||||
setInterval(() => scan(false), 6 * 3600 * 1000);
|
||||
}
|
||||
// s3d's S3 port is reachable only from this computer (the default).
|
||||
function localOnly() {
|
||||
const a = state.status?.config?.apiAddress || '';
|
||||
|
|
@ -2521,6 +2588,7 @@ function setupFooter() {
|
|||
if (bridge) {
|
||||
$('#brandlink').addEventListener('click', (e) => { e.preventDefault(); bridge.invoke('open-external', { url: 'https://pithos.sia/' }).catch(fail); });
|
||||
}
|
||||
if (!bridge && state.status?.host === 'desktop' && current) return desktopUpdates(current, { ver, chip, check });
|
||||
if (!bridge || !current) return; // only the Theseus add-on updates itself
|
||||
|
||||
let latest = null;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue