feat: the redesigned Event Clock
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]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ddd9673f86
commit
50c6e91b00
@@ -0,0 +1,59 @@
|
||||
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" };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user