The new reader app replaces the old UI: Checklist with a rail (focus pills, Next to expire, today's dailies, pinned routines), Timeline in a new and a classic look, Routines, Archive and a Settings area, in a Structured (default) and a Relaxed interface. It follows the approved Round 6 B and Round 7 mockups, corrected screen by screen with the owner. Reader data moves to a v2 store (gacha-tracker:v2:*). Existing v1 data is migrated on first load, a recovery copy is written first, and the v1 keys are never removed, so a rollback finds them unchanged. Redeem codes for Genshin arrive as their own feed. Also: live's urgency ramp, editable per theme; completion and section-complete fireworks; a returning-user tour; the design mockups, specs, plans, reviews and handoff record. Co-Authored-By: Claude Opus 5.5 <[email protected]>
60 lines
2.0 KiB
TypeScript
60 lines
2.0 KiB
TypeScript
import { parseReaderExport, buildReaderExport } from "./export.ts";
|
|
import { READER_KEYS, type SaveResult } from "./schema.ts";
|
|
import { readLegacy, tryWriteJson } from "./store.ts";
|
|
export function finishPendingRestore(storage: Storage): SaveResult {
|
|
try {
|
|
const raw = storage.getItem(READER_KEYS.pending);
|
|
if (raw === null) return { ok: true };
|
|
const pending = JSON.parse(raw) as { payload?: unknown };
|
|
const reader = parseReaderExport(pending.payload);
|
|
if (!reader) return { ok: false, reason: "invalid" };
|
|
const result = tryWriteJson(storage, READER_KEYS.reader, reader);
|
|
if (!result.ok) return result;
|
|
storage.removeItem(READER_KEYS.pending);
|
|
return { ok: true };
|
|
} catch {
|
|
return { ok: false, reason: "unavailable" };
|
|
}
|
|
}
|
|
export function prepareRestore(
|
|
storage: Storage,
|
|
payload: unknown,
|
|
at: string,
|
|
): SaveResult {
|
|
const reader = parseReaderExport(payload);
|
|
if (!reader) return { ok: false, reason: "invalid" };
|
|
try {
|
|
const oldRaw = storage.getItem(READER_KEYS.reader);
|
|
if (oldRaw === null) return { ok: false, reason: "invalid" };
|
|
const before = parseReaderExport({
|
|
format: "gacha-tracker-export",
|
|
version: 2,
|
|
reader: JSON.parse(oldRaw),
|
|
});
|
|
if (!before) return { ok: false, reason: "invalid" };
|
|
const recovery = tryWriteJson(
|
|
storage,
|
|
READER_KEYS.recovery,
|
|
buildReaderExport(before, readLegacy(storage), at),
|
|
);
|
|
if (!recovery.ok) return recovery;
|
|
const prepared = tryWriteJson(storage, READER_KEYS.pending, {
|
|
at,
|
|
payload: buildReaderExport(reader, {}, at),
|
|
});
|
|
if (!prepared.ok) return prepared;
|
|
return finishPendingRestore(storage);
|
|
} catch {
|
|
return { ok: false, reason: "unavailable" };
|
|
}
|
|
}
|
|
export function undoRestore(storage: Storage): SaveResult {
|
|
try {
|
|
const raw = storage.getItem(READER_KEYS.recovery);
|
|
if (raw === null) return { ok: false, reason: "invalid" };
|
|
return prepareRestore(storage, JSON.parse(raw), new Date().toISOString());
|
|
} catch {
|
|
return { ok: false, reason: "unavailable" };
|
|
}
|
|
}
|