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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ddd9673f86
commit
50c6e91b00
@@ -0,0 +1,82 @@
|
||||
import type { Account, ClockRevision } from "../client/state/reader/schema.ts";
|
||||
import type { Region } from "./schema.ts";
|
||||
import {
|
||||
clockFor,
|
||||
resetHourFor,
|
||||
serverOffsetUtc,
|
||||
type Clockable,
|
||||
type ResetTiming,
|
||||
} from "./time.ts";
|
||||
import { dayKey } from "./daily.ts";
|
||||
export interface ReaderClock extends ResetTiming {
|
||||
revision: string;
|
||||
region: Region;
|
||||
}
|
||||
export function resolveReaderClock(account: Account, at: number): ReaderClock {
|
||||
const ordered = account.clocks
|
||||
.filter((r) => r.historicalOnly !== true)
|
||||
.slice()
|
||||
.sort((a, b) => a.effectiveAt.localeCompare(b.effectiveAt));
|
||||
const r =
|
||||
ordered.filter((r) => Date.parse(r.effectiveAt) <= at).at(-1) ??
|
||||
ordered[0]!;
|
||||
return clockFromRevision(r);
|
||||
}
|
||||
export function clockFromRevision(r: ClockRevision): ReaderClock {
|
||||
return {
|
||||
revision: r.id,
|
||||
region: r.region,
|
||||
offsetUtc: r.automaticBase.offsetUtc,
|
||||
resetHour: r.reset?.hour ?? r.automaticBase.hour,
|
||||
resetMinute: r.reset?.minute ?? r.automaticBase.minute,
|
||||
};
|
||||
}
|
||||
export function appendClockRevision(
|
||||
account: Account,
|
||||
reset: ClockRevision["reset"],
|
||||
region: Region,
|
||||
at: string,
|
||||
id: string,
|
||||
): Account {
|
||||
if (
|
||||
!Number.isFinite(Date.parse(at)) ||
|
||||
account.clocks.some((r) => r.id === id) ||
|
||||
(reset &&
|
||||
(reset.hour < 0 ||
|
||||
reset.hour > 23 ||
|
||||
reset.minute < 0 ||
|
||||
reset.minute > 59 ||
|
||||
!Number.isInteger(reset.hour) ||
|
||||
!Number.isInteger(reset.minute)))
|
||||
)
|
||||
throw new Error("Invalid clock revision");
|
||||
return {
|
||||
...account,
|
||||
region,
|
||||
clocks: [
|
||||
...account.clocks,
|
||||
{
|
||||
id,
|
||||
effectiveAt: at,
|
||||
region,
|
||||
reset,
|
||||
automaticBase: {
|
||||
offsetUtc: serverOffsetUtc(region, account.game),
|
||||
hour: resetHourFor(region, account.game),
|
||||
minute: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
export function readerDayKey(at: number, clock: ReaderClock): string {
|
||||
return dayKey(at, clock.region, undefined, clock);
|
||||
}
|
||||
export function readerClockFor(
|
||||
event: Clockable,
|
||||
account: Account,
|
||||
now: number,
|
||||
) {
|
||||
const clock = resolveReaderClock(account, now);
|
||||
return clockFor(event, clock.region, now, clock);
|
||||
}
|
||||
Reference in New Issue
Block a user