diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..6b783636 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/.prettierrc.cjs b/.prettierrc.cjs deleted file mode 100644 index d955dd1f..00000000 --- a/.prettierrc.cjs +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - ...require("@theaninova/prettier-config"), - plugins: ["prettier-plugin-svelte"], - pluginSearchDirs: ["."], - overrides: [{files: "*.svelte", options: {parser: "svelte"}}], -} diff --git a/icons.config.js b/icons.config.js index f5ffb0a6..5861f75a 100644 --- a/icons.config.js +++ b/icons.config.js @@ -112,6 +112,6 @@ const config = { stat_minus_2: "e69c", stat_2: "e699", }, -} +}; -export default config +export default config; diff --git a/package-lock.json b/package-lock.json index 8372e176..c83988f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "@sveltejs/vite-plugin-svelte": "^2.4.5", "@tauri-apps/api": "^1.4.0", "@tauri-apps/cli": "^1.4.0", - "@theaninova/prettier-config": "^1.0.0", "@types/dom-view-transitions": "^1.0.1", "@types/flexsearch": "^0.7.3", "@types/w3c-web-serial": "^1.0.3", @@ -40,8 +39,8 @@ "jsdom": "^22.1.0", "npm-run-all": "^4.1.5", "patch-package": "^8.0.0", - "prettier": "^3.0.3", - "prettier-plugin-svelte": "^3.0.3", + "prettier": "^3.2.5", + "prettier-plugin-svelte": "^3.2.2", "sass": "^1.66.1", "stylelint": "^15.10.3", "stylelint-config-clean-order": "^5.2.0", @@ -3191,12 +3190,6 @@ "node": ">= 10" } }, - "node_modules/@theaninova/prettier-config": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@theaninova/prettier-config/-/prettier-config-1.0.0.tgz", - "integrity": "sha512-DdP5OvUL6Tso+XMNiH+WQJrbvJsIGIudekZKMW4oGt060hqZIzwiASILCiDg6+OigKTC1KObvv4nTBQSjM2g1g==", - "dev": true - }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -8633,9 +8626,9 @@ "dev": true }, "node_modules/prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -8648,13 +8641,13 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.0.3.tgz", - "integrity": "sha512-dLhieh4obJEK1hnZ6koxF+tMUrZbV5YGvRpf2+OADyanjya5j0z1Llo8iGwiHmFWZVG/hLEw/AJD5chXd9r3XA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.2.tgz", + "integrity": "sha512-ZzzE/wMuf48/1+Lf2Ffko0uDa6pyCfgHV6+uAhtg2U0AAXGrhCSW88vEJNAkAxW5qyrFY1y1zZ4J8TgHrjW++Q==", "dev": true, "peerDependencies": { "prettier": "^3.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0" + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "node_modules/pretty-bytes": { diff --git a/package.json b/package.json index 5b5b6f57..0eb6046a 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch", "minify-icons": "node src/tools/minify-icon-font.js", "version": "node src/tools/version.js && git add src-tauri/Cargo.toml && git add src-tauri/tauri.conf.json", - "lint": "prettier --plugin-search-dir . --check .", - "format": "prettier --plugin-search-dir . --write .", + "lint": "prettier --check .", + "format": "prettier --write .", "typesafe-i18n": "typesafe-i18n" }, "devDependencies": { @@ -46,7 +46,6 @@ "@sveltejs/vite-plugin-svelte": "^2.4.5", "@tauri-apps/api": "^1.4.0", "@tauri-apps/cli": "^1.4.0", - "@theaninova/prettier-config": "^1.0.0", "@types/dom-view-transitions": "^1.0.1", "@types/flexsearch": "^0.7.3", "@types/w3c-web-serial": "^1.0.3", @@ -62,8 +61,8 @@ "jsdom": "^22.1.0", "npm-run-all": "^4.1.5", "patch-package": "^8.0.0", - "prettier": "^3.0.3", - "prettier-plugin-svelte": "^3.0.3", + "prettier": "^3.2.5", + "prettier-plugin-svelte": "^3.2.2", "sass": "^1.66.1", "stylelint": "^15.10.3", "stylelint-config-clean-order": "^5.2.0", diff --git a/src/app.d.ts b/src/app.d.ts index 10e088bf..73efc744 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -11,4 +11,4 @@ declare global { } } -export {} +export {}; diff --git a/src/env.d.ts b/src/env.d.ts index 087ff922..f634f50e 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1,20 +1,20 @@ /// interface ImportMetaEnv { - readonly TAURI_FAMILY?: string - readonly TAURI_PLATFORM_VERSION?: string - readonly TAURI_TARGET_TRIPLE?: string - readonly TAURI_ARCH?: string - readonly TAURI_DEBUG?: boolean - readonly TAURI_PLATFORM_TYPE?: string + readonly TAURI_FAMILY?: string; + readonly TAURI_PLATFORM_VERSION?: string; + readonly TAURI_TARGET_TRIPLE?: string; + readonly TAURI_ARCH?: string; + readonly TAURI_DEBUG?: boolean; + readonly TAURI_PLATFORM_TYPE?: string; - readonly VITE_HOMEPAGE_URL: string - readonly VITE_BUGS_URL: string - readonly VITE_DOCS_URL: string - readonly VIET_LEARN_URL: string - readonly VITE_LATEST_FIRMWARE: string + readonly VITE_HOMEPAGE_URL: string; + readonly VITE_BUGS_URL: string; + readonly VITE_DOCS_URL: string; + readonly VIET_LEARN_URL: string; + readonly VITE_LATEST_FIRMWARE: string; } interface ImportMeta { - readonly env: ImportMetaEnv + readonly env: ImportMetaEnv; } diff --git a/src/i18n/de/index.ts b/src/i18n/de/index.ts index 3be92d6e..b80b2f53 100644 --- a/src/i18n/de/index.ts +++ b/src/i18n/de/index.ts @@ -1,4 +1,4 @@ -import type {Translation} from "../i18n-types" +import type { Translation } from "../i18n-types"; const de = { TITLE: "CharaChorder Gerätemanager", @@ -19,7 +19,8 @@ const de = { backup: { TITLE: "Lokale Kopie", INDIVIDUAL: "Einzeldateien", - DISCLAIMER: "Das Backup in diesem Browser gespeichert und bleibt nur auf diesem Computer.", + DISCLAIMER: + "Das Backup in diesem Browser gespeichert und bleibt nur auf diesem Computer.", DOWNLOAD: "Alles herunterladen", RESTORE: "Wiederherstellen", }, @@ -34,7 +35,8 @@ const de = { filter: { ALL: "Alle", }, - LIVE_LAYOUT_INFO: "Diese Aktion wurde auf Basis des Systemtastaturlayouts ermittelt.", + LIVE_LAYOUT_INFO: + "Diese Aktion wurde auf Basis des Systemtastaturlayouts ermittelt.", SHIFT_WARNING: "Diese Aktion hält shift", ALT_CODE_WARNING: "Dieses Alt-Code Makro funktioniert nur unter Windows", }, @@ -70,7 +72,8 @@ const de = { TITLE: "Bootmenü", REBOOT: "Neustarten", BOOTLOADER: "Bootloader", - POWER_WARNING: "Um vom Bootloader aus neu zu starten muss das Gerät neu verbunden werden.", + POWER_WARNING: + "Um vom Bootloader aus neu zu starten muss das Gerät neu verbunden werden.", }, }, browserWarning: { @@ -82,7 +85,8 @@ const de = { INFO_BROWSER_PREFIX: "Auch wenn alle Chromium-basieren Desktop Browser diese Voraussetzung grundsätzlich erfüllen, haben einige Browser ", INFO_BROWSER_INFIX: "wie zum Beispiel Brave", - INFO_BROWSER_SUFFIX: " sich bewusst dazu entschieden die API zu deaktivieren.", + INFO_BROWSER_SUFFIX: + " sich bewusst dazu entschieden die API zu deaktivieren.", DOWNLOAD_APP: "Chrome oder Edge werden offiziell unterstützt, andere Browser könnten aber auch funktionieren.", }, @@ -134,6 +138,6 @@ const de = { RUN: "Ausführen", }, }, -} satisfies Translation +} satisfies Translation; -export default de +export default de; diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 333e3650..b933e2b7 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -1,8 +1,9 @@ -import type {BaseTranslation} from "../i18n-types" +import type { BaseTranslation } from "../i18n-types"; const en = { TITLE: "CharaChorder Device Manager", - DESCRIPTION: "The device manager and configuration tool for CharaChorder devices.", + DESCRIPTION: + "The device manager and configuration tool for CharaChorder devices.", saveActions: { UNDO: "Undo (hold shift to undo all changes)", REDO: "Redo", @@ -14,7 +15,8 @@ const en = { backup: { TITLE: "Local backup", INDIVIDUAL: "Individual backups", - DISCLAIMER: "A backup is made and stored in this browser, and always remains only on your computer.", + DISCLAIMER: + "A backup is made and stored in this browser, and always remains only on your computer.", DOWNLOAD: "Download Everything", RESTORE: "Restore", }, @@ -64,24 +66,28 @@ const en = { TERMINAL: "Terminal", APPLY_SETTINGS: "Flash changes to device", NO_DEVICE: "No device connected", - LINUX_PERMISSIONS: "Most Linux based systems need adjusted permissions in order to connect your device.", + LINUX_PERMISSIONS: + "Most Linux based systems need adjusted permissions in order to connect your device.", bootMenu: { TITLE: "Boot Menu", REBOOT: "Reboot", BOOTLOADER: "Bootloader", - POWER_WARNING: "To reboot from bootloader you need to physically reconnect your device.", + POWER_WARNING: + "To reboot from bootloader you need to physically reconnect your device.", }, }, browserWarning: { TITLE: "Warning", - INFO_SERIAL_PREFIX: "Your current browser is not supported due to this site's unique requirement for ", + INFO_SERIAL_PREFIX: + "Your current browser is not supported due to this site's unique requirement for ", INFO_SERIAL_INFIX: "serial connections", INFO_SERIAL_SUFFIX: ".", INFO_BROWSER_PREFIX: "Though all chromium-based desktop browsers fulfill this requirement, some derivations such as Brave ", INFO_BROWSER_INFIX: "have been known to disable the API intentionally", INFO_BROWSER_SUFFIX: ".", - DOWNLOAD_APP: "Chrome or Edge are officially supported, but other browsers might work as well.", + DOWNLOAD_APP: + "Chrome or Edge are officially supported, but other browsers might work as well.", }, changes: { TITLE: "Import changes", @@ -131,6 +137,6 @@ const en = { RUN: "Run", }, }, -} satisfies BaseTranslation +} satisfies BaseTranslation; -export default en +export default en; diff --git a/src/i18n/formatters.ts b/src/i18n/formatters.ts index d0e4d44c..80e1342d 100644 --- a/src/i18n/formatters.ts +++ b/src/i18n/formatters.ts @@ -1,10 +1,12 @@ -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 = (locale: Locales) => { +export const initFormatters: FormattersInitializer = ( + locale: Locales, +) => { const formatters: Formatters = { // add your formatter functions here - } + }; - return formatters -} + return formatters; +}; diff --git a/src/lib/assets/keymaps/keymap.d.ts b/src/lib/assets/keymaps/keymap.d.ts index e95dbecd..b93c29d1 100644 --- a/src/lib/assets/keymaps/keymap.d.ts +++ b/src/lib/assets/keymaps/keymap.d.ts @@ -1,19 +1,19 @@ export interface KeymapCategory { - name: string - description: string - icon?: string - display?: string - type?: "unassigned" - actions: Record> + name: string; + description: string; + icon?: string; + display?: string; + type?: "unassigned"; + actions: Record>; } export interface ActionInfo { - id: string - title: string - icon: string - display: string - description: string - variant: "left" | "right" - variantOf: number - keyCode: string + id: string; + title: string; + icon: string; + display: string; + description: string; + variant: "left" | "right"; + variantOf: number; + keyCode: string; } diff --git a/src/lib/assets/layouts/lite.yml b/src/lib/assets/layouts/lite.yml index f3c33c24..8271e03e 100644 --- a/src/lib/assets/layouts/lite.yml +++ b/src/lib/assets/layouts/lite.yml @@ -15,10 +15,10 @@ col: - key: 64 - key: 65 - key: 66 - size: [ 2, 1 ] + size: [2, 1] - row: - key: 39 - size: [ 1.5, 1 ] + size: [1.5, 1] - key: 40 - key: 41 - key: 42 @@ -32,10 +32,10 @@ col: - key: 50 - key: 51 - key: 52 - size: [ 1.5, 1 ] + size: [1.5, 1] - row: - key: 26 - size: [ 1.75, 1 ] + size: [1.75, 1] - key: 27 - key: 28 - key: 29 @@ -48,10 +48,10 @@ col: - key: 36 - key: 37 - key: 38 - size: [ 2.25, 1 ] + size: [2.25, 1] - row: - key: 12 - size: [ 2, 1 ] + size: [2, 1] - key: 13 - key: 14 - key: 15 @@ -68,20 +68,19 @@ col: - row: - key: 0 - key: 1 - size: [ 1.25, 1 ] + size: [1.25, 1] - key: 2 - size: [ 1.25, 1 ] + size: [1.25, 1] - key: 3 - size: [ 2, 1 ] + size: [2, 1] - key: 4 - key: 5 - key: 6 - size: [ 2, 1 ] + size: [2, 1] - key: 7 - size: [ 1.25, 1 ] + size: [1.25, 1] - key: 8 - size: [ 1.25, 1 ] + size: [1.25, 1] - key: 9 - key: 10 - key: 11 - diff --git a/src/lib/backup/backup.ts b/src/lib/backup/backup.ts index c832ea93..dd5f19e6 100644 --- a/src/lib/backup/backup.ts +++ b/src/lib/backup/backup.ts @@ -4,33 +4,45 @@ import type { 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" +} 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>(contents: T) { - const downloadUrl = URL.createObjectURL(new Blob([JSON.stringify(contents)], {type: "application/json"})) - const element = document.createElement("a") + 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) + `${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({ charaVersion: 1, type: "backup", - history: [[createChordBackup(), createLayoutBackup(), createSettingsBackup()]], - }) + history: [ + [createChordBackup(), createLayoutBackup(), createSettingsBackup()], + ], + }); } export function createLayoutBackup(): CharaLayoutFile { @@ -38,28 +50,40 @@ export function createLayoutBackup(): CharaLayoutFile { charaVersion: 1, type: "layout", device: get(serialPort)?.device, - layout: get(layout).map(it => it.map(it => it.action)) as [number[], number[], number[]], - } + 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])} + 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)} + 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() + 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)); } else if (isCsvLayout(text)) { - restoreFromFile(csvLayoutToJson(text)) + restoreFromFile(csvLayoutToJson(text)); } else if (isCsvChords(text)) { - restoreFromFile(csvChordsToJson(text)) + restoreFromFile(csvChordsToJson(text)); } else { } } @@ -67,86 +91,90 @@ export async function restoreBackup(event: Event) { export function restoreFromFile( file: CharaBackupFile | CharaSettingsFile | CharaLayoutFile | CharaChordFile, ) { - if (file.charaVersion !== 1) throw new Error("Incompatible backup") + if (file.charaVersion !== 1) throw new Error("Incompatible backup"); switch (file.type) { case "backup": { - const recent = file.history[0] + 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") + alert("Backup is incompatible with this device"); + throw new Error("Backup is incompatible with this device"); } - changes.update(changes => { + changes.update((changes) => { changes.push( ...getChangesFromChordFile(recent[0]), ...getChangesFromLayoutFile(recent[1]), ...getChangesFromSettingsFile(recent[2]), - ) - return changes - }) - break + ); + return changes; + }); + break; } case "chords": { - changes.update(changes => { - changes.push(...getChangesFromChordFile(file)) - return changes - }) - break + changes.update((changes) => { + changes.push(...getChangesFromChordFile(file)); + return changes; + }); + break; } case "layout": { - changes.update(changes => { - changes.push(...getChangesFromLayoutFile(file)) - return changes - }) - break + changes.update((changes) => { + changes.push(...getChangesFromLayoutFile(file)); + return changes; + }); + break; } case "settings": { - changes.update(changes => { - changes.push(...getChangesFromSettingsFile(file)) - return changes - }) - break + changes.update((changes) => { + changes.push(...getChangesFromSettingsFile(file)); + return changes; + }); + break; } default: { - throw new Error(`Unknown backup type "${(file as CharaFile).type}"`) + throw new Error( + `Unknown backup type "${(file as CharaFile).type}"`, + ); } } } export function getChangesFromChordFile(file: CharaChordFile) { - const changes: Change[] = [] - const existingChords = new Set(get(chords).map(({phrase, actions}) => JSON.stringify([actions, phrase]))) + 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 + continue; } changes.push({ type: ChangeType.Chord, actions: input, phrase: output, id: input, - }) + }); } - return changes + return changes; } export function getChangesFromSettingsFile(file: CharaSettingsFile) { - const changes: Change[] = [] + const changes: Change[] = []; for (const [id, value] of file.settings.entries()) { - const setting = get(settings)[id] + const setting = get(settings)[id]; if (setting !== undefined && setting.value !== value) { changes.push({ type: ChangeType.Setting, id, setting: value, - }) + }); } } - return changes + return changes; } export function getChangesFromLayoutFile(file: CharaLayoutFile) { - const changes: Change[] = [] + 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) { @@ -155,9 +183,9 @@ export function getChangesFromLayoutFile(file: CharaLayoutFile) { layer, id, action, - }) + }); } } } - return changes + return changes; } diff --git a/src/lib/backup/compat/legacy-chords.ts b/src/lib/backup/compat/legacy-chords.ts index 2777a271..2450975c 100644 --- a/src/lib/backup/compat/legacy-chords.ts +++ b/src/lib/backup/compat/legacy-chords.ts @@ -1,7 +1,7 @@ -import {KEYMAP_IDS} from "$lib/serial/keymap-codes" -import type {CharaChordFile} from "$lib/share/chara-file" +import { KEYMAP_IDS } from "$lib/serial/keymap-codes"; +import type { CharaChordFile } from "$lib/share/chara-file"; -const SPECIAL_KEYS = new Map([[" ", "SPACE"]]) +const SPECIAL_KEYS = new Map([[" ", "SPACE"]]); export function csvChordsToJson(csv: string): CharaChordFile { return { @@ -10,22 +10,22 @@ export function csvChordsToJson(csv: string): CharaChordFile { chords: csv .trim() .split("\n") - .map(line => { - const [input, output] = line.split(/,(?=[^,]*$)/, 2) + .map((line) => { + const [input, output] = line.split(/,(?=[^,]*$)/, 2); return [ input .split("+") - .map(it => KEYMAP_IDS.get(it.trim())?.code ?? 0) + .map((it) => KEYMAP_IDS.get(it.trim())?.code ?? 0) .sort((a, b) => a - b), output .trim() .split("") - .map(it => KEYMAP_IDS.get(SPECIAL_KEYS.get(it) ?? it)?.code ?? 0), - ] + .map((it) => KEYMAP_IDS.get(SPECIAL_KEYS.get(it) ?? it)?.code ?? 0), + ]; }), - } + }; } export function isCsvChords(csv: string): boolean { - return /^([^+]+( *\+ *[^+]+)* *, *[^+, ]+ *(\n|(?=$)))+$/.test(csv) + return /^([^+]+( *\+ *[^+]+)* *, *[^+, ]+ *(\n|(?=$)))+$/.test(csv); } diff --git a/src/lib/backup/compat/legacy-layout-converted.sample.json b/src/lib/backup/compat/legacy-layout-converted.sample.json index 451b4572..a82a060d 100644 --- a/src/lib/backup/compat/legacy-layout-converted.sample.json +++ b/src/lib/backup/compat/legacy-layout-converted.sample.json @@ -4,21 +4,24 @@ "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 + 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 ], [ - 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 ] ] } diff --git a/src/lib/backup/compat/legacy-layout.spec.ts b/src/lib/backup/compat/legacy-layout.spec.ts index d81a8eb8..d898ee3b 100644 --- a/src/lib/backup/compat/legacy-layout.spec.ts +++ b/src/lib/backup/compat/legacy-layout.spec.ts @@ -1,18 +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" +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 - }) + expect(isCsvLayout(legacyLayout)).to.be.true; + }); it("should not detect chord maps as layouts", () => { - expect(isCsvLayout("e + h + t,the")).to.be.false - }) + expect(isCsvLayout("e + h + t,the")).to.be.false; + }); it("should convert legacy layouts", () => { - expect(csvLayoutToJson(legacyLayout)).to.deep.equal(legacyLayoutConverted) - }) -}) + expect(csvLayoutToJson(legacyLayout)).to.deep.equal(legacyLayoutConverted); + }); +}); diff --git a/src/lib/backup/compat/legacy-layout.ts b/src/lib/backup/compat/legacy-layout.ts index bca022ff..2efbb315 100644 --- a/src/lib/backup/compat/legacy-layout.ts +++ b/src/lib/backup/compat/legacy-layout.ts @@ -1,25 +1,28 @@ -import type {CharaLayoutFile} from "$lib/share/chara-file" +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 { +export function csvLayoutToJson( + csv: string, + device: CharaLayoutFile["device"] = "one", +): CharaLayoutFile { const layout: CharaLayoutFile = { charaVersion: 1, type: "layout", device, layout: [[], [], []], - } + }; for (const layer of csv.trim().split("\n")) { - const [layerId, key, action] = layer.substring(1).split(",").map(Number) + const [layerId, key, action] = layer.substring(1).split(",").map(Number); - layout.layout[Number(layerId) - 1][Number(key)] = Number(action) + layout.layout[Number(layerId) - 1][Number(key)] = Number(action); } - return layout + return layout; } export function isCsvLayout(csv: string): boolean { - return /^(A[123],\d+,\d+\n?)+$/.test(csv) + return /^(A[123],\d+,\d+\n?)+$/.test(csv); } diff --git a/src/lib/components/Action.svelte b/src/lib/components/Action.svelte index 9ca5e393..35674aeb 100644 --- a/src/lib/components/Action.svelte +++ b/src/lib/components/Action.svelte @@ -1,24 +1,31 @@ {#if dynamicMapping} {info.icon ?? info.display ?? info.id ?? `0x${info.code.toString(16)}`} {:else if display === "inline-keys"} {#if !info.icon && info.id?.length === 1} - {info.id} + {info.id} {:else} - {info.icon ?? info.display ?? info.id ?? `0x${info.code.toString(16)}`} {/if} {/if} diff --git a/src/lib/components/ActionListItem.svelte b/src/lib/components/ActionListItem.svelte index 4693cb66..380d085a 100644 --- a/src/lib/components/ActionListItem.svelte +++ b/src/lib/components/ActionListItem.svelte @@ -1,12 +1,14 @@ @@ -143,7 +144,7 @@
  • Action code is out of range
  • {/if} {/if} - {#each filter ? results.filter(it => filter.has(it)) : results as id (id)} + {#each filter ? results.filter( (it) => filter.has(it), ) : results as id (id)}
  • select(id)} />
  • {/each} diff --git a/src/lib/components/layout/GenericLayout.svelte b/src/lib/components/layout/GenericLayout.svelte index 5a043c32..29b9bd43 100644 --- a/src/lib/components/layout/GenericLayout.svelte +++ b/src/lib/components/layout/GenericLayout.svelte @@ -1,177 +1,189 @@ @@ -187,9 +199,9 @@ {key} on:focusin={() => (focusKey = key)} on:click={() => edit(i)} - on:keypress={({key}) => { + on:keypress={({ key }) => { if (key === "Enter") { - edit(i) + edit(i); } }} /> diff --git a/src/lib/components/layout/KeyText.svelte b/src/lib/components/layout/KeyText.svelte index 2cce96e9..92bc49b8 100644 --- a/src/lib/components/layout/KeyText.svelte +++ b/src/lib/components/layout/KeyText.svelte @@ -1,30 +1,35 @@ {#each positions as position, layer} - {@const {action: actionId, isApplied} = $layout[layer][key.id] ?? {action: 0, isApplied: true}} - {@const {code, icon, id, display, title, keyCode, variant} = KEYMAP_CODES[actionId] ?? {code: actionId}} + {@const { action: actionId, isApplied } = $layout[layer][key.id] ?? { + action: 0, + isApplied: true, + }} + {@const { code, icon, id, display, title, keyCode, variant } = KEYMAP_CODES[ + actionId + ] ?? { code: actionId }} {@const dynamicMapping = keyCode && $osLayout.get(keyCode)} {@const tooltip = (title ?? id ?? `0x${code.toString(16)}`) + @@ -50,7 +55,7 @@ ? "0 0 0" : `${direction[0].toPrecision(2)}px ${direction[1].toPrecision(2)}px 0`} style:rotate="{rotate}deg" - use:action={{title: tooltip}} + use:action={{ title: tooltip }} > {#if code !== 0} {dynamicMapping || icon || display || id || `0x${code.toString(16)}`} diff --git a/src/lib/components/layout/KeyboardKey.svelte b/src/lib/components/layout/KeyboardKey.svelte index 6814441c..bbdfa697 100644 --- a/src/lib/components/layout/KeyboardKey.svelte +++ b/src/lib/components/layout/KeyboardKey.svelte @@ -1,20 +1,29 @@ - + {#if key.shape === "square"} - import {serialPort} from "$lib/serial/connection" - import {action} from "$lib/title" - import GenericLayout from "$lib/components/layout/GenericLayout.svelte" - import {getContext} from "svelte" - import type {Writable} from "svelte/store" - import type {VisualLayout} from "$lib/serialization/visual-layout" + import { serialPort } from "$lib/serial/connection"; + import { action } from "$lib/title"; + import GenericLayout from "$lib/components/layout/GenericLayout.svelte"; + import { getContext } from "svelte"; + import type { Writable } from "svelte/store"; + import type { VisualLayout } from "$lib/serialization/visual-layout"; - $: device = $serialPort?.device ?? "ONE" - const activeLayer = getContext>("active-layer") + $: device = $serialPort?.device ?? "ONE"; + const activeLayer = getContext>("active-layer"); const layers = [ ["Numeric Layer", "123", 1], ["Primary Layer", "abc", 0], ["Function Layer", "function", 2], - ] as const + ] as const; const layouts = { - ONE: () => import("$lib/assets/layouts/one.yml").then(it => it.default as VisualLayout), - LITE: () => import("$lib/assets/layouts/lite.yml").then(it => it.default as VisualLayout), - X: () => import("$lib/assets/layouts/generic/103-key.yml").then(it => it.default as VisualLayout), - } + ONE: () => + import("$lib/assets/layouts/one.yml").then( + (it) => it.default as VisualLayout, + ), + LITE: () => + import("$lib/assets/layouts/lite.yml").then( + (it) => it.default as VisualLayout, + ), + X: () => + import("$lib/assets/layouts/generic/103-key.yml").then( + (it) => it.default as VisualLayout, + ), + };
    @@ -27,7 +36,7 @@ {#each layers as [title, icon, value]} - +
    diff --git a/src/lib/dialogs/Dialog.svelte b/src/lib/dialogs/Dialog.svelte index 29aaaf37..d7e213db 100644 --- a/src/lib/dialogs/Dialog.svelte +++ b/src/lib/dialogs/Dialog.svelte @@ -1,11 +1,11 @@ diff --git a/src/lib/dialogs/PickChangesDialog.svelte b/src/lib/dialogs/PickChangesDialog.svelte index 36cbc055..353705b6 100644 --- a/src/lib/dialogs/PickChangesDialog.svelte +++ b/src/lib/dialogs/PickChangesDialog.svelte @@ -1,73 +1,121 @@

    {$LL.changes.TITLE()}

    - +

      - {#if layoutChanges.some(it => it.length > 0)} + {#if layoutChanges.some((it) => it.length > 0)}
      • @@ -78,7 +126,7 @@

        @@ -90,9 +138,10 @@
      • {$LL.changes.settings.TITLE(settingChanges.length)}

      • @@ -101,7 +150,10 @@
      • {$LL.changes.chords.TITLE(totalChordChanges)}

          diff --git a/src/lib/dialogs/confirm-dialog.ts b/src/lib/dialogs/confirm-dialog.ts index ae257566..e0b9fea4 100644 --- a/src/lib/dialogs/confirm-dialog.ts +++ b/src/lib/dialogs/confirm-dialog.ts @@ -1,4 +1,4 @@ -import ConfirmDialog from "$lib/dialogs/ConfirmDialog.svelte" +import ConfirmDialog from "$lib/dialogs/ConfirmDialog.svelte"; export async function askForConfirmation( title: string, @@ -14,18 +14,18 @@ export async function askForConfirmation( confirmTitle, abortTitle, }, - }) + }); - let resolvePromise: (value: boolean) => void - const resultPromise = new Promise(resolve => { - resolvePromise = resolve - }) + let resolvePromise: (value: boolean) => void; + const resultPromise = new Promise((resolve) => { + resolvePromise = resolve; + }); - dialog.$on("abort", () => resolvePromise(false)) - dialog.$on("confirm", () => resolvePromise(true)) + dialog.$on("abort", () => resolvePromise(false)); + dialog.$on("confirm", () => resolvePromise(true)); - const result = await resultPromise - dialog.$destroy() + const result = await resultPromise; + dialog.$destroy(); - return result + return result; } diff --git a/src/lib/fonts/noto-sans-mono.scss b/src/lib/fonts/noto-sans-mono.scss index def4e205..35aef2bb 100644 --- a/src/lib/fonts/noto-sans-mono.scss +++ b/src/lib/fonts/noto-sans-mono.scss @@ -73,8 +73,9 @@ font-stretch: 62.5% 100%; src: url("@fontsource-variable/noto-sans-mono/files/noto-sans-mono-latin-ext-wght-normal.woff2") format("woff2-variations"); - unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, - U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; + unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, + U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, + U+A720-A7FF; } /* noto-sans-mono-latin-wght-normal */ @@ -86,7 +87,7 @@ font-stretch: 62.5% 100%; src: url("@fontsource-variable/noto-sans-mono/files/noto-sans-mono-latin-wght-normal.woff2") format("woff2-variations"); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, - U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, - U+2215, U+FEFF, U+FFFD; + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, + U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } diff --git a/src/lib/os-layout.ts b/src/lib/os-layout.ts index e6b5e937..da60641a 100644 --- a/src/lib/os-layout.ts +++ b/src/lib/os-layout.ts @@ -1,25 +1,27 @@ -import {get, writable} from "svelte/store" +import { get, writable } from "svelte/store"; -export const osLayout = writable>(new Map()) +export const osLayout = writable>(new Map()); async function updateLayout() { - const layout: Map = await (navigator as any).keyboard.getLayoutMap() - const currentLayout = get(osLayout) + const layout: Map = await ( + navigator as any + ).keyboard.getLayoutMap(); + const currentLayout = get(osLayout); if ( layout.size !== currentLayout.size || - [...layout.keys()].some(key => layout.get(key) !== currentLayout.get(key)) + [...layout.keys()].some((key) => layout.get(key) !== currentLayout.get(key)) ) { - osLayout.set(layout) + osLayout.set(layout); } } export function runLayoutDetection(): () => void { if ("keyboard" in navigator) { - updateLayout() - const timer = setInterval(updateLayout, 5000) - return () => clearInterval(timer) + updateLayout(); + const timer = setInterval(updateLayout, 5000); + return () => clearInterval(timer); } else { - console.warn("Keyboard API not supported") - return () => {} + console.warn("Keyboard API not supported"); + return () => {}; } } diff --git a/src/lib/popup.ts b/src/lib/popup.ts index 5272976b..1f612c53 100644 --- a/src/lib/popup.ts +++ b/src/lib/popup.ts @@ -1,28 +1,31 @@ -import tippy from "tippy.js" -import type {Action} from "svelte/action" -import type {ComponentType, SvelteComponent} from "svelte" +import tippy from "tippy.js"; +import type { Action } from "svelte/action"; +import type { ComponentType, SvelteComponent } from "svelte"; -export const popup: Action = (node, Component) => { - let component: SvelteComponent | undefined - let target: HTMLElement | undefined +export const popup: Action = ( + node, + Component, +) => { + let component: SvelteComponent | undefined; + let target: HTMLElement | undefined; const edit = tippy(node, { interactive: true, trigger: "click", onShow(instance) { - target = instance.popper.querySelector(".tippy-content") as HTMLElement - target.classList.add("active") - component ??= new Component({target}) + target = instance.popper.querySelector(".tippy-content") as HTMLElement; + target.classList.add("active"); + component ??= new Component({ target }); }, onHidden() { - component?.$destroy() - target?.classList.remove("active") - component = undefined + component?.$destroy(); + target?.classList.remove("active"); + component = undefined; }, - }) + }); return { destroy() { - edit.destroy() + edit.destroy(); }, - } -} + }; +}; diff --git a/src/lib/preferences.ts b/src/lib/preferences.ts index d56eadbb..5f4c9bb1 100644 --- a/src/lib/preferences.ts +++ b/src/lib/preferences.ts @@ -1,37 +1,43 @@ -import type { Action } from "svelte/action" -import { persistentWritable } from "$lib/storage" +import type { Action } from "svelte/action"; +import { persistentWritable } from "$lib/storage"; export interface UserPreferences { - backup: boolean - autoConnect: boolean + backup: boolean; + autoConnect: boolean; } export const theme = persistentWritable("user-theme", { color: "#6D81C7", mode: "dark" as "light" | "dark" | "auto", -}) +}); -export const userPreferences = persistentWritable("user-preferences", { - backup: false, - autoConnect: false, -}) +export const userPreferences = persistentWritable( + "user-preferences", + { + backup: false, + autoConnect: false, + }, +); -export const preference: Action = (node, key) => { - const unsubscribe = userPreferences.subscribe(it => { - node.checked = it[key] - }) +export const preference: Action = ( + node, + key, +) => { + const unsubscribe = userPreferences.subscribe((it) => { + node.checked = it[key]; + }); function update() { - userPreferences.update(value => { - value[key] = node.checked - return value - }) + userPreferences.update((value) => { + value[key] = node.checked; + return value; + }); } - node.addEventListener("input", update) + node.addEventListener("input", update); return { destroy() { - unsubscribe() - node.removeEventListener("input", update) + unsubscribe(); + node.removeEventListener("input", update); }, - } -} + }; +}; diff --git a/src/lib/serial/TauriSerialDialog.svelte b/src/lib/serial/TauriSerialDialog.svelte index f6449af3..93ebc52e 100644 --- a/src/lib/serial/TauriSerialDialog.svelte +++ b/src/lib/serial/TauriSerialDialog.svelte @@ -1,15 +1,22 @@ {#each ports as port} {@const info = port.getInfo()} - + {/each} @@ -17,7 +24,7 @@ on:click={() => dispatch( "confirm", - ports.find(it => it.getInfo().name === selected), + ports.find((it) => it.getInfo().name === selected), )}>Ok diff --git a/src/lib/serial/chord.spec.ts b/src/lib/serial/chord.spec.ts index 020fd191..bd2a2c6f 100644 --- a/src/lib/serial/chord.spec.ts +++ b/src/lib/serial/chord.spec.ts @@ -1,4 +1,4 @@ -import {describe, it, expect} from "vitest" +import { describe, it, expect } from "vitest"; import { deserializeActions, parseChordActions, @@ -6,43 +6,55 @@ import { serializeActions, stringifyChordActions, stringifyPhrase, -} from "./chord" +} from "./chord"; describe("chords", function () { describe("actions", function () { it("should serialize actions", function () { - expect(serializeActions([32, 51]).toString(16)).toEqual(0xcc200000000000000000000000000n.toString(16)) - }) + expect(serializeActions([32, 51]).toString(16)).toEqual( + 0xcc200000000000000000000000000n.toString(16), + ); + }); it("should deserialize actions", function () { - expect(deserializeActions(0xcc200000000000000000000000000n)).toEqual([32, 51]) - }) + expect(deserializeActions(0xcc200000000000000000000000000n)).toEqual([ + 32, 51, + ]); + }); for (let i = 0; i < 12; i++) { it(`should serialize back-forth ${i} actions`, function () { - const actions = Array.from({length: i}).map((_, i) => i + 1) - expect(deserializeActions(serializeActions(actions))).toEqual(actions) - }) + const actions = Array.from({ length: i }).map((_, i) => i + 1); + expect(deserializeActions(serializeActions(actions))).toEqual(actions); + }); } - }) + }); describe("phrase", function () { it("should stringify", function () { - expect(stringifyPhrase([0x20, 0x68, 0x72, 0xd4, 0x65, 0x1fff])).toEqual("206872D4651FFF") - }) + expect(stringifyPhrase([0x20, 0x68, 0x72, 0xd4, 0x65, 0x1fff])).toEqual( + "206872D4651FFF", + ); + }); it("should parse", function () { - expect(parsePhrase("206872D4651FFF")).toEqual([0x20, 0x68, 0x72, 0xd4, 0x65, 0x1fff]) - }) - }) + expect(parsePhrase("206872D4651FFF")).toEqual([ + 0x20, 0x68, 0x72, 0xd4, 0x65, 0x1fff, + ]); + }); + }); describe("chord actions", function () { it("should stringify", function () { - expect(stringifyChordActions([32, 51])).toEqual("000CC200000000000000000000000000") - }) + expect(stringifyChordActions([32, 51])).toEqual( + "000CC200000000000000000000000000", + ); + }); it("should parse", function () { - expect(parseChordActions("000CC200000000000000000000000000")).toEqual([32, 51]) - }) - }) -}) + expect(parseChordActions("000CC200000000000000000000000000")).toEqual([ + 32, 51, + ]); + }); + }); +}); diff --git a/src/lib/serial/chord.ts b/src/lib/serial/chord.ts index 6e342bd8..0ccf2ac8 100644 --- a/src/lib/serial/chord.ts +++ b/src/lib/serial/chord.ts @@ -1,31 +1,31 @@ -import {compressActions, decompressActions} from "../serialization/actions" +import { compressActions, decompressActions } from "../serialization/actions"; export interface Chord { - actions: number[] - phrase: number[] + actions: number[]; + phrase: number[]; } export function parsePhrase(phrase: string): number[] { return decompressActions( - Uint8Array.from({length: phrase.length / 2}).map((_, i) => + Uint8Array.from({ length: phrase.length / 2 }).map((_, i) => Number.parseInt(phrase.slice(i * 2, i * 2 + 2), 16), ), - ) + ); } export function stringifyPhrase(phrase: number[]): string { return [...compressActions(phrase)] - .map(it => it.toString(16).padStart(2, "0")) + .map((it) => it.toString(16).padStart(2, "0")) .join("") - .toUpperCase() + .toUpperCase(); } export function parseChordActions(actions: string): number[] { - return deserializeActions(BigInt(`0x${actions}`)) + return deserializeActions(BigInt(`0x${actions}`)); } export function stringifyChordActions(actions: number[]): string { - return serializeActions(actions).toString(16).padStart(32, "0").toUpperCase() + return serializeActions(actions).toString(16).padStart(32, "0").toUpperCase(); } /** @@ -34,23 +34,24 @@ export function stringifyChordActions(actions: number[]): string { * Actions are represented as 10-bit codes, for a maximum of 12 actions */ export function serializeActions(actions: number[]): bigint { - let native = 0n + let native = 0n; for (let i = 1; i <= actions.length; i++) { - native |= BigInt(actions[actions.length - i] & 0x3ff) << BigInt((12 - i) * 10) + native |= + BigInt(actions[actions.length - i] & 0x3ff) << BigInt((12 - i) * 10); } - return native + return native; } /** * @see {serializeActions} */ export function deserializeActions(native: bigint): number[] { - const actions = [] + const actions = []; for (let i = 0; i < 12; i++) { - const action = Number(native & 0x3ffn) - actions.push(action) - native >>= 10n + const action = Number(native & 0x3ffn); + actions.push(action); + native >>= 10n; } - return actions + return actions; } diff --git a/src/lib/serial/connection.ts b/src/lib/serial/connection.ts index a09d1624..6cc863b5 100644 --- a/src/lib/serial/connection.ts +++ b/src/lib/serial/connection.ts @@ -1,20 +1,20 @@ -import {get, writable} from "svelte/store" -import {CharaDevice} from "$lib/serial/device" -import type {Chord} from "$lib/serial/chord" -import type {Writable} from "svelte/store" -import type {CharaLayout} from "$lib/serialization/layout" -import {persistentWritable} from "$lib/storage" -import {userPreferences} from "$lib/preferences" -import settingInfo from "$lib/assets/settings.yml" +import { get, writable } from "svelte/store"; +import { CharaDevice } from "$lib/serial/device"; +import type { Chord } from "$lib/serial/chord"; +import type { Writable } from "svelte/store"; +import type { CharaLayout } from "$lib/serialization/layout"; +import { persistentWritable } from "$lib/storage"; +import { userPreferences } from "$lib/preferences"; +import settingInfo from "$lib/assets/settings.yml"; -export const serialPort = writable() +export const serialPort = writable(); export interface SerialLogEntry { - type: "input" | "output" | "system" - value: string + type: "input" | "output" | "system"; + value: string; } -export const serialLog = writable([]) +export const serialLog = writable([]); /** * Chords as read from the device @@ -23,7 +23,7 @@ export const deviceChords = persistentWritable( "chord-library", [], () => get(userPreferences).backup, -) +); /** * Layout as read from the device @@ -32,7 +32,7 @@ export const deviceLayout = persistentWritable( "layout", [[], [], []], () => get(userPreferences).backup, -) +); /** * Settings as read from the device @@ -41,61 +41,66 @@ export const deviceSettings = persistentWritable( "device-settings", [], () => get(userPreferences).backup, -) +); -export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading"> = writable("done") +export const syncStatus: Writable< + "done" | "error" | "downloading" | "uploading" +> = writable("done"); export interface ProgressInfo { - max: number - current: number + max: number; + current: number; } -export const syncProgress = writable(undefined) +export const syncProgress = writable(undefined); export async function initSerial(manual = false) { - const device = get(serialPort) ?? new CharaDevice() - await device.init(manual) - serialPort.set(device) - sync() + const device = get(serialPort) ?? new CharaDevice(); + await device.init(manual); + serialPort.set(device); + sync(); } export async function sync() { - const device = get(serialPort) - if (!device) return - const chordCount = await device.getChordCount() - syncStatus.set("downloading") + const device = get(serialPort); + if (!device) return; + const chordCount = await device.getChordCount(); + syncStatus.set("downloading"); - const max = Object.keys(settingInfo.settings).length + device.keyCount * 3 + chordCount - let current = 0 - syncProgress.set({max, current}) + const max = + Object.keys(settingInfo.settings).length + device.keyCount * 3 + chordCount; + let current = 0; + syncProgress.set({ max, current }); function progressTick() { - current++ - syncProgress.set({max, current}) + current++; + syncProgress.set({ max, current }); } - const parsedSettings: number[] = [] + const parsedSettings: number[] = []; for (const key in settingInfo.settings) { try { - parsedSettings[Number.parseInt(key)] = await device.getSetting(Number.parseInt(key)) + parsedSettings[Number.parseInt(key)] = await device.getSetting( + Number.parseInt(key), + ); } catch {} - progressTick() + progressTick(); } - deviceSettings.set(parsedSettings) + deviceSettings.set(parsedSettings); - const parsedLayout: CharaLayout = [[], [], []] + const parsedLayout: CharaLayout = [[], [], []]; for (let layer = 1; layer <= 3; layer++) { for (let i = 0; i < device.keyCount; i++) { - parsedLayout[layer - 1][i] = await device.getLayoutKey(layer, i) - progressTick() + parsedLayout[layer - 1][i] = await device.getLayoutKey(layer, i); + progressTick(); } } - deviceLayout.set(parsedLayout) + deviceLayout.set(parsedLayout); - const chordInfo = [] + const chordInfo = []; for (let i = 0; i < chordCount; i++) { - chordInfo.push(await device.getChord(i)) - progressTick() + chordInfo.push(await device.getChord(i)); + progressTick(); } - deviceChords.set(chordInfo) - syncStatus.set("done") - syncProgress.set(undefined) + deviceChords.set(chordInfo); + syncStatus.set("done"); + syncProgress.set(undefined); } diff --git a/src/lib/serial/device.ts b/src/lib/serial/device.ts index 36b66165..ece1ebf9 100644 --- a/src/lib/serial/device.ts +++ b/src/lib/serial/device.ts @@ -1,171 +1,190 @@ -import {LineBreakTransformer} from "$lib/serial/line-break-transformer" -import {serialLog} from "$lib/serial/connection" -import type {Chord} from "$lib/serial/chord" -import {SemVer} from "$lib/serial/sem-ver" -import {parseChordActions, parsePhrase, stringifyChordActions, stringifyPhrase} from "$lib/serial/chord" -import {browser} from "$app/environment" +import { LineBreakTransformer } from "$lib/serial/line-break-transformer"; +import { serialLog } from "$lib/serial/connection"; +import type { Chord } from "$lib/serial/chord"; +import { SemVer } from "$lib/serial/sem-ver"; +import { + parseChordActions, + parsePhrase, + stringifyChordActions, + stringifyPhrase, +} from "$lib/serial/chord"; +import { browser } from "$app/environment"; const PORT_FILTERS: Map = new Map([ - ["ONE M0", {usbProductId: 32783, usbVendorId: 9114}], - ["LITE S2", {usbProductId: 33070, usbVendorId: 12346}], - ["LITE M0", {usbProductId: 32796, usbVendorId: 9114}], - ["X", {usbProductId: 33163, usbVendorId: 12346}], -]) + ["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }], + ["LITE S2", { usbProductId: 33070, usbVendorId: 12346 }], + ["LITE M0", { usbProductId: 32796, usbVendorId: 9114 }], + ["X", { usbProductId: 33163, usbVendorId: 12346 }], +]); const KEY_COUNTS = { ONE: 90, LITE: 67, X: 256, -} as const +} as const; -if (browser && navigator.serial === undefined && import.meta.env.TAURI_FAMILY !== undefined) { - await import("./tauri-serial") +if ( + browser && + navigator.serial === undefined && + import.meta.env.TAURI_FAMILY !== undefined +) { + await import("./tauri-serial"); } export async function getViablePorts(): Promise { - return navigator.serial.getPorts().then(ports => - ports.filter(it => { - const {usbProductId, usbVendorId} = it.getInfo() + return navigator.serial.getPorts().then((ports) => + ports.filter((it) => { + const { usbProductId, usbVendorId } = it.getInfo(); for (const filter of PORT_FILTERS.values()) { - if (filter.usbProductId === usbProductId && filter.usbVendorId === usbVendorId) { - return true + if ( + filter.usbProductId === usbProductId && + filter.usbVendorId === usbVendorId + ) { + return true; } } - return false + return false; }), - ) + ); } export async function canAutoConnect() { - return getViablePorts().then(it => it.length === 1) + return getViablePorts().then((it) => it.length === 1); } function timeout(promise: Promise, ms: number): Promise { - let timer: number + let timer: number; return Promise.race([ promise, new Promise((_, reject) => { - timer = setTimeout(() => reject(new Error("Timeout")), ms) as unknown as number + timer = setTimeout( + () => reject(new Error("Timeout")), + ms, + ) as unknown as number; }), - ]).finally(() => clearTimeout(timer)) + ]).finally(() => clearTimeout(timer)); } export class CharaDevice { - private port!: SerialPort - private reader!: ReadableStreamDefaultReader + private port!: SerialPort; + private reader!: ReadableStreamDefaultReader; - private readonly abortController1 = new AbortController() - private readonly abortController2 = new AbortController() + private readonly abortController1 = new AbortController(); + private readonly abortController2 = new AbortController(); - private streamClosed!: Promise + private streamClosed!: Promise; - private lock?: Promise + private lock?: Promise; - private readonly suspendDebounce = 100 - private suspendDebounceId?: number + private readonly suspendDebounce = 100; + private suspendDebounceId?: number; - version!: SemVer - company!: "CHARACHORDER" - device!: "ONE" | "LITE" | "X" - chipset!: "M0" | "S2" - keyCount!: 90 | 67 | 256 + version!: SemVer; + company!: "CHARACHORDER"; + device!: "ONE" | "LITE" | "X"; + chipset!: "M0" | "S2"; + keyCount!: 90 | 67 | 256; get portInfo() { - return this.port.getInfo() + return this.port.getInfo(); } constructor(private readonly baudRate = 115200) {} async init(manual = false) { try { - const ports = await getViablePorts() + const ports = await getViablePorts(); this.port = !manual && ports.length === 1 ? ports[0] - : await navigator.serial.requestPort({filters: [...PORT_FILTERS.values()]}) + : await navigator.serial.requestPort({ + filters: [...PORT_FILTERS.values()], + }); - await this.port.open({baudRate: this.baudRate}) - const info = this.port.getInfo() - serialLog.update(it => { + await this.port.open({ baudRate: this.baudRate }); + const info = this.port.getInfo(); + serialLog.update((it) => { it.push({ type: "system", - value: `Connected; ID: 0x${info.usbProductId?.toString(16)}; Vendor: 0x${info.usbVendorId?.toString( + value: `Connected; ID: 0x${info.usbProductId?.toString( 16, - )}`, - }) - return it - }) - await this.port.close() + )}; Vendor: 0x${info.usbVendorId?.toString(16)}`, + }); + return it; + }); + await this.port.close(); - this.version = new SemVer(await this.send("VERSION").then(([version]) => version)) - const [company, device, chipset] = await this.send("ID") - this.company = company as "CHARACHORDER" - this.device = device as "ONE" | "LITE" | "X" - this.chipset = chipset as "M0" | "S2" - this.keyCount = KEY_COUNTS[this.device] + this.version = new SemVer( + await this.send("VERSION").then(([version]) => version), + ); + const [company, device, chipset] = await this.send("ID"); + this.company = company as "CHARACHORDER"; + this.device = device as "ONE" | "LITE" | "X"; + this.chipset = chipset as "M0" | "S2"; + this.keyCount = KEY_COUNTS[this.device]; } catch (e) { - alert(e) - console.error(e) - throw e + alert(e); + console.error(e); + throw e; } } private async suspend() { - await this.reader.cancel() + await this.reader.cancel(); await this.streamClosed.catch(() => { /** noop */ - }) - this.reader.releaseLock() - await this.port.close() - serialLog.update(it => { + }); + this.reader.releaseLock(); + await this.port.close(); + serialLog.update((it) => { it.push({ type: "system", value: "Connection suspended", - }) - return it - }) + }); + return it; + }); } private async wake() { - await this.port.open({baudRate: this.baudRate}) - const decoderStream = new TextDecoderStream() + await this.port.open({ baudRate: this.baudRate }); + const decoderStream = new TextDecoderStream(); this.streamClosed = this.port.readable!.pipeTo(decoderStream.writable, { signal: this.abortController1.signal, - }) + }); this.reader = decoderStream .readable!.pipeThrough(new TransformStream(new LineBreakTransformer()), { signal: this.abortController2.signal, }) - .getReader() - serialLog.update(it => { + .getReader(); + serialLog.update((it) => { it.push({ type: "system", value: "Connection resumed", - }) - return it - }) + }); + return it; + }); } private async internalRead() { try { - const {value} = await timeout(this.reader.read(), 5000) - serialLog.update(it => { + const { value } = await timeout(this.reader.read(), 5000); + serialLog.update((it) => { it.push({ type: "output", value: value!, - }) - return it - }) - return value! + }); + return it; + }); + return value!; } catch (e) { - serialLog.update(it => { + serialLog.update((it) => { it.push({ type: "output", value: `${e}`, - }) - return it - }) + }); + return it; + }); } } @@ -173,61 +192,64 @@ export class CharaDevice { * Send a command to the device */ private async internalSend(...command: string[]) { - const writer = this.port.writable!.getWriter() + const writer = this.port.writable!.getWriter(); try { - serialLog.update(it => { + serialLog.update((it) => { it.push({ type: "input", value: command.join(" "), - }) - return it - }) - await writer.write(new TextEncoder().encode(`${command.join(" ")}\r\n`)) + }); + return it; + }); + await writer.write(new TextEncoder().encode(`${command.join(" ")}\r\n`)); } finally { - writer.releaseLock() + writer.releaseLock(); } } async forget() { - await this.port.forget() + await this.port.forget(); } /** * Read/write to serial port */ async runWith( - callback: (send: typeof this.internalSend, read: typeof this.internalRead) => T | Promise, + callback: ( + send: typeof this.internalSend, + read: typeof this.internalRead, + ) => T | Promise, ): Promise { while (this.lock) { - await this.lock + await this.lock; } - const send = this.internalSend.bind(this) - const read = this.internalRead.bind(this) - let resolveLock: (result: true) => void - this.lock = new Promise(resolve => { - resolveLock = resolve - }) - let result!: T + const send = this.internalSend.bind(this); + const read = this.internalRead.bind(this); + let resolveLock: (result: true) => void; + this.lock = new Promise((resolve) => { + resolveLock = resolve; + }); + let result!: T; try { if (this.suspendDebounceId) { - clearTimeout(this.suspendDebounceId) + clearTimeout(this.suspendDebounceId); } else { - await this.wake() + await this.wake(); } - result = await callback(send, read) + result = await callback(send, read); } finally { - delete this.lock + delete this.lock; this.suspendDebounceId = setTimeout(() => { // cannot be locked here as all the code until clearTimeout is sync - console.assert(this.lock === undefined) + console.assert(this.lock === undefined); this.lock = this.suspend().then(() => { - delete this.lock - delete this.suspendDebounceId - return true - }) - }, this.suspendDebounce) as any - resolveLock!(true) - return result + delete this.lock; + delete this.suspendDebounceId; + return true; + }); + }, this.suspendDebounce) as any; + resolveLock!(true); + return result; } } @@ -236,34 +258,40 @@ export class CharaDevice { */ async send(...command: string[]) { return this.runWith(async (send, read) => { - await send(...command) - const commandString = command.join(" ").replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") - return read().then(it => it.replace(new RegExp(`^${commandString} `), "").split(" ")) - }) + await send(...command); + const commandString = command + .join(" ") + .replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + return read().then((it) => + it.replace(new RegExp(`^${commandString} `), "").split(" "), + ); + }); } async getChordCount(): Promise { - const [count] = await this.send("CML C0") - return Number.parseInt(count) + const [count] = await this.send("CML C0"); + return Number.parseInt(count); } /** * Retrieves a chord by index */ async getChord(index: number | number[]): Promise { - const [actions, phrase] = await this.send(`CML C1 ${index}`) + const [actions, phrase] = await this.send(`CML C1 ${index}`); return { actions: parseChordActions(actions), phrase: parsePhrase(phrase), - } + }; } /** * Retrieves the phrase for a set of actions */ async getChordPhrase(actions: number[]): Promise { - const [phrase] = await this.send(`CML C2 ${stringifyChordActions(actions)}`) - return phrase === "2" ? undefined : parsePhrase(phrase) + const [phrase] = await this.send( + `CML C2 ${stringifyChordActions(actions)}`, + ); + return phrase === "2" ? undefined : parsePhrase(phrase); } async setChord(chord: Chord) { @@ -272,14 +300,17 @@ export class CharaDevice { "C3", stringifyChordActions(chord.actions), stringifyPhrase(chord.phrase), - ) - if (status !== "0") console.error(`Failed with status ${status}`) + ); + if (status !== "0") console.error(`Failed with status ${status}`); } async deleteChord(chord: Pick) { - const status = await this.send(`CML C4 ${stringifyChordActions(chord.actions)}`) - console.log(status) - if (status.at(-1) !== "2" && status.at(-1) !== "0") throw new Error(`Failed with status ${status}`) + const status = await this.send( + `CML C4 ${stringifyChordActions(chord.actions)}`, + ); + console.log(status); + if (status.at(-1) !== "2" && status.at(-1) !== "0") + throw new Error(`Failed with status ${status}`); } /** @@ -289,9 +320,9 @@ export class CharaDevice { * @param action the assigned action id */ async setLayoutKey(layer: number, id: number, action: number) { - const [status] = await this.send(`VAR B4 A${layer} ${id} ${action}`) - console.log(status) - if (status !== "0") throw new Error(`Failed with status ${status}`) + const [status] = await this.send(`VAR B4 A${layer} ${id} ${action}`); + console.log(status); + if (status !== "0") throw new Error(`Failed with status ${status}`); } /** @@ -301,9 +332,9 @@ export class CharaDevice { * @returns the assigned action id */ async getLayoutKey(layer: number, id: number) { - const [position, status] = await this.send(`VAR B3 A${layer} ${id}`) - if (status !== "0") throw new Error(`Failed with status ${status}`) - return Number(position) + const [position, status] = await this.send(`VAR B3 A${layer} ${id}`); + if (status !== "0") throw new Error(`Failed with status ${status}`); + return Number(position); } /** @@ -314,8 +345,8 @@ export class CharaDevice { * **This does not need to be called for chords** */ async commit() { - const [status] = await this.send("VAR B0") - if (status !== "0") throw new Error(`Failed with status ${status}`) + const [status] = await this.send("VAR B0"); + if (status !== "0") throw new Error(`Failed with status ${status}`); } /** @@ -325,39 +356,47 @@ export class CharaDevice { * To permanently store the settings, you *must* call commit. */ async setSetting(id: number, value: number) { - const [status] = await this.send(`VAR B2 ${id.toString(16).toUpperCase()} ${value}`) - if (status !== "0") throw new Error(`Failed with status ${status}`) + const [status] = await this.send( + `VAR B2 ${id.toString(16).toUpperCase()} ${value}`, + ); + if (status !== "0") throw new Error(`Failed with status ${status}`); } /** * Retrieves a setting from the device */ async getSetting(id: number): Promise { - const [value, status] = await this.send(`VAR B1 ${id.toString(16).toUpperCase()}`) + const [value, status] = await this.send( + `VAR B1 ${id.toString(16).toUpperCase()}`, + ); if (status !== "0") - throw new Error(`Setting "0x${id.toString(16)}" doesn't exist (Status code ${status})`) - return Number(value) + throw new Error( + `Setting "0x${id.toString(16)}" doesn't exist (Status code ${status})`, + ); + return Number(value); } /** * Reboots the device */ async reboot() { - await this.send("RST") + await this.send("RST"); } /** * Reboots the device to the bootloader */ async bootloader() { - await this.send("RST BOOTLOADER") + await this.send("RST BOOTLOADER"); } /** * Resets the device */ - async reset(type: "FACTORY" | "PARAMS" | "KEYMAPS" | "STARTER" | "CLEARCML" | "FUNC") { - await this.send(`RST ${type}`) + async reset( + type: "FACTORY" | "PARAMS" | "KEYMAPS" | "STARTER" | "CLEARCML" | "FUNC", + ) { + await this.send(`RST ${type}`); } /** @@ -366,6 +405,6 @@ export class CharaDevice { * This is useful for debugging when there is a suspected heap or stack issue. */ async getRamBytesAvailable(): Promise { - return Number(await this.send("RAM")) + return Number(await this.send("RAM")); } } diff --git a/src/lib/serial/keymap-codes.ts b/src/lib/serial/keymap-codes.ts index 95e9c4ca..0fe1c999 100644 --- a/src/lib/serial/keymap-codes.ts +++ b/src/lib/serial/keymap-codes.ts @@ -1,35 +1,38 @@ -import type {ActionInfo, KeymapCategory} from "$lib/assets/keymaps/keymap" +import type { ActionInfo, KeymapCategory } from "$lib/assets/keymaps/keymap"; export interface KeyInfo extends Partial { - code: number - category: KeymapCategory + code: number; + category: KeymapCategory; } export const KEYMAP_CATEGORIES = (await Promise.all( - Object.values(import.meta.glob("$lib/assets/keymaps/*.yml")).map(async load => - load().then(it => (it as any).default), + Object.values(import.meta.glob("$lib/assets/keymaps/*.yml")).map( + async (load) => load().then((it) => (it as any).default), ), -)) as KeymapCategory[] +)) as KeymapCategory[]; export const KEYMAP_CODES: Record = Object.fromEntries( - KEYMAP_CATEGORIES.flatMap(category => + KEYMAP_CATEGORIES.flatMap((category) => Object.entries(category.actions).map(([code, action]) => [ Number(code), - {...action, code: Number(code), category}, + { ...action, code: Number(code), category }, ]), ), -) +); export const KEYMAP_KEYCODES: Map = new Map( - KEYMAP_CATEGORIES.flatMap(category => - Object.entries(category.actions).map(([code, action]) => [action.keyCode!, Number(code)] as const), + KEYMAP_CATEGORIES.flatMap((category) => + Object.entries(category.actions).map( + ([code, action]) => [action.keyCode!, Number(code)] as const, + ), ).filter(([keyCode]) => keyCode !== undefined), -) +); export const KEYMAP_IDS: Map = new Map( - KEYMAP_CATEGORIES.flatMap(category => + KEYMAP_CATEGORIES.flatMap((category) => Object.entries(category.actions).map( - ([code, action]) => [action.id!, {...action, code: Number(code), category}] as const, + ([code, action]) => + [action.id!, { ...action, code: Number(code), category }] as const, ), ).filter(([id]) => id !== undefined), -) +); diff --git a/src/lib/serial/line-break-transformer.ts b/src/lib/serial/line-break-transformer.ts index 473d447d..c10b9b66 100644 --- a/src/lib/serial/line-break-transformer.ts +++ b/src/lib/serial/line-break-transformer.ts @@ -1,18 +1,18 @@ export class LineBreakTransformer { - private chunks = "" + private chunks = ""; // noinspection JSUnusedGlobalSymbols transform(chunk: string, controller: TransformStreamDefaultController) { - this.chunks += chunk - const lines = this.chunks.split("\r\n") - this.chunks = lines.pop()! + this.chunks += chunk; + const lines = this.chunks.split("\r\n"); + this.chunks = lines.pop()!; for (const line of lines) { - controller.enqueue(line) + controller.enqueue(line); } } // noinspection JSUnusedGlobalSymbols flush(controller: TransformStreamDefaultController) { - controller.enqueue(this.chunks) + controller.enqueue(this.chunks); } } diff --git a/src/lib/serial/sem-ver.ts b/src/lib/serial/sem-ver.ts index b1e4aaec..665872d9 100644 --- a/src/lib/serial/sem-ver.ts +++ b/src/lib/serial/sem-ver.ts @@ -1,24 +1,24 @@ export class SemVer { - major = 0 - minor = 0 - patch = 0 - preRelease?: string - meta?: string + major = 0; + minor = 0; + patch = 0; + preRelease?: string; + meta?: string; constructor(versionString: string) { const result = /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+))?$/.exec( versionString, - ) + ); if (!result) { - console.error("Invalid version string:", versionString) + console.error("Invalid version string:", versionString); } else { - const [, major, minor, patch, preRelease, meta] = result - this.major = Number.parseInt(major) - this.minor = Number.parseInt(minor) - this.patch = Number.parseInt(patch) - if (preRelease) this.preRelease = preRelease - if (meta) this.meta = meta + const [, major, minor, patch, preRelease, meta] = result; + this.major = Number.parseInt(major); + this.minor = Number.parseInt(minor); + this.patch = Number.parseInt(patch); + if (preRelease) this.preRelease = preRelease; + if (meta) this.meta = meta; } } @@ -27,6 +27,6 @@ export class SemVer { `${this.major}.${this.minor}.${this.patch}` + (this.preRelease ? `-${this.preRelease}` : "") + (this.meta ? `+${this.meta}` : "") - ) + ); } } diff --git a/src/lib/serial/serialization.ts b/src/lib/serial/serialization.ts index b4e981ef..ff176133 100644 --- a/src/lib/serial/serialization.ts +++ b/src/lib/serial/serialization.ts @@ -2,42 +2,53 @@ * Compress JSON.stringify with gzip */ export async function stringifyCompressed(chords: T): Promise { - const stream = new Blob([JSON.stringify(chords)]).stream().pipeThrough(new CompressionStream("gzip")) - return await new Response(stream).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(blob: Blob): Promise { - const stream = blob.stream().pipeThrough(new DecompressionStream("deflate")) - return await new Response(stream).json() + const stream = blob.stream().pipeThrough(new DecompressionStream("deflate")); + 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 { - return new Promise(async resolve => { - const reader = new FileReader() +export async function getSharableUrl( + name: string, + data: any, + baseHref = window.location.href, +): Promise { + return new Promise(async (resolve) => { + const reader = new FileReader(); reader.onloadend = function () { - const base64String = (reader.result as string).replace(/^data:application\/octet-stream;base64,/, "") - const url = new URL(baseHref) - url.searchParams.set(name, base64String) - resolve(url) - } - reader.readAsDataURL(await stringifyCompressed(data)) - }) + const base64String = (reader.result as string).replace( + /^data:application\/octet-stream;base64,/, + "", + ); + const url = new URL(baseHref); + url.searchParams.set(name, base64String); + resolve(url); + }; + reader.readAsDataURL(await stringifyCompressed(data)); + }); } export async function parseSharableUrl( name: string, url: string = window.location.href, ): Promise { - const searchParams = new URL(url).searchParams - if (!searchParams.has(name)) return + const searchParams = new URL(url).searchParams; + if (!searchParams.has(name)) return; - return await fetch(`data:application/octet-stream;base64,${searchParams.get(name)}`) - .then(it => it.blob()) - .then(it => parseCompressed(it)) + return await fetch( + `data:application/octet-stream;base64,${searchParams.get(name)}`, + ) + .then((it) => it.blob()) + .then((it) => parseCompressed(it)); } diff --git a/src/lib/serial/tauri-serial-extension.d.ts b/src/lib/serial/tauri-serial-extension.d.ts index 97a00738..f04bd057 100644 --- a/src/lib/serial/tauri-serial-extension.d.ts +++ b/src/lib/serial/tauri-serial-extension.d.ts @@ -1,8 +1,8 @@ /// interface SerialPortInfo { - name?: string - serialNumber?: string - manufacturer?: string - product?: string + name?: string; + serialNumber?: string; + manufacturer?: string; + product?: string; } diff --git a/src/lib/serial/tauri-serial.ts b/src/lib/serial/tauri-serial.ts index 338681d7..acf3f9c7 100644 --- a/src/lib/serial/tauri-serial.ts +++ b/src/lib/serial/tauri-serial.ts @@ -1,65 +1,77 @@ -import {invoke} from "@tauri-apps/api" -import TauriSerialDialog from "$lib/serial/TauriSerialDialog.svelte" +import { invoke } from "@tauri-apps/api"; +import TauriSerialDialog from "$lib/serial/TauriSerialDialog.svelte"; export type TauriSerialPort = Pick< SerialPort, "getInfo" | "open" | "close" | "readable" | "writable" | "forget" -> +>; function NativeSerialPort(info: SerialPortInfo): TauriSerialPort { return { getInfo() { - return info + return info; }, - async open({baudRate}: SerialOptions) { - await invoke("plugin:serial|open", {path: info.name, baudRate}) + async open({ baudRate }: SerialOptions) { + await invoke("plugin:serial|open", { path: info.name, baudRate }); }, async close() { - await invoke("plugin:serial|close", {path: info.name}) + await invoke("plugin:serial|close", { path: info.name }); }, async forget() { // noop }, readable: new ReadableStream({ async pull(controller) { - const result = await invoke("plugin:serial|read", {path: info.name}) - controller.enqueue(new Uint8Array(result)) + const result = await invoke("plugin:serial|read", { + path: info.name, + }); + controller.enqueue(new Uint8Array(result)); }, }), writable: new WritableStream({ async write(chunk) { - await invoke("plugin:serial|write", {path: info.name, chunk: Array.from(chunk)}) + await invoke("plugin:serial|write", { + path: info.name, + chunk: Array.from(chunk), + }); }, }), - } + }; } // @ts-expect-error polyfill // noinspection JSConstantReassignment navigator.serial = { async getPorts(): Promise { - return invoke("plugin:serial|get_serial_ports").then(ports => + return invoke("plugin:serial|get_serial_ports").then((ports) => ports.map(NativeSerialPort), - ) as Promise + ) as Promise; }, async requestPort(options?: SerialPortRequestOptions): Promise { - const ports = await navigator.serial.getPorts().then(ports => + const ports = await navigator.serial.getPorts().then((ports) => options?.filters !== undefined - ? ports.filter(port => - options.filters!.some(({usbVendorId, usbProductId}) => { - const info = port.getInfo() + ? ports.filter((port) => + options.filters!.some(({ usbVendorId, usbProductId }) => { + const info = port.getInfo(); return ( - (usbVendorId === undefined || info.usbVendorId === usbVendorId) && - (usbProductId === undefined || info.usbProductId === usbProductId) - ) + (usbVendorId === undefined || + info.usbVendorId === usbVendorId) && + (usbProductId === undefined || + info.usbProductId === usbProductId) + ); }), ) : ports, - ) + ); - const dialog = new TauriSerialDialog({target: document.body, props: {ports}}) - const port = await new Promise(resolve => dialog.$on("confirm", resolve)) - dialog.$destroy() - return port + const dialog = new TauriSerialDialog({ + target: document.body, + props: { ports }, + }); + const port = await new Promise((resolve) => + dialog.$on("confirm", resolve), + ); + dialog.$destroy(); + return port; }, -} +}; diff --git a/src/lib/serialization/actions.spec.ts b/src/lib/serialization/actions.spec.ts index 72743be8..76e3662e 100644 --- a/src/lib/serialization/actions.spec.ts +++ b/src/lib/serialization/actions.spec.ts @@ -1,12 +1,12 @@ -import {describe, it, expect} from "vitest" -import {compressActions, decompressActions} from "./actions" +import { describe, it, expect } from "vitest"; +import { compressActions, decompressActions } from "./actions"; describe("layout", function () { - const actions = [1023, 255, 256, 42, 32, 532, 8000] + const actions = [1023, 255, 256, 42, 32, 532, 8000]; describe("compression", function () { it("should compress back and forth arrays divisible by 4", function () { - expect(decompressActions(compressActions(actions))).toEqual(actions) - }) - }) -}) + expect(decompressActions(compressActions(actions))).toEqual(actions); + }); + }); +}); diff --git a/src/lib/serialization/actions.ts b/src/lib/serialization/actions.ts index b837663b..b6e01d0b 100644 --- a/src/lib/serialization/actions.ts +++ b/src/lib/serialization/actions.ts @@ -4,15 +4,15 @@ * Action codes <32 are invalid. */ export function compressActions(actions: number[]): Uint8Array { - const buffer = new Uint8Array(actions.length * 2) - let i = 0 + const buffer = new Uint8Array(actions.length * 2); + let i = 0; for (const action of actions) { if (action > 0xff) { - buffer[i++] = action >>> 8 + buffer[i++] = action >>> 8; } - buffer[i++] = action & 0xff + buffer[i++] = action & 0xff; } - return buffer.slice(0, i) + return buffer.slice(0, i); } /** @@ -21,13 +21,13 @@ export function compressActions(actions: number[]): Uint8Array { * @see {compressActions} */ export function decompressActions(raw: Uint8Array): number[] { - const actions: number[] = [] + const actions: number[] = []; for (let i = 0; i < raw.length; i++) { - let action = raw[i] + let action = raw[i]; if (action > 0 && action < 32) { - action = (action << 8) | raw[++i] + action = (action << 8) | raw[++i]; } - actions.push(action) + actions.push(action); } - return actions + return actions; } diff --git a/src/lib/serialization/base64.spec.ts b/src/lib/serialization/base64.spec.ts index aa96c5b8..f5c0364f 100644 --- a/src/lib/serialization/base64.spec.ts +++ b/src/lib/serialization/base64.spec.ts @@ -1,12 +1,14 @@ -import {describe, it, expect} from "vitest" -import {fromBase64, toBase64} from "./base64" +import { describe, it, expect } from "vitest"; +import { fromBase64, toBase64 } from "./base64"; describe("base64", function () { - const data = new Uint8Array([24, 235, 22, 67, 84, 73, 23, 77, 21]) + const data = new Uint8Array([24, 235, 22, 67, 84, 73, 23, 77, 21]); it("should convert back-forth", async function () { - expect(await fromBase64(await toBase64(new Blob([data]))).then(it => it.arrayBuffer())).toEqual( - data.buffer, - ) - }) -}) + expect( + await fromBase64(await toBase64(new Blob([data]))).then((it) => + it.arrayBuffer(), + ), + ).toEqual(data.buffer); + }); +}); diff --git a/src/lib/serialization/base64.ts b/src/lib/serialization/base64.ts index 2bc71f4c..3b124ad4 100644 --- a/src/lib/serialization/base64.ts +++ b/src/lib/serialization/base64.ts @@ -5,8 +5,8 @@ * meaning some chars are swapped for compatibility */ export async function toBase64(blob: Blob): Promise { - return new Promise(async resolve => { - const reader = new FileReader() + return new Promise(async (resolve) => { + const reader = new FileReader(); reader.onloadend = function () { resolve( `${(reader.result as string) @@ -14,17 +14,20 @@ export async function toBase64(blob: Blob): Promise { .replaceAll("+", ".") .replaceAll("/", "_") .replaceAll("=", "-")}`, - ) - } - reader.readAsDataURL(blob) - }) + ); + }; + reader.readAsDataURL(blob); + }); } -export async function fromBase64(base64: string, fetch = window.fetch): Promise { +export async function fromBase64( + base64: string, + fetch = window.fetch, +): Promise { return fetch( `data:application/octet-stream;base64,${base64 .replaceAll(".", "+") .replaceAll("_", "/") .replaceAll("-", "=")}`, - ).then(it => it.blob()) + ).then((it) => it.blob()); } diff --git a/src/lib/serialization/layout.sample.json b/src/lib/serialization/layout.sample.json index 710e1584..7519c9f5 100644 --- a/src/lib/serialization/layout.sample.json +++ b/src/lib/serialization/layout.sample.json @@ -1,21 +1,25 @@ [ [ - 600, 47, 45, 515, 297, 601, 119, 562, 103, 122, 602, 107, 118, 109, 99, 603, 114, 298, 32, 101, 604, 105, - 127, 46, 111, 605, 39, 512, 44, 117, 552, 513, 514, 550, 540, 607, 335, 338, 336, 337, 608, 565, 568, 566, - 567, 609, 563, 63, 519, 297, 610, 98, 120, 536, 113, 611, 102, 112, 104, 100, 612, 97, 296, 544, 116, 613, - 108, 299, 106, 110, 614, 121, 516, 59, 115, 553, 517, 518, 551, 542, 616, 336, 338, 335, 337, 617, 566, - 568, 565, 567 + 600, 47, 45, 515, 297, 601, 119, 562, 103, 122, 602, 107, 118, 109, 99, 603, + 114, 298, 32, 101, 604, 105, 127, 46, 111, 605, 39, 512, 44, 117, 552, 513, + 514, 550, 540, 607, 335, 338, 336, 337, 608, 565, 568, 566, 567, 609, 563, + 63, 519, 297, 610, 98, 120, 536, 113, 611, 102, 112, 104, 100, 612, 97, 296, + 544, 116, 613, 108, 299, 106, 110, 614, 121, 516, 59, 115, 553, 517, 518, + 551, 542, 616, 336, 338, 335, 337, 617, 566, 568, 565, 567 ], [ - 0, 92, 45, 515, 297, 0, 119, 562, 91, 93, 0, 55, 56, 57, 48, 0, 49, 298, 51, 50, 0, 52, 127, 54, 53, 0, - 96, 512, 61, 124, 0, 513, 514, 550, 540, 0, 569, 572, 570, 571, 0, 565, 568, 566, 567, 0, 563, 63, 519, - 297, 0, 98, 120, 91, 93, 0, 55, 56, 57, 48, 0, 49, 296, 51, 50, 0, 52, 299, 54, 53, 0, 61, 516, 59, 115, - 0, 517, 518, 551, 542, 0, 570, 572, 569, 571, 0, 566, 568, 565, 567 + 0, 92, 45, 515, 297, 0, 119, 562, 91, 93, 0, 55, 56, 57, 48, 0, 49, 298, 51, + 50, 0, 52, 127, 54, 53, 0, 96, 512, 61, 124, 0, 513, 514, 550, 540, 0, 569, + 572, 570, 571, 0, 565, 568, 566, 567, 0, 563, 63, 519, 297, 0, 98, 120, 91, + 93, 0, 55, 56, 57, 48, 0, 49, 296, 51, 50, 0, 52, 299, 54, 53, 0, 61, 516, + 59, 115, 0, 517, 518, 551, 542, 0, 570, 572, 569, 571, 0, 566, 568, 565, 567 ], [ - 0, 47, 45, 515, 297, 0, 119, 324, 325, 122, 0, 320, 321, 322, 323, 0, 314, 298, 316, 315, 0, 317, 127, - 319, 318, 0, 39, 512, 44, 117, 552, 513, 514, 0, 540, 0, 335, 338, 336, 337, 0, 569, 572, 570, 571, 0, - 563, 63, 519, 297, 0, 98, 324, 325, 113, 0, 320, 321, 322, 323, 0, 314, 296, 316, 315, 0, 317, 299, 319, - 318, 0, 121, 516, 59, 115, 553, 517, 518, 0, 542, 0, 336, 338, 335, 337, 0, 570, 572, 569, 571 + 0, 47, 45, 515, 297, 0, 119, 324, 325, 122, 0, 320, 321, 322, 323, 0, 314, + 298, 316, 315, 0, 317, 127, 319, 318, 0, 39, 512, 44, 117, 552, 513, 514, 0, + 540, 0, 335, 338, 336, 337, 0, 569, 572, 570, 571, 0, 563, 63, 519, 297, 0, + 98, 324, 325, 113, 0, 320, 321, 322, 323, 0, 314, 296, 316, 315, 0, 317, + 299, 319, 318, 0, 121, 516, 59, 115, 553, 517, 518, 0, 542, 0, 336, 338, + 335, 337, 0, 570, 572, 569, 571 ] ] diff --git a/src/lib/serialization/layout.ts b/src/lib/serialization/layout.ts index cb3585a6..26d60141 100644 --- a/src/lib/serialization/layout.ts +++ b/src/lib/serialization/layout.ts @@ -1,37 +1,49 @@ -import {compressActions, decompressActions} from "./actions" -import {fromBase64, toBase64} from "$lib/serialization/base64" +import { compressActions, decompressActions } from "./actions"; +import { fromBase64, toBase64 } from "$lib/serialization/base64"; export interface NewCharaLayout { - charaLayoutVersion: 1 - device: "one" | "lite" | string + charaLayoutVersion: 1; + device: "one" | "lite" | string; /** * Layers A1-A3, with numeric action codes on each */ - layers: [number[], number[], number[]] + layers: [number[], number[], number[]]; } -export type CharaLayout = [number[], number[], number[]] +export type CharaLayout = [number[], number[], number[]]; /** * Serialize a layout into a micro package */ export async function serializeLayout(layout: CharaLayout): Promise { - const items = compressActions(layout.flat()) - const stream = new Blob([items]).stream().pipeThrough(new CompressionStream("deflate")) - return new Response(stream).blob() + const items = compressActions(layout.flat()); + const stream = new Blob([items]) + .stream() + .pipeThrough(new CompressionStream("deflate")); + return new Response(stream).blob(); } export async function deserializeLayout(layout: Blob): Promise { - const stream = layout.stream().pipeThrough(new DecompressionStream("deflate")) - const raw = await new Response(stream).arrayBuffer() - const actions = decompressActions(new Uint8Array(raw)) - return [actions.slice(0, 90), actions.slice(90, 180), actions.slice(180, 270)] + const stream = layout + .stream() + .pipeThrough(new DecompressionStream("deflate")); + const raw = await new Response(stream).arrayBuffer(); + const actions = decompressActions(new Uint8Array(raw)); + return [ + actions.slice(0, 90), + actions.slice(90, 180), + actions.slice(180, 270), + ]; } -export async function layoutAsUrlComponent(layout: CharaLayout): Promise { - return serializeLayout(layout).then(toBase64) +export async function layoutAsUrlComponent( + layout: CharaLayout, +): Promise { + return serializeLayout(layout).then(toBase64); } -export async function layoutFromUrlComponent(base64: string): Promise { - return fromBase64(base64).then(deserializeLayout) +export async function layoutFromUrlComponent( + base64: string, +): Promise { + return fromBase64(base64).then(deserializeLayout); } diff --git a/src/lib/serialization/visual-layout.ts b/src/lib/serialization/visual-layout.ts index 21f0e7ad..f23a96bd 100644 --- a/src/lib/serialization/visual-layout.ts +++ b/src/lib/serialization/visual-layout.ts @@ -1,45 +1,45 @@ export interface VisualLayout { - name: string - col: VisualLayoutRow[] + name: string; + col: VisualLayoutRow[]; } interface Positionable { - offset: [number, number] - rotate: number + offset: [number, number]; + rotate: number; } export interface VisualLayoutRow extends Positionable { - row: Array + row: Array; } export interface VisualLayoutKey extends Positionable { - key: number - size?: [number, number] + key: number; + size?: [number, number]; } export interface VisualLayoutSwitch extends Positionable { switch: { - n: number - e: number - w: number - s: number - d: number - } + n: number; + e: number; + w: number; + s: number; + d: number; + }; } export interface CompiledLayout { - name: string - size: [number, number] - keys: CompiledLayoutKey[] + name: string; + size: [number, number]; + keys: CompiledLayoutKey[]; } export interface CompiledLayoutKey { - id: number - shape: "quarter-circle" | "square" - cornerRadius: number - size: [number, number] - pos: [number, number] - rotate: number + id: number; + shape: "quarter-circle" | "square"; + cornerRadius: number; + size: [number, number]; + pos: [number, number]; + rotate: number; } export function compileLayout(layout: VisualLayout): CompiledLayout { @@ -47,18 +47,18 @@ export function compileLayout(layout: VisualLayout): CompiledLayout { name: layout.name, size: [0, 0], keys: [], - } + }; - let y = 0 - for (const {row, offset} of layout.col) { - let x = offset?.[0] ?? 0 - y += offset?.[1] ?? 0 - let maxHeight = 0 + let y = 0; + for (const { row, offset } of layout.col) { + let x = offset?.[0] ?? 0; + y += offset?.[1] ?? 0; + let maxHeight = 0; for (const info of row) { - const [ox, oy] = info.offset || [0, 0] - const rotate = info.rotate || 0 + const [ox, oy] = info.offset || [0, 0]; + const rotate = info.rotate || 0; if ("key" in info) { - const [width, height] = info.size ?? [1, 1] + const [width, height] = info.size ?? [1, 1]; compiled.keys.push({ id: info.key, @@ -67,14 +67,19 @@ export function compileLayout(layout: VisualLayout): CompiledLayout { pos: [x + ox, y + oy], cornerRadius: 0.1, rotate, - }) + }); - x += width + ox - maxHeight = Math.max(maxHeight, height + oy) + x += width + ox; + maxHeight = Math.max(maxHeight, height + oy); } else if ("switch" in info) { - const cx = x + ox + 1 - const cy = y + oy + 1 - for (const [i, id] of [info.switch.s, info.switch.w, info.switch.n, info.switch.e].entries()) { + const cx = x + ox + 1; + const cy = y + oy + 1; + for (const [i, id] of [ + info.switch.s, + info.switch.w, + info.switch.n, + info.switch.e, + ].entries()) { compiled.keys.push({ id, shape: "quarter-circle", @@ -82,7 +87,7 @@ export function compileLayout(layout: VisualLayout): CompiledLayout { size: [2, 0.6], pos: [cx, cy], rotate: 90 * i + 45, - }) + }); } compiled.keys.push({ id: info.switch.d, @@ -91,16 +96,16 @@ export function compileLayout(layout: VisualLayout): CompiledLayout { size: [0.8, 0.8], pos: [x + 0.6 + ox, y + 0.6 + oy], rotate: 0, - }) + }); - x += 2 + ox - maxHeight = Math.max(maxHeight, 2 + oy) + x += 2 + ox; + maxHeight = Math.max(maxHeight, 2 + oy); } } - y += maxHeight - compiled.size[0] = Math.max(compiled.size[0], x) + y += maxHeight; + compiled.size[0] = Math.max(compiled.size[0], x); } - compiled.size[1] = y + compiled.size[1] = y; - return compiled + return compiled; } diff --git a/src/lib/setting.ts b/src/lib/setting.ts index 32080a34..263e610c 100644 --- a/src/lib/setting.ts +++ b/src/lib/setting.ts @@ -1,66 +1,78 @@ -import type {Action} from "svelte/action" -import {changes, ChangeType, settings} from "$lib/undo-redo" +import type { Action } from "svelte/action"; +import { changes, ChangeType, settings } from "$lib/undo-redo"; -export const setting: Action = function ( - node: HTMLInputElement, - {id, inverse, scale}, -) { - node.setAttribute("disabled", "") - const type = node.getAttribute("type") as "number" | "checkbox" - const min = node.hasAttribute("min") ? Number(node.getAttribute("min")) : undefined - const max = node.hasAttribute("max") ? Number(node.getAttribute("max")) : undefined +export const setting: Action< + HTMLInputElement, + { id: number; inverse?: number; scale?: number } +> = function (node: HTMLInputElement, { id, inverse, scale }) { + node.setAttribute("disabled", ""); + const type = node.getAttribute("type") as "number" | "checkbox"; + const min = node.hasAttribute("min") + ? Number(node.getAttribute("min")) + : undefined; + const max = node.hasAttribute("max") + ? Number(node.getAttribute("max")) + : undefined; - const unsubscribe = settings.subscribe(async settings => { + const unsubscribe = settings.subscribe(async (settings) => { if (id in settings) { - const {value, isApplied} = settings[id] + const { value, isApplied } = settings[id]; if (type === "number") { node.value = ( - inverse !== undefined ? inverse / value : scale !== undefined ? scale * value : value - ).toString() + inverse !== undefined + ? inverse / value + : scale !== undefined + ? scale * value + : value + ).toString(); } else { - node.checked = value !== 0 + node.checked = value !== 0; } if (isApplied) { - node.classList.remove("pending-changes") + node.classList.remove("pending-changes"); } else { - node.classList.add("pending-changes") + node.classList.add("pending-changes"); } - node.removeAttribute("disabled") + node.removeAttribute("disabled"); } else { - node.setAttribute("disabled", "") + node.setAttribute("disabled", ""); } - }) + }); async function listener() { - let value: number + let value: number; if (type === "number") { - value = Number(node.value) - if (Number.isNaN(value)) return + value = Number(node.value); + if (Number.isNaN(value)) return; value = Math.floor( - inverse !== undefined ? inverse / value : scale !== undefined ? value / scale : value, - ) - if (min !== undefined) value = Math.max(min, value) - if (max !== undefined) value = Math.min(max, value) + inverse !== undefined + ? inverse / value + : scale !== undefined + ? value / scale + : value, + ); + if (min !== undefined) value = Math.max(min, value); + if (max !== undefined) value = Math.min(max, value); } else { - value = node.checked ? 1 : 0 + value = node.checked ? 1 : 0; } - changes.update(changes => { + changes.update((changes) => { changes.push({ type: ChangeType.Setting, id: id, setting: value, - }) - return changes - }) + }); + return changes; + }); } - node.addEventListener("change", listener) + node.addEventListener("change", listener); return { destroy() { - node.removeEventListener("change", listener) - unsubscribe() + node.removeEventListener("change", listener); + unsubscribe(); }, - } -} + }; +}; diff --git a/src/lib/share.ts b/src/lib/share.ts index 51d997aa..a50c6a76 100644 --- a/src/lib/share.ts +++ b/src/lib/share.ts @@ -1,22 +1,25 @@ -import type {Action} from "svelte/action" -import {readonly, writable} from "svelte/store" +import type { Action } from "svelte/action"; +import { readonly, writable } from "svelte/store"; -const setCanShare = writable(false) -export const canShare = readonly(setCanShare) +const setCanShare = writable(false); +export const canShare = readonly(setCanShare); -let shareCallback: ((event: Event) => void) | undefined +let shareCallback: ((event: Event) => void) | undefined; export function triggerShare(event: Event) { - shareCallback?.(event) + shareCallback?.(event); } -export const share: Action void> = (node, callback: (event: Event) => void) => { - setCanShare.set(true) - shareCallback = callback +export const share: Action void> = ( + node, + callback: (event: Event) => void, +) => { + setCanShare.set(true); + shareCallback = callback; return { destroy() { - setCanShare.set(false) - shareCallback = undefined + setCanShare.set(false); + shareCallback = undefined; }, - } -} + }; +}; diff --git a/src/lib/share/action-array.spec.ts b/src/lib/share/action-array.spec.ts index 6562fd0c..3eac2f1e 100644 --- a/src/lib/share/action-array.spec.ts +++ b/src/lib/share/action-array.spec.ts @@ -1,14 +1,19 @@ -import {describe, it, expect} from "vitest" -import {deserializeActionArray, serializeActionArray} from "./action-array" +import { describe, it, expect } from "vitest"; +import { deserializeActionArray, serializeActionArray } from "./action-array"; describe("action array", () => { it("should work with number arrays", () => { - expect(deserializeActionArray(serializeActionArray([62, 256, 1235]))).toEqual([62, 256, 1235]) - }) + expect( + deserializeActionArray(serializeActionArray([62, 256, 1235])), + ).toEqual([62, 256, 1235]); + }); it("should work with nested arrays", () => { - expect(deserializeActionArray(serializeActionArray([[], [[]]]))).toEqual([[], [[]]]) - }) + expect(deserializeActionArray(serializeActionArray([[], [[]]]))).toEqual([ + [], + [[]], + ]); + }); it("should compress back and forth", () => { expect( @@ -23,31 +28,37 @@ describe("action array", () => { [43, 746, 634], [34, 63], [332, 34], - ]) - }) + ]); + }); it("should compress a full layout", () => { const layout = Object.freeze([ Object.freeze([ - 0, 0, 0, 0, 0, 53, 119, 45, 103, 122, 52, 107, 118, 109, 99, 51, 114, 36, 59, 101, 50, 105, 34, 46, - 111, 49, 39, 515, 44, 117, 0, 512, 514, 513, 550, 0, 319, 318, 321, 320, 326, 315, 314, 317, 316, 0, - 0, 0, 0, 0, 54, 98, 120, 536, 113, 55, 102, 112, 104, 100, 56, 97, 296, 544, 116, 57, 108, 299, 106, - 110, 48, 121, 297, 61, 115, 0, 518, 516, 517, 553, 0, 336, 338, 335, 337, 0, 325, 322, 323, 324, + 0, 0, 0, 0, 0, 53, 119, 45, 103, 122, 52, 107, 118, 109, 99, 51, 114, + 36, 59, 101, 50, 105, 34, 46, 111, 49, 39, 515, 44, 117, 0, 512, 514, + 513, 550, 0, 319, 318, 321, 320, 326, 315, 314, 317, 316, 0, 0, 0, 0, 0, + 54, 98, 120, 536, 113, 55, 102, 112, 104, 100, 56, 97, 296, 544, 116, + 57, 108, 299, 106, 110, 48, 121, 297, 61, 115, 0, 518, 516, 517, 553, 0, + 336, 338, 335, 337, 0, 325, 322, 323, 324, ]), Object.freeze([ - 0, 0, 0, 0, 0, 0, 91, 0, 0, 0, 0, 53, 0, 47, 52, 0, 51, 298, 0, 50, 0, 0, 127, 0, 49, 0, 0, 515, 0, 0, - 0, 512, 514, 513, 550, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 0, 536, 0, 0, 54, 0, 92, - 55, 0, 56, 296, 544, 57, 0, 96, 299, 0, 48, 0, 0, 297, 0, 0, 0, 518, 516, 517, 553, 0, 336, 338, 335, - 337, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 91, 0, 0, 0, 0, 53, 0, 47, 52, 0, 51, 298, 0, 50, 0, + 0, 127, 0, 49, 0, 0, 515, 0, 0, 0, 512, 514, 513, 550, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 0, 536, 0, 0, 54, 0, 92, 55, 0, 56, + 296, 544, 57, 0, 96, 299, 0, 48, 0, 0, 297, 0, 0, 0, 518, 516, 517, 553, + 0, 336, 338, 335, 337, 0, 0, 0, 0, 0, ]), Object.freeze([ - 0, 0, 0, 0, 0, 0, 64, 95, 43, 0, 0, 126, 38, 63, 40, 0, 35, 298, 36, 123, 0, 33, 127, 37, 60, 0, 34, - 515, 0, 0, 0, 512, 514, 513, 550, 0, 333, 331, 330, 334, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 536, - 0, 0, 94, 58, 124, 41, 0, 42, 296, 544, 125, 0, 126, 299, 0, 62, 0, 0, 297, 0, 0, 0, 518, 516, 517, - 553, 0, 336, 338, 335, 337, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 64, 95, 43, 0, 0, 126, 38, 63, 40, 0, 35, 298, 36, + 123, 0, 33, 127, 37, 60, 0, 34, 515, 0, 0, 0, 512, 514, 513, 550, 0, + 333, 331, 330, 334, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 536, 0, 0, + 94, 58, 124, 41, 0, 42, 296, 544, 125, 0, 126, 299, 0, 62, 0, 0, 297, 0, + 0, 0, 518, 516, 517, 553, 0, 336, 338, 335, 337, 0, 0, 0, 0, 0, ]), - ]) + ]); - expect(deserializeActionArray(serializeActionArray(layout as number[][]))).toEqual(layout) - }) -}) + expect( + deserializeActionArray(serializeActionArray(layout as number[][])), + ).toEqual(layout); + }); +}); diff --git a/src/lib/share/action-array.ts b/src/lib/share/action-array.ts index 1ad9b5f8..d8488257 100644 --- a/src/lib/share/action-array.ts +++ b/src/lib/share/action-array.ts @@ -1,55 +1,63 @@ -import {compressActions, decompressActions} from "../serialization/actions" -import {CHARA_FILE_TYPES} from "../share/share-url" +import { compressActions, decompressActions } from "../serialization/actions"; +import { CHARA_FILE_TYPES } from "../share/share-url"; -export type ActionArray = number[] | ActionArray[] +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) + let out = new Uint8Array(5); + const writer = new DataView(out.buffer); + writer.setUint32(0, array.length); if (array.length === 0) { - return out + return out; } else if (typeof array[0] === "number") { - writer.setUint8(4, CHARA_FILE_TYPES.indexOf("number")) - const compressed = compressActions(array as number[]) - writer.setUint32(0, compressed.length) - return concatUint8Arrays(out, compressed) + writer.setUint8(4, CHARA_FILE_TYPES.indexOf("number")); + const compressed = compressActions(array as number[]); + writer.setUint32(0, compressed.length); + return concatUint8Arrays(out, compressed); } else if (Array.isArray(array[0])) { - writer.setUint8(4, CHARA_FILE_TYPES.indexOf("array")) - return concatUint8Arrays(out, ...(array as ActionArray[]).map(serializeActionArray)) + writer.setUint8(4, CHARA_FILE_TYPES.indexOf("array")); + return concatUint8Arrays( + out, + ...(array as ActionArray[]).map(serializeActionArray), + ); } else { - throw new Error("Not implemented") + throw new Error("Not implemented"); } } -export function deserializeActionArray(raw: Uint8Array, cursor = {pos: 0}): ActionArray { - const reader = new DataView(raw.buffer) - const length = reader.getUint32(cursor.pos) - cursor.pos += 4 - const type = CHARA_FILE_TYPES[reader.getUint8(cursor.pos)] - cursor.pos++ +export function deserializeActionArray( + raw: Uint8Array, + cursor = { pos: 0 }, +): ActionArray { + const reader = new DataView(raw.buffer); + const length = reader.getUint32(cursor.pos); + cursor.pos += 4; + const type = CHARA_FILE_TYPES[reader.getUint8(cursor.pos)]; + cursor.pos++; if (type === "number") { - const decompressed = decompressActions(raw.slice(cursor.pos, cursor.pos + length)) - cursor.pos += length - return decompressed + const decompressed = decompressActions( + raw.slice(cursor.pos, cursor.pos + length), + ); + cursor.pos += length; + return decompressed; } else if (type === "array") { - const out = [] + const out = []; for (let i = 0; i < length; i++) { - out.push(deserializeActionArray(raw, cursor)) + out.push(deserializeActionArray(raw, cursor)); } - return out + return out; } else { - throw new Error("Not implemented") + 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 + 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 + out.set(array, offset); + offset += array.length; } - return out + return out; } diff --git a/src/lib/share/chara-file.ts b/src/lib/share/chara-file.ts index d28cf770..393a88a8 100644 --- a/src/lib/share/chara-file.ts +++ b/src/lib/share/chara-file.ts @@ -1,23 +1,23 @@ export interface CharaFile { - charaVersion: 1 - type: T + charaVersion: 1; + type: T; } export interface CharaLayoutFile extends CharaFile<"layout"> { - device?: "ONE" | "LITE" | string - layout: [number[], number[], number[]] + device?: "ONE" | "LITE" | string; + layout: [number[], number[], number[]]; } export interface CharaChordFile extends CharaFile<"chords"> { - chords: [number[], number[]][] + chords: [number[], number[]][]; } export interface CharaSettingsFile extends CharaFile<"settings"> { - settings: number[] + settings: number[]; } export interface CharaBackupFile extends CharaFile<"backup"> { - history: [CharaChordFile, CharaLayoutFile, CharaSettingsFile][] + history: [CharaChordFile, CharaLayoutFile, CharaSettingsFile][]; } -export type CharaFiles = CharaLayoutFile | CharaChordFile | CharaSettingsFile +export type CharaFiles = CharaLayoutFile | CharaChordFile | CharaSettingsFile; diff --git a/src/lib/share/share-url.ts b/src/lib/share/share-url.ts index 77fef7f4..2f8a5866 100644 --- a/src/lib/share/share-url.ts +++ b/src/lib/share/share-url.ts @@ -1,13 +1,19 @@ -import type {CharaFile, CharaFiles} from "../share/chara-file" -import type {ActionArray} from "../share/action-array" -import {deserializeActionArray, serializeActionArray} from "../share/action-array" -import {fromBase64, toBase64} from "../serialization/base64" +import type { CharaFile, CharaFiles } from "../share/chara-file"; +import type { ActionArray } from "../share/action-array"; +import { + deserializeActionArray, + serializeActionArray, +} from "../share/action-array"; +import { fromBase64, toBase64 } from "../serialization/base64"; type CharaLayoutOrder = { [K in CharaFiles["type"]]: Array< - [Exclude, keyof CharaFile>, (typeof CHARA_FILE_TYPES)[number]] - > -} + [ + Exclude, keyof CharaFile>, + (typeof CHARA_FILE_TYPES)[number], + ] + >; +}; const keys: CharaLayoutOrder = { layout: [ @@ -16,51 +22,60 @@ const keys: CharaLayoutOrder = { ], chords: [["chords", "array"]], settings: [["settings", "array"]], -} +}; -export const CHARA_FILE_TYPES = ["unknown", "number", "string", "array"] as const +export const CHARA_FILE_TYPES = [ + "unknown", + "number", + "string", + "array", +] as const; -const sep = "\n" +const sep = "\n"; -export async function charaFileToUriComponent(file: T): Promise { - let url = `${file.type}${sep}${file.charaVersion}` +export async function charaFileToUriComponent( + file: T, +): Promise { + let url = `${file.type}${sep}${file.charaVersion}`; for (const [key, type] of keys[file.type]) { - const value = file[key as keyof T] - url += sep + const value = file[key as keyof T]; + url += sep; if (type === "string") { - url += value as 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()) + .pipeThrough(new CompressionStream("deflate")); + url += await toBase64(await new Response(stream).blob()); } else { - throw new Error("Not implemented") + throw new Error("Not implemented"); } } - return url + return url; } export async function charaFileFromUriComponent( uriComponent: string, fetch = window.fetch, ): Promise { - const [fileType, version, ...values] = uriComponent.split(sep) - const file: any = {type: fileType, charaVersion: Number(version)} + const [fileType, version, ...values] = uriComponent.split(sep); + const file: any = { type: fileType, charaVersion: Number(version) }; for (const [key, type] of keys[fileType as keyof typeof keys]) { - const value = values.shift()! + const value = values.shift()!; if (type === "string") { - file[key] = value + file[key] = value; } else if (type === "array") { - const stream = (await fromBase64(value, fetch)).stream().pipeThrough(new DecompressionStream("deflate")) - const actions = new Uint8Array(await new Response(stream).arrayBuffer()) - console.log(actions) - file[key] = deserializeActionArray(actions) + const stream = (await fromBase64(value, fetch)) + .stream() + .pipeThrough(new DecompressionStream("deflate")); + const actions = new Uint8Array(await new Response(stream).arrayBuffer()); + console.log(actions); + file[key] = deserializeActionArray(actions); } } - return file + return file; } diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 38ed7d59..92746229 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -1,17 +1,25 @@ -import type {Writable} from "svelte/store" -import {writable} from "svelte/store" -import {browser} from "$app/environment" +import type { Writable } from "svelte/store"; +import { writable } from "svelte/store"; +import { browser } from "$app/environment"; -export function persistentWritable(key: string, value: T, condition?: () => boolean): Writable { +export function persistentWritable( + key: string, + value: T, + condition?: () => boolean, +): Writable { if (browser) { - const persistedValue = localStorage.getItem(key) - const store = persistedValue !== null ? writable(JSON.parse(persistedValue)) : writable(value) - store.subscribe(value => { - if (!condition || condition()) localStorage.setItem(key, JSON.stringify(value)) - }) + const persistedValue = localStorage.getItem(key); + const store = + persistedValue !== null + ? writable(JSON.parse(persistedValue)) + : writable(value); + store.subscribe((value) => { + if (!condition || condition()) + localStorage.setItem(key, JSON.stringify(value)); + }); - return store + return store; } else { - return writable(value) + return writable(value); } } diff --git a/src/lib/style/theme.server.ts b/src/lib/style/theme.server.ts index 03120a4d..d60948a8 100644 --- a/src/lib/style/theme.server.ts +++ b/src/lib/style/theme.server.ts @@ -1,10 +1,14 @@ -import {argbFromHex, hexFromArgb, themeFromSourceColor} from "@material/material-color-utilities" +import { + argbFromHex, + hexFromArgb, + themeFromSourceColor, +} from "@material/material-color-utilities"; -export const themeBase = "#6D81C7" -export const themeSuccessBase = "#00ff00" +export const themeBase = "#6D81C7"; +export const themeSuccessBase = "#00ff00"; const theme = themeFromSourceColor(argbFromHex(themeBase), [ - {name: "success", value: argbFromHex(themeSuccessBase), blend: true}, -]) + { name: "success", value: argbFromHex(themeSuccessBase), blend: true }, +]); -export const themeColor = hexFromArgb(theme.schemes.dark.background) +export const themeColor = hexFromArgb(theme.schemes.dark.background); diff --git a/src/lib/title.ts b/src/lib/title.ts index ab42e73f..591402d7 100644 --- a/src/lib/title.ts +++ b/src/lib/title.ts @@ -1,14 +1,14 @@ -import type {Action} from "svelte/action" -import tippy from "tippy.js" -import type {SvelteComponent} from "svelte" -import Tooltip from "$lib/components/Tooltip.svelte" -import hotkeys from "hotkeys-js" +import type { Action } from "svelte/action"; +import tippy from "tippy.js"; +import type { SvelteComponent } from "svelte"; +import Tooltip from "$lib/components/Tooltip.svelte"; +import hotkeys from "hotkeys-js"; -export const action: Action = ( +export const action: Action = ( node: Element, - {title, shortcut}, + { title, shortcut }, ) => { - let component: SvelteComponent | undefined + let component: SvelteComponent | undefined; const tooltip = tippy(node, { arrow: false, theme: "tooltip", @@ -16,30 +16,30 @@ export const action: Action = ( onShow(instance) { component ??= new Tooltip({ target: instance.popper.querySelector(".tippy-content") as Element, - props: {title, shortcut}, - }) + props: { title, shortcut }, + }); }, onHidden() { - component?.$destroy() - component = undefined + component?.$destroy(); + component = undefined; }, - }) + }); if (shortcut && node instanceof HTMLElement) { hotkeys(shortcut, function (keyboardEvent) { - keyboardEvent.preventDefault() - node.click() - }) + keyboardEvent.preventDefault(); + node.click(); + }); } return { update(updated) { - title = updated.title - shortcut = updated.shortcut + title = updated.title; + shortcut = updated.shortcut; }, destroy() { - tooltip.destroy() - hotkeys.unbind(shortcut) + tooltip.destroy(); + hotkeys.unbind(shortcut); }, - } -} + }; +}; diff --git a/src/lib/tooltip.ts b/src/lib/tooltip.ts index c54bfcda..826b08cf 100644 --- a/src/lib/tooltip.ts +++ b/src/lib/tooltip.ts @@ -1,13 +1,16 @@ -import type {Action} from "svelte/action" -import tippy from "tippy.js" -import type {Props} from "tippy.js" +import type { Action } from "svelte/action"; +import tippy from "tippy.js"; +import type { Props } from "tippy.js"; -export const tooltip: Action> = function (node, props) { - const instance = tippy(node, props) +export const tooltip: Action> = function ( + node, + props, +) { + const instance = tippy(node, props); return { destroy() { - instance.destroy() + instance.destroy(); }, - } -} + }; +}; diff --git a/src/lib/undo-redo.ts b/src/lib/undo-redo.ts index 8280172b..0885e05e 100644 --- a/src/lib/undo-redo.ts +++ b/src/lib/undo-redo.ts @@ -1,8 +1,12 @@ -import {persistentWritable} from "$lib/storage" -import {derived} from "svelte/store" -import type {Chord} from "$lib/serial/chord" -import {deviceChords, deviceLayout, deviceSettings} from "$lib/serial/connection" -import {KEYMAP_CODES} from "$lib/serial/keymap-codes" +import { persistentWritable } from "$lib/storage"; +import { derived } from "svelte/store"; +import type { Chord } from "$lib/serial/chord"; +import { + deviceChords, + deviceLayout, + deviceSettings, +} from "$lib/serial/connection"; +import { KEYMAP_CODES } from "$lib/serial/keymap-codes"; export enum ChangeType { Layout, @@ -11,77 +15,79 @@ export enum ChangeType { } export interface LayoutChange { - type: ChangeType.Layout - id: number - layer: number - action: number + type: ChangeType.Layout; + id: number; + layer: number; + action: number; } export interface ChordChange { - type: ChangeType.Chord - deleted?: true - id: number[] - actions: number[] - phrase: number[] + type: ChangeType.Chord; + deleted?: true; + id: number[]; + actions: number[]; + phrase: number[]; } export interface SettingChange { - type: ChangeType.Setting - id: number - setting: number + type: ChangeType.Setting; + id: number; + setting: number; } export interface ChangeInfo { - isApplied: boolean - isCommitted?: boolean + isApplied: boolean; + isCommitted?: boolean; } -export type Change = LayoutChange | ChordChange | SettingChange +export type Change = LayoutChange | ChordChange | SettingChange; -export const changes = persistentWritable("changes", []) +export const changes = persistentWritable("changes", []); export interface Overlay { - layout: [Map, Map, Map] - chords: Map - settings: Map + layout: [Map, Map, Map]; + chords: Map; + settings: Map; } -export const overlay = derived(changes, changes => { +export const overlay = derived(changes, (changes) => { const overlay: Overlay = { layout: [new Map(), new Map(), new Map()], chords: new Map(), settings: new Map(), - } + }; for (const change of changes) { switch (change.type) { case ChangeType.Layout: - overlay.layout[change.layer].set(change.id, change.action) - break + overlay.layout[change.layer].set(change.id, change.action); + break; case ChangeType.Chord: overlay.chords.set(JSON.stringify(change.id), { actions: change.actions, phrase: change.phrase, deleted: change.deleted ?? false, - }) - break + }); + break; case ChangeType.Setting: - overlay.settings.set(change.id, change.setting) - break + overlay.settings.set(change.id, change.setting); + break; } } - return overlay -}) + return overlay; +}); -export const settings = derived([overlay, deviceSettings], ([overlay, settings]) => - settings.map<{value: number} & ChangeInfo>((value, id) => ({ - value: overlay.settings.get(id) ?? value, - isApplied: !overlay.settings.has(id), - })), -) +export const settings = derived( + [overlay, deviceSettings], + ([overlay, settings]) => + settings.map<{ value: number } & ChangeInfo>((value, id) => ({ + value: overlay.settings.get(id) ?? value, + isApplied: !overlay.settings.has(id), + })), +); -export type KeyInfo = {action: number} & ChangeInfo +export type KeyInfo = { action: number } & ChangeInfo; export const layout = derived([overlay, deviceLayout], ([overlay, layout]) => layout.map( (actions, layer) => @@ -90,47 +96,52 @@ export const layout = derived([overlay, deviceLayout], ([overlay, layout]) => isApplied: !overlay.layout[layer].has(id), })) as [KeyInfo, KeyInfo, KeyInfo], ), -) +); export type ChordInfo = Chord & - ChangeInfo & {phraseChanged: boolean; actionsChanged: boolean; sortBy: string} & { - id: number[] - deleted: boolean - } + ChangeInfo & { + phraseChanged: boolean; + actionsChanged: boolean; + sortBy: string; + } & { + id: number[]; + deleted: boolean; + }; export const chords = derived([overlay, deviceChords], ([overlay, chords]) => { - const newChords = new Set(overlay.chords.keys()) + const newChords = new Set(overlay.chords.keys()); - const changedChords = chords.map(chord => { - const id = JSON.stringify(chord.actions) + const changedChords = chords.map((chord) => { + const id = JSON.stringify(chord.actions); if (overlay.chords.has(id)) { - newChords.delete(id) - const changedChord = overlay.chords.get(id)! + newChords.delete(id); + const changedChord = overlay.chords.get(id)!; return { id: chord.actions, // use the old phrase for stable editing - sortBy: chord.phrase.map(it => KEYMAP_CODES[it]?.id ?? it).join(), + sortBy: chord.phrase.map((it) => KEYMAP_CODES[it]?.id ?? it).join(), actions: changedChord.actions, phrase: changedChord.phrase, actionsChanged: id !== JSON.stringify(changedChord.actions), - phraseChanged: JSON.stringify(chord.phrase) !== JSON.stringify(changedChord.phrase), + phraseChanged: + JSON.stringify(chord.phrase) !== JSON.stringify(changedChord.phrase), isApplied: false, deleted: changedChord.deleted, - } + }; } else { return { id: chord.actions, - sortBy: chord.phrase.map(it => KEYMAP_CODES[it]?.id ?? it).join(), + sortBy: chord.phrase.map((it) => KEYMAP_CODES[it]?.id ?? it).join(), actions: chord.actions, phrase: chord.phrase, phraseChanged: false, actionsChanged: false, isApplied: true, deleted: false, - } + }; } - }) + }); for (const id of newChords) { - const chord = overlay.chords.get(id)! + const chord = overlay.chords.get(id)!; changedChords.push({ sortBy: "", isApplied: false, @@ -140,8 +151,10 @@ export const chords = derived([overlay, deviceChords], ([overlay, chords]) => { id: JSON.parse(id), phrase: chord.phrase, actions: chord.actions, - }) + }); } - return changedChords.sort(({sortBy: a}, {sortBy: b}) => a.localeCompare(b)) -}) + return changedChords.sort(({ sortBy: a }, { sortBy: b }) => + a.localeCompare(b), + ); +}); diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte index cc9498c6..0c933eda 100644 --- a/src/routes/+error.svelte +++ b/src/routes/+error.svelte @@ -1,5 +1,5 @@

          {$page.status}

          diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index b4fef69b..e435354d 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,8 +1,12 @@ -import {themeBase, themeColor, themeSuccessBase} from "$lib/style/theme.server" -import type {LayoutServerLoad} from "./$types" +import { + themeBase, + themeColor, + themeSuccessBase, +} from "$lib/style/theme.server"; +import type { LayoutServerLoad } from "./$types"; export const load = (async () => ({ themeSuccessBase, themeBase, themeColor, -})) satisfies LayoutServerLoad +})) satisfies LayoutServerLoad; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index d3998871..dd14e93f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,38 +1,43 @@ diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 73f669d9..f47dea4c 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1,14 +1,16 @@ -import type {LayoutLoad} from "./$types" -import {browser} from "$app/environment" -import {charaFileFromUriComponent} from "$lib/share/share-url" +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 prerender = true; +export const trailingSlash = "always"; -export const load = (async ({url, data, fetch}) => { - const importFile = browser && new URLSearchParams(url.search).get("import") +export const load = (async ({ url, data, fetch }) => { + const importFile = browser && new URLSearchParams(url.search).get("import"); return { ...data, - importFile: importFile ? await charaFileFromUriComponent(importFile, fetch) : undefined, - } -}) satisfies LayoutLoad + importFile: importFile + ? await charaFileFromUriComponent(importFile, fetch) + : undefined, + }; +}) satisfies LayoutLoad; diff --git a/src/routes/+page.ts b/src/routes/+page.ts index 2c36f17c..ceb57a7e 100644 --- a/src/routes/+page.ts +++ b/src/routes/+page.ts @@ -1,6 +1,6 @@ -import {redirect} from "@sveltejs/kit" -import type {PageLoad} from "./$types" +import { redirect } from "@sveltejs/kit"; +import type { PageLoad } from "./$types"; export const load = (() => { - throw redirect(302, "/config/") -}) satisfies PageLoad + throw redirect(302, "/config/"); +}) satisfies PageLoad; diff --git a/src/routes/BackupPopup.svelte b/src/routes/BackupPopup.svelte index 46c1b023..e6448586 100644 --- a/src/routes/BackupPopup.svelte +++ b/src/routes/BackupPopup.svelte @@ -1,6 +1,6 @@
          -

          +

          + +

          {$LL.backup.DISCLAIMER()}

          @@ -36,7 +43,8 @@ >download{$LL.backup.DOWNLOAD()} diff --git a/src/routes/BrowserWarning.svelte b/src/routes/BrowserWarning.svelte index 32c01202..09171be3 100644 --- a/src/routes/BrowserWarning.svelte +++ b/src/routes/BrowserWarning.svelte @@ -1,5 +1,5 @@ @@ -12,8 +12,9 @@ >{$LL.browserWarning.INFO_SERIAL_INFIX()}{$LL.browserWarning.INFO_SERIAL_SUFFIX()} {$LL.browserWarning.INFO_BROWSER_PREFIX()} - {$LL.browserWarning.INFO_BROWSER_INFIX()}{$LL.browserWarning.INFO_BROWSER_INFIX()}{$LL.browserWarning.INFO_BROWSER_SUFFIX()}

          diff --git a/src/routes/ConfigTabs.svelte b/src/routes/ConfigTabs.svelte index 82377325..91a87ef3 100644 --- a/src/routes/ConfigTabs.svelte +++ b/src/routes/ConfigTabs.svelte @@ -1,18 +1,34 @@
          {/if} diff --git a/src/routes/EditActions.svelte b/src/routes/EditActions.svelte index b0e3bdb8..bae7a2cb 100644 --- a/src/routes/EditActions.svelte +++ b/src/routes/EditActions.svelte @@ -1,9 +1,16 @@ {#if $changes.length !== 0} save{$LL.saveActions.SAVE()} {/if} diff --git a/src/routes/Footer.svelte b/src/routes/Footer.svelte index c1c568a0..692c9db1 100644 --- a/src/routes/Footer.svelte +++ b/src/routes/Footer.svelte @@ -1,44 +1,47 @@