mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-21 17:32:41 +00:00
more serialization
This commit is contained in:
@@ -30,6 +30,11 @@ const config: IconsConfig = {
|
|||||||
"123",
|
"123",
|
||||||
"abc",
|
"abc",
|
||||||
"function",
|
"function",
|
||||||
|
"cloud_done",
|
||||||
|
"backup",
|
||||||
|
"cloud_download",
|
||||||
|
"share",
|
||||||
|
"ios_share",
|
||||||
],
|
],
|
||||||
codePoints: {
|
codePoints: {
|
||||||
speed: "e9e4",
|
speed: "e9e4",
|
||||||
@@ -39,6 +44,7 @@ const config: IconsConfig = {
|
|||||||
counter_1: "f784",
|
counter_1: "f784",
|
||||||
counter_2: "f783",
|
counter_2: "f783",
|
||||||
counter_3: "f782",
|
counter_3: "f782",
|
||||||
|
ios_share: "e6b8",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import {describe, it, expect} from "vitest"
|
|||||||
import {
|
import {
|
||||||
chordAsCommandCompatible,
|
chordAsCommandCompatible,
|
||||||
chordFromCommandCompatible,
|
chordFromCommandCompatible,
|
||||||
chordsFromFile,
|
|
||||||
chordsToFile,
|
|
||||||
deserializeActions,
|
deserializeActions,
|
||||||
serializeActions,
|
serializeActions,
|
||||||
} from "./chord"
|
} from "./chord"
|
||||||
@@ -41,16 +39,4 @@ describe("chords", function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("chl file format", function () {
|
|
||||||
const fileData: Chord[] = [
|
|
||||||
{phrase: [1, 2, 3, 4], actions: [5, 6, 7, 8, 9]},
|
|
||||||
{phrase: [10, 11], actions: [12, 13, 14, 15]},
|
|
||||||
{phrase: [16], actions: [17]},
|
|
||||||
]
|
|
||||||
|
|
||||||
it("should should convert back-forth a file", function () {
|
|
||||||
expect(chordsFromFile(chordsToFile(fileData))).toEqual(fileData)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -57,97 +57,3 @@ export function deserializeActions(native: bigint): number[] {
|
|||||||
|
|
||||||
return actions
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHL_VERSION = 1
|
|
||||||
const CHL_MAGIC = "CHL"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binary serialization of the chord library
|
|
||||||
*
|
|
||||||
* Layout is as follows:
|
|
||||||
* ```rs
|
|
||||||
* struct Chords {
|
|
||||||
* magic: "CHL"
|
|
||||||
* version: u8
|
|
||||||
* chordCount: u32
|
|
||||||
* chords: chord[]
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* struct Chord {
|
|
||||||
* id: u32
|
|
||||||
* phrase: u128
|
|
||||||
* actionCount: u16
|
|
||||||
* actions: u8[]
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* Serialized as little endian.
|
|
||||||
*
|
|
||||||
* @param chords
|
|
||||||
*/
|
|
||||||
export function chordsToFile(chords: Chord[]): ArrayBuffer {
|
|
||||||
const actionsTotalCount = chords.reduce((size, chord) => size + chord.actions.length, 0)
|
|
||||||
|
|
||||||
const buffer = new ArrayBuffer(4 + 4 + chords.length * (4 + 16 + 2) + actionsTotalCount)
|
|
||||||
const view = new DataView(buffer)
|
|
||||||
let byteOffset = 0
|
|
||||||
|
|
||||||
for (const byte of CHL_MAGIC.split("")) {
|
|
||||||
view.setUint8(byteOffset++, byte.codePointAt(0)!)
|
|
||||||
}
|
|
||||||
view.setUint8(byteOffset++, CHL_VERSION)
|
|
||||||
view.setUint32(byteOffset, chords.length, true)
|
|
||||||
byteOffset += 4
|
|
||||||
for (const chord of chords) {
|
|
||||||
const actions = serializeActions(chord.actions)
|
|
||||||
view.setBigUint64(byteOffset, actions >> 64n, true)
|
|
||||||
byteOffset += 8
|
|
||||||
view.setBigUint64(byteOffset, actions & 0xffff_ffff_ffff_ffffn, true)
|
|
||||||
byteOffset += 8
|
|
||||||
|
|
||||||
view.setUint16(byteOffset, chord.phrase.length, true)
|
|
||||||
byteOffset += 2
|
|
||||||
for (const action of chord.phrase) {
|
|
||||||
view.setUint8(byteOffset++, action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see {chordsToFile}
|
|
||||||
*/
|
|
||||||
export function chordsFromFile(buffer: ArrayBuffer): Chord[] {
|
|
||||||
const view = new DataView(buffer)
|
|
||||||
let byteOffset = 0
|
|
||||||
|
|
||||||
const magic = []
|
|
||||||
for (let i = 0; i < CHL_MAGIC.length; i++) {
|
|
||||||
magic.push(view.getUint8(byteOffset++))
|
|
||||||
}
|
|
||||||
const magicString = String.fromCodePoint(...magic)
|
|
||||||
if (magicString !== CHL_MAGIC) throw new Error(`Not a .chl file [magic ${magicString}]`)
|
|
||||||
if (view.getUint8(byteOffset++) !== CHL_VERSION) throw Error("Invalid .chl [version]")
|
|
||||||
|
|
||||||
const chords: Chord[] = Array.from({length: view.getUint32(byteOffset, true)})
|
|
||||||
byteOffset += 4
|
|
||||||
for (let i = 0; i < chords.length; i++) {
|
|
||||||
let actions = view.getBigUint64(byteOffset, true) << 64n
|
|
||||||
byteOffset += 8
|
|
||||||
actions |= view.getBigUint64(byteOffset, true)
|
|
||||||
byteOffset += 8
|
|
||||||
|
|
||||||
const phrase: number[] = Array.from({length: view.getUint16(byteOffset, true)})
|
|
||||||
byteOffset += 2
|
|
||||||
for (let i = 0; i < phrase.length; i++) {
|
|
||||||
phrase[i] = view.getUint8(byteOffset++)
|
|
||||||
}
|
|
||||||
|
|
||||||
chords[i] = {
|
|
||||||
actions: deserializeActions(actions),
|
|
||||||
phrase,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return chords
|
|
||||||
}
|
|
||||||
|
|||||||
33
src/lib/serial/serialization.ts
Normal file
33
src/lib/serial/serialization.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Compress JSON.stringify with gzip
|
||||||
|
*/
|
||||||
|
export async function stringifyCompressed(chords: any): Promise<Blob> {
|
||||||
|
const stream = new Blob([JSON.stringify(chords)]).stream().pipeThrough(new CompressionStream("gzip"))
|
||||||
|
return await new Response(stream).blob()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decompress JSON.parse with gzip
|
||||||
|
*/
|
||||||
|
export async function parseCompressed<T>(blob: Blob): Promise<T> {
|
||||||
|
const stream = blob.stream().pipeThrough(new DecompressionStream("gzip"))
|
||||||
|
return await new Response(stream).json()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share JS object as url query param
|
||||||
|
*/
|
||||||
|
export async function getSharableUrl(name: string, data: any, baseHref = window.location.href): Promise<URL> {
|
||||||
|
return new Promise(async resolve => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = function () {
|
||||||
|
const base64String = (reader.result as string)
|
||||||
|
.replace(/^data:application\/octet-stream;base64,/, "")
|
||||||
|
.replace(/==$/, "")
|
||||||
|
const url = new URL(baseHref)
|
||||||
|
url.searchParams.set(name, base64String)
|
||||||
|
resolve(url)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(await stringifyCompressed(data))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {chords} from "$lib/serial/connection"
|
import {chords} from "$lib/serial/connection"
|
||||||
import type {Chord} from "$lib/serial/connection"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||||
import FlexSearch from "flexsearch"
|
import FlexSearch from "flexsearch"
|
||||||
import type {Index} from "flexsearch"
|
import type {Index} from "flexsearch"
|
||||||
import {tick} from "svelte"
|
import {tick} from "svelte"
|
||||||
|
import type {Chord} from "$lib/serial/chord"
|
||||||
|
import {getSharableUrl, stringifyCompressed} from "$lib/serial/serialization"
|
||||||
|
|
||||||
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
|
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
|
||||||
|
|
||||||
@@ -26,6 +27,20 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function downloadBackup() {
|
||||||
|
const downloadUrl = URL.createObjectURL(await stringifyCompressed($chords))
|
||||||
|
const element = document.createElement("a")
|
||||||
|
element.setAttribute("download", "chords.json.gz")
|
||||||
|
element.href = downloadUrl
|
||||||
|
element.setAttribute("target", "_blank")
|
||||||
|
element.click()
|
||||||
|
URL.revokeObjectURL(downloadUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createShareUrl() {
|
||||||
|
console.log(await getSharableUrl("chords", $chords))
|
||||||
|
}
|
||||||
|
|
||||||
$: items = searchFilter?.map(it => [$chords[it], it]) ?? $chords.map((it, i) => [it, i])
|
$: items = searchFilter?.map(it => [$chords[it], it]) ?? $chords.map((it, i) => [it, i])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -55,6 +70,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</table>
|
</table>
|
||||||
|
<div class="backup">
|
||||||
|
<button class="icon" title="Sharable URL" on:click={createShareUrl()}>share</button>
|
||||||
|
<div class="icon" title="Chords have been backed up to this browser.">cloud_done</div>
|
||||||
|
<button class="icon" title="Restore local chord backup">backup</button>
|
||||||
|
<button class="icon" title="Download local chord backup" on:click={downloadBackup}>cloud_download</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@@ -85,6 +106,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
--scrollbar-color: var(--md-sys-color-surface-variant);
|
--scrollbar-color: var(--md-sys-color-surface-variant);
|
||||||
|
|
||||||
@@ -94,6 +120,7 @@
|
|||||||
|
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
padding-inline: 8px;
|
padding-inline: 8px;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user