Files
gacha-event-tracker/src/client/state/export.ts
T
Lucas WintherandClaude Opus 5.5 e97e8b002f feat(export): carry tasks and amounts in the backup
They exist on this device and nowhere else, so an export without them is
a lossy backup. Both are additive keys, as custom games were: older files
lack them and still import. Tasks travel as stored, including any this
build cannot read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 13:34:56 +02:00

142 lines
4.2 KiB
TypeScript

import type { CustomEvents, CustomGames } from "../../shared/custom.ts";
import type { DailyLogMap } from "./useDailyLog.ts";
import type { Prefs } from "./usePrefs.ts";
import type { ProgressMap } from "./useProgress.ts";
export interface ExportData {
format: "gacha-tracker-export";
version: 1;
exportedAt: string;
progress: Record<string, unknown>;
daily: DailyLogMap;
ignored: Record<string, { at: string }>;
customGames: CustomGames;
customEvents: CustomEvents;
/** Weekly and monthly tasks, as stored — readable to this build or not. */
tasks?: Record<string, unknown> | undefined;
/** Numbers entered against targets, per item per period. */
amounts?: Record<string, Record<string, unknown>> | undefined;
prefs?: Prefs | undefined;
}
export interface ParsedImport {
progress: ProgressMap | null;
daily: DailyLogMap | null;
ignored: Record<string, { at: string }> | null;
customGames: unknown;
customEvents: unknown;
/** Validated by the task and amount stores' own merges, not here. */
tasks: unknown;
amounts: unknown;
prefs: unknown | null;
}
/**
* Builds the serializable JSON export object.
* When `prefs` is provided, includes preferences for moving hosts ("Export all").
* When omitted, exports progress and custom data only ("Export").
*/
export function buildExportData(
progress: Record<string, unknown>,
daily: DailyLogMap,
ignored: Record<string, { at: string }>,
own: {
games: CustomGames;
events: CustomEvents;
tasks?: Record<string, unknown> | undefined;
amounts?: Record<string, Record<string, unknown>> | undefined;
},
prefs?: Prefs | undefined,
): ExportData {
return {
format: "gacha-tracker-export",
version: 1,
exportedAt: new Date().toISOString(),
progress,
daily,
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 } : {}),
};
}
/**
* Validates and parses raw JSON from an import file.
* Returns null if the file is not a valid Event Clock export.
*/
export function parseImportData(parsed: unknown): ParsedImport | null {
if (typeof parsed !== "object" || parsed === null) return null;
const data = parsed as {
format?: string;
progress?: unknown;
completions?: unknown;
daily?: unknown;
ignored?: unknown;
customGames?: unknown;
customEvents?: unknown;
tasks?: unknown;
amounts?: unknown;
prefs?: unknown;
};
if (data.format !== "gacha-tracker-export") return null;
const asRecord = (v: unknown) =>
typeof v === "object" && v !== null
? (v as Record<string, unknown>)
: null;
const asMarks = (v: unknown) =>
typeof v === "object" && v !== null
? (v as Record<string, { at: string }>)
: null;
let progress: ProgressMap | null = asRecord(data.progress) as ProgressMap | null;
const legacy = asRecord(data.completions) as Record<string, { at: string }> | null;
if (progress === null && legacy !== null) {
progress = Object.fromEntries(
Object.entries(legacy).map(([id, m]) => [id, { ...m, status: "done" }]),
);
}
const daily =
typeof data.daily === "object" && data.daily !== null
? (data.daily as DailyLogMap)
: null;
const ignored = asMarks(data.ignored);
const prefs =
typeof data.prefs === "object" && data.prefs !== null ? data.prefs : null;
return {
progress,
daily,
ignored,
customGames: data.customGames,
customEvents: data.customEvents,
tasks: data.tasks,
amounts: data.amounts,
prefs,
};
}
/**
* Initiates a browser download of a JSON object as a file.
*/
export function triggerDownload(data: unknown, filename: string): void {
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.style.display = "none";
document.body.append(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 0);
}