mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-07 02:22:52 +00:00
feat: new sharing system
feat: support legacy layout import
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -9,6 +9,10 @@ const de = {
|
||||
DOWNLOAD: "Kopie Speichern",
|
||||
RESTORE: "Wiederherstellen",
|
||||
},
|
||||
share: {
|
||||
URL_COPIED: "Teilbare URL kopiert!",
|
||||
EXTRA_DOWNLOAD: "Als Datei herunterladen",
|
||||
},
|
||||
profile: {
|
||||
TITLE: "Profil",
|
||||
LANGUAGE: "Sprache",
|
||||
|
||||
@@ -8,6 +8,10 @@ const en = {
|
||||
DOWNLOAD: "Download Backup",
|
||||
RESTORE: "Restore",
|
||||
},
|
||||
share: {
|
||||
URL_COPIED: "Sharable URL copied!",
|
||||
EXTRA_DOWNLOAD: "Download as file",
|
||||
},
|
||||
profile: {
|
||||
TITLE: "Profile",
|
||||
LANGUAGE: "Language",
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { FormattersInitializer } from 'typesafe-i18n'
|
||||
import type { Locales, Formatters } from './i18n-types'
|
||||
import type {FormattersInitializer} from "typesafe-i18n"
|
||||
import type {Locales, Formatters} from "./i18n-types"
|
||||
|
||||
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
|
||||
const formatters: Formatters = {
|
||||
// add your formatter functions here
|
||||
}
|
||||
|
||||
const formatters: Formatters = {
|
||||
// add your formatter functions here
|
||||
}
|
||||
|
||||
return formatters
|
||||
return formatters
|
||||
}
|
||||
|
||||
24
src/lib/compat/legacy-layout-converted.sample.json
Normal file
24
src/lib/compat/legacy-layout-converted.sample.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"charaVersion": 1,
|
||||
"type": "layout",
|
||||
"device": "one",
|
||||
"layout": [
|
||||
[
|
||||
309, 304, 312, 303, 306, 290, 282, 301, 266, 285, 289, 270, 281, 272, 262, 288, 277, 298, 307, 264, 287,
|
||||
268, 332, 311, 274, 286, 308, 329, 310, 280, 358, 512, 515, 513, 514, 313, 319, 318, 321, 320, 326, 315,
|
||||
314, 317, 316, 312, 330, 331, 333, 334, 291, 261, 283, 536, 276, 292, 265, 275, 267, 263, 293, 260, 296,
|
||||
544, 279, 294, 271, 299, 269, 273, 295, 284, 297, 302, 278, 357, 516, 519, 517, 518, 327, 336, 338, 335,
|
||||
337, 328, 325, 322, 323, 324
|
||||
],
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
],
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
]
|
||||
]
|
||||
}
|
||||
270
src/lib/compat/legacy-layout.sample.csv
Normal file
270
src/lib/compat/legacy-layout.sample.csv
Normal file
@@ -0,0 +1,270 @@
|
||||
A1,0,309
|
||||
A1,1,304
|
||||
A1,2,312
|
||||
A1,3,303
|
||||
A1,4,306
|
||||
A1,5,290
|
||||
A1,6,282
|
||||
A1,7,301
|
||||
A1,8,266
|
||||
A1,9,285
|
||||
A1,10,289
|
||||
A1,11,270
|
||||
A1,12,281
|
||||
A1,13,272
|
||||
A1,14,262
|
||||
A1,15,288
|
||||
A1,16,277
|
||||
A1,17,298
|
||||
A1,18,307
|
||||
A1,19,264
|
||||
A1,20,287
|
||||
A1,21,268
|
||||
A1,22,332
|
||||
A1,23,311
|
||||
A1,24,274
|
||||
A1,25,286
|
||||
A1,26,308
|
||||
A1,27,329
|
||||
A1,28,310
|
||||
A1,29,280
|
||||
A1,30,358
|
||||
A1,31,512
|
||||
A1,32,515
|
||||
A1,33,513
|
||||
A1,34,514
|
||||
A1,35,313
|
||||
A1,36,319
|
||||
A1,37,318
|
||||
A1,38,321
|
||||
A1,39,320
|
||||
A1,40,326
|
||||
A1,41,315
|
||||
A1,42,314
|
||||
A1,43,317
|
||||
A1,44,316
|
||||
A1,45,312
|
||||
A1,46,330
|
||||
A1,47,331
|
||||
A1,48,333
|
||||
A1,49,334
|
||||
A1,50,291
|
||||
A1,51,261
|
||||
A1,52,283
|
||||
A1,53,536
|
||||
A1,54,276
|
||||
A1,55,292
|
||||
A1,56,265
|
||||
A1,57,275
|
||||
A1,58,267
|
||||
A1,59,263
|
||||
A1,60,293
|
||||
A1,61,260
|
||||
A1,62,296
|
||||
A1,63,544
|
||||
A1,64,279
|
||||
A1,65,294
|
||||
A1,66,271
|
||||
A1,67,299
|
||||
A1,68,269
|
||||
A1,69,273
|
||||
A1,70,295
|
||||
A1,71,284
|
||||
A1,72,297
|
||||
A1,73,302
|
||||
A1,74,278
|
||||
A1,75,357
|
||||
A1,76,516
|
||||
A1,77,519
|
||||
A1,78,517
|
||||
A1,79,518
|
||||
A1,80,327
|
||||
A1,81,336
|
||||
A1,82,338
|
||||
A1,83,335
|
||||
A1,84,337
|
||||
A1,85,328
|
||||
A1,86,325
|
||||
A1,87,322
|
||||
A1,88,323
|
||||
A1,89,324
|
||||
A2,0,0
|
||||
A2,1,0
|
||||
A2,2,0
|
||||
A2,3,0
|
||||
A2,4,0
|
||||
A2,5,0
|
||||
A2,6,0
|
||||
A2,7,0
|
||||
A2,8,0
|
||||
A2,9,0
|
||||
A2,10,0
|
||||
A2,11,0
|
||||
A2,12,0
|
||||
A2,13,0
|
||||
A2,14,0
|
||||
A2,15,0
|
||||
A2,16,0
|
||||
A2,17,0
|
||||
A2,18,0
|
||||
A2,19,0
|
||||
A2,20,0
|
||||
A2,21,0
|
||||
A2,22,0
|
||||
A2,23,0
|
||||
A2,24,0
|
||||
A2,25,0
|
||||
A2,26,0
|
||||
A2,27,0
|
||||
A2,28,0
|
||||
A2,29,0
|
||||
A2,30,0
|
||||
A2,31,0
|
||||
A2,32,0
|
||||
A2,33,0
|
||||
A2,34,0
|
||||
A2,35,0
|
||||
A2,36,0
|
||||
A2,37,0
|
||||
A2,38,0
|
||||
A2,39,0
|
||||
A2,40,0
|
||||
A2,41,0
|
||||
A2,42,0
|
||||
A2,43,0
|
||||
A2,44,0
|
||||
A2,45,0
|
||||
A2,46,0
|
||||
A2,47,0
|
||||
A2,48,0
|
||||
A2,49,0
|
||||
A2,50,0
|
||||
A2,51,0
|
||||
A2,52,0
|
||||
A2,53,0
|
||||
A2,54,0
|
||||
A2,55,0
|
||||
A2,56,0
|
||||
A2,57,0
|
||||
A2,58,0
|
||||
A2,59,0
|
||||
A2,60,0
|
||||
A2,61,0
|
||||
A2,62,0
|
||||
A2,63,0
|
||||
A2,64,0
|
||||
A2,65,0
|
||||
A2,66,0
|
||||
A2,67,0
|
||||
A2,68,0
|
||||
A2,69,0
|
||||
A2,70,0
|
||||
A2,71,0
|
||||
A2,72,0
|
||||
A2,73,0
|
||||
A2,74,0
|
||||
A2,75,0
|
||||
A2,76,0
|
||||
A2,77,0
|
||||
A2,78,0
|
||||
A2,79,0
|
||||
A2,80,0
|
||||
A2,81,0
|
||||
A2,82,0
|
||||
A2,83,0
|
||||
A2,84,0
|
||||
A2,85,0
|
||||
A2,86,0
|
||||
A2,87,0
|
||||
A2,88,0
|
||||
A2,89,0
|
||||
A3,0,0
|
||||
A3,1,0
|
||||
A3,2,0
|
||||
A3,3,0
|
||||
A3,4,0
|
||||
A3,5,0
|
||||
A3,6,0
|
||||
A3,7,0
|
||||
A3,8,0
|
||||
A3,9,0
|
||||
A3,10,0
|
||||
A3,11,0
|
||||
A3,12,0
|
||||
A3,13,0
|
||||
A3,14,0
|
||||
A3,15,0
|
||||
A3,16,0
|
||||
A3,17,0
|
||||
A3,18,0
|
||||
A3,19,0
|
||||
A3,20,0
|
||||
A3,21,0
|
||||
A3,22,0
|
||||
A3,23,0
|
||||
A3,24,0
|
||||
A3,25,0
|
||||
A3,26,0
|
||||
A3,27,0
|
||||
A3,28,0
|
||||
A3,29,0
|
||||
A3,30,0
|
||||
A3,31,0
|
||||
A3,32,0
|
||||
A3,33,0
|
||||
A3,34,0
|
||||
A3,35,0
|
||||
A3,36,0
|
||||
A3,37,0
|
||||
A3,38,0
|
||||
A3,39,0
|
||||
A3,40,0
|
||||
A3,41,0
|
||||
A3,42,0
|
||||
A3,43,0
|
||||
A3,44,0
|
||||
A3,45,0
|
||||
A3,46,0
|
||||
A3,47,0
|
||||
A3,48,0
|
||||
A3,49,0
|
||||
A3,50,0
|
||||
A3,51,0
|
||||
A3,52,0
|
||||
A3,53,0
|
||||
A3,54,0
|
||||
A3,55,0
|
||||
A3,56,0
|
||||
A3,57,0
|
||||
A3,58,0
|
||||
A3,59,0
|
||||
A3,60,0
|
||||
A3,61,0
|
||||
A3,62,0
|
||||
A3,63,0
|
||||
A3,64,0
|
||||
A3,65,0
|
||||
A3,66,0
|
||||
A3,67,0
|
||||
A3,68,0
|
||||
A3,69,0
|
||||
A3,70,0
|
||||
A3,71,0
|
||||
A3,72,0
|
||||
A3,73,0
|
||||
A3,74,0
|
||||
A3,75,0
|
||||
A3,76,0
|
||||
A3,77,0
|
||||
A3,78,0
|
||||
A3,79,0
|
||||
A3,80,0
|
||||
A3,81,0
|
||||
A3,82,0
|
||||
A3,83,0
|
||||
A3,84,0
|
||||
A3,85,0
|
||||
A3,86,0
|
||||
A3,87,0
|
||||
A3,88,0
|
||||
A3,89,0
|
||||
|
18
src/lib/compat/legacy-layout.spec.ts
Normal file
18
src/lib/compat/legacy-layout.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {describe, expect, it} from "vitest"
|
||||
import legacyLayout from "./legacy-layout.sample.csv?raw"
|
||||
import legacyLayoutConverted from "./legacy-layout-converted.sample.json"
|
||||
import {csvLayoutToJson, isCsvLayout} from "./legacy-layout"
|
||||
|
||||
describe("legacy layout", () => {
|
||||
it("should detect a legacy layout", () => {
|
||||
expect(isCsvLayout(legacyLayout)).to.be.true
|
||||
})
|
||||
|
||||
it("should not detect chord maps as layouts", () => {
|
||||
expect(isCsvLayout("e + h + t,the")).to.be.false
|
||||
})
|
||||
|
||||
it("should convert legacy layouts", () => {
|
||||
expect(csvLayoutToJson(legacyLayout)).to.deep.equal(legacyLayoutConverted)
|
||||
})
|
||||
})
|
||||
25
src/lib/compat/legacy-layout.ts
Normal file
25
src/lib/compat/legacy-layout.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type {CharaLayoutFile} from "$lib/share/chara-file"
|
||||
|
||||
/**
|
||||
* Converts a legacy CSV-based layout to the modern JSON-based format
|
||||
*/
|
||||
export function csvLayoutToJson(csv: string, device: CharaLayoutFile["device"] = "one"): CharaLayoutFile {
|
||||
const layout: CharaLayoutFile = {
|
||||
charaVersion: 1,
|
||||
type: "layout",
|
||||
device,
|
||||
layout: [[], [], []],
|
||||
}
|
||||
|
||||
for (const layer of csv.split("\n")) {
|
||||
const [layerId, key, action] = layer.substring(1).split(",").map(Number)
|
||||
|
||||
layout.layout[Number(layerId) - 1][Number(key)] = Number(action)
|
||||
}
|
||||
|
||||
return layout
|
||||
}
|
||||
|
||||
export function isCsvLayout(csv: string): boolean {
|
||||
return /^(A[123],\d+,\d+\n?)+$/.test(csv)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
||||
|
||||
export let exact: number | undefined = undefined
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {serialLog, serialPort} from "$lib/serial/connection"
|
||||
import {slide} from "svelte/transition"
|
||||
|
||||
function submit(event: InputEvent) {
|
||||
function submit(event: Event) {
|
||||
event.preventDefault()
|
||||
$serialPort.send(value.trim())
|
||||
value = ""
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
results = query ? index.search(searchInput.value) : defaultActions
|
||||
}
|
||||
|
||||
let customValue = undefined
|
||||
let customValue: number | undefined = undefined
|
||||
const defaultActions: string[] = [
|
||||
charaActions,
|
||||
mouseActions,
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
font-family: "Material Symbols Rounded";
|
||||
font-size: 24px;
|
||||
font-feature-settings: "liga";
|
||||
font-variation-settings: "FILL" var(--icon-fill, 0), "wght" var(--icon-weigth, 400),
|
||||
font-variation-settings:
|
||||
"FILL" var(--icon-fill, 0),
|
||||
"wght" var(--icon-weigth, 400),
|
||||
"GRAD" var(--icon-grade, 0);
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function toBase64(blob: Blob): Promise<string> {
|
||||
.replace(/^data:application\/octet-stream;base64,/, "")
|
||||
.replaceAll("+", ".")
|
||||
.replaceAll("/", "_")
|
||||
.replaceAll("=", "-")}-`,
|
||||
.replaceAll("=", "-")}`,
|
||||
)
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
@@ -23,7 +23,6 @@ export async function toBase64(blob: Blob): Promise<string> {
|
||||
export async function fromBase64(base64: string): Promise<Blob> {
|
||||
return fetch(
|
||||
`data:application/octet-stream;base64,${base64
|
||||
.replace(/-$/, "")
|
||||
.replaceAll(".", "+")
|
||||
.replaceAll("_", "/")
|
||||
.replaceAll("-", "=")}`,
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import {compressActions, decompressActions} from "./actions"
|
||||
import {fromBase64, toBase64} from "$lib/serialization/base64"
|
||||
|
||||
export interface NewCharaLayout {
|
||||
charaLayoutVersion: 1
|
||||
device: "one" | "lite" | string
|
||||
/**
|
||||
* Layers A1-A3, with numeric action codes on each
|
||||
*/
|
||||
layers: [number[], number[], number[]]
|
||||
}
|
||||
|
||||
export type CharaLayout = [number[], number[], number[]]
|
||||
|
||||
/**
|
||||
|
||||
52
src/lib/share/action-array.ts
Normal file
52
src/lib/share/action-array.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {compressActions, decompressActions} from "$lib/serialization/actions"
|
||||
import {CHARA_FILE_TYPES} from "$lib/share/share-url"
|
||||
|
||||
export type ActionArray = number[] | ActionArray[]
|
||||
export function serializeActionArray(array: ActionArray): Uint8Array {
|
||||
let out = new Uint8Array(5)
|
||||
const writer = new DataView(out.buffer)
|
||||
writer.setUint32(0, array.length)
|
||||
|
||||
if (array.length === 0) {
|
||||
return out
|
||||
} else if (typeof array[0] === "number") {
|
||||
writer.setUint8(4, CHARA_FILE_TYPES.indexOf("number"))
|
||||
return concatUint8Arrays(out, compressActions(array as number[]))
|
||||
} else if (Array.isArray(array[0])) {
|
||||
writer.setUint8(4, CHARA_FILE_TYPES.indexOf("array"))
|
||||
return concatUint8Arrays(out, ...(array as ActionArray[]).map(serializeActionArray))
|
||||
} else {
|
||||
throw new Error("Not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
export function deserializeActionArray(raw: Uint8Array): ActionArray {
|
||||
const reader = new DataView(raw.buffer)
|
||||
const length = reader.getUint32(0)
|
||||
const type = CHARA_FILE_TYPES[reader.getUint8(4)]
|
||||
|
||||
if (type === "number") {
|
||||
return decompressActions(raw.slice(5, 5 + length))
|
||||
} else if (type === "array") {
|
||||
const innerLength = reader.getUint32(5)
|
||||
const out = []
|
||||
let cursor = 5
|
||||
for (let i = 0; i < length; i++) {
|
||||
out.push(deserializeActionArray(raw.slice(cursor, cursor + innerLength)))
|
||||
cursor += innerLength
|
||||
}
|
||||
return out
|
||||
} else {
|
||||
throw new Error("Not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
export function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array {
|
||||
const out = new Uint8Array(arrays.reduce((a, b) => a + b.length, 0))
|
||||
let offset = 0
|
||||
for (const array of arrays) {
|
||||
out.set(array, offset)
|
||||
offset += array.length
|
||||
}
|
||||
return out
|
||||
}
|
||||
15
src/lib/share/chara-file.ts
Normal file
15
src/lib/share/chara-file.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface CharaFile<T extends string> {
|
||||
charaVersion: 1
|
||||
type: T
|
||||
}
|
||||
|
||||
export interface CharaLayoutFile extends CharaFile<"layout"> {
|
||||
device: "one" | "lite" | string
|
||||
layout: [number[], number[], number[]]
|
||||
}
|
||||
|
||||
export interface CharaChordFile extends CharaFile<"chords"> {
|
||||
chords: [number[], number[]]
|
||||
}
|
||||
|
||||
export type CharaFiles = CharaLayoutFile | CharaChordFile
|
||||
61
src/lib/share/share-url.ts
Normal file
61
src/lib/share/share-url.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type {CharaFile, CharaFiles} from "$lib/share/chara-file"
|
||||
import type {ActionArray} from "$lib/share/action-array"
|
||||
import {deserializeActionArray, serializeActionArray} from "$lib/share/action-array"
|
||||
import {fromBase64, toBase64} from "$lib/serialization/base64"
|
||||
|
||||
type CharaLayoutOrder = {
|
||||
[K in CharaFiles["type"]]: Array<
|
||||
[Exclude<keyof Extract<CharaFiles, {type: K}>, keyof CharaFile<any>>, (typeof CHARA_FILE_TYPES)[number]]
|
||||
>
|
||||
}
|
||||
|
||||
const keys: CharaLayoutOrder = {
|
||||
layout: [
|
||||
["layout", "array"],
|
||||
["device", "string"],
|
||||
],
|
||||
chords: [["chords", "array"]],
|
||||
}
|
||||
|
||||
export const CHARA_FILE_TYPES = ["unknown", "number", "string", "array"] as const
|
||||
|
||||
const sep = "\n"
|
||||
|
||||
export async function charaFileToUriComponent<T extends CharaFiles>(file: T): Promise<string> {
|
||||
let url = `${file.type}${sep}${file.charaVersion}`
|
||||
|
||||
for (const [key, type] of keys[file.type]) {
|
||||
const value = file[key as keyof T]
|
||||
url += sep
|
||||
if (type === "string") {
|
||||
url += value as string
|
||||
} else if (type === "array") {
|
||||
const stream = new Blob([serializeActionArray(value as ActionArray)])
|
||||
.stream()
|
||||
.pipeThrough(new CompressionStream("deflate"))
|
||||
url += await toBase64(await new Response(stream).blob())
|
||||
} else {
|
||||
throw new Error("Not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export async function charaFileFromUriComponent<T extends CharaFiles>(uriComponent: string): Promise<T> {
|
||||
const [fileType, version, ...values] = uriComponent.split(sep)
|
||||
const file: any = {type: fileType, version: Number(version)}
|
||||
|
||||
for (const [key, type] of keys[fileType as keyof typeof keys]) {
|
||||
const value = values.pop()!
|
||||
if (type === "string") {
|
||||
file[key] = value
|
||||
} else if (type === "array") {
|
||||
const stream = (await fromBase64(value)).stream().pipeThrough(new DecompressionStream("deflate"))
|
||||
const actions = new Uint8Array(await new Response(stream).arrayBuffer())
|
||||
file[key] = deserializeActionArray(actions)
|
||||
}
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
@@ -57,17 +57,6 @@
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
display: inline-block;
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
background: var(--md-sys-color-on-error);
|
||||
}
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
|
||||
6
src/routes/config/SharePopup.svelte
Normal file
6
src/routes/config/SharePopup.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
import LL from "../../i18n/i18n-svelte"
|
||||
</script>
|
||||
|
||||
<h4>{$LL.share.URL_COPIED()}</h4>
|
||||
<button>{$LL.share.EXTRA_DOWNLOAD()}</button>
|
||||
@@ -3,33 +3,70 @@
|
||||
import {layout} from "$lib/serial/connection"
|
||||
import tippy from "tippy.js"
|
||||
import {onMount} from "svelte"
|
||||
import {layoutAsUrlComponent, layoutFromUrlComponent} from "$lib/serialization/layout"
|
||||
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 SharePopup from "../SharePopup.svelte"
|
||||
|
||||
onMount(async () => {
|
||||
const url = new URL(window.location.href)
|
||||
if (url.searchParams.has("layout")) {
|
||||
$layout = await layoutFromUrlComponent(url.searchParams.get("layout")!)
|
||||
if (url.searchParams.has("import")) {
|
||||
const file = await charaFileFromUriComponent(url.searchParams.get("import")!)
|
||||
if (file.type === "layout") {
|
||||
$layout = file.layout
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function shareLayout(event: Event) {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set("layout", await layoutAsUrlComponent($layout))
|
||||
url.searchParams.set(
|
||||
"import",
|
||||
await charaFileToUriComponent({
|
||||
charaVersion: 1,
|
||||
type: "layout",
|
||||
device: "one",
|
||||
layout: $layout,
|
||||
}),
|
||||
)
|
||||
await navigator.clipboard.writeText(url.toString())
|
||||
let shareComponent: SharePopup
|
||||
tippy(event.target as HTMLElement, {
|
||||
content: "Share url copied!",
|
||||
delay: [0, 1000000],
|
||||
onCreate(instance) {
|
||||
const target = instance.popper.querySelector(".tippy-content")!
|
||||
shareComponent = new SharePopup({target})
|
||||
},
|
||||
onHidden(instance) {
|
||||
instance.destroy()
|
||||
},
|
||||
onDestroy(instance) {
|
||||
shareComponent.$destroy()
|
||||
},
|
||||
}).show()
|
||||
}
|
||||
|
||||
async function importLayout() {
|
||||
const file = await fileInput.files?.item(0)?.text()
|
||||
if (!file) return
|
||||
const importedLayout = isCsvLayout(file) ? csvLayoutToJson(file) : (JSON.parse(file) as CharaLayoutFile)
|
||||
if (importedLayout.type === "layout" && importedLayout.charaVersion === 1) $layout = importedLayout.layout
|
||||
}
|
||||
|
||||
let fileInput: HTMLInputElement
|
||||
</script>
|
||||
|
||||
<svelte:window use:share={shareLayout} />
|
||||
|
||||
<section>
|
||||
<label class="icon"
|
||||
>upload_file<input
|
||||
bind:this={fileInput}
|
||||
on:input={importLayout}
|
||||
type="file"
|
||||
accept="text/csv, application/json"
|
||||
/></label
|
||||
>
|
||||
<Layout />
|
||||
</section>
|
||||
|
||||
@@ -37,4 +74,8 @@
|
||||
section {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user