diff --git a/ui/app.css b/ui/app.css index 9554806..dc1f16a 100644 --- a/ui/app.css +++ b/ui/app.css @@ -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); } diff --git a/ui/app.js b/ui/app.js index 26305c3..f6b8508 100644 --- a/ui/app.js +++ b/ui/app.js @@ -721,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'), @@ -750,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, ); @@ -767,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.'), @@ -778,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+/);