diff --git a/ui/src/lib/app.svelte.ts b/ui/src/lib/app.svelte.ts index a4041fe..58ed777 100644 --- a/ui/src/lib/app.svelte.ts +++ b/ui/src/lib/app.svelte.ts @@ -56,7 +56,7 @@ import { } from './session'; import type { CoachSeries } from './coachmark'; import { connection, reportOffline, reportOnline, resetConnection } from './connection.svelte'; -import { offlineMode, setOfflineMode } from './offline.svelte'; +import { offlineMode } from './offline.svelte'; import { shouldBootOffline } from './offline'; import { isConnectionCode } from './retry'; import { clearGameCache, getCachedGame, setCachedGame } from './gamecache'; @@ -465,7 +465,7 @@ export async function refreshProfile(): Promise { if (!app.session) return; try { app.profile = await gateway.profileGet(); - void saveProfile(app.profile); // keep the offline-boot cache fresh + void saveProfile($state.snapshot(app.profile)); // keep the offline-boot cache fresh (plain snapshot) } catch { // Best-effort; the banner just stays as it was until the next fetch. } @@ -524,8 +524,10 @@ async function adoptSession(s: Session): Promise { try { app.profile = await gateway.profileGet(); // Persist the profile so a later offline cold start can launch from it (its variant - // preferences, dictionary versions and display name) without a network fetch. - void saveProfile(app.profile); + // preferences, dictionary versions and display name) without a network fetch. Snapshot to a + // plain object first — the reactive $state proxy is not structured-cloneable, so it would fail + // the IndexedDB write and fall back to a localStorage entry that loadProfile never reads. + void saveProfile($state.snapshot(app.profile)); // The live interface language follows the device — the explicit local choice (saved in // prefs) or the system guess made at bootstrap — and is no longer overridden from the // account here: the Telegram bot a user signs in through must not dictate the UI, so a @@ -801,9 +803,10 @@ export async function bootstrap(): Promise { // Deliberate offline mode carried over from a prior session: with no network the session adoption // + profile fetch below would hang the splash, so skip them and launch straight from the cached - // session + profile into the offline lobby. Without both (e.g. cleared storage, or an install that - // was never online), drop the sticky flag and fall through to the normal online flow — the first - // launch must be online. + // session + profile into the offline lobby. Without a cached profile (e.g. storage cleared) fall + // through to the normal online flow but KEEP the sticky flag — the mode is the player's choice, and + // an online boot re-persists the profile so the next launch goes offline. (A truly offline launch + // with no cached profile is unreachable: enabling offline requires a prior online session.) if (offlineMode.active) { const [offlineSession, offlineProfile] = await Promise.all([loadSession(), loadProfile()]); if (shouldBootOffline({ offlineActive: true, hasSession: !!offlineSession, hasProfile: !!offlineProfile })) { @@ -814,7 +817,6 @@ export async function bootstrap(): Promise { app.ready = true; return; } - setOfflineMode(false); } const saved = await loadSession();