201 lines
5.3 KiB
TypeScript
201 lines
5.3 KiB
TypeScript
// ── Timer & Routine Export / Import ──────────────────────────
|
|
// Export all timers + routines as JSON, import from JSON file
|
|
|
|
import type { Timer } from './timer-engine';
|
|
import type { Routine } from './routines';
|
|
|
|
// ── Types ─────────────────────────────────────────────────────
|
|
|
|
export interface ExportData {
|
|
version: 1;
|
|
exportedAt: number;
|
|
app: 'chronomind';
|
|
timers: Timer[];
|
|
routines?: Routine[];
|
|
}
|
|
|
|
export interface ImportResult {
|
|
success: boolean;
|
|
imported: number;
|
|
skipped: number;
|
|
errors: string[];
|
|
}
|
|
|
|
// ── Export ─────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Export timers as a downloadable JSON blob.
|
|
*/
|
|
export function exportTimers(timers: Timer[]): ExportData {
|
|
return {
|
|
version: 1,
|
|
exportedAt: Date.now(),
|
|
app: 'chronomind',
|
|
timers: timers.map((t) => ({ ...t })), // shallow clone
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Export timers + routines as a downloadable JSON blob.
|
|
*/
|
|
export function exportAll(timers: Timer[], routines: Routine[]): ExportData {
|
|
return {
|
|
version: 1,
|
|
exportedAt: Date.now(),
|
|
app: 'chronomind',
|
|
timers: timers.map((t) => ({ ...t })),
|
|
routines: routines.map((r) => ({ ...r, steps: r.steps.map((s) => ({ ...s })) })),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Trigger a browser download of the export data.
|
|
*/
|
|
export function downloadExport(timers: Timer[]): void {
|
|
const data = exportTimers(timers);
|
|
const json = JSON.stringify(data, null, 2);
|
|
const blob = new Blob([json], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `chronomind-export-${new Date().toISOString().slice(0, 10)}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
/**
|
|
* Trigger a browser download of timers + routines.
|
|
*/
|
|
export function downloadExportAll(timers: Timer[], routines: Routine[]): void {
|
|
const data = exportAll(timers, routines);
|
|
const json = JSON.stringify(data, null, 2);
|
|
const blob = new Blob([json], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `chronomind-export-${new Date().toISOString().slice(0, 10)}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// ── Import ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Validate and parse an import file.
|
|
*/
|
|
export function parseImportData(jsonStr: string): ExportData | null {
|
|
try {
|
|
const data = JSON.parse(jsonStr);
|
|
if (!data || data.app !== 'chronomind' || !Array.isArray(data.timers)) {
|
|
return null;
|
|
}
|
|
return data as ExportData;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Import timers from parsed export data, deduplicating by ID.
|
|
*/
|
|
export function importTimers(
|
|
exportData: ExportData,
|
|
existingTimers: Timer[]
|
|
): ImportResult {
|
|
const errors: string[] = [];
|
|
let imported = 0;
|
|
let skipped = 0;
|
|
|
|
const existingIds = new Set(existingTimers.map((t) => t.id));
|
|
const newTimers: Timer[] = [];
|
|
|
|
for (const timer of exportData.timers) {
|
|
// Basic validation
|
|
if (!timer.id || !timer.type || !timer.label) {
|
|
errors.push(`Invalid timer: missing required fields (id, type, or label)`);
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
if (existingIds.has(timer.id)) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
// Validate required numeric fields
|
|
if (typeof timer.createdAt !== 'number' || typeof timer.targetTime !== 'number') {
|
|
errors.push(`Timer "${timer.label}": invalid timestamps`);
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
newTimers.push(timer);
|
|
imported++;
|
|
}
|
|
|
|
return {
|
|
success: errors.length === 0,
|
|
imported,
|
|
skipped,
|
|
errors,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Import routines from parsed export data, deduplicating by ID.
|
|
*/
|
|
export function importRoutines(
|
|
exportData: ExportData,
|
|
existingRoutines: Routine[]
|
|
): ImportResult {
|
|
if (!exportData.routines || exportData.routines.length === 0) {
|
|
return { success: true, imported: 0, skipped: 0, errors: [] };
|
|
}
|
|
|
|
const errors: string[] = [];
|
|
let imported = 0;
|
|
let skipped = 0;
|
|
|
|
const existingIds = new Set(existingRoutines.map((r) => r.id));
|
|
|
|
for (const routine of exportData.routines) {
|
|
if (!routine.id || !routine.name || !Array.isArray(routine.steps)) {
|
|
errors.push(`Invalid routine: missing required fields (id, name, or steps)`);
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
if (existingIds.has(routine.id)) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
imported++;
|
|
}
|
|
|
|
return {
|
|
success: errors.length === 0,
|
|
imported,
|
|
skipped,
|
|
errors,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Read a File object and return the parsed content as string.
|
|
*/
|
|
export function readFileAsText(file: File): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result as string);
|
|
reader.onerror = () => reject(new Error('Failed to read file'));
|
|
reader.readAsText(file);
|
|
});
|
|
}
|