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]>
142 lines
4.2 KiB
TypeScript
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);
|
|
}
|