From fe42dcd2ab7b0ecc718f5516961b82a15ea3e7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Fri, 12 Dec 2025 17:41:54 +0100 Subject: [PATCH] fix: crash when saving empty chords --- icons.config.js | 3 + src/lib/backup/backup.ts | 54 ++-- src/lib/components/Terminal.svelte | 5 + src/lib/serial/device.ts | 19 +- src/lib/undo-redo.ts | 16 + src/routes/(app)/config/EditActions.svelte | 335 ++++++++++++++------- src/routes/(app)/config/Navigation.svelte | 97 +++++- 7 files changed, 379 insertions(+), 150 deletions(-) diff --git a/icons.config.js b/icons.config.js index 8456c7ec..8d697954 100644 --- a/icons.config.js +++ b/icons.config.js @@ -83,6 +83,8 @@ const config = { "play_arrow", "extension", "upload_file", + "file_export", + "file_save", "commit", "bug_report", "delete", @@ -167,6 +169,7 @@ const config = { experiment: "e686", dictionary: "f539", visibility_off: "e8f5", + file_save: "f17f", }, }; diff --git a/src/lib/backup/backup.ts b/src/lib/backup/backup.ts index 7ce65718..2bfb9012 100644 --- a/src/lib/backup/backup.ts +++ b/src/lib/backup/backup.ts @@ -72,22 +72,26 @@ export function createSettingsBackup(): CharaSettingsFile { }; } -export async function restoreBackup(event: Event) { +export async function restoreBackup( + event: Event, + only?: "chords" | "layout" | "settings", +) { const input = (event.target as HTMLInputElement).files![0]; if (!input) return; const text = await input.text(); if (input.name.endsWith(".json")) { - restoreFromFile(JSON.parse(text)); + restoreFromFile(JSON.parse(text), only); } else if (isCsvLayout(text)) { - restoreFromFile(csvLayoutToJson(text)); + restoreFromFile(csvLayoutToJson(text), only); } else if (isCsvChords(text)) { - restoreFromFile(csvChordsToJson(text)); + restoreFromFile(csvChordsToJson(text), only); } else { } } export function restoreFromFile( file: CharaBackupFile | CharaSettingsFile | CharaLayoutFile | CharaChordFile, + only?: "chords" | "layout" | "settings", ) { if (file.charaVersion !== 1) throw new Error("Incompatible backup"); switch (file.type) { @@ -112,33 +116,45 @@ export function restoreFromFile( changes.update((changes) => { changes.push([ - ...getChangesFromChordFile(recent[0]), - ...getChangesFromLayoutFile(recent[1]), - ...getChangesFromSettingsFile(recent[2]), + ...(!only || only === "chords" + ? getChangesFromChordFile(recent[0]) + : []), + ...(!only || only === "layout" + ? getChangesFromLayoutFile(recent[1]) + : []), + ...(!only || only === "settings" + ? getChangesFromSettingsFile(recent[2]) + : []), ]); return changes; }); break; } case "chords": { - changes.update((changes) => { - changes.push(getChangesFromChordFile(file)); - return changes; - }); + if (!only || only === "chords") { + changes.update((changes) => { + changes.push(getChangesFromChordFile(file)); + return changes; + }); + } break; } case "layout": { - changes.update((changes) => { - changes.push(getChangesFromLayoutFile(file)); - return changes; - }); + if (!only || only === "layout") { + changes.update((changes) => { + changes.push(getChangesFromLayoutFile(file)); + return changes; + }); + } break; } case "settings": { - changes.update((changes) => { - changes.push(getChangesFromSettingsFile(file)); - return changes; - }); + if (!only || only === "settings") { + changes.update((changes) => { + changes.push(getChangesFromSettingsFile(file)); + return changes; + }); + } break; } default: { diff --git a/src/lib/components/Terminal.svelte b/src/lib/components/Terminal.svelte index 293e9d77..f2984825 100644 --- a/src/lib/components/Terminal.svelte +++ b/src/lib/components/Terminal.svelte @@ -1,7 +1,12 @@ - import { fade, fly } from "svelte/transition"; + import { fly, slide } from "svelte/transition"; import { canShare, triggerShare } from "$lib/share"; - import { action } from "$lib/title"; + import { actionTooltip } from "$lib/title"; import { activeProfile, serialPort } from "$lib/serial/connection"; import LL from "$i18n/i18n-svelte"; import EditActions from "./EditActions.svelte"; import { page } from "$app/state"; import { expoOut } from "svelte/easing"; + import { + createChordBackup, + createLayoutBackup, + createSettingsBackup, + downloadFile, + restoreBackup, + } from "$lib/backup/backup"; + + const routeOrder = [ + "/(app)/config/settings", + "/(app)/config/chords", + "/(app)/config/layout", + ]; + + let pageIndex = $derived( + routeOrder.findIndex((it) => page.route.id?.startsWith(it)), + ); + let importExport: HTMLDivElement | undefined = $state(undefined); + + $effect(() => { + pageIndex; + importExport?.animate( + [ + { transform: "translateX(0)", opacity: 1 }, + { transform: "translateX(-8px)", opacity: 0, offset: 0.2 }, + { transform: "translateX(8px)", opacity: 0, offset: 0.7 }, + { transform: "translateX(0)", opacity: 1 }, + ], + { + duration: 1500, + easing: "cubic-bezier(0.19, 1, 0.22, 1)", + }, + ); + }); + + function importBackup(event: Event) { + switch (pageIndex) { + case 0: + restoreBackup(event, "settings"); + break; + case 1: + restoreBackup(event, "chords"); + break; + case 2: + restoreBackup(event, "layout"); + break; + } + (event.target as HTMLInputElement).value = ""; + } + + function exportBackup() { + switch (pageIndex) { + case 0: + downloadFile(createSettingsBackup()); + break; + case 1: + downloadFile(createChordBackup()); + break; + case 2: + downloadFile(createLayoutBackup()); + break; + } + }