feat: let readers ignore events they don't care about
Ignoring is not completing. "Done" keeps an event visible and counted; "not interested" removes it from both views, which is the entire point. Completions and ignores are the same shape and want the same guarantees, so they now share one mark-set implementation — union merge on import, never a removal, since nothing else holds a copy. Both ride in the export file. Ignored events stay recoverable: the count and a reveal toggle appear in settings once there is something to reveal. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
dd596b5a43
commit
71bff72ee9
@@ -12,6 +12,7 @@ const NS = "gacha-tracker:v1";
|
||||
|
||||
export const KEYS = {
|
||||
completions: `${NS}:completions`,
|
||||
ignored: `${NS}:ignored`,
|
||||
prefs: `${NS}:prefs`,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { KEYS, readJson, writeJson } from "./storage.ts";
|
||||
|
||||
export interface Completion {
|
||||
completedAt: string;
|
||||
}
|
||||
export type Completions = Record<string, Completion>;
|
||||
|
||||
/**
|
||||
* Completion marks, keyed by event ID.
|
||||
*
|
||||
* Writes are optimistic and local — there is no round trip and no failure case
|
||||
* to design for.
|
||||
*/
|
||||
export function useCompletions() {
|
||||
const [completions, setCompletions] = useState<Completions>(() =>
|
||||
readJson<Completions>(KEYS.completions, {}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
writeJson(KEYS.completions, completions);
|
||||
}, [completions]);
|
||||
|
||||
const toggle = useCallback((id: string) => {
|
||||
setCompletions((prev) => {
|
||||
if (prev[id] !== undefined) {
|
||||
const { [id]: _removed, ...rest } = prev;
|
||||
return rest;
|
||||
}
|
||||
return { ...prev, [id]: { completedAt: new Date().toISOString() } };
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Import merges and never removes. A completion present on either side stays
|
||||
* completed — an import that silently wiped marks would be unrecoverable,
|
||||
* since nothing else holds a copy.
|
||||
*/
|
||||
const merge = useCallback((incoming: Completions) => {
|
||||
setCompletions((prev) => {
|
||||
const next = { ...prev };
|
||||
for (const [id, value] of Object.entries(incoming)) {
|
||||
const existing = next[id];
|
||||
// Keep the earlier of the two marks; union of IDs, never a removal.
|
||||
next[id] =
|
||||
existing === undefined || value.completedAt < existing.completedAt
|
||||
? value
|
||||
: existing;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { completions, toggle, merge };
|
||||
}
|
||||
@@ -8,6 +8,8 @@ export interface Prefs {
|
||||
/** Games the reader has switched off. Stored as hidden so a newly added game shows up by default. */
|
||||
hiddenGames: GameId[];
|
||||
showCompleted: boolean;
|
||||
/** Reveal events the reader has ignored, so they can be restored. */
|
||||
showIgnored: boolean;
|
||||
/** False until the reader confirms or changes the guessed region. */
|
||||
regionConfirmed: boolean;
|
||||
/** False until the reader has picked their games on first run. */
|
||||
@@ -19,6 +21,7 @@ function defaults(): Prefs {
|
||||
region: guessRegion(),
|
||||
hiddenGames: [],
|
||||
showCompleted: true,
|
||||
showIgnored: false,
|
||||
regionConfirmed: false,
|
||||
onboarded: false,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user