Files
DeviceManager/src/lib/backup/backup.ts

163 lines
4.6 KiB
TypeScript

import type {
CharaBackupFile,
CharaChordFile,
CharaFile,
CharaLayoutFile,
CharaSettingsFile,
} from "$lib/share/chara-file.js"
import type {Change} from "$lib/undo-redo.js"
import {changes, ChangeType, chords, layout, settings} from "$lib/undo-redo.js"
import {get} from "svelte/store"
import {serialPort} from "../serial/connection"
import {csvLayoutToJson, isCsvLayout} from "$lib/backup/compat/legacy-layout"
import {isCsvChords, csvChordsToJson} from "./compat/legacy-chords"
export function downloadFile<T extends CharaFile<string>>(contents: T) {
const downloadUrl = URL.createObjectURL(new Blob([JSON.stringify(contents)], {type: "application/json"}))
const element = document.createElement("a")
element.setAttribute(
"download",
`${contents.type}-${get(serialPort)?.device}-${new Date().toISOString()}.json`,
)
element.href = downloadUrl
element.setAttribute("target", "_blank")
element.click()
URL.revokeObjectURL(downloadUrl)
}
export function downloadBackup() {
downloadFile<CharaBackupFile>({
charaVersion: 1,
type: "backup",
history: [[createChordBackup(), createLayoutBackup(), createSettingsBackup()]],
})
}
export function createLayoutBackup(): CharaLayoutFile {
return {
charaVersion: 1,
type: "layout",
device: get(serialPort)?.device,
layout: get(layout).map(it => it.map(it => it.action)) as [number[], number[], number[]],
}
}
export function createChordBackup(): CharaChordFile {
return {charaVersion: 1, type: "chords", chords: get(chords).map(it => [it.actions, it.phrase])}
}
export function createSettingsBackup(): CharaSettingsFile {
return {charaVersion: 1, type: "settings", settings: get(settings).map(it => it.value)}
}
export async function restoreBackup(event: Event) {
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))
} else if (isCsvLayout(text)) {
restoreFromFile(csvLayoutToJson(text))
} else if (isCsvChords(text)) {
restoreFromFile(csvChordsToJson(text))
}
}
export function restoreFromFile(
file: CharaBackupFile | CharaSettingsFile | CharaLayoutFile | CharaChordFile,
) {
if (file.charaVersion !== 1) throw new Error("Incompatible backup")
switch (file.type) {
case "backup": {
const recent = file.history[0]
if (recent[1].device !== get(serialPort)?.device) {
alert("Backup is incompatible with this device")
throw new Error("Backup is incompatible with this device")
}
changes.update(changes => {
changes.push(
...getChangesFromChordFile(recent[0]),
...getChangesFromLayoutFile(recent[1]),
...getChangesFromSettingsFile(recent[2]),
)
return changes
})
break
}
case "chords": {
changes.update(changes => {
changes.push(...getChangesFromChordFile(file))
return changes
})
break
}
case "layout": {
changes.update(changes => {
changes.push(...getChangesFromLayoutFile(file))
return changes
})
break
}
case "settings": {
changes.update(changes => {
changes.push(...getChangesFromSettingsFile(file))
return changes
})
break
}
default: {
throw new Error(`Unknown backup type "${(file as CharaFile<string>).type}"`)
}
}
}
export function getChangesFromChordFile(file: CharaChordFile) {
const changes: Change[] = []
const existingChords = new Set(get(chords).map(({phrase, actions}) => JSON.stringify([actions, phrase])))
for (const [input, output] of file.chords) {
if (existingChords.has(JSON.stringify([input, output]))) {
continue
}
changes.push({
type: ChangeType.Chord,
actions: input,
phrase: output,
id: input,
})
}
return changes
}
export function getChangesFromSettingsFile(file: CharaSettingsFile) {
const changes: Change[] = []
for (const [id, value] of file.settings.entries()) {
const setting = get(settings)[id]
if (setting !== undefined && setting.value !== value) {
changes.push({
type: ChangeType.Setting,
id,
setting: value,
})
}
}
return changes
}
export function getChangesFromLayoutFile(file: CharaLayoutFile) {
const changes: Change[] = []
for (const [layer, keys] of file.layout.entries()) {
for (const [id, action] of keys.entries()) {
if (get(layout)[layer][id].action !== action) {
changes.push({
type: ChangeType.Layout,
layer,
id,
action,
})
}
}
}
return changes
}