// Settings, every part of the app in one place: studios and accounts, app pairing and the ignore list, store reads // (on launch, every N minutes, how many apps at once, icons), the offline cache, language and theme, notifications, // local tools, the CLI sidecar, privacy, keyboard shortcuts, updates and reset. A section list on the left jumps. import { useEffect, useState } from 'react'; import { useScrollSpy } from '../ui/useScrollSpy'; import { api, type CacheInfo, errorText, onUpdateProgress, type Settings as S, type UpdateInfo } from '../api'; import { type Key, t } from '../i18n'; import { ago, bytes, date, num } from '../lib/format'; import { pairCandidates, type UnifiedApp, withPair, withUnpair } from '../lib/identity'; import { useNav } from '../nav'; import { usePrefs } from '../prefs-ctx'; import { useData } from '../store'; import { Icon, type IconName, StoreGlyph } from '../ui/icons'; import { AppIcon, Card, Chip, Kbd, MOD, Notice, Seg, StudioMark } from '../ui/kit'; export const SECTIONS: [string, IconName, Key][] = [ ['studios', 'building', 'set.studios'], ['pairing', 'link', 'set.pairing'], ['refresh', 'refresh', 'set.refresh'], ['cache', 'database', 'set.cache'], ['appearance', 'sun', 'settings.appearance'], ['notifications', 'bell', 'set.notifications'], ['dependencies', 'package', 'nav.dependencies'], ['cli', 'terminal', 'set.cli'], ['privacy', 'shield', 'set.privacy'], ['shortcuts', 'keyboard', 'settings.shortcuts'], ['updates', 'download', 'set.updates'], ['reset', 'rotate', 'set.reset'], ]; const SECTION_IDS = SECTIONS.map(([id]) => id); function Toggle({ id, label, hint, checked, onChange }: { id: string; label: string; hint?: string; checked: boolean; onChange: (v: boolean) => void }) { return ( ); } function PairRow({ app, all }: { app: UnifiedApp; all: UnifiedApp[] }) { const d = useData(); const cands = pairCandidates(app, all); const r = (app.ios ?? app.android)!; return (
{app.name}
{r.app.bundleId}
{app.ios && app.android ? {t(`pair.${app.pairing}` as Key)} : {t('pair.single')}} {app.ios && app.android ? : cands.length ? ( ) : {t('set.noCandidates')}} ); } export function Settings({ onTheme: _onTheme, section }: { onTheme: () => void; section?: string }) { const d = useData(); const nav = useNav(); const { prefs, update } = usePrefs(); const [error, setError] = useState(null); const [cache, setCache] = useState(null); const { active, jump } = useScrollSpy(SECTION_IDS, 'set-', section ?? 'studios', prefs.motion !== 'reduce'); const s = d.settings; const tools = d.sidecar?.tools; const set = (patch: (x: S) => S) => d.updateSettings(patch(s)); const loadCache = () => api.cacheInfo().then(setCache, () => {}); useEffect(() => { loadCache(); }, []); useEffect(() => { if (section) jump(section, { instant: true }); }, [section, jump]); const clear = () => api.clearCache().then(() => { d.toast({ kind: 'ok', title: t('settings.cacheCleared') }); loadCache(); }, e => setError(errorText(e))); const reset = async () => { try { if (await api.reset()) { try { localStorage.clear(); } catch { /* private mode */ } await d.reload(); d.toast({ kind: 'ok', title: t('set.resetDone') }); nav.openWelcome(); } } catch (e) { setError(errorText(e)); } }; const storeOnly = d.apps.filter(a => !a.project && !a.ignored); const paired = storeOnly.filter(a => a.ios && a.android); const singles = storeOnly.filter(a => !(a.ios && a.android)); return (

{t('nav.settings')}

{t('set.lead')}

{error && {error}}
nav.go({ screen: 'accounts' })}>{t('set.manage')}}> {d.studios.length ? (
{d.studios.map(st => { const accts = d.accounts.filter(a => a.studio === st.id); return ( ); })}
) :

{t('set.noStudios')}

}
{storeOnly.length ? ( {[...paired, ...singles].map(a => )}
{t('col.app')}{t('set.pairedHow')}{t('col.actions')}
) :

{t('set.noStoreOnly')}

}
{t('set.ignoredLine', { count: d.apps.filter(a => a.ignored).length, accounts: d.overrides.ignored.filter(i => i.startsWith('account:')).length })}
{t('set.autoRefresh')} set(x => ({ ...x, refresh: { ...x.refresh, autoMinutes: Number(v) as S['refresh']['autoMinutes'] } }))} options={[['0', t('set.off')], ['15', t('set.min', { n: 15 })], ['30', t('set.min', { n: 30 })], ['60', t('set.hour', { n: 1 })], ['180', t('set.hour', { n: 3 })], ['720', t('set.hour', { n: 12 })]]} /> {t('set.autoHint')}
set(x => ({ ...x, refresh: { ...x.refresh, onLaunch: v } }))} /> set(x => ({ ...x, refresh: { ...x.refresh, icons: v } }))} />
{t('set.parallelHint')}
{d.live ? t('foot.lastRead', { when: ago(d.live.at) }) : t('foot.neverRead')}
{t('set.cacheSize')}
{cache ? `${bytes(cache.file_bytes)} · ${t('set.cacheRows', { count: cache.rows })}` : '–'}
{t('set.cacheOldest')}
{cache?.oldest ? `${date(cache.oldest)} (${ago(cache.oldest)})` : '–'}
{t('set.keep')} set(x => ({ ...x, cache: { keepDays: Number(v) as S['cache']['keepDays'] } })).then(loadCache)} options={[['7', t('set.days', { n: 7 })], ['30', t('set.days', { n: 30 })], ['90', t('set.days', { n: 90 })], ['365', t('set.year')], ['0', t('set.always')]]} />

{t('settings.dataText')}

{t('settings.language')} update({ ...prefs, lang })} label={t('settings.language')} options={[['system', t('settings.system')], ['en', 'English'], ['tr', 'Türkçe']]} />
{t('settings.theme')} update({ ...prefs, theme })} label={t('settings.theme')} options={[['system', t('settings.system'), 'settings'], ['light', t('settings.light'), 'sun'], ['dark', t('settings.dark'), 'moon']]} />
{t('settings.motion')} update({ ...prefs, motion })} label={t('settings.motion')} options={[['system', t('settings.followSystem')], ['reduce', t('settings.reduce')]]} />
set(x => ({ ...x, notify: { ...x.notify, reviewState: v } }))} /> set(x => ({ ...x, notify: { ...x.notify, lowReviews: v } }))} /> set(x => ({ ...x, notify: { ...x.notify, crash: v } }))} />
nav.go({ screen: 'dependencies' })}>{t('set.manage')}}> {tools ? (
{tools.tools.map(x => {x.name}{x.version ? {x.version} : null}{x.source === 'app' && {t('set.appCopy')}})}
) :

–

}

{t('set.depsText')}

{t('settings.app')}
{d.info ? `storebound desktop ${d.info.version}${d.info.debug ? ` (${t('settings.debug')})` : ''} · ${d.info.platform}` : '–'}
CLI
{d.sidecar ? `storebound ${d.sidecar.version}` : d.sidecarError ?? t('foot.cliStarting')}
{t('set.protocol')}
{d.sidecar ? `${d.sidecar.protocol} · ${t('settings.methods', { count: d.sidecar.methods.length })}` : '–'}
{t('set.state')}
{d.sidecar?.running ? t('set.running') : d.sidecarError ? t('foot.cliDown') : t('foot.cliStarting')}
{t('set.writes')}
{t('set.writesText')}
  • {t('set.p1')}
  • {t('set.p2')}
  • {t('set.p3')}
  • {t('set.p4')}
{t('settings.scPalette')}
{t('nav.portfolio')}
{t('settings.scWorkspace')}
–
{t('settings.scTabs')}
{t('settings.scTabArrows')}
{t('settings.scClose')}
{t('set.scQuit')}
{t('set.channel')} set(x => ({ ...x, channel }))} options={[['stable', t('set.stable')], ['beta', t('set.beta')]]} />

{t('set.updatesText')}

{t('set.resetText', { accounts: d.accounts.length, studios: d.studios.length, folders: d.projects.length })}

{t('set.footer', { n: num(d.apps.length) })}

); } /** Settings > Updates: checks on click only; installs after the host's native dialog (the signature is checked there). */ function Updates() { const [info, setInfo] = useState(null); const [busy, setBusy] = useState<'check' | 'install' | null>(null); const [error, setError] = useState(null); const [got, setGot] = useState<{ received: number; total: number | null } | null>(null); useEffect(() => { const un = onUpdateProgress(setGot); return () => { un.then(f => f()); }; }, []); const check = async () => { setBusy('check'); setError(null); try { setInfo(await api.updateCheck()); } catch (e) { setError(errorText(e)); } finally { setBusy(null); } }; const install = async (version: string) => { setBusy('install'); setError(null); try { await api.updateInstall(version); } catch (e) { setError(errorText(e)); } finally { setBusy(null); setGot(null); } }; const channel = info ? t(info.channel === 'beta' ? 'set.beta' : 'set.stable') : ''; return (
{error && {error}} {info && !info.available &&

{t('set.updateNone', { version: info.current, channel })}

} {info?.available &&

{t('set.updateAvailable', { version: info.available })}{info.notes ? ` ${info.notes}` : ''}

} {busy === 'install' && got &&

{t('set.updateInstalling', { done: got.total ? `${Math.round((got.received / got.total) * 100)}%` : bytes(got.received) })}

}
{info?.available && }
); }