mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-10 20:12:48 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c93246ee8c
|
|||
|
22905c2b56
|
|||
|
074f1da48d
|
|||
|
e7a52221d2
|
|||
|
f03b4d586b
|
|||
|
4cd9ce536d
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -3,7 +3,7 @@ name: Build
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
8
.github/workflows/publish.yml
vendored
8
.github/workflows/publish.yml
vendored
@@ -1,8 +1,8 @@
|
|||||||
name: 'publish'
|
name: "publish"
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
with:
|
with:
|
||||||
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
|
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
|
||||||
releaseName: 'App v__VERSION__'
|
releaseName: "App v__VERSION__"
|
||||||
releaseBody: 'See the assets to download this version and install.'
|
releaseBody: "See the assets to download this version and install."
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
/src-tauri/target
|
||||||
|
|
||||||
# Ignore files for PNPM, NPM and YARN
|
# Ignore files for PNPM, NPM and YARN
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"useTabs": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"printWidth": 100,
|
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
|
||||||
"pluginSearchDirs": ["."],
|
|
||||||
"overrides": [{"files": "*.svelte", "options": {"parser": "svelte"}}]
|
|
||||||
}
|
|
||||||
6
.prettierrc.cjs
Normal file
6
.prettierrc.cjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
...require("@theaninova/prettier-config"),
|
||||||
|
plugins: ["prettier-plugin-svelte"],
|
||||||
|
pluginSearchDirs: ["."],
|
||||||
|
overrides: [{files: "*.svelte", options: {parser: "svelte"}}],
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://unpkg.com/typesafe-i18n@5.26.0/schema/typesafe-i18n.json",
|
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json",
|
||||||
"baseLocale": "en",
|
"baseLocale": "en",
|
||||||
"adapter": "svelte"
|
"adapter": "svelte"
|
||||||
}
|
}
|
||||||
|
|||||||
37
CONTRIBUTING.md
Normal file
37
CONTRIBUTING.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
## UX Principles
|
||||||
|
|
||||||
|
- **Opinionated.** There should never be two ways to do the same thing.
|
||||||
|
- **Intuitive.** If a feature needs a description to explain it,
|
||||||
|
the feature has failed.
|
||||||
|
- **Simple.** No useless buttons that always need to be pressed.
|
||||||
|
|
||||||
|
## UI Design
|
||||||
|
|
||||||
|
The UI design is based on Material 3.
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Nix
|
||||||
|
|
||||||
|
[Enable flakes](https://nixos.wiki/wiki/Flakes#Enable_flakes), then start the development shell using
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nix develop
|
||||||
|
```
|
||||||
|
|
||||||
|
You may need to run through some additional setup to get Rust running inside IntelliJ.
|
||||||
|
|
||||||
|
### Other platforms
|
||||||
|
|
||||||
|
- NodeJS >=18.16
|
||||||
|
- Python >=3.10
|
||||||
|
- Rust Stable (For Tauri Development)
|
||||||
|
|
||||||
|
I know, python in JS projects is extremely annoying, unfortunately,
|
||||||
|
it seems to be the only platform that offers a functional
|
||||||
|
way to subset variable woff2 fonts with ligatures.
|
||||||
|
|
||||||
|
In other words, either have python as a development dependency or
|
||||||
|
serve a 3.5MB icons font of which 99.5% is completely unused.
|
||||||
25
README.md
25
README.md
@@ -11,31 +11,6 @@ Get the latest desktop release [here](https://github.com/Theaninova/dotio/releas
|
|||||||
I aim to create a new site that offers an easier, visually pleasing
|
I aim to create a new site that offers an easier, visually pleasing
|
||||||
and more complete way to configure and learn CharaChorder devices.
|
and more complete way to configure and learn CharaChorder devices.
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Nix
|
|
||||||
|
|
||||||
[Enable flakes](https://nixos.wiki/wiki/Flakes#Enable_flakes), then start the development shell using
|
|
||||||
|
|
||||||
```shell
|
|
||||||
nix develop
|
|
||||||
```
|
|
||||||
|
|
||||||
You may need to run through some additional setup to get Rust running inside IntelliJ.
|
|
||||||
|
|
||||||
### Other platforms
|
|
||||||
|
|
||||||
- NodeJS >=18.16
|
|
||||||
- Python >=3.10
|
|
||||||
- Rust Stable (For Tauri Development)
|
|
||||||
|
|
||||||
I know, python in JS projects is extremely annoying, unfortunately,
|
|
||||||
it seems to be the only platform that offers a functional
|
|
||||||
way to subset variable woff2 fonts with ligatures.
|
|
||||||
|
|
||||||
In other words, either have python as a development dependency or
|
|
||||||
serve a 3.5MB icons font of which 99.5% is completely unused.
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### SSH Setup
|
### SSH Setup
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ const config: IconsConfig = {
|
|||||||
"translate",
|
"translate",
|
||||||
"play_arrow",
|
"play_arrow",
|
||||||
"extension",
|
"extension",
|
||||||
|
"upload_file",
|
||||||
|
"commit",
|
||||||
|
"bug_report",
|
||||||
|
"delete",
|
||||||
|
"remove_selection",
|
||||||
|
"bolt",
|
||||||
|
"undo",
|
||||||
|
"redo",
|
||||||
],
|
],
|
||||||
codePoints: {
|
codePoints: {
|
||||||
speed: "e9e4",
|
speed: "e9e4",
|
||||||
@@ -70,6 +78,8 @@ const config: IconsConfig = {
|
|||||||
counter_3: "f782",
|
counter_3: "f782",
|
||||||
ios_share: "e6b8",
|
ios_share: "e6b8",
|
||||||
light_mode: "e518",
|
light_mode: "e518",
|
||||||
|
upload_file: "e9fc",
|
||||||
|
no_sound: "e710",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1273
package-lock.json
generated
1273
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
62
package.json
62
package.json
@@ -1,8 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "amacc1ng",
|
"name": "amacc1ng",
|
||||||
"version": "0.5.0",
|
"version": "0.6.5",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Theaninova/amacc1ng.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/Theaninova/amacc1ng",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/Theaninova/amacc1ng/issues"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm-run-all --parallel vite typesafe-i18n",
|
"dev": "npm-run-all --parallel vite typesafe-i18n",
|
||||||
"dev:tauri": "tauri dev",
|
"dev:tauri": "tauri dev",
|
||||||
@@ -16,60 +24,60 @@
|
|||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||||
"minify-icons": "ts-node-esm src/tools/minify-icon-font.ts",
|
"minify-icons": "ts-node-esm src/tools/minify-icon-font.ts",
|
||||||
|
"version": "ts-node-esm src/tools/version.ts && git add src-tauri/Cargo.toml && git add src-tauri/tauri.conf.json",
|
||||||
"lint": "prettier --plugin-search-dir . --check .",
|
"lint": "prettier --plugin-search-dir . --check .",
|
||||||
"format": "prettier --plugin-search-dir . --write .",
|
"format": "prettier --plugin-search-dir . --write .",
|
||||||
"typesafe-i18n": "typesafe-i18n"
|
"typesafe-i18n": "typesafe-i18n"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/autocomplete": "^6.9.0",
|
"@codemirror/autocomplete": "^6.9.0",
|
||||||
"@codemirror/commands": "^6.2.4",
|
"@codemirror/commands": "^6.2.5",
|
||||||
"@codemirror/lang-javascript": "^6.1.9",
|
"@codemirror/lang-javascript": "^6.2.1",
|
||||||
"@codemirror/language": "^6.8.0",
|
"@codemirror/language": "^6.9.0",
|
||||||
"@codemirror/state": "^6.2.1",
|
"@codemirror/state": "^6.2.1",
|
||||||
"@fontsource-variable/material-symbols-rounded": "^5.0.6",
|
"@fontsource-variable/material-symbols-rounded": "^5.0.11",
|
||||||
"@fontsource-variable/noto-sans-mono": "^5.0.7",
|
"@fontsource-variable/noto-sans-mono": "^5.0.12",
|
||||||
"@material/material-color-utilities": "^0.2.7",
|
"@material/material-color-utilities": "^0.2.7",
|
||||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||||
"@sveltejs/adapter-static": "^2.0.3",
|
"@sveltejs/adapter-static": "^2.0.3",
|
||||||
"@sveltejs/kit": "^1.22.4",
|
"@sveltejs/kit": "^1.24.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^2.4.3",
|
"@sveltejs/vite-plugin-svelte": "^2.4.5",
|
||||||
"@tauri-apps/api": "^1.4.0",
|
"@tauri-apps/api": "^1.4.0",
|
||||||
"@tauri-apps/cli": "^1.4.0",
|
"@tauri-apps/cli": "^1.4.0",
|
||||||
"@theaninova/prettier-config": "^1.0.0",
|
"@theaninova/prettier-config": "^1.0.0",
|
||||||
"@types/dom-view-transitions": "^1.0.1",
|
"@types/dom-view-transitions": "^1.0.1",
|
||||||
"@types/flexsearch": "^0.7.3",
|
"@types/flexsearch": "^0.7.3",
|
||||||
"@types/w3c-web-serial": "^1.0.3",
|
"@types/w3c-web-serial": "^1.0.3",
|
||||||
"@vite-pwa/sveltekit": "^0.2.5",
|
"@vite-pwa/sveltekit": "^0.2.7",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.15",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"cypress": "^12.17.3",
|
"cypress": "^13.1.0",
|
||||||
"flexsearch": "^0.7.31",
|
"flexsearch": "^0.7.31",
|
||||||
"fontkit": "^2.0.2",
|
"fontkit": "^2.0.2",
|
||||||
"glob": "^10.3.3",
|
"glob": "^10.3.4",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"prettier": "^3.0.1",
|
"prettier": "^3.0.3",
|
||||||
"prettier-plugin-svelte": "^3.0.3",
|
"prettier-plugin-svelte": "^3.0.3",
|
||||||
"sass": "^1.64.2",
|
"sass": "^1.66.1",
|
||||||
"stylelint": "^15.10.2",
|
"stylelint": "^15.10.3",
|
||||||
"stylelint-config-clean-order": "^5.0.1",
|
"stylelint-config-clean-order": "^5.2.0",
|
||||||
"stylelint-config-html": "^1.1.0",
|
"stylelint-config-html": "^1.1.0",
|
||||||
"stylelint-config-prettier-scss": "^1.0.0",
|
"stylelint-config-prettier-scss": "^1.0.0",
|
||||||
"stylelint-config-recommended-scss": "^12.0.0",
|
"stylelint-config-recommended-scss": "^13.0.0",
|
||||||
"stylelint-config-standard-scss": "^10.0.0",
|
"stylelint-config-standard-scss": "^11.0.0",
|
||||||
"svelte": "^4.1.2",
|
"svelte": "^4.2.0",
|
||||||
"svelte-check": "^3.4.6",
|
"svelte-check": "^3.5.1",
|
||||||
"svelte-preprocess": "^5.0.4",
|
"svelte-preprocess": "^5.0.4",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typesafe-i18n": "^5.26.0",
|
"typesafe-i18n": "^5.26.2",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^4.4.8",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-mkcert": "^1.16.0",
|
"vite-plugin-mkcert": "^1.16.0",
|
||||||
"vite-plugin-pwa": "^0.16.4",
|
"vite-plugin-pwa": "^0.16.5",
|
||||||
"vitest": "^0.34.1"
|
"vitest": "^0.34.4"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module"
|
||||||
"prettier": "@theaninova/prettier-config"
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -85,7 +85,7 @@ checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.5.0"
|
version = "0.6.5"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["Thea Schöbl <dev@theaninova.de>"]
|
authors = ["Thea Schöbl <dev@theaninova.de>"]
|
||||||
license = "AGPL-3"
|
license = "AGPL-3"
|
||||||
|
|||||||
@@ -6,21 +6,14 @@
|
|||||||
"devPath": "http://localhost:5173",
|
"devPath": "http://localhost:5173",
|
||||||
"distDir": "../build"
|
"distDir": "../build"
|
||||||
},
|
},
|
||||||
"package": {
|
"package": { "productName": "amacc1ng", "version": "0.6.5" },
|
||||||
"productName": "amacc1ng",
|
|
||||||
"version": "0.5.0"
|
|
||||||
},
|
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": { "all": false },
|
||||||
"all": false
|
|
||||||
},
|
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"category": "DeveloperTool",
|
"category": "DeveloperTool",
|
||||||
"copyright": "AGPL-3.0-or-later",
|
"copyright": "AGPL-3.0-or-later",
|
||||||
"deb": {
|
"deb": { "depends": [] },
|
||||||
"depends": []
|
|
||||||
},
|
|
||||||
"externalBin": [],
|
"externalBin": [],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
@@ -47,9 +40,7 @@
|
|||||||
"timestampUrl": ""
|
"timestampUrl": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": {
|
"security": { "csp": null },
|
||||||
"csp": null
|
|
||||||
},
|
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|||||||
3
src/env.d.ts
vendored
3
src/env.d.ts
vendored
@@ -12,3 +12,6 @@ interface ImportMetaEnv {
|
|||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv
|
readonly env: ImportMetaEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare const HOMEPAGE_URL: string
|
||||||
|
declare const BUGS_URL: string
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import type {Translation} from "../i18n-types"
|
|||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
TITLE: "amaCC1ng",
|
TITLE: "amaCC1ng",
|
||||||
|
saveActions: {
|
||||||
|
UNDO: "Rückgängig",
|
||||||
|
REDO: "Wiederholen",
|
||||||
|
APPLY: "Anwenden",
|
||||||
|
SAVE: "Änderungen auf das Gerät schreiben",
|
||||||
|
},
|
||||||
backup: {
|
backup: {
|
||||||
TITLE: "Sicherungskopie",
|
TITLE: "Sicherungskopie",
|
||||||
DISCLAIMER:
|
DISCLAIMER:
|
||||||
@@ -9,6 +15,18 @@ const de = {
|
|||||||
DOWNLOAD: "Kopie Speichern",
|
DOWNLOAD: "Kopie Speichern",
|
||||||
RESTORE: "Wiederherstellen",
|
RESTORE: "Wiederherstellen",
|
||||||
},
|
},
|
||||||
|
modal: {
|
||||||
|
CLOSE: "Schließen",
|
||||||
|
},
|
||||||
|
actionSearch: {
|
||||||
|
PLACEHOLDER: "Nach Aktionen suchen",
|
||||||
|
CURRENT_ACTION: "Aktuelle Aktion",
|
||||||
|
DELETE: "Entfernen",
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
URL_COPIED: "Teilbare URL kopiert!",
|
||||||
|
EXTRA_DOWNLOAD: "Als Datei herunterladen",
|
||||||
|
},
|
||||||
profile: {
|
profile: {
|
||||||
TITLE: "Profil",
|
TITLE: "Profil",
|
||||||
LANGUAGE: "Sprache",
|
LANGUAGE: "Sprache",
|
||||||
|
|||||||
@@ -2,12 +2,30 @@ import type {BaseTranslation} from "../i18n-types"
|
|||||||
|
|
||||||
const en = {
|
const en = {
|
||||||
TITLE: "amaCC1ng",
|
TITLE: "amaCC1ng",
|
||||||
|
saveActions: {
|
||||||
|
UNDO: "Undo",
|
||||||
|
REDO: "Redo",
|
||||||
|
APPLY: "Apply",
|
||||||
|
SAVE: "Write changes to your device",
|
||||||
|
},
|
||||||
backup: {
|
backup: {
|
||||||
TITLE: "Local Backup",
|
TITLE: "Local Backup",
|
||||||
DISCLAIMER: "Backups remain on your computer and are never shared with us or uploaded to our servers.",
|
DISCLAIMER: "Backups remain on your computer and are never shared with us or uploaded to our servers.",
|
||||||
DOWNLOAD: "Download Backup",
|
DOWNLOAD: "Download Backup",
|
||||||
RESTORE: "Restore",
|
RESTORE: "Restore",
|
||||||
},
|
},
|
||||||
|
modal: {
|
||||||
|
CLOSE: "Close",
|
||||||
|
},
|
||||||
|
actionSearch: {
|
||||||
|
PLACEHOLDER: "Search for actions",
|
||||||
|
CURRENT_ACTION: "Current action",
|
||||||
|
DELETE: "Remove",
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
URL_COPIED: "Sharable URL copied!",
|
||||||
|
EXTRA_DOWNLOAD: "Download as file",
|
||||||
|
},
|
||||||
profile: {
|
profile: {
|
||||||
TITLE: "Profile",
|
TITLE: "Profile",
|
||||||
LANGUAGE: "Language",
|
LANGUAGE: "Language",
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { FormattersInitializer } from 'typesafe-i18n'
|
import type {FormattersInitializer} from "typesafe-i18n"
|
||||||
import type { Locales, Formatters } from './i18n-types'
|
import type {Locales, Formatters} from "./i18n-types"
|
||||||
|
|
||||||
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
|
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
|
||||||
|
const formatters: Formatters = {
|
||||||
|
// add your formatter functions here
|
||||||
|
}
|
||||||
|
|
||||||
const formatters: Formatters = {
|
return formatters
|
||||||
// add your formatter functions here
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatters
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import type {Action} from "svelte/action"
|
|
||||||
import Index from "flexsearch"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
|
||||||
import tippy from "tippy.js"
|
|
||||||
import ActionAutocomplete from "$lib/components/ActionAutocomplete.svelte"
|
|
||||||
import {browser} from "$app/environment"
|
|
||||||
|
|
||||||
const index = browser ? new Index({tokenize: "full"}) : undefined
|
|
||||||
for (const action of Object.values(KEYMAP_CODES)) {
|
|
||||||
index?.add(
|
|
||||||
action.code,
|
|
||||||
`${action.title || ""} ${action.variant || ""} ${action.category} ${action.id || ""} ${
|
|
||||||
action.description || ""
|
|
||||||
}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const exact = Object.fromEntries(
|
|
||||||
Object.values(KEYMAP_CODES)
|
|
||||||
.filter(it => !!it.id)
|
|
||||||
.map(it => [it.id, it] as const),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const actionAutocomplete: Action<HTMLInputElement> = node => {
|
|
||||||
if (!browser) return
|
|
||||||
|
|
||||||
let completionComponent: ActionAutocomplete
|
|
||||||
const completionDialog = tippy(node, {
|
|
||||||
interactive: true,
|
|
||||||
placement: "bottom-start",
|
|
||||||
hideOnClick: false,
|
|
||||||
theme: "surface-variant search-completion",
|
|
||||||
arrow: false,
|
|
||||||
trigger: "focus",
|
|
||||||
offset: [0, 0],
|
|
||||||
onCreate(instance) {
|
|
||||||
const target = instance.popper.querySelector(".tippy-content")!
|
|
||||||
completionComponent = new ActionAutocomplete({target, props: {width: node.clientWidth}})
|
|
||||||
},
|
|
||||||
onDestroy() {
|
|
||||||
completionComponent.$destroy()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
function input(event: Event) {
|
|
||||||
completionComponent.$set({
|
|
||||||
results: index!.search(node.value),
|
|
||||||
exact: exact[node.value],
|
|
||||||
code: Number(node.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
node.addEventListener("input", input)
|
|
||||||
|
|
||||||
return {
|
|
||||||
destroy() {
|
|
||||||
node.removeEventListener("input", input)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import type {Chord} from "$lib/serial/chord"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
|
||||||
|
|
||||||
interface Language {
|
|
||||||
name: string
|
|
||||||
noLazyMode?: boolean
|
|
||||||
orderedByFrequency?: boolean
|
|
||||||
words: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function calculateChordCoverage(chords: Chord[]) {
|
|
||||||
const language: Language = await fetch("/languages/english.json").then(it => it.json())
|
|
||||||
|
|
||||||
const words = new Set(language.words)
|
|
||||||
for (const chord of chords) {
|
|
||||||
words.delete(chord.phrase.map(it => KEYMAP_CODES[it].id!).join(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
coverage: words.size / language.words.length,
|
|
||||||
missing: [...words.values()],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
src/lib/compat/legacy-layout-converted.sample.json
Normal file
24
src/lib/compat/legacy-layout-converted.sample.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"charaVersion": 1,
|
||||||
|
"type": "layout",
|
||||||
|
"device": "one",
|
||||||
|
"layout": [
|
||||||
|
[
|
||||||
|
309, 304, 312, 303, 306, 290, 282, 301, 266, 285, 289, 270, 281, 272, 262, 288, 277, 298, 307, 264, 287,
|
||||||
|
268, 332, 311, 274, 286, 308, 329, 310, 280, 358, 512, 515, 513, 514, 313, 319, 318, 321, 320, 326, 315,
|
||||||
|
314, 317, 316, 312, 330, 331, 333, 334, 291, 261, 283, 536, 276, 292, 265, 275, 267, 263, 293, 260, 296,
|
||||||
|
544, 279, 294, 271, 299, 269, 273, 295, 284, 297, 302, 278, 357, 516, 519, 517, 518, 327, 336, 338, 335,
|
||||||
|
337, 328, 325, 322, 323, 324
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
270
src/lib/compat/legacy-layout.sample.csv
Normal file
270
src/lib/compat/legacy-layout.sample.csv
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
A1,0,309
|
||||||
|
A1,1,304
|
||||||
|
A1,2,312
|
||||||
|
A1,3,303
|
||||||
|
A1,4,306
|
||||||
|
A1,5,290
|
||||||
|
A1,6,282
|
||||||
|
A1,7,301
|
||||||
|
A1,8,266
|
||||||
|
A1,9,285
|
||||||
|
A1,10,289
|
||||||
|
A1,11,270
|
||||||
|
A1,12,281
|
||||||
|
A1,13,272
|
||||||
|
A1,14,262
|
||||||
|
A1,15,288
|
||||||
|
A1,16,277
|
||||||
|
A1,17,298
|
||||||
|
A1,18,307
|
||||||
|
A1,19,264
|
||||||
|
A1,20,287
|
||||||
|
A1,21,268
|
||||||
|
A1,22,332
|
||||||
|
A1,23,311
|
||||||
|
A1,24,274
|
||||||
|
A1,25,286
|
||||||
|
A1,26,308
|
||||||
|
A1,27,329
|
||||||
|
A1,28,310
|
||||||
|
A1,29,280
|
||||||
|
A1,30,358
|
||||||
|
A1,31,512
|
||||||
|
A1,32,515
|
||||||
|
A1,33,513
|
||||||
|
A1,34,514
|
||||||
|
A1,35,313
|
||||||
|
A1,36,319
|
||||||
|
A1,37,318
|
||||||
|
A1,38,321
|
||||||
|
A1,39,320
|
||||||
|
A1,40,326
|
||||||
|
A1,41,315
|
||||||
|
A1,42,314
|
||||||
|
A1,43,317
|
||||||
|
A1,44,316
|
||||||
|
A1,45,312
|
||||||
|
A1,46,330
|
||||||
|
A1,47,331
|
||||||
|
A1,48,333
|
||||||
|
A1,49,334
|
||||||
|
A1,50,291
|
||||||
|
A1,51,261
|
||||||
|
A1,52,283
|
||||||
|
A1,53,536
|
||||||
|
A1,54,276
|
||||||
|
A1,55,292
|
||||||
|
A1,56,265
|
||||||
|
A1,57,275
|
||||||
|
A1,58,267
|
||||||
|
A1,59,263
|
||||||
|
A1,60,293
|
||||||
|
A1,61,260
|
||||||
|
A1,62,296
|
||||||
|
A1,63,544
|
||||||
|
A1,64,279
|
||||||
|
A1,65,294
|
||||||
|
A1,66,271
|
||||||
|
A1,67,299
|
||||||
|
A1,68,269
|
||||||
|
A1,69,273
|
||||||
|
A1,70,295
|
||||||
|
A1,71,284
|
||||||
|
A1,72,297
|
||||||
|
A1,73,302
|
||||||
|
A1,74,278
|
||||||
|
A1,75,357
|
||||||
|
A1,76,516
|
||||||
|
A1,77,519
|
||||||
|
A1,78,517
|
||||||
|
A1,79,518
|
||||||
|
A1,80,327
|
||||||
|
A1,81,336
|
||||||
|
A1,82,338
|
||||||
|
A1,83,335
|
||||||
|
A1,84,337
|
||||||
|
A1,85,328
|
||||||
|
A1,86,325
|
||||||
|
A1,87,322
|
||||||
|
A1,88,323
|
||||||
|
A1,89,324
|
||||||
|
A2,0,0
|
||||||
|
A2,1,0
|
||||||
|
A2,2,0
|
||||||
|
A2,3,0
|
||||||
|
A2,4,0
|
||||||
|
A2,5,0
|
||||||
|
A2,6,0
|
||||||
|
A2,7,0
|
||||||
|
A2,8,0
|
||||||
|
A2,9,0
|
||||||
|
A2,10,0
|
||||||
|
A2,11,0
|
||||||
|
A2,12,0
|
||||||
|
A2,13,0
|
||||||
|
A2,14,0
|
||||||
|
A2,15,0
|
||||||
|
A2,16,0
|
||||||
|
A2,17,0
|
||||||
|
A2,18,0
|
||||||
|
A2,19,0
|
||||||
|
A2,20,0
|
||||||
|
A2,21,0
|
||||||
|
A2,22,0
|
||||||
|
A2,23,0
|
||||||
|
A2,24,0
|
||||||
|
A2,25,0
|
||||||
|
A2,26,0
|
||||||
|
A2,27,0
|
||||||
|
A2,28,0
|
||||||
|
A2,29,0
|
||||||
|
A2,30,0
|
||||||
|
A2,31,0
|
||||||
|
A2,32,0
|
||||||
|
A2,33,0
|
||||||
|
A2,34,0
|
||||||
|
A2,35,0
|
||||||
|
A2,36,0
|
||||||
|
A2,37,0
|
||||||
|
A2,38,0
|
||||||
|
A2,39,0
|
||||||
|
A2,40,0
|
||||||
|
A2,41,0
|
||||||
|
A2,42,0
|
||||||
|
A2,43,0
|
||||||
|
A2,44,0
|
||||||
|
A2,45,0
|
||||||
|
A2,46,0
|
||||||
|
A2,47,0
|
||||||
|
A2,48,0
|
||||||
|
A2,49,0
|
||||||
|
A2,50,0
|
||||||
|
A2,51,0
|
||||||
|
A2,52,0
|
||||||
|
A2,53,0
|
||||||
|
A2,54,0
|
||||||
|
A2,55,0
|
||||||
|
A2,56,0
|
||||||
|
A2,57,0
|
||||||
|
A2,58,0
|
||||||
|
A2,59,0
|
||||||
|
A2,60,0
|
||||||
|
A2,61,0
|
||||||
|
A2,62,0
|
||||||
|
A2,63,0
|
||||||
|
A2,64,0
|
||||||
|
A2,65,0
|
||||||
|
A2,66,0
|
||||||
|
A2,67,0
|
||||||
|
A2,68,0
|
||||||
|
A2,69,0
|
||||||
|
A2,70,0
|
||||||
|
A2,71,0
|
||||||
|
A2,72,0
|
||||||
|
A2,73,0
|
||||||
|
A2,74,0
|
||||||
|
A2,75,0
|
||||||
|
A2,76,0
|
||||||
|
A2,77,0
|
||||||
|
A2,78,0
|
||||||
|
A2,79,0
|
||||||
|
A2,80,0
|
||||||
|
A2,81,0
|
||||||
|
A2,82,0
|
||||||
|
A2,83,0
|
||||||
|
A2,84,0
|
||||||
|
A2,85,0
|
||||||
|
A2,86,0
|
||||||
|
A2,87,0
|
||||||
|
A2,88,0
|
||||||
|
A2,89,0
|
||||||
|
A3,0,0
|
||||||
|
A3,1,0
|
||||||
|
A3,2,0
|
||||||
|
A3,3,0
|
||||||
|
A3,4,0
|
||||||
|
A3,5,0
|
||||||
|
A3,6,0
|
||||||
|
A3,7,0
|
||||||
|
A3,8,0
|
||||||
|
A3,9,0
|
||||||
|
A3,10,0
|
||||||
|
A3,11,0
|
||||||
|
A3,12,0
|
||||||
|
A3,13,0
|
||||||
|
A3,14,0
|
||||||
|
A3,15,0
|
||||||
|
A3,16,0
|
||||||
|
A3,17,0
|
||||||
|
A3,18,0
|
||||||
|
A3,19,0
|
||||||
|
A3,20,0
|
||||||
|
A3,21,0
|
||||||
|
A3,22,0
|
||||||
|
A3,23,0
|
||||||
|
A3,24,0
|
||||||
|
A3,25,0
|
||||||
|
A3,26,0
|
||||||
|
A3,27,0
|
||||||
|
A3,28,0
|
||||||
|
A3,29,0
|
||||||
|
A3,30,0
|
||||||
|
A3,31,0
|
||||||
|
A3,32,0
|
||||||
|
A3,33,0
|
||||||
|
A3,34,0
|
||||||
|
A3,35,0
|
||||||
|
A3,36,0
|
||||||
|
A3,37,0
|
||||||
|
A3,38,0
|
||||||
|
A3,39,0
|
||||||
|
A3,40,0
|
||||||
|
A3,41,0
|
||||||
|
A3,42,0
|
||||||
|
A3,43,0
|
||||||
|
A3,44,0
|
||||||
|
A3,45,0
|
||||||
|
A3,46,0
|
||||||
|
A3,47,0
|
||||||
|
A3,48,0
|
||||||
|
A3,49,0
|
||||||
|
A3,50,0
|
||||||
|
A3,51,0
|
||||||
|
A3,52,0
|
||||||
|
A3,53,0
|
||||||
|
A3,54,0
|
||||||
|
A3,55,0
|
||||||
|
A3,56,0
|
||||||
|
A3,57,0
|
||||||
|
A3,58,0
|
||||||
|
A3,59,0
|
||||||
|
A3,60,0
|
||||||
|
A3,61,0
|
||||||
|
A3,62,0
|
||||||
|
A3,63,0
|
||||||
|
A3,64,0
|
||||||
|
A3,65,0
|
||||||
|
A3,66,0
|
||||||
|
A3,67,0
|
||||||
|
A3,68,0
|
||||||
|
A3,69,0
|
||||||
|
A3,70,0
|
||||||
|
A3,71,0
|
||||||
|
A3,72,0
|
||||||
|
A3,73,0
|
||||||
|
A3,74,0
|
||||||
|
A3,75,0
|
||||||
|
A3,76,0
|
||||||
|
A3,77,0
|
||||||
|
A3,78,0
|
||||||
|
A3,79,0
|
||||||
|
A3,80,0
|
||||||
|
A3,81,0
|
||||||
|
A3,82,0
|
||||||
|
A3,83,0
|
||||||
|
A3,84,0
|
||||||
|
A3,85,0
|
||||||
|
A3,86,0
|
||||||
|
A3,87,0
|
||||||
|
A3,88,0
|
||||||
|
A3,89,0
|
||||||
|
18
src/lib/compat/legacy-layout.spec.ts
Normal file
18
src/lib/compat/legacy-layout.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import {describe, expect, it} from "vitest"
|
||||||
|
import legacyLayout from "./legacy-layout.sample.csv?raw"
|
||||||
|
import legacyLayoutConverted from "./legacy-layout-converted.sample.json"
|
||||||
|
import {csvLayoutToJson, isCsvLayout} from "./legacy-layout"
|
||||||
|
|
||||||
|
describe("legacy layout", () => {
|
||||||
|
it("should detect a legacy layout", () => {
|
||||||
|
expect(isCsvLayout(legacyLayout)).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not detect chord maps as layouts", () => {
|
||||||
|
expect(isCsvLayout("e + h + t,the")).to.be.false
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should convert legacy layouts", () => {
|
||||||
|
expect(csvLayoutToJson(legacyLayout)).to.deep.equal(legacyLayoutConverted)
|
||||||
|
})
|
||||||
|
})
|
||||||
25
src/lib/compat/legacy-layout.ts
Normal file
25
src/lib/compat/legacy-layout.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type {CharaLayoutFile} from "$lib/share/chara-file"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a legacy CSV-based layout to the modern JSON-based format
|
||||||
|
*/
|
||||||
|
export function csvLayoutToJson(csv: string, device: CharaLayoutFile["device"] = "one"): CharaLayoutFile {
|
||||||
|
const layout: CharaLayoutFile = {
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "layout",
|
||||||
|
device,
|
||||||
|
layout: [[], [], []],
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const layer of csv.split("\n")) {
|
||||||
|
const [layerId, key, action] = layer.substring(1).split(",").map(Number)
|
||||||
|
|
||||||
|
layout.layout[Number(layerId) - 1][Number(key)] = Number(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCsvLayout(csv: string): boolean {
|
||||||
|
return /^(A[123],\d+,\d+\n?)+$/.test(csv)
|
||||||
|
}
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
|
||||||
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
|
||||||
|
|
||||||
export let exact: number | undefined = undefined
|
|
||||||
export let code: number = Number.NaN
|
|
||||||
export let results: number[] = []
|
|
||||||
|
|
||||||
export let width: number
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="list" style="width: {width}px">
|
|
||||||
{#if exact !== undefined}
|
|
||||||
<div class="exact">
|
|
||||||
<i>Exact match</i>
|
|
||||||
<ActionListItem id={exact} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if !exact && code}
|
|
||||||
{#if code >= 2 ** 5 && code < 2 ** 13}
|
|
||||||
<button>USE CODE</button>
|
|
||||||
{:else}
|
|
||||||
<div>Action code is out of range</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{#each results as id (id)}
|
|
||||||
<ActionListItem {id} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.list {
|
|
||||||
--scrollbar-color: var(--md-sys-color-on-surface-variant);
|
|
||||||
|
|
||||||
scrollbar-gutter: stable both-edges;
|
|
||||||
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
max-height: 500px;
|
|
||||||
padding-block: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exact {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
border: 1px solid var(--md-sys-color-primary);
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
> i {
|
|
||||||
padding-inline: 8px;
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
border-radius: 0 0 8px 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
$: key = (typeof id === "number" ? KEYMAP_CODES[id] ?? id : id) as number | KeyInfo
|
$: key = (typeof id === "number" ? KEYMAP_CODES[id] ?? id : id) as number | KeyInfo
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button>
|
<button on:click>
|
||||||
{#if typeof key === "object"}
|
{#if typeof key === "object"}
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<b>
|
<b>
|
||||||
@@ -43,6 +43,13 @@
|
|||||||
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {serialLog, serialPort} from "$lib/serial/connection"
|
import {serialLog, serialPort} from "$lib/serial/connection"
|
||||||
import {slide} from "svelte/transition"
|
import {slide} from "svelte/transition"
|
||||||
|
|
||||||
function submit(event: InputEvent) {
|
function submit(event: Event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
$serialPort.send(value.trim())
|
$serialPort.send(value.trim())
|
||||||
value = ""
|
value = ""
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import LayoutCC1 from "$lib/components/layout/LayoutCC1.svelte"
|
|
||||||
import {chords, highlightActions} from "$lib/serial/connection"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes.js"
|
|
||||||
|
|
||||||
$: content = Array.from({length: 10}).map(() => $chords[Math.floor(Math.random() * $chords.length)])
|
|
||||||
|
|
||||||
let cursor = [0, 0]
|
|
||||||
let input = []
|
|
||||||
|
|
||||||
$: {
|
|
||||||
$highlightActions = content[cursor[0]]?.actions ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
function keypress(event: KeyboardEvent) {
|
|
||||||
cursor++
|
|
||||||
input.push(event.key)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window on:keypress={keypress} />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<section>
|
|
||||||
<!-- <div class="cursor" style="translate: calc({cursor}ch - 50%) -50%" /> -->
|
|
||||||
{#each content as word, i}
|
|
||||||
{#if word}
|
|
||||||
{#each word.phrase as letter, j}
|
|
||||||
<span>{KEYMAP_CODES[letter].id}</span>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<LayoutCC1 />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
font-size: 1.3rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.letter {
|
|
||||||
position: relative;
|
|
||||||
filter: brightness(50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cursor {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 0;
|
|
||||||
translate: -50% -50%;
|
|
||||||
|
|
||||||
width: 2px;
|
|
||||||
height: 1em;
|
|
||||||
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
|
|
||||||
transition: all 250ms ease;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,167 +1,306 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes.js"
|
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||||
import charaActions from "$lib/assets/keymaps/chara-chorder.yml"
|
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
||||||
import mouseActions from "$lib/assets/keymaps/mouse.yml"
|
import Index from "flexsearch"
|
||||||
import keyboardActions from "$lib/assets/keymaps/keyboard.yml"
|
import {createEventDispatcher} from "svelte"
|
||||||
import asciiActions from "$lib/assets/keymaps/ascii.yml"
|
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
||||||
import cp1252Actions from "$lib/assets/keymaps/cp-1252.yml"
|
import LL from "../../../i18n/i18n-svelte"
|
||||||
import FlexSearch from "flexsearch"
|
|
||||||
|
|
||||||
const index = new FlexSearch({tokenize: "full"})
|
export let currentAction: number
|
||||||
|
|
||||||
for (const code in KEYMAP_CODES) {
|
const index = new Index({tokenize: "full"})
|
||||||
const key = KEYMAP_CODES[code]
|
for (const action of Object.values(KEYMAP_CODES)) {
|
||||||
index.add(
|
index?.add(
|
||||||
code,
|
action.code,
|
||||||
`${key.id || key.code} ${key.title || ""} ${key.variant || ""} ${key.description || ""}`.trim(),
|
`${action.title || ""} ${action.variant || ""} ${action.category} ${action.id || ""} ${
|
||||||
|
action.description || ""
|
||||||
|
}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const exactIndex: Record<string, KeyInfo> = Object.fromEntries(
|
||||||
|
Object.values(KEYMAP_CODES)
|
||||||
|
.filter(it => !!it.id)
|
||||||
|
.map(it => [it.id, it] as const),
|
||||||
|
)
|
||||||
|
|
||||||
function search() {
|
function search() {
|
||||||
const query = searchInput.value
|
results = index!.search(searchBox.value)
|
||||||
customValue = query && !Number.isNaN(Number(query)) ? Number(query) : undefined
|
exact = exactIndex[searchBox.value]?.code
|
||||||
results = query ? index.search(searchInput.value) : defaultActions
|
code = Number(searchBox.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
let customValue = undefined
|
function select(id?: number) {
|
||||||
const defaultActions: string[] = [
|
if (id !== undefined) {
|
||||||
charaActions,
|
dispatch("select", id)
|
||||||
mouseActions,
|
}
|
||||||
keyboardActions,
|
}
|
||||||
asciiActions,
|
|
||||||
cp1252Actions,
|
function keyboardNavigation(event: KeyboardEvent) {
|
||||||
].flatMap(it => Object.keys(it.actions))
|
if (event.shiftKey && event.key === "Enter") {
|
||||||
let results: string[] = defaultActions
|
dispatch("select", exact)
|
||||||
let searchInput: HTMLInputElement
|
} else if (event.shiftKey && event.key === "Escape") {
|
||||||
|
dispatch("select", 0)
|
||||||
|
} else if (event.key === "Escape") {
|
||||||
|
dispatch("close")
|
||||||
|
} else if (event.key === "ArrowDown") {
|
||||||
|
const element =
|
||||||
|
resultList.querySelector("li:focus-within")?.nextSibling ?? resultList.querySelector("li:not(.exact)")
|
||||||
|
if (element instanceof HTMLLIElement) {
|
||||||
|
element.querySelector("button")?.focus()
|
||||||
|
}
|
||||||
|
} else if (event.key === "ArrowUp") {
|
||||||
|
const element =
|
||||||
|
resultList.querySelector("li:focus-within")?.previousSibling ??
|
||||||
|
resultList.querySelector("li:not(.exact)")
|
||||||
|
if (element instanceof HTMLLIElement) {
|
||||||
|
element.querySelector("button")?.focus()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
searchBox.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
let results: number[] = []
|
||||||
|
let exact: number | undefined = undefined
|
||||||
|
let code: number = Number.NaN
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let searchBox: HTMLInputElement
|
||||||
|
let resultList: HTMLUListElement
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<svelte:window on:keydown={keyboardNavigation} />
|
||||||
<input type="search" on:input={search} placeholder="Search Actions" bind:this={searchInput} />
|
|
||||||
|
|
||||||
<div class="results">
|
<dialog open on:click|self={() => dispatch("close")}>
|
||||||
{#if customValue !== undefined}
|
<div class="content">
|
||||||
<button class="custom">
|
<div class="search-row">
|
||||||
Custom ActionID
|
<input
|
||||||
<span class="key">0x{customValue.toString(16).toUpperCase()}</span>
|
type="search"
|
||||||
</button>
|
bind:this={searchBox}
|
||||||
{/if}
|
autofocus
|
||||||
|
on:input={search}
|
||||||
{#each results as id}
|
on:keypress={event => {
|
||||||
{@const key = KEYMAP_CODES[id]}
|
if (event.key === "Enter") {
|
||||||
<button title={key.description}>
|
select(exact)
|
||||||
<div class="title">
|
}
|
||||||
<b>
|
}}
|
||||||
{key.title || ""}
|
placeholder={$LL.actionSearch.PLACEHOLDER()}
|
||||||
{#if key.variant === "left"}
|
/>
|
||||||
(Left)
|
<button on:click={() => select(0)}
|
||||||
{:else if key.variant === "right"}
|
><div><span class="icon key-hint">shift</span>+<span class="key-hint">ESC</span></div>
|
||||||
(Right)
|
{$LL.actionSearch.DELETE()}</button
|
||||||
{/if}
|
>
|
||||||
</b>
|
<button title={$LL.modal.CLOSE()} class="icon" on:click={() => dispatch("close")}>close</button>
|
||||||
{#if key.description}
|
</div>
|
||||||
<i>{key.description}</i>
|
<aside>
|
||||||
{/if}
|
<h3>{$LL.actionSearch.CURRENT_ACTION()}</h3>
|
||||||
</div>
|
<ActionListItem id={currentAction} />
|
||||||
<span class:icon={!!key.icon} class="key">{key.icon || key.id || key.code}</span>
|
</aside>
|
||||||
</button>
|
<ul bind:this={resultList}>
|
||||||
{/each}
|
{#if exact !== undefined}
|
||||||
|
<li class="exact">
|
||||||
|
<i
|
||||||
|
>Exact match <span class="icon key-hint">shift</span>+<span class="icon key-hint"
|
||||||
|
>keyboard_return</span
|
||||||
|
></i
|
||||||
|
>
|
||||||
|
<ActionListItem id={exact} on:click={() => select(exact)} />
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if !exact && code}
|
||||||
|
{#if code >= 2 ** 5 && code < 2 ** 13}
|
||||||
|
<li><button on:click={() => select(code)}>USE CODE</button></li>
|
||||||
|
{:else}
|
||||||
|
<li>Action code is out of range</li>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#each results as id (id)}
|
||||||
|
<li><ActionListItem {id} on:click={() => select(id)} /></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</dialog>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
section {
|
dialog {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
width: calc(min(100vw - 10px, 512px));
|
|
||||||
height: calc(min(90vh, 600px));
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="search"] {
|
|
||||||
width: 100%;
|
|
||||||
height: 48px;
|
|
||||||
padding-inline: 16px;
|
|
||||||
|
|
||||||
font-family: "Noto Sans Mono", monospace;
|
|
||||||
font-size: 18px;
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
border: none;
|
|
||||||
border-radius: 24px;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: inherit;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "plus";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.key {
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
min-width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
padding: 4px;
|
|
||||||
|
|
||||||
font-size: 18px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
border: 1px solid var(--md-sys-color-outline);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
text-align: start;
|
|
||||||
|
|
||||||
> b {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
font-family: "Noto Sans Mono", monospace;
|
background: rgba(0 0 0 / 60%);
|
||||||
font-size: 14px;
|
|
||||||
color: inherit;
|
|
||||||
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom {
|
aside {
|
||||||
padding: 8px;
|
pointer-events: none;
|
||||||
padding-inline-start: 16px;
|
|
||||||
|
margin: 8px;
|
||||||
|
|
||||||
|
opacity: 0.4;
|
||||||
border: 1px dashed var(--md-sys-color-outline);
|
border: 1px dashed var(--md-sys-color-outline);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
width: fit-content;
|
||||||
|
margin-block-start: -13px;
|
||||||
|
margin-block-end: 0;
|
||||||
|
margin-inline-start: 16px;
|
||||||
|
padding-inline: 8px;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-inline: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
margin-inline: 16px;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
color: currentcolor;
|
||||||
|
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 100%;
|
||||||
|
|
||||||
|
&:not(.icon) {
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: calc(min(30cm, 90%));
|
||||||
|
height: calc(min(100% - 128px, 90%));
|
||||||
|
|
||||||
|
color: var(--md-sys-color-on-background);
|
||||||
|
|
||||||
|
background: var(--md-sys-color-background);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.results {
|
input[type="search"] {
|
||||||
overflow-y: scroll;
|
width: 100%;
|
||||||
|
height: 64px;
|
||||||
|
margin-block-end: 8px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 16px;
|
||||||
|
color: currentcolor;
|
||||||
|
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--md-sys-color-primary-container);
|
||||||
|
|
||||||
|
transition: all 250ms ease;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-bottom: 1px solid var(--md-sys-color-primary);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
--scrollbar-color: var(--md-sys-color-surface-variant);
|
||||||
|
|
||||||
|
scrollbar-gutter: both-edges stable;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
padding-inline: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exact {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
height: 100%;
|
width: 100%;
|
||||||
|
margin-block-start: 8px;
|
||||||
|
|
||||||
|
border: 1px solid var(--md-sys-color-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
> i {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
padding-inline: 6px;
|
||||||
|
|
||||||
|
color: var(--md-sys-color-on-primary);
|
||||||
|
|
||||||
|
background: var(--md-sys-color-primary);
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-hint {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: 20px;
|
||||||
|
margin-block: 6px;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: currentcolor;
|
||||||
|
|
||||||
|
border: 1px solid currentcolor;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.icon {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,11 +13,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<select bind:value={device}>
|
|
||||||
<option value="ONE">CC1</option>
|
|
||||||
<option value="LITE">Lite</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{#each layers as [title, icon, value]}
|
{#each layers as [title, icon, value]}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -7,14 +7,14 @@
|
|||||||
<div class="col layout" style="gap: 0">
|
<div class="col layout" style="gap: 0">
|
||||||
<div class="row" style="gap: 156px">
|
<div class="row" style="gap: 156px">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<RingInput {activeLayer} keys={{d: 30, e: 31, n: 32, w: 33, s: 34}} type="tertiary" />
|
<RingInput {activeLayer} keys={{d: 30, e: 31, n: 32, w: 33, s: 34}} />
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<RingInput {activeLayer} keys={{d: 25, e: 26, n: 27, w: 28, s: 29}} />
|
<RingInput {activeLayer} keys={{d: 25, e: 26, n: 27, w: 28, s: 29}} />
|
||||||
<RingInput {activeLayer} keys={{d: 40, e: 41, n: 42, w: 43, s: 44}} type="secondary" />
|
<RingInput {activeLayer} keys={{d: 40, e: 41, n: 42, w: 43, s: 44}} />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<RingInput {activeLayer} keys={{d: 20, e: 21, n: 22, w: 23, s: 24}} />
|
<RingInput {activeLayer} keys={{d: 20, e: 21, n: 22, w: 23, s: 24}} />
|
||||||
<RingInput {activeLayer} keys={{d: 35, e: 36, n: 37, w: 38, s: 39}} type="secondary" />
|
<RingInput {activeLayer} keys={{d: 35, e: 36, n: 37, w: 38, s: 39}} />
|
||||||
</div>
|
</div>
|
||||||
<RingInput {activeLayer} keys={{d: 15, e: 16, n: 17, w: 18, s: 19}} />
|
<RingInput {activeLayer} keys={{d: 15, e: 16, n: 17, w: 18, s: 19}} />
|
||||||
</div>
|
</div>
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
<RingInput {activeLayer} keys={{d: 50, w: 51, n: 52, e: 53, s: 54}} />
|
<RingInput {activeLayer} keys={{d: 50, w: 51, n: 52, e: 53, s: 54}} />
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="gap: 320px; margin-top: -12px">
|
<div class="row" style="gap: 320px; margin-top: -12px">
|
||||||
<RingInput {activeLayer} keys={{d: 0, e: 1, n: 2, w: 3, s: 4}} type="secondary" />
|
<RingInput {activeLayer} keys={{d: 0, e: 1, n: 2, w: 3, s: 4}} />
|
||||||
<RingInput {activeLayer} keys={{d: 45, w: 46, n: 47, e: 48, s: 49}} type="secondary" />
|
<RingInput {activeLayer} keys={{d: 45, w: 46, n: 47, e: 48, s: 49}} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {highlightActions, layout} from "$lib/serial/connection"
|
import {changes, highlightActions, layout} from "$lib/serial/connection"
|
||||||
|
import type {Change} from "$lib/serial/connection"
|
||||||
import type {CharaLayout} from "$lib/serialization/layout"
|
import type {CharaLayout} from "$lib/serialization/layout"
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
||||||
@@ -7,9 +8,6 @@
|
|||||||
|
|
||||||
export let activeLayer = 0
|
export let activeLayer = 0
|
||||||
export let keys: Record<"d" | "s" | "n" | "w" | "e", number>
|
export let keys: Record<"d" | "s" | "n" | "w" | "e", number>
|
||||||
export let type: "primary" | "secondary" | "tertiary" = "primary"
|
|
||||||
|
|
||||||
const layerNames = ["Primary Layer", "Number Layer", "Function Layer"]
|
|
||||||
|
|
||||||
const virtualLayerMap = [1, 0, 2]
|
const virtualLayerMap = [1, 0, 2]
|
||||||
const characterOffset = 8
|
const characterOffset = 8
|
||||||
@@ -20,26 +18,32 @@
|
|||||||
return 25 * quadrant + layerOffsetIndex * layerOffset
|
return 25 * quadrant + layerOffsetIndex * layerOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActions(id: number, layout: CharaLayout): KeyInfo[] {
|
function getActions(id: number, layout: CharaLayout, changes: Change[]): [KeyInfo, KeyInfo | undefined][] {
|
||||||
return Array.from({length: 3}).map((_, i) => {
|
return Array.from({length: 3}).map((_, i) => {
|
||||||
const actionId = layout?.[i][id]
|
const actionId = layout?.[i][id]
|
||||||
return KEYMAP_CODES[actionId]
|
const changedId = changes.findLast(it => it?.layout?.[i]?.[id] !== undefined)?.layout![i]![id]
|
||||||
|
if (changedId !== undefined) {
|
||||||
|
return [KEYMAP_CODES[changedId], KEYMAP_CODES[actionId]]
|
||||||
|
} else {
|
||||||
|
return [KEYMAP_CODES[actionId], undefined]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="radial {type}">
|
<div class="radial">
|
||||||
{#each [keys.n, keys.e, keys.s, keys.w, keys.d] as id, quadrant}
|
{#each [keys.n, keys.e, keys.s, keys.w, keys.d] as id, quadrant}
|
||||||
{@const actions = getActions(id, $layout)}
|
{@const actions = getActions(id, $layout, $changes)}
|
||||||
<button
|
<button
|
||||||
use:editableLayout={{id, quadrant}}
|
use:editableLayout={{activeLayer, id}}
|
||||||
class:active={actions.some(it => it && $highlightActions?.includes(it.code))}
|
class:active={actions.some(([it]) => it && $highlightActions?.includes(it.code))}
|
||||||
>
|
>
|
||||||
{#each actions as keyInfo, layer}
|
{#each actions as [keyInfo, old], layer}
|
||||||
{#if keyInfo}
|
{#if keyInfo}
|
||||||
<span
|
<span
|
||||||
class:active={virtualLayerMap[activeLayer] === virtualLayerMap[layer]}
|
class:active={virtualLayerMap[activeLayer] === virtualLayerMap[layer]}
|
||||||
class:icon={!!keyInfo.icon}
|
class:icon={!!keyInfo.icon}
|
||||||
|
class:changed={!!old}
|
||||||
style="offset-distance: {offsetDistance(quadrant, layer, activeLayer)}%"
|
style="offset-distance: {offsetDistance(quadrant, layer, activeLayer)}%"
|
||||||
>{keyInfo.icon || keyInfo.id || keyInfo.code}</span
|
>{keyInfo.icon || keyInfo.id || keyInfo.code}</span
|
||||||
>
|
>
|
||||||
@@ -95,7 +99,9 @@
|
|||||||
|
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
|
|
||||||
transition: scale $transition-time ease, opacity $transition-time ease,
|
transition:
|
||||||
|
scale $transition-time ease,
|
||||||
|
opacity $transition-time ease,
|
||||||
offset-distance $transition-time ease;
|
offset-distance $transition-time ease;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
@@ -107,6 +113,11 @@
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.changed {
|
||||||
|
color: var(--md-sys-color-on-secondary-container);
|
||||||
|
background: var(--md-sys-color-secondary-container);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -170,12 +181,4 @@
|
|||||||
mask-image: none;
|
mask-image: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary > button {
|
|
||||||
filter: brightness(80%) contrast(120%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tertiary > button {
|
|
||||||
filter: brightness(80%) contrast(110%);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
import tippy from "tippy.js"
|
|
||||||
import InputEdit from "$lib/components/layout/InputEdit.svelte"
|
|
||||||
import type {Action} from "svelte/action"
|
import type {Action} from "svelte/action"
|
||||||
|
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
||||||
|
import {changes, layout} from "$lib/serial/connection"
|
||||||
|
import {get} from "svelte/store"
|
||||||
|
|
||||||
export const editableLayout: Action<HTMLButtonElement, {id: number; quadrant: number}> = (
|
export const editableLayout: Action<HTMLButtonElement, {activeLayer: number; id: number}> = (
|
||||||
node,
|
node,
|
||||||
{id, quadrant},
|
{id, activeLayer},
|
||||||
) => {
|
) => {
|
||||||
let component: InputEdit | undefined
|
let component: ActionSelector | undefined
|
||||||
const edit = tippy(node, {
|
function present() {
|
||||||
interactive: true,
|
component?.$destroy()
|
||||||
appendTo: document.body,
|
component = new ActionSelector({
|
||||||
trigger: "click",
|
target: document.body,
|
||||||
placement: (["top", "right", "bottom", "left"] as const)[quadrant],
|
props: {currentAction: get(layout)[activeLayer][id]},
|
||||||
onShow(instance) {
|
})
|
||||||
component ??= new InputEdit({
|
component.$on("close", () => {
|
||||||
target: instance.popper.querySelector(".tippy-content")!,
|
component!.$destroy()
|
||||||
props: {id},
|
})
|
||||||
|
component.$on("select", ({detail}) => {
|
||||||
|
changes.update(changes => {
|
||||||
|
changes.push({layout: {[activeLayer]: {[id]: detail}}})
|
||||||
|
return changes
|
||||||
})
|
})
|
||||||
},
|
component!.$destroy()
|
||||||
onHidden() {
|
})
|
||||||
component?.$destroy()
|
}
|
||||||
component = undefined
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
|
node.addEventListener("click", present)
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
edit.destroy()
|
node.removeEventListener("click", present)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
font-family: "Material Symbols Rounded";
|
font-family: "Material Symbols Rounded";
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-feature-settings: "liga";
|
font-feature-settings: "liga";
|
||||||
font-variation-settings: "FILL" var(--icon-fill, 0), "wght" var(--icon-weigth, 400),
|
font-variation-settings:
|
||||||
|
"FILL" var(--icon-fill, 0),
|
||||||
|
"wght" var(--icon-weigth, 400),
|
||||||
"GRAD" var(--icon-grade, 0);
|
"GRAD" var(--icon-grade, 0);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ export const layout = persistentWritable<CharaLayout>(
|
|||||||
() => get(userPreferences).backup,
|
() => get(userPreferences).backup,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export interface Change {
|
||||||
|
layout?: Record<number, Record<number, number>>
|
||||||
|
chords?: never
|
||||||
|
settings?: Record<number, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changes = persistentWritable<Change[]>("changes", [])
|
||||||
|
|
||||||
export const settings = writable({})
|
export const settings = writable({})
|
||||||
|
|
||||||
export const unsavedChanges = writable(new Map<number, number>())
|
export const unsavedChanges = writable(new Map<number, number>())
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export async function toBase64(blob: Blob): Promise<string> {
|
|||||||
.replace(/^data:application\/octet-stream;base64,/, "")
|
.replace(/^data:application\/octet-stream;base64,/, "")
|
||||||
.replaceAll("+", ".")
|
.replaceAll("+", ".")
|
||||||
.replaceAll("/", "_")
|
.replaceAll("/", "_")
|
||||||
.replaceAll("=", "-")}-`,
|
.replaceAll("=", "-")}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(blob)
|
reader.readAsDataURL(blob)
|
||||||
@@ -23,7 +23,6 @@ export async function toBase64(blob: Blob): Promise<string> {
|
|||||||
export async function fromBase64(base64: string): Promise<Blob> {
|
export async function fromBase64(base64: string): Promise<Blob> {
|
||||||
return fetch(
|
return fetch(
|
||||||
`data:application/octet-stream;base64,${base64
|
`data:application/octet-stream;base64,${base64
|
||||||
.replace(/-$/, "")
|
|
||||||
.replaceAll(".", "+")
|
.replaceAll(".", "+")
|
||||||
.replaceAll("_", "/")
|
.replaceAll("_", "/")
|
||||||
.replaceAll("-", "=")}`,
|
.replaceAll("-", "=")}`,
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import {compressActions, decompressActions} from "./actions"
|
import {compressActions, decompressActions} from "./actions"
|
||||||
import {fromBase64, toBase64} from "$lib/serialization/base64"
|
import {fromBase64, toBase64} from "$lib/serialization/base64"
|
||||||
|
|
||||||
|
export interface NewCharaLayout {
|
||||||
|
charaLayoutVersion: 1
|
||||||
|
device: "one" | "lite" | string
|
||||||
|
/**
|
||||||
|
* Layers A1-A3, with numeric action codes on each
|
||||||
|
*/
|
||||||
|
layers: [number[], number[], number[]]
|
||||||
|
}
|
||||||
|
|
||||||
export type CharaLayout = [number[], number[], number[]]
|
export type CharaLayout = [number[], number[], number[]]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
52
src/lib/share/action-array.ts
Normal file
52
src/lib/share/action-array.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import {compressActions, decompressActions} from "$lib/serialization/actions"
|
||||||
|
import {CHARA_FILE_TYPES} from "$lib/share/share-url"
|
||||||
|
|
||||||
|
export type ActionArray = number[] | ActionArray[]
|
||||||
|
export function serializeActionArray(array: ActionArray): Uint8Array {
|
||||||
|
let out = new Uint8Array(5)
|
||||||
|
const writer = new DataView(out.buffer)
|
||||||
|
writer.setUint32(0, array.length)
|
||||||
|
|
||||||
|
if (array.length === 0) {
|
||||||
|
return out
|
||||||
|
} else if (typeof array[0] === "number") {
|
||||||
|
writer.setUint8(4, CHARA_FILE_TYPES.indexOf("number"))
|
||||||
|
return concatUint8Arrays(out, compressActions(array as number[]))
|
||||||
|
} else if (Array.isArray(array[0])) {
|
||||||
|
writer.setUint8(4, CHARA_FILE_TYPES.indexOf("array"))
|
||||||
|
return concatUint8Arrays(out, ...(array as ActionArray[]).map(serializeActionArray))
|
||||||
|
} else {
|
||||||
|
throw new Error("Not implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deserializeActionArray(raw: Uint8Array): ActionArray {
|
||||||
|
const reader = new DataView(raw.buffer)
|
||||||
|
const length = reader.getUint32(0)
|
||||||
|
const type = CHARA_FILE_TYPES[reader.getUint8(4)]
|
||||||
|
|
||||||
|
if (type === "number") {
|
||||||
|
return decompressActions(raw.slice(5, 5 + length))
|
||||||
|
} else if (type === "array") {
|
||||||
|
const innerLength = reader.getUint32(5)
|
||||||
|
const out = []
|
||||||
|
let cursor = 5
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
out.push(deserializeActionArray(raw.slice(cursor, cursor + innerLength)))
|
||||||
|
cursor += innerLength
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
} else {
|
||||||
|
throw new Error("Not implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array {
|
||||||
|
const out = new Uint8Array(arrays.reduce((a, b) => a + b.length, 0))
|
||||||
|
let offset = 0
|
||||||
|
for (const array of arrays) {
|
||||||
|
out.set(array, offset)
|
||||||
|
offset += array.length
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
15
src/lib/share/chara-file.ts
Normal file
15
src/lib/share/chara-file.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export interface CharaFile<T extends string> {
|
||||||
|
charaVersion: 1
|
||||||
|
type: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharaLayoutFile extends CharaFile<"layout"> {
|
||||||
|
device: "one" | "lite" | string
|
||||||
|
layout: [number[], number[], number[]]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharaChordFile extends CharaFile<"chords"> {
|
||||||
|
chords: [number[], number[]]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CharaFiles = CharaLayoutFile | CharaChordFile
|
||||||
61
src/lib/share/share-url.ts
Normal file
61
src/lib/share/share-url.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type {CharaFile, CharaFiles} from "$lib/share/chara-file"
|
||||||
|
import type {ActionArray} from "$lib/share/action-array"
|
||||||
|
import {deserializeActionArray, serializeActionArray} from "$lib/share/action-array"
|
||||||
|
import {fromBase64, toBase64} from "$lib/serialization/base64"
|
||||||
|
|
||||||
|
type CharaLayoutOrder = {
|
||||||
|
[K in CharaFiles["type"]]: Array<
|
||||||
|
[Exclude<keyof Extract<CharaFiles, {type: K}>, keyof CharaFile<any>>, (typeof CHARA_FILE_TYPES)[number]]
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys: CharaLayoutOrder = {
|
||||||
|
layout: [
|
||||||
|
["layout", "array"],
|
||||||
|
["device", "string"],
|
||||||
|
],
|
||||||
|
chords: [["chords", "array"]],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CHARA_FILE_TYPES = ["unknown", "number", "string", "array"] as const
|
||||||
|
|
||||||
|
const sep = "\n"
|
||||||
|
|
||||||
|
export async function charaFileToUriComponent<T extends CharaFiles>(file: T): Promise<string> {
|
||||||
|
let url = `${file.type}${sep}${file.charaVersion}`
|
||||||
|
|
||||||
|
for (const [key, type] of keys[file.type]) {
|
||||||
|
const value = file[key as keyof T]
|
||||||
|
url += sep
|
||||||
|
if (type === "string") {
|
||||||
|
url += value as string
|
||||||
|
} else if (type === "array") {
|
||||||
|
const stream = new Blob([serializeActionArray(value as ActionArray)])
|
||||||
|
.stream()
|
||||||
|
.pipeThrough(new CompressionStream("deflate"))
|
||||||
|
url += await toBase64(await new Response(stream).blob())
|
||||||
|
} else {
|
||||||
|
throw new Error("Not implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function charaFileFromUriComponent<T extends CharaFiles>(uriComponent: string): Promise<T> {
|
||||||
|
const [fileType, version, ...values] = uriComponent.split(sep)
|
||||||
|
const file: any = {type: fileType, version: Number(version)}
|
||||||
|
|
||||||
|
for (const [key, type] of keys[fileType as keyof typeof keys]) {
|
||||||
|
const value = values.pop()!
|
||||||
|
if (type === "string") {
|
||||||
|
file[key] = value
|
||||||
|
} else if (type === "array") {
|
||||||
|
const stream = (await fromBase64(value)).stream().pipeThrough(new DecompressionStream("deflate"))
|
||||||
|
const actions = new Uint8Array(await new Response(stream).arrayBuffer())
|
||||||
|
file[key] = deserializeActionArray(actions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
import {loadLocale} from "../i18n/i18n-util.sync"
|
import {loadLocale} from "../i18n/i18n-util.sync"
|
||||||
import {detectLocale} from "../i18n/i18n-util"
|
import {detectLocale} from "../i18n/i18n-util"
|
||||||
import type {Locales} from "../i18n/i18n-types"
|
import type {Locales} from "../i18n/i18n-types"
|
||||||
|
import Footer from "./Footer.svelte"
|
||||||
|
|
||||||
const locale = ((browser && localStorage.getItem("locale")) as Locales) || detectLocale()
|
const locale = ((browser && localStorage.getItem("locale")) as Locales) || detectLocale()
|
||||||
loadLocale(locale)
|
loadLocale(locale)
|
||||||
@@ -68,6 +69,8 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
|
||||||
{#if import.meta.env.TAURI_FAMILY === undefined && browser && !("serial" in navigator)}
|
{#if import.meta.env.TAURI_FAMILY === undefined && browser && !("serial" in navigator)}
|
||||||
<BrowserWarning />
|
<BrowserWarning />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -57,17 +57,6 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
|
|
||||||
background: var(--md-sys-color-on-error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog::backdrop {
|
dialog::backdrop {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import LL from "../../i18n/i18n-svelte"
|
import LL from "../i18n/i18n-svelte"
|
||||||
|
|
||||||
$: paths = [
|
$: paths = [
|
||||||
{href: "/config/chords/", title: $LL.configure.chords.TITLE(), icon: "piano"},
|
{href: "/config/chords/", title: $LL.configure.chords.TITLE(), icon: "piano"},
|
||||||
85
src/routes/EditActions.svelte
Normal file
85
src/routes/EditActions.svelte
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import LL from "../i18n/i18n-svelte"
|
||||||
|
import {changes} from "$lib/serial/connection"
|
||||||
|
import type {Change} from "$lib/serial/connection"
|
||||||
|
import {fly} from "svelte/transition"
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
redoQueue = [$changes.pop()!, ...redoQueue]
|
||||||
|
changes.update(it => it)
|
||||||
|
}
|
||||||
|
|
||||||
|
function redo() {
|
||||||
|
const [change, ...queue] = redoQueue
|
||||||
|
changes.update(it => {
|
||||||
|
it.push(change)
|
||||||
|
return it
|
||||||
|
})
|
||||||
|
redoQueue = queue
|
||||||
|
}
|
||||||
|
let redoQueue: Change[] = []
|
||||||
|
|
||||||
|
function apply() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button title={$LL.saveActions.UNDO()} class="icon" disabled={$changes.length === 0} on:click={undo}
|
||||||
|
>undo</button
|
||||||
|
>
|
||||||
|
<button title={$LL.saveActions.REDO()} class="icon" disabled={redoQueue.length === 0} on:click={redo}
|
||||||
|
>redo</button
|
||||||
|
>
|
||||||
|
<div class="separator" />
|
||||||
|
<button title={$LL.saveActions.SAVE()} class="icon">save</button>
|
||||||
|
{#if $changes.length !== 0}
|
||||||
|
<button class="click-me" transition:fly={{x: 8}}
|
||||||
|
><span class="icon">bolt</span>{$LL.saveActions.APPLY()}</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
color: currentcolor;
|
||||||
|
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
transition: all 250ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-me {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin-inline: 8px;
|
||||||
|
padding-block: 2px;
|
||||||
|
padding-inline-start: 4px;
|
||||||
|
padding-inline-end: 8px;
|
||||||
|
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--md-sys-color-primary);
|
||||||
|
|
||||||
|
border: 2px solid var(--md-sys-color-primary);
|
||||||
|
border-radius: 18px;
|
||||||
|
outline: 2px dashed var(--md-sys-color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
width: 1px;
|
||||||
|
height: 24px;
|
||||||
|
background: var(--md-sys-color-outline-variant);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
44
src/routes/Footer.svelte
Normal file
44
src/routes/Footer.svelte
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script>
|
||||||
|
import {version} from "$app/environment"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href={HOMEPAGE_URL} rel="noreferrer" target="_blank"><span class="icon">commit</span> v{version}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={BUGS_URL} rel="noreferrer" target="_blank"
|
||||||
|
><span class="icon">bug_report</span> File an issue</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
font-size: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {serialPort, syncStatus, unsavedChanges} from "$lib/serial/connection"
|
import {serialPort, syncStatus, unsavedChanges} from "$lib/serial/connection"
|
||||||
import {page} from "$app/stores"
|
|
||||||
import {slide, fly} from "svelte/transition"
|
import {slide, fly} from "svelte/transition"
|
||||||
import {canShare, triggerShare} from "$lib/share"
|
import {canShare, triggerShare} from "$lib/share"
|
||||||
import {popup} from "$lib/popup"
|
import {popup} from "$lib/popup"
|
||||||
@@ -11,17 +10,8 @@
|
|||||||
import {userPreferences} from "$lib/preferences"
|
import {userPreferences} from "$lib/preferences"
|
||||||
import LL from "../i18n/i18n-svelte"
|
import LL from "../i18n/i18n-svelte"
|
||||||
import Profile from "./Profile.svelte"
|
import Profile from "./Profile.svelte"
|
||||||
|
import ConfigTabs from "./ConfigTabs.svelte"
|
||||||
const training = [
|
import EditActions from "./EditActions.svelte"
|
||||||
{slug: "cpm", title: "CPM - Characters Per Minute", icon: "music_note"},
|
|
||||||
{slug: "chords", title: "ChM - Chords Mastered", icon: "piano"},
|
|
||||||
{slug: "avg-wpm", title: "aWPM - Average Words Per Minute", icon: "avg_pace"},
|
|
||||||
{slug: "sentences", title: "StM - Sentences Mastered", icon: "lyrics"},
|
|
||||||
{slug: "top-wpm", title: "tWPM - Top Words Per Minute", icon: "speed"},
|
|
||||||
{slug: "cm", title: "CM - Concepts Mastered", icon: "cognition"},
|
|
||||||
]
|
|
||||||
|
|
||||||
let placeboProgress = false
|
|
||||||
|
|
||||||
async function flashChanges() {
|
async function flashChanges() {
|
||||||
$syncStatus = "uploading"
|
$syncStatus = "uploading"
|
||||||
@@ -49,34 +39,32 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/" class="title">{$LL.TITLE()}</a>
|
<div class="actions">
|
||||||
|
<EditActions />
|
||||||
<div class="steps">
|
|
||||||
{#each training as {slug, title, icon}}
|
|
||||||
<a
|
|
||||||
href="/train/{slug}/"
|
|
||||||
{title}
|
|
||||||
class="icon train {slug}"
|
|
||||||
class:active={$page.url.pathname === `/train/${slug}/`}>{icon}</a
|
|
||||||
>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConfigTabs />
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{#if $canShare}
|
{#if $canShare}
|
||||||
<button transition:fly={{x: -8}} class="icon" on:click={triggerShare}>share</button>
|
<button transition:fly={{x: -8}} class="icon" on:click={triggerShare}>share</button>
|
||||||
<div transition:slide class="separator"/>
|
<div transition:slide class="separator" />
|
||||||
{/if}
|
{/if}
|
||||||
{#if import.meta.env.TAURI_FAMILY === undefined}
|
{#if import.meta.env.TAURI_FAMILY === undefined}
|
||||||
{#await import("$lib/components/PwaStatus.svelte") then {default: PwaStatus}}
|
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
||||||
<PwaStatus/>
|
<PwaStatus />
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
{#if $unsavedChanges.size > 0}
|
{#if $unsavedChanges.size > 0}
|
||||||
<button disabled={$syncStatus === 'uploading'} on:click={flashChanges} transition:fly={{x: -8}}
|
<button
|
||||||
title={$LL.deviceManager.APPLY_SETTINGS()} class="icon">save
|
disabled={$syncStatus === "uploading"}
|
||||||
|
on:click={flashChanges}
|
||||||
|
transition:fly={{x: -8}}
|
||||||
|
title={$LL.deviceManager.APPLY_SETTINGS()}
|
||||||
|
class="icon"
|
||||||
|
>save
|
||||||
</button>
|
</button>
|
||||||
<div transition:slide class="separator"/>
|
<div transition:slide class="separator" />
|
||||||
{/if}
|
{/if}
|
||||||
{#if $serialPort}
|
{#if $serialPort}
|
||||||
<button title={$LL.backup.TITLE()} use:popup={BackupPopup} class="icon {$syncStatus}">
|
<button title={$LL.backup.TITLE()} use:popup={BackupPopup} class="icon {$syncStatus}">
|
||||||
@@ -92,11 +80,11 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
bind:this={connectButton}
|
bind:this={connectButton}
|
||||||
title="Devices"
|
title="Devices"
|
||||||
use:popup={ConnectionPopup}
|
use:popup={ConnectionPopup}
|
||||||
class="icon connect"
|
class="icon connect"
|
||||||
class:error={$serialPort === undefined}
|
class:error={$serialPort === undefined}
|
||||||
>
|
>
|
||||||
cable
|
cable
|
||||||
</button>
|
</button>
|
||||||
@@ -167,16 +155,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
|
width: calc(min(100%, 28cm));
|
||||||
margin-block: 8px;
|
margin-block: 8px;
|
||||||
margin-inline: 16px;
|
margin-inline: auto;
|
||||||
|
padding-inline: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
|
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
@@ -195,7 +187,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
padding: 4px;
|
padding: 2px;
|
||||||
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -210,49 +202,16 @@
|
|||||||
color: var(--md-sys-color-on-error);
|
color: var(--md-sys-color-on-error);
|
||||||
background: var(--md-sys-color-error);
|
background: var(--md-sys-color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active,
|
|
||||||
&:active {
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
translate: -50% 0;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
> a.icon {
|
|
||||||
aspect-ratio: unset;
|
|
||||||
margin-inline: -4px;
|
|
||||||
padding-inline: 16px;
|
|
||||||
|
|
||||||
font-size: 24px;
|
|
||||||
color: var(--md-sys-on-surface-variant);
|
|
||||||
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
clip-path: polygon(25% 50%, 0% 0%, 75% 0%, 100% 50%, 75% 100%, 0% 100%);
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
&.active,
|
|
||||||
&:active {
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background: var(--md-sys-color-tertiary);
|
|
||||||
|
|
||||||
&,
|
|
||||||
~ * {
|
|
||||||
translate: 8px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.account {
|
.icon.account {
|
||||||
@@ -260,7 +219,7 @@
|
|||||||
color: var(--md-sys-color-on-secondary-container);
|
color: var(--md-sys-color-on-secondary-container);
|
||||||
background: var(--md-sys-color-secondary-container);
|
background: var(--md-sys-color-secondary-container);
|
||||||
}
|
}
|
||||||
|
|
||||||
:disabled {
|
:disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|||||||
6
src/routes/config/SharePopup.svelte
Normal file
6
src/routes/config/SharePopup.svelte
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
import LL from "../../i18n/i18n-svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h4>{$LL.share.URL_COPIED()}</h4>
|
||||||
|
<button>{$LL.share.EXTRA_DOWNLOAD()}</button>
|
||||||
@@ -2,10 +2,8 @@
|
|||||||
import {chords} from "$lib/serial/connection"
|
import {chords} from "$lib/serial/connection"
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||||
import Index from "flexsearch"
|
import Index from "flexsearch"
|
||||||
import {tick} from "svelte"
|
|
||||||
import type {Chord} from "$lib/serial/chord"
|
import type {Chord} from "$lib/serial/chord"
|
||||||
import LL from "../../../i18n/i18n-svelte"
|
import LL from "../../../i18n/i18n-svelte"
|
||||||
import {actionAutocomplete} from "$lib/action-autocomplete"
|
|
||||||
|
|
||||||
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
|
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
|
||||||
|
|
||||||
@@ -20,11 +18,8 @@
|
|||||||
let searchFilter: number[] | undefined
|
let searchFilter: number[] | undefined
|
||||||
|
|
||||||
function search(event: Event) {
|
function search(event: Event) {
|
||||||
document.startViewTransition(async () => {
|
const query = (event.target as HTMLInputElement).value
|
||||||
const query = (event.target as HTMLInputElement).value
|
searchFilter = query && searchIndex ? searchIndex.search(query) : undefined
|
||||||
searchFilter = query && searchIndex ? searchIndex.search(query) : undefined
|
|
||||||
await tick()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: items = searchFilter?.map(it => [$chords[it], it] as const) ?? $chords.map((it, i) => [it, i] as const)
|
$: items = searchFilter?.map(it => [$chords[it], it] as const) ?? $chords.map((it, i) => [it, i] as const)
|
||||||
@@ -38,7 +33,7 @@
|
|||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder={$LL.configure.chords.search.PLACEHOLDER($chords.length)}
|
placeholder={$LL.configure.chords.search.PLACEHOLDER($chords.length)}
|
||||||
use:actionAutocomplete
|
on:input={search}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,33 +3,70 @@
|
|||||||
import {layout} from "$lib/serial/connection"
|
import {layout} from "$lib/serial/connection"
|
||||||
import tippy from "tippy.js"
|
import tippy from "tippy.js"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {layoutAsUrlComponent, layoutFromUrlComponent} from "$lib/serialization/layout"
|
|
||||||
import Layout from "$lib/components/layout/Layout.svelte"
|
import Layout from "$lib/components/layout/Layout.svelte"
|
||||||
|
import {csvLayoutToJson, isCsvLayout} from "$lib/compat/legacy-layout"
|
||||||
|
import {charaFileFromUriComponent, charaFileToUriComponent} from "$lib/share/share-url"
|
||||||
|
import type {CharaLayoutFile} from "$lib/share/chara-file"
|
||||||
|
import SharePopup from "../SharePopup.svelte"
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
if (url.searchParams.has("layout")) {
|
if (url.searchParams.has("import")) {
|
||||||
$layout = await layoutFromUrlComponent(url.searchParams.get("layout")!)
|
const file = await charaFileFromUriComponent(url.searchParams.get("import")!)
|
||||||
|
if (file.type === "layout") {
|
||||||
|
$layout = file.layout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function shareLayout(event: Event) {
|
async function shareLayout(event: Event) {
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
url.searchParams.set("layout", await layoutAsUrlComponent($layout))
|
url.searchParams.set(
|
||||||
|
"import",
|
||||||
|
await charaFileToUriComponent({
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "layout",
|
||||||
|
device: "one",
|
||||||
|
layout: $layout,
|
||||||
|
}),
|
||||||
|
)
|
||||||
await navigator.clipboard.writeText(url.toString())
|
await navigator.clipboard.writeText(url.toString())
|
||||||
|
let shareComponent: SharePopup
|
||||||
tippy(event.target as HTMLElement, {
|
tippy(event.target as HTMLElement, {
|
||||||
content: "Share url copied!",
|
onCreate(instance) {
|
||||||
delay: [0, 1000000],
|
const target = instance.popper.querySelector(".tippy-content")!
|
||||||
|
shareComponent = new SharePopup({target})
|
||||||
|
},
|
||||||
onHidden(instance) {
|
onHidden(instance) {
|
||||||
instance.destroy()
|
instance.destroy()
|
||||||
},
|
},
|
||||||
|
onDestroy(instance) {
|
||||||
|
shareComponent.$destroy()
|
||||||
|
},
|
||||||
}).show()
|
}).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function importLayout() {
|
||||||
|
const file = await fileInput.files?.item(0)?.text()
|
||||||
|
if (!file) return
|
||||||
|
const importedLayout = isCsvLayout(file) ? csvLayoutToJson(file) : (JSON.parse(file) as CharaLayoutFile)
|
||||||
|
if (importedLayout.type === "layout" && importedLayout.charaVersion === 1) $layout = importedLayout.layout
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileInput: HTMLInputElement
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window use:share={shareLayout} />
|
<svelte:window use:share={shareLayout} />
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
<label class="icon"
|
||||||
|
>upload_file<input
|
||||||
|
bind:this={fileInput}
|
||||||
|
on:input={importLayout}
|
||||||
|
type="file"
|
||||||
|
accept="text/csv, application/json"
|
||||||
|
/></label
|
||||||
|
>
|
||||||
<Layout />
|
<Layout />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -37,4 +74,8 @@
|
|||||||
section {
|
section {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import {redirect} from "@sveltejs/kit"
|
|
||||||
import type {PageLoad} from "./$types"
|
|
||||||
|
|
||||||
export const load = (() => {
|
|
||||||
throw redirect(302, "/train/cpm/")
|
|
||||||
}) satisfies PageLoad
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<script>
|
|
||||||
import TypingInput from "$lib/components/TypingInput.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TypingInput />
|
|
||||||
20
src/tools/version.ts
Normal file
20
src/tools/version.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import {readFile, writeFile} from "fs/promises"
|
||||||
|
import {fileURLToPath} from "url"
|
||||||
|
import * as path from "path"
|
||||||
|
import {format} from "prettier"
|
||||||
|
|
||||||
|
const projectDir = path.resolve(fileURLToPath(import.meta.url), "..", "..", "..")
|
||||||
|
|
||||||
|
const {version} = JSON.parse(await readFile(path.join(projectDir, "package.json"), "utf8"))
|
||||||
|
|
||||||
|
const tauriConfigPath = path.join(projectDir, "src-tauri", "tauri.conf.json")
|
||||||
|
|
||||||
|
const tauriConfig = JSON.parse(await readFile(tauriConfigPath, "utf8"))
|
||||||
|
tauriConfig.package.version = version
|
||||||
|
await writeFile(tauriConfigPath, await format(JSON.stringify(tauriConfig), {parser: "json"}))
|
||||||
|
|
||||||
|
const cargoTomlPath = path.join(projectDir, "src-tauri", "Cargo.toml")
|
||||||
|
|
||||||
|
const cargoToml = await readFile(cargoTomlPath, "utf8")
|
||||||
|
const modified = cargoToml.replace(/^\s*version\s*=\s*"\d\.\d.\d"\s*$/m, `version = "${version}"`)
|
||||||
|
await writeFile(cargoTomlPath, modified)
|
||||||
@@ -1,42 +1,49 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>iFrame Sandbox</title>
|
<title>iFrame Sandbox</title>
|
||||||
<script>
|
<script>
|
||||||
let ongoingRequest
|
let ongoingRequest
|
||||||
let resolveRequest
|
let resolveRequest
|
||||||
let source
|
let source
|
||||||
async function post(channel, args) {
|
async function post(channel, args) {
|
||||||
while (ongoingRequest) {
|
while (ongoingRequest) {
|
||||||
await ongoingRequest
|
await ongoingRequest
|
||||||
}
|
|
||||||
ongoingRequest = new Promise(resolve => {
|
|
||||||
resolveRequest = resolve
|
|
||||||
source.postMessage([channel, args], "*")
|
|
||||||
})
|
|
||||||
ongoingRequest.then(() => {
|
|
||||||
ongoingRequest = undefined
|
|
||||||
})
|
|
||||||
return ongoingRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('message', event => {
|
|
||||||
if ("response" in event.data) {
|
|
||||||
resolveRequest(event.data.response)
|
|
||||||
} else {
|
|
||||||
source = event.source
|
|
||||||
|
|
||||||
var Action = event.data.actionCodes
|
|
||||||
Object.assign(Action, Object.fromEntries(Object.values(event.data.actionCodes).filter(it => !!it.id).map(it => [it.id, it])))
|
|
||||||
|
|
||||||
var Chara = {}
|
|
||||||
for (const fn of event.data.charaChannels) {
|
|
||||||
Chara[fn] = (...args) => post(fn, args)
|
|
||||||
}
|
}
|
||||||
|
ongoingRequest = new Promise(resolve => {
|
||||||
eval(`(async function(){${event.data.script}})()`)
|
resolveRequest = resolve
|
||||||
|
source.postMessage([channel, args], "*")
|
||||||
|
})
|
||||||
|
ongoingRequest.then(() => {
|
||||||
|
ongoingRequest = undefined
|
||||||
|
})
|
||||||
|
return ongoingRequest
|
||||||
}
|
}
|
||||||
})
|
|
||||||
</script>
|
window.addEventListener("message", event => {
|
||||||
</head>
|
if ("response" in event.data) {
|
||||||
</html>
|
resolveRequest(event.data.response)
|
||||||
|
} else {
|
||||||
|
source = event.source
|
||||||
|
|
||||||
|
var Action = event.data.actionCodes
|
||||||
|
Object.assign(
|
||||||
|
Action,
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.values(event.data.actionCodes)
|
||||||
|
.filter(it => !!it.id)
|
||||||
|
.map(it => [it.id, it]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
var Chara = {}
|
||||||
|
for (const fn of event.data.charaChannels) {
|
||||||
|
Chara[fn] = (...args) => post(fn, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
eval(`(async function(){${event.data.script}})()`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import adapter from "@sveltejs/adapter-static"
|
import adapter from "@sveltejs/adapter-static"
|
||||||
import preprocess from "svelte-preprocess"
|
import preprocess from "svelte-preprocess"
|
||||||
import autoprefixer from "autoprefixer"
|
import autoprefixer from "autoprefixer"
|
||||||
|
import {readFile} from "fs/promises"
|
||||||
|
import {fileURLToPath} from "url"
|
||||||
|
|
||||||
|
const {version} = JSON.parse(await readFile(fileURLToPath(new URL("package.json", import.meta.url)), "utf8"))
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
preprocess: [preprocess({postcss: {plugins: autoprefixer()}})],
|
preprocess: [preprocess({postcss: {plugins: autoprefixer()}})],
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter(),
|
adapter: adapter(),
|
||||||
|
version: {
|
||||||
|
name: version,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,14 @@ import {sveltekit} from "@sveltejs/kit/vite"
|
|||||||
import {defineConfig} from "vite"
|
import {defineConfig} from "vite"
|
||||||
import {SvelteKitPWA} from "@vite-pwa/sveltekit"
|
import {SvelteKitPWA} from "@vite-pwa/sveltekit"
|
||||||
import ViteYaml from "@modyfi/vite-plugin-yaml"
|
import ViteYaml from "@modyfi/vite-plugin-yaml"
|
||||||
|
import {readFile} from "fs/promises"
|
||||||
|
import {fileURLToPath} from "url"
|
||||||
|
|
||||||
const isTauri = "TAURI_FAMILY" in process.env
|
const isTauri = "TAURI_FAMILY" in process.env
|
||||||
console.info(isTauri ? "Building for Tauri" : "Building for PWA")
|
console.info(isTauri ? "Building for Tauri" : "Building for PWA")
|
||||||
|
const {homepage, bugs} = JSON.parse(
|
||||||
|
await readFile(fileURLToPath(new URL("package.json", import.meta.url)), "utf8"),
|
||||||
|
)
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
@@ -16,6 +21,10 @@ export default defineConfig({
|
|||||||
external: isTauri ? [/virtual:pwa.*/] : [],
|
external: isTauri ? [/virtual:pwa.*/] : [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
define: {
|
||||||
|
HOMEPAGE_URL: `"${homepage}"`,
|
||||||
|
BUGS_URL: `"${bugs.url}"`,
|
||||||
|
},
|
||||||
envPrefix: "TAURI_",
|
envPrefix: "TAURI_",
|
||||||
plugins: [
|
plugins: [
|
||||||
ViteYaml(),
|
ViteYaml(),
|
||||||
|
|||||||
Reference in New Issue
Block a user