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,69 @@
|
||||
import {
|
||||
asDisplayEvent,
|
||||
asOccurrenceEvent,
|
||||
CustomEvent,
|
||||
type DisplayEvent,
|
||||
} from "../../../shared/custom.ts";
|
||||
import { ruleIdOf, nextOccurrences } from "../../../shared/recurrence.ts";
|
||||
import { endingSoonestFirst } from "../../../shared/time.ts";
|
||||
import {
|
||||
readerClockFor,
|
||||
resolveReaderClock,
|
||||
} from "../../../shared/readerClock.ts";
|
||||
import type { RowEvent } from "../../components/EventRow.tsx";
|
||||
import type { Region } from "../../../shared/schema.ts";
|
||||
import type { ReaderState } from "./schema.ts";
|
||||
export type ReaderEventRow = RowEvent & {
|
||||
accountId: string;
|
||||
region: Region;
|
||||
clockRevision: string;
|
||||
};
|
||||
export function readerEventRows(
|
||||
state: ReaderState,
|
||||
events: readonly DisplayEvent[],
|
||||
now: number,
|
||||
): ReaderEventRow[] {
|
||||
const source = new Map(
|
||||
events.filter((e) => e.status === "published").map((e) => [e.id, e]),
|
||||
);
|
||||
for (const raw of Object.values(state.customEvents)) {
|
||||
const p = CustomEvent.safeParse(raw);
|
||||
if (!p.success) continue;
|
||||
const e = p.data;
|
||||
if (e.repeat === null) source.set(e.id, asDisplayEvent(e));
|
||||
else
|
||||
for (const o of nextOccurrences(e, now, 2))
|
||||
source.set(o.id, asOccurrenceEvent(e, o));
|
||||
}
|
||||
const out: ReaderEventRow[] = [];
|
||||
for (const a of Object.values(state.accounts)) {
|
||||
if (
|
||||
a.retiredAt ||
|
||||
!a.intent.events ||
|
||||
(Array.isArray(state.prefs.hiddenGames) &&
|
||||
state.prefs.hiddenGames.includes(a.game))
|
||||
)
|
||||
continue;
|
||||
const scoped = new Map(source);
|
||||
for (const seen of Object.values(state.seenEvents))
|
||||
if (
|
||||
seen.accountId === a.id &&
|
||||
!scoped.has(seen.event.id) &&
|
||||
(seen.event.sourceUrl !== null ||
|
||||
state.customEvents[ruleIdOf(seen.event.id)])
|
||||
)
|
||||
scoped.set(seen.event.id, seen.event);
|
||||
for (const event of scoped.values()) {
|
||||
if (event.game !== a.game) continue;
|
||||
const clock = readerClockFor(event, a, now);
|
||||
out.push({
|
||||
event,
|
||||
clock,
|
||||
accountId: a.id,
|
||||
region: a.region,
|
||||
clockRevision: resolveReaderClock(a, now).revision,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out.sort(endingSoonestFirst);
|
||||
}
|
||||
Reference in New Issue
Block a user