mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-09 11:32:49 +00:00
fix: crash when saving empty chords
This commit is contained in:
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { serialLog, serialPort } from "$lib/serial/connection";
|
||||
import { onMount } from "svelte";
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
onMount(() => {
|
||||
io.scrollTo({ top: io.scrollHeight });
|
||||
});
|
||||
|
||||
function submit(event: Event) {
|
||||
event.preventDefault();
|
||||
$serialPort?.send(0, value.trim());
|
||||
|
||||
@@ -342,23 +342,24 @@ export class CharaDevice {
|
||||
.join(" ")
|
||||
.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
||||
const readResult = await read(timeout);
|
||||
if (readResult === undefined) {
|
||||
return readResult
|
||||
?.replace(new RegExp(`^${commandString} `), "")
|
||||
.split(" ");
|
||||
}).then((it) => {
|
||||
if (it === undefined) {
|
||||
console.error("No response");
|
||||
return Array(expectedLength).fill("NO_RESPONSE") as LengthArray<
|
||||
string,
|
||||
T
|
||||
>;
|
||||
}
|
||||
const array = readResult
|
||||
.replace(new RegExp(`^${commandString} `), "")
|
||||
.split(" ");
|
||||
if (array.length < expectedLength) {
|
||||
if (it.length < expectedLength) {
|
||||
console.error("Response too short");
|
||||
return array.concat(
|
||||
Array(expectedLength - array.length).fill("TOO_SHORT"),
|
||||
return it.concat(
|
||||
Array(expectedLength - it.length).fill("TOO_SHORT"),
|
||||
) as LengthArray<string, T>;
|
||||
}
|
||||
return array as LengthArray<string, T>;
|
||||
return it as LengthArray<string, T>;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -401,7 +402,7 @@ export class CharaDevice {
|
||||
stringifyChordActions(chord.actions),
|
||||
stringifyPhrase(chord.phrase),
|
||||
]);
|
||||
if (status !== "0") console.error(`Failed with status ${status}`);
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||
}
|
||||
|
||||
async deleteChord(chord: Pick<Chord, "actions">) {
|
||||
|
||||
@@ -179,6 +179,22 @@ export const chords = derived(
|
||||
},
|
||||
);
|
||||
|
||||
export const duplicateChords = derived(chords, (chords) => {
|
||||
const duplicates = new Set<string>();
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const chord of chords) {
|
||||
const key = JSON.stringify(chord.actions);
|
||||
if (seen.has(key)) {
|
||||
duplicates.add(key);
|
||||
} else {
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return duplicates;
|
||||
});
|
||||
|
||||
export const chordHashes = derived(
|
||||
chords,
|
||||
(chords) =>
|
||||
|
||||
@@ -7,20 +7,24 @@
|
||||
layout,
|
||||
overlay,
|
||||
settings,
|
||||
duplicateChords,
|
||||
} from "$lib/undo-redo";
|
||||
import type { Change } from "$lib/undo-redo";
|
||||
import type { Change, ChordChange } from "$lib/undo-redo";
|
||||
import { fly } from "svelte/transition";
|
||||
import { action } from "$lib/title";
|
||||
import { action, actionTooltip } from "$lib/title";
|
||||
import {
|
||||
deviceChords,
|
||||
deviceLayout,
|
||||
deviceSettings,
|
||||
serialLog,
|
||||
serialPort,
|
||||
sync,
|
||||
syncProgress,
|
||||
syncStatus,
|
||||
} from "$lib/serial/connection";
|
||||
import { askForConfirmation } from "$lib/dialogs/confirm-dialog";
|
||||
import ProgressButton from "$lib/ProgressButton.svelte";
|
||||
import { tick } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
function undo(event: MouseEvent) {
|
||||
if (event.shiftKey) {
|
||||
@@ -43,91 +47,11 @@
|
||||
let redoQueue: Change[][] = $state([]);
|
||||
let error = $state<Error | undefined>(undefined);
|
||||
let progressButton: HTMLButtonElement | undefined = $state();
|
||||
let shouldSaveNext = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if ($serialPort && $syncStatus == "done" && shouldSaveNext) {
|
||||
shouldSaveNext = false;
|
||||
save();
|
||||
}
|
||||
});
|
||||
|
||||
async function save() {
|
||||
async function saveLayoutChanges(progress: () => void): Promise<boolean> {
|
||||
const port = $serialPort;
|
||||
if (!port) return false;
|
||||
try {
|
||||
const port = $serialPort;
|
||||
if (!port) {
|
||||
document
|
||||
.getElementById("connect-popup")
|
||||
?.showPopover({ source: progressButton });
|
||||
shouldSaveNext = true;
|
||||
return;
|
||||
}
|
||||
$syncStatus = "uploading";
|
||||
|
||||
const layoutChanges = $overlay.layout.reduce(
|
||||
(acc, profile) =>
|
||||
acc + profile.reduce((acc, layer) => acc + layer.size, 0),
|
||||
0,
|
||||
);
|
||||
const settingChanges = $overlay.settings.reduce(
|
||||
(acc, profile) => acc + profile.size,
|
||||
0,
|
||||
);
|
||||
const chordChanges = $overlay.chords.size;
|
||||
const needsCommit = settingChanges > 0 || layoutChanges > 0;
|
||||
const progressMax = layoutChanges + settingChanges + chordChanges;
|
||||
|
||||
let progressCurrent = 0;
|
||||
|
||||
syncProgress.set({
|
||||
max: progressMax,
|
||||
current: progressCurrent,
|
||||
});
|
||||
|
||||
console.log($overlay);
|
||||
|
||||
for (const [id, chord] of $overlay.chords) {
|
||||
if (!chord.deleted) {
|
||||
if (id !== JSON.stringify(chord.actions)) {
|
||||
const existingChord = await port.getChordPhrase(chord.actions);
|
||||
if (
|
||||
existingChord !== undefined &&
|
||||
!(await askForConfirmation(
|
||||
$LL.configure.chords.conflict.TITLE(),
|
||||
$LL.configure.chords.conflict.DESCRIPTION(),
|
||||
$LL.configure.chords.conflict.CONFIRM(),
|
||||
$LL.configure.chords.conflict.ABORT(),
|
||||
chord,
|
||||
))
|
||||
) {
|
||||
changes.update((changes) =>
|
||||
changes
|
||||
.map((it) =>
|
||||
it.filter(
|
||||
(it) =>
|
||||
!(
|
||||
it.type === ChangeType.Chord &&
|
||||
JSON.stringify(it.id) === id
|
||||
),
|
||||
),
|
||||
)
|
||||
.filter((it) => it.length > 0),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await port.deleteChord({ actions: JSON.parse(id) });
|
||||
}
|
||||
await port.setChord({ actions: chord.actions, phrase: chord.phrase });
|
||||
} else {
|
||||
await port.deleteChord({ actions: chord.actions });
|
||||
}
|
||||
syncProgress.set({
|
||||
max: progressMax,
|
||||
current: progressCurrent++,
|
||||
});
|
||||
}
|
||||
|
||||
for (const [profile, layout] of $overlay.layout.entries()) {
|
||||
if (layout === undefined) continue;
|
||||
for (const [layer, actions] of layout.entries()) {
|
||||
@@ -135,66 +59,245 @@
|
||||
for (const [id, action] of actions) {
|
||||
if (action === undefined) continue;
|
||||
await port.setLayoutKey(profile, layer + 1, id, action);
|
||||
syncProgress.set({
|
||||
max: progressMax,
|
||||
current: progressCurrent++,
|
||||
});
|
||||
progress();
|
||||
}
|
||||
}
|
||||
}
|
||||
$deviceLayout = $layout.map((profile) =>
|
||||
profile.map((layer) => layer.map<number>(({ action }) => action)),
|
||||
);
|
||||
changes.update((changes) =>
|
||||
changes
|
||||
.map((it) => it.filter((it) => it.type !== ChangeType.Layout))
|
||||
.filter((it) => it.length > 0),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
await tick();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function saveSettings(progress: () => void): Promise<boolean> {
|
||||
const port = $serialPort;
|
||||
if (!port) return false;
|
||||
try {
|
||||
for (const [profile, settings] of $overlay.settings.entries()) {
|
||||
if (settings === undefined) continue;
|
||||
for (const [id, setting] of settings.entries()) {
|
||||
if (setting === undefined) continue;
|
||||
await port.setSetting(profile, id, setting);
|
||||
syncProgress.set({
|
||||
max: progressMax,
|
||||
current: progressCurrent++,
|
||||
});
|
||||
progress();
|
||||
}
|
||||
}
|
||||
|
||||
// Yes, this is a completely arbitrary and unnecessary delay.
|
||||
// The only purpose of it is to create a sense of weight,
|
||||
// aka make it more "energy intensive" to click.
|
||||
// The only conceivable way users could reach the commit limit in this case
|
||||
// would be if they click it every time they change a setting.
|
||||
// Because of that, we don't need to show a fearmongering message such as
|
||||
// "Your device will break after you click this 10,000 times!"
|
||||
if (needsCommit) {
|
||||
await port.commit();
|
||||
}
|
||||
|
||||
$deviceLayout = $layout.map((profile) =>
|
||||
profile.map((layer) => layer.map<number>(({ action }) => action)),
|
||||
);
|
||||
$deviceChords = $chords
|
||||
.filter(({ deleted }) => !deleted)
|
||||
.map(({ actions, phrase }) => ({ actions, phrase }));
|
||||
$deviceSettings = $settings.map((profile) =>
|
||||
profile.map(({ value }) => value),
|
||||
);
|
||||
$changes = [];
|
||||
changes.update((changes) =>
|
||||
changes
|
||||
.map((it) => it.filter((it) => it.type !== ChangeType.Setting))
|
||||
.filter((it) => it.length > 0),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
await tick();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function safeDeleteChord(actions: number[]): Promise<boolean> {
|
||||
const port = $serialPort;
|
||||
if (!port) return false;
|
||||
try {
|
||||
await port.deleteChord({ actions });
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
try {
|
||||
if ((await port.getChordPhrase(actions)) === undefined) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function saveChords(progress: () => void): Promise<boolean> {
|
||||
const port = $serialPort;
|
||||
if (!port) return false;
|
||||
let ok = true;
|
||||
|
||||
const empty = new Set<string>();
|
||||
for (const [id, chord] of $overlay.chords) {
|
||||
if (chord.actions.length === 0 || chord.phrase.length === 0) {
|
||||
empty.add(id);
|
||||
}
|
||||
}
|
||||
changes.update((changes) => {
|
||||
changes.push([
|
||||
...empty.keys().map(
|
||||
(id) =>
|
||||
({
|
||||
type: ChangeType.Chord,
|
||||
id: JSON.parse(id),
|
||||
deleted: true,
|
||||
actions: [],
|
||||
phrase: [],
|
||||
}) satisfies ChordChange,
|
||||
),
|
||||
]);
|
||||
return changes;
|
||||
});
|
||||
await tick();
|
||||
|
||||
const deleted = new Set<string>();
|
||||
const changed = new Map<string, number[]>();
|
||||
for (const [id, chord] of $overlay.chords) {
|
||||
if (!chord.deleted) continue;
|
||||
if (await safeDeleteChord(JSON.parse(id))) {
|
||||
deleted.add(id);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
progress();
|
||||
}
|
||||
deviceChords.update((chords) =>
|
||||
chords.filter((chord) => !deleted.has(JSON.stringify(chord.actions))),
|
||||
);
|
||||
deleted.clear();
|
||||
await tick();
|
||||
|
||||
for (const [id, chord] of $overlay.chords) {
|
||||
if (chord.deleted) continue;
|
||||
if ($duplicateChords.has(JSON.stringify(chord.actions))) {
|
||||
ok = false;
|
||||
} else {
|
||||
let skip = false;
|
||||
if (id !== JSON.stringify(chord.actions)) {
|
||||
if (await safeDeleteChord(JSON.parse(id))) {
|
||||
deleted.add(id);
|
||||
} else {
|
||||
skip = true;
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (!skip) {
|
||||
try {
|
||||
await port.setChord({
|
||||
actions: chord.actions,
|
||||
phrase: chord.phrase,
|
||||
});
|
||||
deleted.add(JSON.stringify(chord.actions));
|
||||
changed.set(JSON.stringify(chord.actions), chord.phrase);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ok = false;
|
||||
}
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
progress();
|
||||
}
|
||||
deviceChords.update((chords) => {
|
||||
chords.filter((chord) => !deleted.has(JSON.stringify(chord.actions)));
|
||||
for (const [id, phrase] of changed) {
|
||||
chords.push({ actions: JSON.parse(id), phrase });
|
||||
}
|
||||
return chords;
|
||||
});
|
||||
await tick();
|
||||
return ok;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
let needsSync = false;
|
||||
try {
|
||||
const port = $serialPort;
|
||||
if (!port) {
|
||||
document
|
||||
.getElementById("connect-popup")
|
||||
?.showPopover({ source: progressButton });
|
||||
return;
|
||||
}
|
||||
$syncStatus = "uploading";
|
||||
|
||||
const layoutChanges = $overlay.layout.reduce(
|
||||
(acc, profile) =>
|
||||
acc +
|
||||
(profile?.reduce((acc, layer) => acc + (layer?.size ?? 0), 0) ?? 0),
|
||||
0,
|
||||
);
|
||||
const settingChanges = $overlay.settings.reduce(
|
||||
(acc, profile) => acc + (profile?.size ?? 0),
|
||||
0,
|
||||
);
|
||||
const chordChanges = $overlay.chords.size;
|
||||
needsSync = chordChanges > 0;
|
||||
const needsCommit = settingChanges > 0 || layoutChanges > 0;
|
||||
const progressMax = layoutChanges + settingChanges + chordChanges;
|
||||
|
||||
let progressCurrent = 0;
|
||||
|
||||
function updateProgress() {
|
||||
syncProgress.set({
|
||||
max: progressMax,
|
||||
current: Math.min(progressMax, progressCurrent++),
|
||||
});
|
||||
}
|
||||
updateProgress();
|
||||
|
||||
let layoutSuccess = await saveLayoutChanges(updateProgress);
|
||||
let settingsSuccess = await saveSettings(updateProgress);
|
||||
|
||||
if (needsCommit) {
|
||||
try {
|
||||
await port.commit();
|
||||
} catch (e) {
|
||||
console.error("Error during commit:", e);
|
||||
layoutSuccess = false;
|
||||
}
|
||||
}
|
||||
let chordsSuccess = await saveChords(updateProgress);
|
||||
|
||||
if (layoutSuccess && settingsSuccess && chordsSuccess) {
|
||||
changes.set([]);
|
||||
needsSync = true;
|
||||
} else {
|
||||
throw new Error("Some changes could not be saved.");
|
||||
}
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
console.error("Error while saving changes:", error);
|
||||
serialLog.update((log) => {
|
||||
log.push({ type: "system", value: error?.message ?? "Error" });
|
||||
return log;
|
||||
});
|
||||
goto("/terminal");
|
||||
} finally {
|
||||
$syncStatus = "done";
|
||||
}
|
||||
|
||||
if (needsSync) {
|
||||
await sync();
|
||||
}
|
||||
}
|
||||
|
||||
let progressPopover: HTMLElement | undefined = $state();
|
||||
</script>
|
||||
|
||||
<button
|
||||
use:action={{ title: $LL.saveActions.UNDO(), shortcut: "ctrl+z" }}
|
||||
{@attach actionTooltip($LL.saveActions.UNDO(), "ctrl+z")}
|
||||
class="icon"
|
||||
disabled={$changes.length === 0}
|
||||
onclick={undo}>undo</button
|
||||
>
|
||||
<button
|
||||
use:action={{ title: $LL.saveActions.REDO(), shortcut: "ctrl+y" }}
|
||||
{@attach actionTooltip($LL.saveActions.REDO(), "ctrl+y")}
|
||||
class="icon"
|
||||
disabled={redoQueue.length === 0}
|
||||
onclick={redo}>redo</button
|
||||
@@ -202,7 +305,7 @@
|
||||
{#if $changes.length !== 0 || $syncStatus === "uploading" || $syncStatus === "error"}
|
||||
<div
|
||||
transition:fly={{ x: 10 }}
|
||||
use:action={{ title: $LL.saveActions.SAVE(), shortcut: "ctrl+shift+s" }}
|
||||
{@attach actionTooltip($LL.saveActions.SAVE(), "ctrl+shift+s")}
|
||||
>
|
||||
<ProgressButton
|
||||
disabled={$syncStatus !== "done"}
|
||||
|
||||
@@ -1,12 +1,75 @@
|
||||
<script lang="ts">
|
||||
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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
@@ -14,10 +77,10 @@
|
||||
<EditActions />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="profiles">
|
||||
{#if $serialPort && $serialPort.profileCount > 1 && !page.route.id?.startsWith("/(app)/config/chords")}
|
||||
<div
|
||||
transition:fade={{ duration: 250, easing: expoOut }}
|
||||
transition:fly={{ y: -8, duration: 250, easing: expoOut }}
|
||||
class="profiles"
|
||||
>
|
||||
{#each Array.from({ length: $serialPort.profileCount }, (_, i) => i) as profile}
|
||||
@@ -37,13 +100,13 @@
|
||||
<div class="actions">
|
||||
{#if $canShare}
|
||||
<button
|
||||
use:action={{ title: $LL.share.TITLE() }}
|
||||
{@attach actionTooltip($LL.share.TITLE())}
|
||||
transition:fly={{ x: -8 }}
|
||||
class="icon"
|
||||
onclick={triggerShare}>share</button
|
||||
>
|
||||
<button
|
||||
use:action={{ title: $LL.print.TITLE() }}
|
||||
{@attach actionTooltip($LL.print.TITLE())}
|
||||
transition:fly={{ x: -8 }}
|
||||
class="icon"
|
||||
onclick={() => print()}>print</button
|
||||
@@ -54,10 +117,32 @@
|
||||
<PwaStatus />
|
||||
{/await}
|
||||
{/if}
|
||||
<div class="import-export" bind:this={importExport}>
|
||||
<label
|
||||
><input type="file" oninput={importBackup} />
|
||||
<span class="icon">upload_file</span>Import</label
|
||||
>
|
||||
<button onclick={exportBackup}
|
||||
><span class="icon">file_save</span>Export</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style lang="scss">
|
||||
.profiles {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.import-export {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
|
||||
Reference in New Issue
Block a user