feat(filter): focus one game at a time
Switching games on and off says which games you play, and is set once. It's the wrong tool for what a player of four games does while reading — clear one game, move to the next — which cost two taps per game and left the settings panel no longer describing what they play. Focus is a lens over that filter, not a second filter: a bar at the top, above everything it narrows, with a "next game" control that steps through and ends by returning to all. It never touches hiddenGames, and a focus on a game since switched off is ignored rather than obeyed, so it can't strand you on a blank page whose cause is elsewhere. Each chip carries that game's outstanding count, so a game with nothing waiting says so before you visit it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2a7a117705
commit
d70720e2af
@@ -62,3 +62,46 @@ export function firstToExpire<T extends Row>(rows: readonly T[]): T | null {
|
||||
}
|
||||
return best ?? rows[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The focused game, if it is still a game the reader can see.
|
||||
*
|
||||
* A focus on a game they have since switched off, or that has dropped out of
|
||||
* the feed, is ignored rather than obeyed — the alternative is an empty page
|
||||
* whose reason is a setting two screens away.
|
||||
*/
|
||||
export function resolveFocus(
|
||||
focus: GameId | null,
|
||||
enabled: readonly GameId[],
|
||||
): GameId | null {
|
||||
return focus !== null && enabled.includes(focus) ? focus : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The next game in the rotation: all → first → … → last → all.
|
||||
*
|
||||
* Working through games one at a time is a loop, and it ends by coming back to
|
||||
* everything rather than silently starting over — otherwise there is no way out
|
||||
* of the rotation except finding the "all" chip again.
|
||||
*/
|
||||
export function advanceFocus(
|
||||
focus: GameId | null,
|
||||
enabled: readonly GameId[],
|
||||
): GameId | null {
|
||||
if (enabled.length === 0) return null;
|
||||
const at = focus === null ? -1 : enabled.indexOf(focus);
|
||||
// An unknown focus (switched-off game) restarts the rotation rather than
|
||||
// jumping to index 0 of nowhere.
|
||||
return enabled[at + 1] ?? null;
|
||||
}
|
||||
|
||||
/** How many rows each game still has outstanding, for the focus chips. */
|
||||
export function countByGame<T extends Row>(
|
||||
rows: readonly T[],
|
||||
): Partial<Record<GameId, number>> {
|
||||
const out: Partial<Record<GameId, number>> = {};
|
||||
for (const row of rows) {
|
||||
out[row.event.game] = (out[row.event.game] ?? 0) + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,15 @@ export interface Prefs {
|
||||
region: Region;
|
||||
/** Games the reader has switched off. Stored as hidden so a newly added game shows up by default. */
|
||||
hiddenGames: GameId[];
|
||||
/**
|
||||
* One game to look at right now, or null for all of them.
|
||||
*
|
||||
* A lens, not a setting: it never changes `hiddenGames`, and a focus on a
|
||||
* game that is switched off or has left the feed is ignored rather than
|
||||
* obeyed (`resolveFocus`), so it can never leave the reader on a blank page
|
||||
* with no visible cause.
|
||||
*/
|
||||
focusGame: GameId | null;
|
||||
/** How the list is ordered. Deadline order is the default and the fallback. */
|
||||
sort: SortMode;
|
||||
/**
|
||||
@@ -29,6 +38,7 @@ function defaults(): Prefs {
|
||||
return {
|
||||
region: guessRegion(),
|
||||
hiddenGames: [],
|
||||
focusGame: null,
|
||||
sort: "ending",
|
||||
detectDaily: true,
|
||||
showCompleted: true,
|
||||
|
||||
Reference in New Issue
Block a user