aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.js95
-rw-r--r--src/features/ark.js56
-rw-r--r--src/i18n.js2
-rw-r--r--src/locales/ar.json4
-rw-r--r--src/locales/bn.json4
-rw-r--r--src/locales/de.json4
-rw-r--r--src/locales/es.json4
-rw-r--r--src/locales/fr.json4
-rw-r--r--src/locales/hi.json4
-rw-r--r--src/locales/id.json4
-rw-r--r--src/locales/ja.json4
-rw-r--r--src/locales/ko.json4
-rw-r--r--src/locales/mr.json4
-rw-r--r--src/locales/pt.json4
-rw-r--r--src/locales/ru.json4
-rw-r--r--src/locales/sw.json4
-rw-r--r--src/locales/ta.json4
-rw-r--r--src/locales/te.json4
-rw-r--r--src/locales/tr.json4
-rw-r--r--src/locales/ur.json4
-rw-r--r--src/locales/vi.json4
-rw-r--r--src/locales/zh.json4
-rw-r--r--src/style.css2
23 files changed, 162 insertions, 69 deletions
diff --git a/src/app.js b/src/app.js
index 64564be..46a7054 100644
--- a/src/app.js
+++ b/src/app.js
@@ -2515,9 +2515,11 @@ function lock({ offerPassword = false } = {}) {
// ================================================================ WALLET
// The avatar IS your profile now — one tap opens it (edit lives there).
// Settings and Lock stand beside it as their own icons; no popup menu.
+// The nostr identity the header avatar stands for, when there is one.
+const headerIdentity = () => (featureHook('nostrLoginIdentity') || {}).pubkey || (wallet.nostrPubkey && wallet.nostrPubkey())
+ || (activeAccount() || {}).nostrPk || null;
function avatarMenu() {
- const me = (featureHook('nostrLoginIdentity') || {}).pubkey || (wallet.nostrPubkey && wallet.nostrPubkey())
- || (activeAccount() || {}).nostrPk;
+ const me = headerIdentity();
// Older watch-only accounts may have no remembered Nostr identity. Keep
// account access available even when there is no profile to open.
const node = (me && featureHook('headerAvatar', me))
@@ -2527,13 +2529,11 @@ function avatarMenu() {
});
return h('button', {
class: 'header-avatar',
- title: me ? t('profYourProfile') : t('accounts'),
- 'aria-label': me ? t('profYourProfile') : t('accounts'),
+ title: me ? t('profYourProfile') : t('walletSettings'),
+ 'aria-label': me ? t('profYourProfile') : t('walletSettings'),
onClick: () => {
clearFeatureNav(); ui.screen = 'wallet';
- if (!me || !featureHook('showProfile', me)) {
- ui.screen = 'accounts';
- }
+ if (!me || !featureHook('showProfile', me)) { openWalletSettings(); return; }
render();
},
}, node);
@@ -3089,17 +3089,18 @@ function claimTargets() {
// Account switcher: pick a wallet, add another, or lock the session.
+function logoutConfirmScreen() {
+ return h('div', { class: 'col', style: 'gap:16px' },
+ brandHeader(false),
+ h('div', { class: 'card col' },
+ h('h3', {}, t('logout') + '?'),
+ h('p', { class: 'small muted' }, t('logoutPopBlurb')),
+ h('button', { class: 'btn-primary btn-block', onClick: () => { ui.logoutConfirm = null; ctx.logout(); } }, t('logout')),
+ h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.logoutConfirm = null; render(); } }, t('back'))));
+}
function accountsScreen() {
if (ui.pw) return h('div', { class: 'col', style: 'gap:16px' }, brandHeader(false), pwPromptCard());
- if (ui.logoutConfirm) {
- return h('div', { class: 'col', style: 'gap:16px' },
- brandHeader(false),
- h('div', { class: 'card col' },
- h('h3', {}, t('logout') + '?'),
- h('p', { class: 'small muted' }, t('logoutPopBlurb')),
- h('button', { class: 'btn-primary btn-block', onClick: () => { ui.logoutConfirm = null; ctx.logout(); } }, t('logout')),
- h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.logoutConfirm = null; render(); } }, t('back'))));
- }
+ if (ui.logoutConfirm) return logoutConfirmScreen();
if (ui.addWallet) {
const aw = ui.addWallet;
const fulls = accounts.filter((x) => x.type === 'full' && x.mnemonic);
@@ -3387,20 +3388,53 @@ function autolockCard(a) {
);
}
-// Per-wallet settings screen — reached from Accounts → ⚙ for any account
-// (not just the active one; full accounts keep their seed in memory).
+// The balance card's gear: one face's settings. Spending leads with its
+// coins (renewal, the cooperative move, a unilateral exit and what it costs);
+// both carry the lock, the password and the recovery phrase that used to sit
+// on the Accounts page.
+function openWalletSettings(view) {
+ clearFeatureNav();
+ ui.settingsId = activeId;
+ ui.settingsView = view || accountSel();
+ ui.editLabel = null; ui.revealShown = false; ui.pubkeyShown = false; ui.loadSeed = null;
+ ui.screen = 'accountSettings';
+ render();
+ try { window.scrollTo(0, 0); } catch {}
+}
+function securityCard() {
+ if (!activeAccount()) return null;
+ // With a nostr identity, signing in elsewhere and logging out live on your
+ // profile; a wallet without one (an old watch-only) has no profile, so
+ // they live here.
+ const anon = !headerIdentity();
+ return h('div', { class: 'card col', style: 'gap:8px' },
+ h('h3', {}, t('securityTitle')),
+ lockBtn(),
+ hasVault() ? h('button', { class: 'btn-ghost btn-block', onClick: startChangePw }, t('changePassword')) : null,
+ anon ? h('button', { class: 'btn-block', onClick: signInAnother }, t('signInAnother')) : null,
+ anon ? h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.logoutConfirm = true; render(); } }, t('logout')) : null);
+}
function accountSettingsScreen() {
+ if (ui.pw) return h('div', { class: 'col', style: 'gap:16px' }, brandHeader(false), pwPromptCard());
+ if (ui.logoutConfirm) return logoutConfirmScreen();
const a = accounts.find((x) => x.id === ui.settingsId) || activeAccount();
if (!a) { ui.screen = 'accounts'; return accountsScreen(); }
+ const view = ui.settingsView || accountSel();
+ const spending = view === 'spending' && a.id === activeId;
+ const leave = () => { ui.editLabel = null; ui.revealShown = false; ui.pubkeyShown = false; ui.loadSeed = null; goBack(() => { ui.screen = 'wallet'; }); };
return h(
'div',
{ class: 'col', style: 'gap:16px' },
- brandHeader(false),
+ brandHeader(!!activeAccount()),
+ h('div', { class: 'row gap6', style: 'align-items:center;margin:2px 0 -4px 4px' },
+ h('span', { style: 'display:flex;color:var(--muted)', html: GEAR_ICON(20) }),
+ h('h3', { style: 'margin:0' }, viewLabel(a, view))),
+ spending ? featureHook('spendingSettings') : null,
+ securityCard(),
recoveryCard(a),
- walletNameCard(a),
- pubkeyCard(a),
+ view === 'spending' ? null : pubkeyCard(a),
autolockCard(a),
- h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.editLabel = null; ui.revealShown = false; ui.pubkeyShown = false; ui.loadSeed = null; goBack(() => { ui.screen = 'accounts'; }); } }, t('back'))
+ h('button', { class: 'btn-ghost btn-block', onClick: leave }, t('back'))
);
}
@@ -4153,6 +4187,7 @@ function setAccountSel(a, dir) {
render();
}
+const GEAR_ICON = (n) => `<svg width="${n}" height="${n}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" style="display:block"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.09a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.09a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`;
const SWAP_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block"><path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/></svg>';
function balanceCard() {
@@ -4185,12 +4220,13 @@ function balanceCard() {
// peeks at the edge and drags/snaps into place (native scroll-snap), the
// way wallet cards behave in BlueWallet. Snapping IS the account switch.
// Single-purpose and savings-only wallets keep the lone face.
- const manageBtn = () => h('button', {
- class: 'balance-switch',
- onClick: () => { ui.screen = 'accounts'; render(); },
- },
- h('span', { html: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block"><rect x="2" y="6" width="20" height="14" rx="2"/><path d="M2 10h20"/><circle cx="17" cy="15" r="1.3"/></svg>' }),
- h('span', { class: 'bsw-label' }, t('manageAccounts')));
+ // Each face's own settings: Spending's coins and exits, the seed, the lock.
+ const manageBtn = (view) => h('button', {
+ class: 'balance-switch balance-gear',
+ title: t('walletSettings'), 'aria-label': t('walletSettings'),
+ onClick: () => openWalletSettings(view),
+ html: GEAR_ICON(17),
+ });
const faceFor = (view) => {
const viewSpending = view === 'spending';
return h('div', { class: 'balance-face' },
@@ -4208,7 +4244,7 @@ function balanceCard() {
h('div', { class: 'small faint', style: 'text-transform:uppercase;letter-spacing:.06em' },
kindLocked ? viewLabel(acc0, view)
: hasSpending ? (viewSpending ? t('spendingLabel') : t('savingLabel')) : t('balance'))),
- manageBtn()),
+ manageBtn(view)),
h('div', { class: 'amt', style: firstLoad ? 'opacity:.3' : '' },
firstLoad ? h('span', { class: 'spinner sm', style: 'margin-right:8px' }) : null,
animatedAmount('bal:' + view, viewSpending ? spending : saving), ' ', unitTag('unit')),
@@ -5714,6 +5750,7 @@ async function importSnapshotFile(e) {
// only invoked at runtime (first render happens after loadLocale below).
const ctx = {
h, ui, render, wallet, toast, copy, copyBtn, pasteBtn, blankSend, goBack, canGoBack, goHome, openExternal, showSend,
+ openWalletSettings: (view) => openWalletSettings(view),
fmtAmount, unitLabel, unitTag, getUnit: () => unit, toggleUnit, download,
// in fiat the typed figure is money, so it needs today's price to become sats
parseAmount: (v, u) => parseAmount(v, u, rateNow()),
diff --git a/src/features/ark.js b/src/features/ark.js
index 87bb5c9..16786ec 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -1364,7 +1364,7 @@ export function arkFeature(ctx) {
// A renewal in flight, beside the in-progress exits: the tap opens the
// coins page, where the round's progress is spelled out.
function arkRefreshActionItem(a) {
- return h('div', { class: 'item', style: 'cursor:pointer', onClick: () => { ui.arkCoinsPage = true; render(); } },
+ return h('div', { class: 'item', style: 'cursor:pointer', onClick: () => ctx.openWalletSettings('spending') },
h('div', { class: 'ico' }, '\u21bb'),
h('div', { class: 'grow' },
h('div', { class: 'row gap6', style: 'align-items:center' },
@@ -2190,7 +2190,7 @@ export function arkFeature(ctx) {
// The screen behind that notice's Manage link: every spendable coin with
// its expiry and arkoor depth, what an uncooperative exit costs today, and
// what a renewal costs and buys.
- function arkCoinsPage() {
+ function arkCoinsPage({ embedded = false } = {}) {
// The notice that opens this page reads the persisted state; so must the
// page, or a tap before the manager has connected (the first seconds
// after boot) found no coins and closed itself — Manage needed two taps.
@@ -2206,8 +2206,13 @@ export function arkFeature(ctx) {
const inRound = new Set(renewing.flatMap((a) => a.inputIds || []));
const pendingCoins = ((st && st.vtxos) || []).filter((v) => v.state === 'pending' && inRound.has(v.id));
const tip = (mgr && mgr._tipH) || 0;
- const back = () => ctx.goBack(() => { ui.arkCoinsPage = null; ui.arkCoinsSel = null; }); // the page is a history entry now
- if (!spend.length && !pendingCoins.length) { ui.arkCoinsPage = null; return null; } // nothing left to manage
+ const back = embedded
+ ? () => { ui.arkCoinsSel = null; ctx.goHome(); }
+ : () => ctx.goBack(() => { ui.arkCoinsPage = null; ui.arkCoinsSel = null; }); // the page is a history entry now
+ if (!spend.length && !pendingCoins.length) {
+ if (embedded) return h('div', { class: 'card col', style: 'gap:6px' }, h('h3', { style: 'margin:0' }, t('arkCoinsTitle')), h('p', { class: 'small muted', style: 'margin:0' }, t('arkCoinsNone')));
+ ui.arkCoinsPage = null; return null; // nothing left to manage
+ }
if (!mgr) connectArk().then(() => render()).catch(() => {});
const totalSat = spend.reduce((n, v) => n + v.amountSat, 0);
const heldSat = totalSat + pendingCoins.reduce((n, v) => n + v.amountSat, 0);
@@ -2325,9 +2330,18 @@ export function arkFeature(ctx) {
} }, ui.arkBusy === 'exit' ? h('span', { class: 'spinner sm' }) : t('arkExitConfirmGo'))),
h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.arkExitConfirm = null; render(); } }, t('cancel')));
}
+ // what a unilateral exit costs, which used to sit under the balance (and
+ // only when the coins' chains ran deep): here it is always said
+ let depth = null;
+ if (embedded) {
+ const cached = wallet.loadFeatureState('arkDepth', null);
+ const fee = exitFee || (cached && cached.exitFee) || 0;
+ if (fee > 0) depth = t('arkDepthNotice', { fee: fmtSats(fee) });
+ }
return h('div', { class: 'col', style: 'gap:16px' },
h('div', { class: 'card col', style: 'gap:8px' },
h('h3', { style: 'margin:0' }, t('arkCoinsTitle')),
+ depth ? h('div', { class: 'notice info small' }, depth) : null,
h('p', { class: 'small muted', style: 'margin:0' },
!spend.length
? t('arkCoinsIntroRenewing', { total: fmtSats(heldSat) + ' sats' })
@@ -2414,7 +2428,7 @@ export function arkFeature(ctx) {
h('p', { class: 'small muted', style: 'margin:0' },
t('arkCoinsExitDesc', { fee: fmtSats(exitFee), after: fmtSats(afterFee) })),
h('button', { class: 'btn-ghost btn-block', disabled: !!ui.arkBusy || !spend.length || !mgr, onClick: () => { ui.arkExitConfirm = true; render(); } }, t('arkCoinsExitBtn'))),
- h('button', { class: 'btn-ghost btn-block', onClick: back }, t('back')));
+ embedded ? null : h('button', { class: 'btn-ghost btn-block', onClick: back }, t('back')));
}
// What each server charges and promises, from its own published info —
@@ -2946,7 +2960,7 @@ export function arkFeature(ctx) {
h('div', { class: 'row gap6' },
h('button', { class: 'btn-ghost grow', onClick: () => {
ui.arkOffboardSend = null; ui.arkError = ''; ui.send = blankSend();
- if (o.fromCoins) ctx.goBack(() => { ui.arkCoinsPage = true; }); else render();
+ if (o.fromCoins) ctx.goBack(() => ctx.openWalletSettings('spending')); else render();
} }, t('back')),
ui.arkBusy === 'offboard'
? h('button', { class: 'btn-primary grow', disabled: true }, h('span', { class: 'spinner sm' }))
@@ -3480,6 +3494,11 @@ export function arkFeature(ctx) {
new Promise((r) => setTimeout(r, 800)),
]);
},
+ // the Spending face's settings page leads with its coins
+ spendingSettings() {
+ if (wallet.watchOnly) return null; // a locked wallet can't renew or exit
+ return arkCoinsPage({ embedded: true });
+ },
// the wallet's own pages keep the top nav: these are pages, not takeovers
screenView() {
const page = ui.arkCoinsPage ? arkCoinsPage() : ui.arkExitPage ? arkExitPage() : null;
@@ -3933,15 +3952,14 @@ export function arkFeature(ctx) {
});
else if (expiredSat > 0) lines.push({ text: t('arkExpiredDust', { amount: fmtSats(expiredSat) + ' sats' }), manage: true });
}
- const depthNotice = exitDepthNotice();
- if (depthNotice) lines.push({ text: depthNotice, manage: true });
+ // What a unilateral exit would cost is the Spending settings page's
+ // business (the gear on the card), not a line under the balance.
// An upcoming renewal that will COST something — stated before it
// happens. Ark is new to nearly everyone, and a fee nobody announced
// reads as money gone missing (it did, once). Free renewals — the
// normal case now — pass without comment, and one line under the
- // balance is enough: the depth advisory outranks this forecast, and
- // Manage carries the full story either way.
- if (!depthNotice && ark && ark.state && ark._tipH && !wallet.watchOnly) {
+ // balance is enough: the settings page carries the full story.
+ if (ark && ark.state && ark._tipH && !wallet.watchOnly) {
const spend = (ark.state.vtxos || []).filter((v) => v.state === 'spendable' && v.expiryHeight && !v.expiryRejected);
if (spend.length) {
const tip = ark._tipH;
@@ -3963,16 +3981,12 @@ export function arkFeature(ctx) {
});
}
}
- const manageBtn = () => h('button', { class: 'linklike small', style: 'align-self:flex-end', onClick: () => { ui.arkCoinsPage = true; render(); } }, t('arkDepthBtn'));
- // Management follows the Spending balance, independently of notices
- // or renewal eligibility. Keep expired coins reachable as well.
- const balance = arkBalance();
- const coins = (arkStateNow()?.vtxos || []).some((v) => v.state === 'spendable');
- const hasBalance = balance && balance.spendableSat + balance.pendingSat > 0;
- const manage = hasBalance || coins || lines.some((l) => l.manage) ? manageBtn() : null;
- if (!lines.length) {
- return manage ? [h('div', { class: 'small muted', style: 'margin:10px 0 0;text-align:center' }, manage)] : [];
- }
+ // The coins themselves are always one tap away on the card's gear; a
+ // notice that is about them links straight there.
+ const manage = lines.some((l) => l.manage)
+ ? h('button', { class: 'linklike small', style: 'align-self:flex-end', onClick: () => ctx.openWalletSettings('spending') }, t('arkDepthBtn'))
+ : null;
+ if (!lines.length) return [];
const err = lines.some((l) => l.err);
// one plain advisory keeps its quiet centred line; anything more, or
// anything urgent, is a card
diff --git a/src/i18n.js b/src/i18n.js
index 9b258b1..a7bc987 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -473,6 +473,7 @@ const STR = {
arkDepthRenewed: 'Renewal requested',
arkDepthRenewBtn: 'Renew now',
arkCoinsTitle: 'Spending coins',
+ arkCoinsNone: 'No coins in Spending yet.',
arkCoinsIntro: 'Your Spending balance lives in {n} coins totalling {total}. Each can be pulled on-chain without the server’s help, at a cost that grows with the coin’s history.',
arkCoinsIntroOne: 'Your Spending balance lives in a single coin of {total}. It can be pulled on-chain without the server’s help, at a cost that grows with the coin’s history.',
arkCoinsColAmount: 'Amount',
@@ -816,6 +817,7 @@ const STR = {
explorerUrlHint: 'Esplora / electrs REST base, e.g. https://mempool.space/api',
walletName: 'Account name',
walletSettings: 'Account settings',
+ securityTitle: 'Security',
walletSettingsDesc: 'Name, recovery phrase, public key, and auto-logout for this account.',
autolockTitle: 'Log out automatically',
autolockDesc: "After the app has been in the background this long, log this wallet out. It's removed from this device (including from Save to device), so keep its recovery phrase. Other accounts aren't affected. The timer never runs while the app is open and focused.",
diff --git a/src/locales/ar.json b/src/locales/ar.json
index 09b789e..a3693d5 100644
--- a/src/locales/ar.json
+++ b/src/locales/ar.json
@@ -878,5 +878,7 @@
"followHistRestore": "استعادة {n}",
"followHistNone": "لا توجد نسخة أقدم فيها أحد تفتقده قائمتك.",
"followHistNothing": "كل من في تلك النسخة موجود في قائمتك بالفعل.",
- "followHistRestored": "تمت استعادة {n} متابَعًا."
+ "followHistRestored": "تمت استعادة {n} متابَعًا.",
+ "securityTitle": "الأمان",
+ "arkCoinsNone": "لا توجد عملات في الإنفاق بعد."
}
diff --git a/src/locales/bn.json b/src/locales/bn.json
index c5fc411..07c7fe2 100644
--- a/src/locales/bn.json
+++ b/src/locales/bn.json
@@ -878,5 +878,7 @@
"followHistRestore": "{n} জন ফিরিয়ে আনুন",
"followHistNone": "কোনো পুরোনো কপিতে এমন কেউ নেই যিনি আপনার তালিকায় নেই।",
"followHistNothing": "ওই কপির সবাই ইতিমধ্যে আপনার তালিকায় আছেন।",
- "followHistRestored": "{n} জনকে আবার ফলো করা হয়েছে।"
+ "followHistRestored": "{n} জনকে আবার ফলো করা হয়েছে।",
+ "securityTitle": "নিরাপত্তা",
+ "arkCoinsNone": "স্পেন্ডিং-এ এখনও কোনো কয়েন নেই।"
}
diff --git a/src/locales/de.json b/src/locales/de.json
index 626d548..75cbf74 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -878,5 +878,7 @@
"followHistRestore": "{n} wiederherstellen",
"followHistNone": "Keine ältere Kopie enthält jemanden, der in deiner Liste fehlt.",
"followHistNothing": "Alle aus dieser Kopie sind schon in deiner Liste.",
- "followHistRestored": "{n} Follows wiederhergestellt."
+ "followHistRestored": "{n} Follows wiederhergestellt.",
+ "securityTitle": "Sicherheit",
+ "arkCoinsNone": "Noch keine Coins in Ausgaben."
}
diff --git a/src/locales/es.json b/src/locales/es.json
index db30441..21adaa2 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -878,5 +878,7 @@
"followHistRestore": "Restaurar {n}",
"followHistNone": "Ninguna copia anterior tiene a alguien que falte en tu lista.",
"followHistNothing": "Todos los de esa copia ya están en tu lista.",
- "followHistRestored": "Se restauraron {n} seguidos."
+ "followHistRestored": "Se restauraron {n} seguidos.",
+ "securityTitle": "Seguridad",
+ "arkCoinsNone": "Aún no hay monedas en Gastos."
}
diff --git a/src/locales/fr.json b/src/locales/fr.json
index aeccf9c..899a586 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -878,5 +878,7 @@
"followHistRestore": "Restaurer {n}",
"followHistNone": "Aucune copie plus ancienne ne contient quelqu’un qui manque à votre liste.",
"followHistNothing": "Tout le monde dans cette copie est déjà dans votre liste.",
- "followHistRestored": "{n} abonnements restaurés."
+ "followHistRestored": "{n} abonnements restaurés.",
+ "securityTitle": "Sécurité",
+ "arkCoinsNone": "Aucune pièce dans Dépenses pour l’instant."
}
diff --git a/src/locales/hi.json b/src/locales/hi.json
index e683ef2..b26525c 100644
--- a/src/locales/hi.json
+++ b/src/locales/hi.json
@@ -878,5 +878,7 @@
"followHistRestore": "{n} बहाल करें",
"followHistNone": "किसी पुरानी कॉपी में ऐसा कोई नहीं जो आपकी सूची में न हो।",
"followHistNothing": "उस कॉपी के सभी लोग पहले से आपकी सूची में हैं।",
- "followHistRestored": "{n} फ़ॉलो बहाल किए गए।"
+ "followHistRestored": "{n} फ़ॉलो बहाल किए गए।",
+ "securityTitle": "सुरक्षा",
+ "arkCoinsNone": "स्पेंडिंग में अभी कोई कॉइन नहीं।"
}
diff --git a/src/locales/id.json b/src/locales/id.json
index beebc86..cff81be 100644
--- a/src/locales/id.json
+++ b/src/locales/id.json
@@ -214,5 +214,7 @@
"followHistRestore": "Pulihkan {n}",
"followHistNone": "Tidak ada salinan lama yang berisi orang yang hilang dari daftarmu.",
"followHistNothing": "Semua orang di salinan itu sudah ada di daftarmu.",
- "followHistRestored": "{n} ikutan dipulihkan."
+ "followHistRestored": "{n} ikutan dipulihkan.",
+ "securityTitle": "Keamanan",
+ "arkCoinsNone": "Belum ada koin di Belanja."
}
diff --git a/src/locales/ja.json b/src/locales/ja.json
index 9745063..58a188e 100644
--- a/src/locales/ja.json
+++ b/src/locales/ja.json
@@ -878,5 +878,7 @@
"followHistRestore": "{n}人を復元",
"followHistNone": "あなたのリストに欠けている人を含む古いコピーはありません。",
"followHistNothing": "そのコピーの全員がすでにリストにいます。",
- "followHistRestored": "{n}人のフォローを復元しました。"
+ "followHistRestored": "{n}人のフォローを復元しました。",
+ "securityTitle": "セキュリティ",
+ "arkCoinsNone": "支出にはまだコインがありません。"
}
diff --git a/src/locales/ko.json b/src/locales/ko.json
index 098bb7e..84da178 100644
--- a/src/locales/ko.json
+++ b/src/locales/ko.json
@@ -877,5 +877,7 @@
"followHistRestore": "{n}명 복원",
"followHistNone": "목록에 빠진 사람이 들어 있는 예전 사본이 없습니다.",
"followHistNothing": "그 사본의 모든 사람이 이미 목록에 있습니다.",
- "followHistRestored": "팔로우 {n}명을 복원했습니다."
+ "followHistRestored": "팔로우 {n}명을 복원했습니다.",
+ "securityTitle": "보안",
+ "arkCoinsNone": "지출에 아직 코인이 없습니다."
}
diff --git a/src/locales/mr.json b/src/locales/mr.json
index fa63503..66239cd 100644
--- a/src/locales/mr.json
+++ b/src/locales/mr.json
@@ -214,5 +214,7 @@
"followHistRestore": "{n} पुनर्संचयित करा",
"followHistNone": "तुमच्या यादीत नसलेले कोणी असलेली जुनी प्रत नाही.",
"followHistNothing": "त्या प्रतीतील सर्वजण आधीच तुमच्या यादीत आहेत.",
- "followHistRestored": "{n} फॉलो पुनर्संचयित केले."
+ "followHistRestored": "{n} फॉलो पुनर्संचयित केले.",
+ "securityTitle": "सुरक्षा",
+ "arkCoinsNone": "स्पेंडिंगमध्ये अद्याप कोणतेही कॉइन नाहीत."
}
diff --git a/src/locales/pt.json b/src/locales/pt.json
index e9e0ef8..3723f3f 100644
--- a/src/locales/pt.json
+++ b/src/locales/pt.json
@@ -878,5 +878,7 @@
"followHistRestore": "Restaurar {n}",
"followHistNone": "Nenhuma cópia antiga tem alguém que falte na sua lista.",
"followHistNothing": "Todos dessa cópia já estão na sua lista.",
- "followHistRestored": "{n} seguidos restaurados."
+ "followHistRestored": "{n} seguidos restaurados.",
+ "securityTitle": "Segurança",
+ "arkCoinsNone": "Ainda não há moedas em Gastos."
}
diff --git a/src/locales/ru.json b/src/locales/ru.json
index b89fbe0..66fb170 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -878,5 +878,7 @@
"followHistRestore": "Восстановить {n}",
"followHistNone": "Нет старых копий с людьми, которых не хватает в вашем списке.",
"followHistNothing": "Все из этой копии уже есть в вашем списке.",
- "followHistRestored": "Восстановлено подписок: {n}."
+ "followHistRestored": "Восстановлено подписок: {n}.",
+ "securityTitle": "Безопасность",
+ "arkCoinsNone": "В расходах пока нет монет."
}
diff --git a/src/locales/sw.json b/src/locales/sw.json
index 75f5a8b..b357408 100644
--- a/src/locales/sw.json
+++ b/src/locales/sw.json
@@ -214,5 +214,7 @@
"followHistRestore": "Rejesha {n}",
"followHistNone": "Hakuna nakala ya zamani yenye mtu anayekosekana kwenye orodha yako.",
"followHistNothing": "Wote kwenye nakala hiyo tayari wamo kwenye orodha yako.",
- "followHistRestored": "Umerejesha unaowafuata {n}."
+ "followHistRestored": "Umerejesha unaowafuata {n}.",
+ "securityTitle": "Usalama",
+ "arkCoinsNone": "Bado hakuna sarafu kwenye Matumizi."
}
diff --git a/src/locales/ta.json b/src/locales/ta.json
index 5007c0a..82dfb0a 100644
--- a/src/locales/ta.json
+++ b/src/locales/ta.json
@@ -214,5 +214,7 @@
"followHistRestore": "{n} மீட்டெடு",
"followHistNone": "உங்கள் பட்டியலில் இல்லாதவர் உள்ள பழைய நகல் இல்லை.",
"followHistNothing": "அந்த நகலில் உள்ள அனைவரும் ஏற்கனவே உங்கள் பட்டியலில் உள்ளனர்.",
- "followHistRestored": "{n} பின்தொடர்வுகள் மீட்டெடுக்கப்பட்டன."
+ "followHistRestored": "{n} பின்தொடர்வுகள் மீட்டெடுக்கப்பட்டன.",
+ "securityTitle": "பாதுகாப்பு",
+ "arkCoinsNone": "செலவுக் கணக்கில் இன்னும் நாணயங்கள் இல்லை."
}
diff --git a/src/locales/te.json b/src/locales/te.json
index a138c26..5c4751d 100644
--- a/src/locales/te.json
+++ b/src/locales/te.json
@@ -212,5 +212,7 @@
"followHistRestore": "{n} పునరుద్ధరించండి",
"followHistNone": "మీ జాబితాలో లేని వ్యక్తి ఉన్న పాత కాపీ ఏదీ లేదు.",
"followHistNothing": "ఆ కాపీలోని అందరూ ఇప్పటికే మీ జాబితాలో ఉన్నారు.",
- "followHistRestored": "{n} ఫాలోలు పునరుద్ధరించబడ్డాయి."
+ "followHistRestored": "{n} ఫాలోలు పునరుద్ధరించబడ్డాయి.",
+ "securityTitle": "భద్రత",
+ "arkCoinsNone": "ఖర్చు ఖాతాలో ఇంకా నాణేలు లేవు."
}
diff --git a/src/locales/tr.json b/src/locales/tr.json
index d8fcc50..6150425 100644
--- a/src/locales/tr.json
+++ b/src/locales/tr.json
@@ -877,5 +877,7 @@
"followHistRestore": "{n} kişiyi geri yükle",
"followHistNone": "Hiçbir eski kopyada listende eksik olan biri yok.",
"followHistNothing": "O kopyadaki herkes zaten listende.",
- "followHistRestored": "{n} takip geri yüklendi."
+ "followHistRestored": "{n} takip geri yüklendi.",
+ "securityTitle": "Güvenlik",
+ "arkCoinsNone": "Harcama’da henüz coin yok."
}
diff --git a/src/locales/ur.json b/src/locales/ur.json
index 5059007..458a0f4 100644
--- a/src/locales/ur.json
+++ b/src/locales/ur.json
@@ -214,5 +214,7 @@
"followHistRestore": "{n} بحال کریں",
"followHistNone": "کسی پرانی کاپی میں ایسا کوئی نہیں جو آپ کی فہرست میں نہ ہو۔",
"followHistNothing": "اس کاپی کے سب لوگ پہلے سے آپ کی فہرست میں ہیں۔",
- "followHistRestored": "{n} فالو بحال ہو گئے۔"
+ "followHistRestored": "{n} فالو بحال ہو گئے۔",
+ "securityTitle": "سیکیورٹی",
+ "arkCoinsNone": "خرچ میں ابھی کوئی کوائن نہیں۔"
}
diff --git a/src/locales/vi.json b/src/locales/vi.json
index 91ae1e4..06916ab 100644
--- a/src/locales/vi.json
+++ b/src/locales/vi.json
@@ -214,5 +214,7 @@
"followHistRestore": "Khôi phục {n}",
"followHistNone": "Không bản cũ nào có người mà danh sách của bạn đang thiếu.",
"followHistNothing": "Mọi người trong bản đó đã có trong danh sách của bạn.",
- "followHistRestored": "Đã khôi phục {n} người theo dõi."
+ "followHistRestored": "Đã khôi phục {n} người theo dõi.",
+ "securityTitle": "Bảo mật",
+ "arkCoinsNone": "Chưa có coin nào trong Chi tiêu."
}
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 6b4c3a4..d6ea3f7 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -878,5 +878,7 @@
"followHistRestore": "恢复 {n} 人",
"followHistNone": "没有旧副本包含你列表中缺少的人。",
"followHistNothing": "该副本中的所有人都已在你的列表中。",
- "followHistRestored": "已恢复 {n} 个关注。"
+ "followHistRestored": "已恢复 {n} 个关注。",
+ "securityTitle": "安全",
+ "arkCoinsNone": "支出账户中还没有币。"
}
diff --git a/src/style.css b/src/style.css
index 0edb692..6a8af53 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1264,6 +1264,8 @@ body::after {
}
.balance-switch:hover { color: var(--ink); border-color: var(--line-strong); }
.balance-switch svg { opacity: .75; }
+/* the face's settings: a round icon button, no label */
+.balance-gear { width: 34px; height: 34px; padding: 0; justify-content: center; }
/* Move-money row: the action labels are long ("Move to Spending"), so let the
row wrap to two full-width lines rather than crushing the amount field. */
.balance-move .input-group { flex-wrap: wrap; }