diff --git a/src/client/App.tsx b/src/client/App.tsx index 2f8084a..9c89aca 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -17,6 +17,8 @@ import { useAppUpdate } from "./state/useAppUpdate.ts"; import { useMarkSet } from "./state/useMarkSet.ts"; import { useProgress } from "./state/useProgress.ts"; import { useDailyLog, type DailyLogMap } from "./state/useDailyLog.ts"; +import { useTasks } from "./state/useTasks.ts"; +import { useAmounts } from "./state/useAmounts.ts"; import { adoptNewLanes, usePrefs, type Prefs } from "./state/usePrefs.ts"; import { buildExportData, parseImportData, triggerDownload } from "./state/export.ts"; import { snapDayWidth } from "./state/zoom.ts"; @@ -39,8 +41,6 @@ import { metaOnTheme, useTheme } from "./state/theme.ts"; import { displayEventFor, recordFor, - type CustomEvents, - type CustomGames, type LaneId, } from "../shared/custom.ts"; import { metaFor } from "../shared/games.ts"; @@ -113,6 +113,12 @@ export function App() { const ignored = useMarkSet(KEYS.ignored); const prog = useProgress(); const daily = useDailyLog(); + const tasks = useTasks(); + const amounts = useAmounts(); + const mergeExtra = (incomingTasks: unknown, incomingAmounts: unknown) => { + tasks.merge(incomingTasks); + amounts.merge(incomingAmounts); + }; const custom = useCustom(now); // Colour only: which ground the page is drawn on, written to the document by // the hook. Nothing else in the app asks what it is — the tokens in @@ -687,6 +693,8 @@ export function App() { exportProgress(prog.progress, daily.logs, ignored.marks, { games: custom.games, events: custom.events, + tasks: tasks.raw, + amounts: amounts.amounts, }) } onImport={(file) => @@ -696,12 +704,15 @@ export function App() { daily.merge, ignored.merge, custom.merge, + mergeExtra, ) } onExportAll={() => exportAll(prog.progress, daily.logs, ignored.marks, prefs, { games: custom.games, events: custom.events, + tasks: tasks.raw, + amounts: amounts.amounts, }) } onImportAll={(file) => @@ -711,6 +722,7 @@ export function App() { daily.merge, ignored.merge, custom.merge, + mergeExtra, restorePrefs, ) } @@ -942,7 +954,7 @@ function exportProgress( progress: Record, daily: DailyLogMap, ignored: Record, - own: { games: CustomGames; events: CustomEvents }, + own: Own, ) { const data = buildExportData(progress, daily, ignored, own); triggerDownload( @@ -956,7 +968,7 @@ function exportAll( daily: DailyLogMap, ignored: Record, prefs: Prefs, - own: { games: CustomGames; events: CustomEvents }, + own: Own, ) { const data = buildExportData(progress, daily, ignored, own, prefs); triggerDownload( @@ -965,12 +977,16 @@ function exportAll( ); } +/** Everything reader-authored that travels in an export beside progress. */ +type Own = Parameters[3]; + async function importProgress( file: File, mergeProgress: (c: Record) => void, mergeDaily: (c: DailyLogMap) => void, mergeIgnored: (c: Record) => void, mergeCustom: (games: unknown, events: unknown) => void, + mergeExtra: (tasks: unknown, amounts: unknown) => void, restorePrefs?: ((prefs: unknown) => void) | undefined, ) { try { @@ -984,6 +1000,7 @@ async function importProgress( if (data.daily !== null) mergeDaily(data.daily); if (data.ignored !== null) mergeIgnored(data.ignored); mergeCustom(data.customGames, data.customEvents); + mergeExtra(data.tasks, data.amounts); if (restorePrefs !== undefined) { if (data.prefs !== null) { diff --git a/src/client/state/export.ts b/src/client/state/export.ts index 55e6058..0b31f0f 100644 --- a/src/client/state/export.ts +++ b/src/client/state/export.ts @@ -12,6 +12,10 @@ export interface ExportData { ignored: Record; customGames: CustomGames; customEvents: CustomEvents; + /** Weekly and monthly tasks, as stored — readable to this build or not. */ + tasks?: Record | undefined; + /** Numbers entered against targets, per item per period. */ + amounts?: Record> | undefined; prefs?: Prefs | undefined; } @@ -21,6 +25,9 @@ export interface ParsedImport { ignored: Record | null; customGames: unknown; customEvents: unknown; + /** Validated by the task and amount stores' own merges, not here. */ + tasks: unknown; + amounts: unknown; prefs: unknown | null; } @@ -33,7 +40,12 @@ export function buildExportData( progress: Record, daily: DailyLogMap, ignored: Record, - own: { games: CustomGames; events: CustomEvents }, + own: { + games: CustomGames; + events: CustomEvents; + tasks?: Record | undefined; + amounts?: Record> | undefined; + }, prefs?: Prefs | undefined, ): ExportData { return { @@ -45,6 +57,10 @@ export function buildExportData( ignored, customGames: own.games, customEvents: own.events, + // Additive keys, like customGames was: a file without them is simply from + // a device that had none. + ...(own.tasks !== undefined ? { tasks: own.tasks } : {}), + ...(own.amounts !== undefined ? { amounts: own.amounts } : {}), ...(prefs !== undefined ? { prefs } : {}), }; } @@ -63,6 +79,8 @@ export function parseImportData(parsed: unknown): ParsedImport | null { ignored?: unknown; customGames?: unknown; customEvents?: unknown; + tasks?: unknown; + amounts?: unknown; prefs?: unknown; }; if (data.format !== "gacha-tracker-export") return null; @@ -98,6 +116,8 @@ export function parseImportData(parsed: unknown): ParsedImport | null { ignored, customGames: data.customGames, customEvents: data.customEvents, + tasks: data.tasks, + amounts: data.amounts, prefs, }; } diff --git a/test/progress.test.ts b/test/progress.test.ts index c38fe92..6243ef8 100644 --- a/test/progress.test.ts +++ b/test/progress.test.ts @@ -234,3 +234,27 @@ describe("a goal this build cannot read is never cleared by accident", () => { expect(dailyPatch(false, true)).toStrictEqual({ daily: false, goal: undefined }); }); }); + +describe("tasks and amounts in the export", () => { + const T = "2026-10-13T12:00:00.000Z"; + const tasks = { + "mytask:aaaaaa1111": { id: "mytask:aaaaaa1111", game: "zzz", title: "Activity" }, + // Unreadable to this build, and carried anyway: the export is a backup. + "mytask:cccccc3333": { id: "mytask:cccccc3333", measure: { kind: "streak" } }, + }; + const amounts = { "mytask:aaaaaa1111": { "2026-10-12": { value: 1900, at: T } } }; + + test("a round trip carries both, readable or not", () => { + const data = buildExportData({}, {}, {}, { games: {}, events: {}, tasks, amounts }); + const parsed = parseImportData(JSON.parse(JSON.stringify(data)))!; + expect(parsed.tasks).toEqual(tasks); + expect(parsed.amounts).toEqual(amounts); + }); + + test("an export from before tasks has neither, and is still valid", () => { + const parsed = parseImportData({ format: "gacha-tracker-export", version: 1, progress: {} })!; + expect(parsed).not.toBeNull(); + expect(parsed.tasks).toBeUndefined(); + expect(parsed.amounts).toBeUndefined(); + }); +});