mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-21 09:23:00 +00:00
feat: layout url import
feat: backup import (except chords) feat: legacy layout import feat: separate layout, chord & setting backup downloads
This commit is contained in:
@@ -24,6 +24,8 @@
|
||||
import {runLayoutDetection} from "$lib/os-layout.js"
|
||||
import PageTransition from "./PageTransition.svelte"
|
||||
import SyncOverlay from "./SyncOverlay.svelte"
|
||||
import {restoreFromFile} from "$lib/backup/backup"
|
||||
import {goto} from "$app/navigation"
|
||||
|
||||
const locale = ((browser && localStorage.getItem("locale")) as Locales) || detectLocale()
|
||||
loadLocale(locale)
|
||||
@@ -57,6 +59,12 @@
|
||||
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) {
|
||||
await initSerial()
|
||||
}
|
||||
if (data.importFile) {
|
||||
restoreFromFile(data.importFile)
|
||||
const url = new URL(location.href)
|
||||
url.searchParams.delete("import")
|
||||
await goto(url.href, {replaceState: true})
|
||||
}
|
||||
})
|
||||
|
||||
let webManifestLink = ""
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
import type {LayoutLoad} from "./$types"
|
||||
import {browser} from "$app/environment"
|
||||
import {charaFileFromUriComponent} from "$lib/share/share-url"
|
||||
|
||||
export const prerender = true
|
||||
export const trailingSlash = "always"
|
||||
|
||||
export const load = (async ({url, data, fetch}) => {
|
||||
const importFile = new URLSearchParams(url.search).get("import")
|
||||
return {
|
||||
...data,
|
||||
importFile: browser && importFile ? await charaFileFromUriComponent(importFile, fetch) : undefined,
|
||||
}
|
||||
}) satisfies LayoutLoad
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {redirect} from "@sveltejs/kit"
|
||||
import type {PageLoad} from "./$types"
|
||||
|
||||
export const load = (() => {
|
||||
throw redirect(302, "/config/")
|
||||
export const load = (async ({url}) => {
|
||||
const newUrl = new URL(url)
|
||||
newUrl.pathname = "/config/"
|
||||
throw redirect(302, newUrl)
|
||||
}) satisfies PageLoad
|
||||
|
||||
@@ -1,103 +1,14 @@
|
||||
<script lang="ts">
|
||||
import {serialPort} from "$lib/serial/connection"
|
||||
import {preference} from "$lib/preferences"
|
||||
import LL from "../i18n/i18n-svelte"
|
||||
import type {
|
||||
CharaBackupFile,
|
||||
CharaChordFile,
|
||||
CharaSettingsFile,
|
||||
CharaLayoutFile,
|
||||
} from "$lib/share/chara-file.js"
|
||||
import {changes, ChangeType, chords, layout, settings} from "$lib/undo-redo.js"
|
||||
import type {Change} from "$lib/undo-redo.js"
|
||||
|
||||
async function downloadBackup() {
|
||||
const downloadUrl = URL.createObjectURL(
|
||||
new Blob(
|
||||
[
|
||||
JSON.stringify({
|
||||
charaVersion: 1,
|
||||
type: "backup",
|
||||
history: [
|
||||
[
|
||||
{charaVersion: 1, type: "chords", chords: $chords.map(it => [it.actions, it.phrase])},
|
||||
{
|
||||
charaVersion: 1,
|
||||
type: "layout",
|
||||
device: $serialPort?.device,
|
||||
layout: $layout.map(it => it.map(it => it.action)) as [number[], number[], number[]],
|
||||
},
|
||||
{charaVersion: 1, type: "settings", settings: $settings.map(it => it.value)},
|
||||
],
|
||||
],
|
||||
} satisfies CharaBackupFile),
|
||||
],
|
||||
{type: "application/json"},
|
||||
),
|
||||
)
|
||||
const element = document.createElement("a")
|
||||
element.setAttribute("download", "backup.json")
|
||||
element.href = downloadUrl
|
||||
element.setAttribute("target", "_blank")
|
||||
element.click()
|
||||
URL.revokeObjectURL(downloadUrl)
|
||||
}
|
||||
|
||||
async function restoreBackup(event: Event) {
|
||||
const input = (event.target as HTMLInputElement).files![0]
|
||||
if (!input) return
|
||||
const backup: CharaBackupFile = JSON.parse(await input.text())
|
||||
if (backup.charaVersion !== 1 || backup.type !== "backup") throw new Error("Invalid Backup")
|
||||
|
||||
const recent = backup.history[0]
|
||||
if (recent[1].device !== $serialPort?.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
|
||||
})
|
||||
}
|
||||
|
||||
function getChangesFromChordFile(file: CharaChordFile) {
|
||||
const changes: Change[] = []
|
||||
// TODO...
|
||||
return changes
|
||||
}
|
||||
|
||||
function getChangesFromSettingsFile(file: CharaSettingsFile) {
|
||||
const changes: Change[] = []
|
||||
for (const [id, value] of file.settings.entries()) {
|
||||
if ($settings[id].value !== value) {
|
||||
changes.push({
|
||||
type: ChangeType.Setting,
|
||||
id,
|
||||
setting: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
function getChangesFromLayoutFile(file: CharaLayoutFile) {
|
||||
const changes: Change[] = []
|
||||
for (const [layer, keys] of file.layout.entries()) {
|
||||
for (const [id, action] of keys.entries()) {
|
||||
if ($layout[layer][id].action !== action) {
|
||||
changes.push({
|
||||
type: ChangeType.Layout,
|
||||
layer,
|
||||
id,
|
||||
action,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return changes
|
||||
}
|
||||
import {
|
||||
createChordBackup,
|
||||
createLayoutBackup,
|
||||
createSettingsBackup,
|
||||
downloadBackup,
|
||||
downloadFile,
|
||||
restoreBackup,
|
||||
} from "$lib/backup/backup"
|
||||
</script>
|
||||
|
||||
<section>
|
||||
@@ -105,6 +16,21 @@
|
||||
<p class="disclaimer">
|
||||
<i>{$LL.backup.DISCLAIMER()}</i>
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend>{$LL.backup.INDIVIDUAL()}</legend>
|
||||
<button on:click={() => downloadFile(createChordBackup())}>
|
||||
<span class="icon">piano</span>
|
||||
{$LL.configure.chords.TITLE()}
|
||||
</button>
|
||||
<button on:click={() => downloadFile(createLayoutBackup())}>
|
||||
<span class="icon">keyboard</span>
|
||||
{$LL.configure.layout.TITLE()}
|
||||
</button>
|
||||
<button on:click={() => downloadFile(createSettingsBackup())}>
|
||||
<span class="icon">settings</span>
|
||||
{$LL.configure.settings.TITLE()}
|
||||
</button>
|
||||
</fieldset>
|
||||
<div class="save">
|
||||
<button class="primary" on:click={downloadBackup}
|
||||
><span class="icon">save</span>{$LL.backup.DOWNLOAD()}</button
|
||||
@@ -130,6 +56,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: flex;
|
||||
margin-block: 16px;
|
||||
border: 1px solid currentcolor;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {redirect} from "@sveltejs/kit"
|
||||
import type {PageLoad} from "./$types"
|
||||
|
||||
export const load = (() => {
|
||||
throw redirect(302, "/config/chords/")
|
||||
export const load = (({url}) => {
|
||||
const newUrl = new URL(url)
|
||||
newUrl.pathname = "/config/layout/"
|
||||
throw redirect(302, newUrl)
|
||||
}) satisfies PageLoad
|
||||
|
||||
@@ -1,40 +1,29 @@
|
||||
<script lang="ts">
|
||||
import {share} from "$lib/share"
|
||||
import {deviceLayout} from "$lib/serial/connection"
|
||||
import tippy from "tippy.js"
|
||||
import {onMount, setContext} from "svelte"
|
||||
import {setContext} from "svelte"
|
||||
import Layout from "$lib/components/layout/Layout.svelte"
|
||||
import {csvLayoutToJson, isCsvLayout} from "$lib/compat/legacy-layout"
|
||||
import {charaFileFromUriComponent, charaFileToUriComponent} from "$lib/share/share-url"
|
||||
import type {CharaLayoutFile} from "$lib/share/chara-file"
|
||||
import {charaFileToUriComponent} from "$lib/share/share-url"
|
||||
import SharePopup from "../SharePopup.svelte"
|
||||
import type {VisualLayoutConfig} from "$lib/components/layout/visual-layout"
|
||||
import {writable} from "svelte/store"
|
||||
|
||||
onMount(async () => {
|
||||
const url = new URL(window.location.href)
|
||||
if (url.searchParams.has("import")) {
|
||||
const file = await charaFileFromUriComponent(url.searchParams.get("import")!)
|
||||
if (file.type === "layout") {
|
||||
$deviceLayout = file.layout
|
||||
}
|
||||
}
|
||||
})
|
||||
import {layout} from "$lib/undo-redo"
|
||||
|
||||
async function shareLayout(event: Event) {
|
||||
const url = new URL(window.location.href)
|
||||
const url = new URL(window.location.origin)
|
||||
url.searchParams.set(
|
||||
"import",
|
||||
await charaFileToUriComponent({
|
||||
charaVersion: 1,
|
||||
type: "layout",
|
||||
device: "one",
|
||||
layout: $deviceLayout,
|
||||
layout: $layout.map(it => it.map(it => it.action)) as [number[], number[], number[]],
|
||||
}),
|
||||
)
|
||||
await navigator.clipboard.writeText(url.toString())
|
||||
let shareComponent: SharePopup
|
||||
tippy(event.target as HTMLElement, {
|
||||
interactive: true,
|
||||
onCreate(instance) {
|
||||
const target = instance.popper.querySelector(".tippy-content")!
|
||||
shareComponent = new SharePopup({target})
|
||||
|
||||
Reference in New Issue
Block a user