feat(04-03): implement Export/Import/Reset data logic in parent area
- exportData: downloads progress+settings as zauberwald-backup.json - importData: validates JSON structure, confirms overwrite, saves and reloads - resetAllData: double confirmation, clears all 5 IndexedDB stores, reloads - Buttons wired in loadParentData after code entry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,16 @@ async function loadParentData(): Promise<void> {
|
|||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
await loadSettings(db, progress, settings);
|
await loadSettings(db, progress, settings);
|
||||||
|
|
||||||
|
// Wire data action buttons
|
||||||
|
document.getElementById("export-btn")!.onclick = () => exportData(db);
|
||||||
|
document.getElementById("import-btn")!.onclick = () =>
|
||||||
|
(document.getElementById("import-file") as HTMLInputElement).click();
|
||||||
|
document.getElementById("import-file")!.addEventListener("change", (e) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
|
if (file) importData(db, file);
|
||||||
|
});
|
||||||
|
document.getElementById("reset-btn")!.onclick = () => resetAllData(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,6 +196,7 @@ async function loadSettings(
|
|||||||
const currentSettings = settings ?? {
|
const currentSettings = settings ?? {
|
||||||
id: 1 as const,
|
id: 1 as const,
|
||||||
audioEnabled: false,
|
audioEnabled: false,
|
||||||
|
speechEnabled: false,
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
apiCallsToday: { text: 0, image: 0 },
|
apiCallsToday: { text: 0, image: 0 },
|
||||||
lastApiCallDate: "",
|
lastApiCallDate: "",
|
||||||
@@ -199,6 +210,7 @@ async function loadSettings(
|
|||||||
const currentSettings = settings ?? {
|
const currentSettings = settings ?? {
|
||||||
id: 1 as const,
|
id: 1 as const,
|
||||||
audioEnabled: false,
|
audioEnabled: false,
|
||||||
|
speechEnabled: false,
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
apiCallsToday: { text: 0, image: 0 },
|
apiCallsToday: { text: 0, image: 0 },
|
||||||
lastApiCallDate: "",
|
lastApiCallDate: "",
|
||||||
@@ -209,6 +221,108 @@ async function loadSettings(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DB_STORES = ["progress", "companionAssets", "styleReference", "forestElements", "settings"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show feedback message in the data section.
|
||||||
|
*/
|
||||||
|
function showDataFeedback(message: string, isError = false): void {
|
||||||
|
const el = document.getElementById("data-feedback")!;
|
||||||
|
el.textContent = message;
|
||||||
|
el.style.display = "block";
|
||||||
|
el.style.color = isError ? "#D46A6A" : "";
|
||||||
|
setTimeout(() => {
|
||||||
|
el.style.display = "none";
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export progress and settings as a JSON file download.
|
||||||
|
*/
|
||||||
|
async function exportData(db: IDBDatabase): Promise<void> {
|
||||||
|
const progress = await getProgress(db);
|
||||||
|
const settings = await getSettings(db);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
version: 1,
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
progress,
|
||||||
|
settings,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = "zauberwald-backup.json";
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
showDataFeedback("Exportiert!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import progress and settings from a JSON file.
|
||||||
|
*/
|
||||||
|
async function importData(db: IDBDatabase, file: File): Promise<void> {
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
|
||||||
|
// Validate structure
|
||||||
|
if (
|
||||||
|
!parsed.progress ||
|
||||||
|
typeof parsed.progress.currentLevel !== "number" ||
|
||||||
|
!Array.isArray(parsed.progress.completedLevels) ||
|
||||||
|
typeof parsed.progress.totalSessions !== "number" ||
|
||||||
|
typeof parsed.progress.selectedCharacter !== "string" ||
|
||||||
|
typeof parsed.progress.selectedLayout !== "string"
|
||||||
|
) {
|
||||||
|
showDataFeedback("Ungueltige Datei", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!parsed.settings || typeof parsed.settings.audioEnabled !== "boolean") {
|
||||||
|
showDataFeedback("Ungueltige Datei", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm("Fortschritt ueberschreiben? Aktuelle Daten gehen verloren.")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveProgress(db, { ...parsed.progress, id: 1 });
|
||||||
|
await saveSettings(db, { ...parsed.settings, id: 1 });
|
||||||
|
|
||||||
|
showDataFeedback("Importiert!");
|
||||||
|
setTimeout(() => location.reload(), 1500);
|
||||||
|
} catch {
|
||||||
|
showDataFeedback("Ungueltige Datei", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear a single IndexedDB object store.
|
||||||
|
*/
|
||||||
|
function clearStore(db: IDBDatabase, storeName: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction(storeName, "readwrite");
|
||||||
|
const request = tx.objectStore(storeName).clear();
|
||||||
|
request.onsuccess = () => resolve();
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all data with double confirmation, then reload.
|
||||||
|
*/
|
||||||
|
async function resetAllData(db: IDBDatabase): Promise<void> {
|
||||||
|
if (!confirm("Wirklich alles loeschen?")) return;
|
||||||
|
if (!confirm("Alle Fortschritte und Einstellungen werden geloescht. Bist du sicher?")) return;
|
||||||
|
|
||||||
|
await Promise.all(DB_STORES.map((store) => clearStore(db, store)));
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show brief "Gespeichert!" feedback next to an element.
|
* Show brief "Gespeichert!" feedback next to an element.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user