mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2025-12-22 18:56:21 +00:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
2a872bafac
|
|||
|
a940d1b480
|
|||
|
f3b1d76666
|
|||
|
0b2695a380
|
|||
|
048dee0a6d
|
|||
|
977bdf3043
|
|||
|
9ca30f412e
|
|||
|
f2a18cafe8
|
|||
|
b27182dc35
|
|||
|
74ce6af318
|
|||
|
782f1fc38b
|
|||
|
|
087ff36d5d | ||
|
bd1c6147fd
|
|||
|
891abda0fb
|
|||
|
3611f65e24
|
|||
|
f76882a09c
|
|||
|
ff7e4f7b2e
|
|||
|
1c1c86241f
|
|||
|
dc8b3c3d66
|
|||
|
|
65911419b0 | ||
|
|
ccfb09e261 | ||
|
b841469505
|
|||
|
bc06e8ee80
|
|||
|
24fc861ef4
|
|||
|
5801e5fbbe
|
|||
|
92b52e08f7
|
|||
|
4192210d27
|
|||
|
|
0e5640a1ee | ||
|
7f27499003
|
|||
|
|
b6ded5f94c | ||
|
|
63d0ad7ae8 | ||
|
|
1c8f53caf6 | ||
|
1d60b12d43
|
|||
|
e85a731410
|
|||
|
050af564ab
|
|||
|
6545124aa2
|
|||
| b93724add3 | |||
|
|
e1092113f6 | ||
|
|
0bb4bbe838 | ||
|
089812c555
|
|||
|
45c5f21cc4
|
|||
|
fb5959998a
|
|||
|
f319714489
|
|||
|
fb1f5b7ec7
|
|||
|
ac16cfd3bf
|
|||
|
9d5b0e01d2
|
|||
|
e7517f821d
|
|||
|
762f73063a
|
|||
|
7ca9e04dd3
|
|||
|
4d73dad780
|
|||
|
5419824c06
|
|||
|
075d05dd0b
|
|||
|
9266702cbb
|
|||
|
77e2d2b20e
|
|||
|
7819f546a6
|
|||
|
e37b38085d
|
|||
|
a3bf9ac32b
|
|||
|
|
5bd3245084 |
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
@@ -1,6 +1,12 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on: [push]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -8,9 +14,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 🚚 Checkout
|
- name: 🚚 Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: 🐍 Use Python 3.x
|
- name: 🐍 Use Python 3.x
|
||||||
uses: actions/setup-python@v3.1.4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
cache: pip
|
cache: pip
|
||||||
@@ -20,11 +26,11 @@ jobs:
|
|||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
- name: 🐉 Use Node.js 22.4.x
|
- name: 🐉 Use Node.js 22.14.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22.4.x
|
node-version: 22.14.x
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- name: ⏬ Install Node dependencies
|
- name: ⏬ Install Node dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
@@ -41,10 +47,11 @@ jobs:
|
|||||||
echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||||
|
|
||||||
- name: Publish Stable
|
- name: Publish Stable
|
||||||
if: ${{ github.ref == 'refs/tags/v*' }}
|
if: ${{ github.ref == 'refs/tags/v*' && !github.event.pull_request.head.repo.fork }}
|
||||||
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/www/
|
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/www/
|
||||||
|
|
||||||
- name: Publish Branch
|
- name: Publish Branch
|
||||||
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${GITHUB_REF##*/}
|
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${GITHUB_REF##*/}
|
||||||
- name: Publish Commit
|
- name: Publish Commit
|
||||||
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${{ github.sha }}
|
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${{ github.sha }}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ node_modules
|
|||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
/src-tauri/target
|
/src-tauri/target
|
||||||
|
/openssl*
|
||||||
|
/src/i18n/i18n*
|
||||||
|
|
||||||
# Ignore files for PNPM, NPM and YARN
|
# Ignore files for PNPM, NPM and YARN
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-css-order"],
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"stylelint-config-standard-scss",
|
"stylelint-config-standard-scss",
|
||||||
"stylelint-config-recommended-scss",
|
"stylelint-config-recommended-scss",
|
||||||
"stylelint-config-html/svelte",
|
"stylelint-config-html/svelte",
|
||||||
"stylelint-config-clean-order",
|
|
||||||
"stylelint-config-prettier-scss"
|
"stylelint-config-prettier-scss"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
|||||||
@@ -35,3 +35,9 @@ way to subset variable woff2 fonts with ligatures.
|
|||||||
|
|
||||||
In other words, either have python as a development dependency or
|
In other words, either have python as a development dependency or
|
||||||
serve a 3.5MB icons font of which 99.5% is completely unused.
|
serve a 3.5MB icons font of which 99.5% is completely unused.
|
||||||
|
|
||||||
|
To generate the icons use the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm run minify-icons
|
||||||
|
```
|
||||||
|
|||||||
24
flake.lock
generated
24
flake.lock
generated
@@ -5,11 +5,11 @@
|
|||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1710146030,
|
"lastModified": 1731533236,
|
||||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -20,11 +20,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1722415718,
|
"lastModified": 1743259260,
|
||||||
"narHash": "sha256-5US0/pgxbMksF92k1+eOa8arJTJiPvsdZj9Dl+vJkM4=",
|
"narHash": "sha256-ArWLUgRm1tKHiqlhnymyVqi5kLNCK5ghvm06mfCl4QY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "c3392ad349a5227f4a3464dce87bcc5046692fce",
|
"rev": "eb0e0f21f15c559d2ac7633dc81d079d1caf5f5f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -36,11 +36,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1718428119,
|
"lastModified": 1736320768,
|
||||||
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
|
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
|
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -62,11 +62,11 @@
|
|||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1722391647,
|
"lastModified": 1743388531,
|
||||||
"narHash": "sha256-JTi7l1oxnatF1uX/gnGMlRnyFMtylRw4MqhCUdoN2K4=",
|
"narHash": "sha256-OBcNE+2/TD1AMgq8HKMotSQF8ZPJEFGZdRoBJ7t/HIc=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "0fd4a5d2098faa516a9b83022aec7db766cd1de8",
|
"rev": "011de3c895927300651d9c2cb8e062adf17aa665",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
16
flake.nix
16
flake.nix
@@ -14,7 +14,13 @@
|
|||||||
flake-utils.lib.eachDefaultSystem (
|
flake-utils.lib.eachDefaultSystem (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
overlays = [ (import rust-overlay) ];
|
overlays = [
|
||||||
|
(import rust-overlay)
|
||||||
|
(final: prev: {
|
||||||
|
nodejs = prev.nodejs_22;
|
||||||
|
corepack = prev.corepack_22;
|
||||||
|
})
|
||||||
|
];
|
||||||
pkgs = import nixpkgs { inherit system overlays; };
|
pkgs = import nixpkgs { inherit system overlays; };
|
||||||
rust-bin = pkgs.rust-bin.stable.latest.default.override {
|
rust-bin = pkgs.rust-bin.stable.latest.default.override {
|
||||||
extensions = [
|
extensions = [
|
||||||
@@ -46,8 +52,8 @@
|
|||||||
];
|
];
|
||||||
packages =
|
packages =
|
||||||
(with pkgs; [
|
(with pkgs; [
|
||||||
nodejs_22
|
nodejs
|
||||||
nodePackages.pnpm
|
pnpm
|
||||||
rust-bin
|
rust-bin
|
||||||
fontMin
|
fontMin
|
||||||
])
|
])
|
||||||
@@ -59,7 +65,7 @@
|
|||||||
openssl_3
|
openssl_3
|
||||||
glib
|
glib
|
||||||
gtk3
|
gtk3
|
||||||
libsoup
|
libsoup_2_4
|
||||||
webkitgtk
|
webkitgtk
|
||||||
librsvg
|
librsvg
|
||||||
# serial plugin
|
# serial plugin
|
||||||
@@ -70,7 +76,7 @@
|
|||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
buildInputs = packages;
|
buildInputs = packages;
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
|
#export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const config = {
|
|||||||
"node_modules/@fontsource-variable/material-symbols-rounded/files/material-symbols-rounded-latin-full-normal.woff2",
|
"node_modules/@fontsource-variable/material-symbols-rounded/files/material-symbols-rounded-latin-full-normal.woff2",
|
||||||
outputPath: "src/lib/assets/icons.min.woff2",
|
outputPath: "src/lib/assets/icons.min.woff2",
|
||||||
icons: [
|
icons: [
|
||||||
|
"rocket_launch",
|
||||||
"deployed_code_update",
|
"deployed_code_update",
|
||||||
"adjust",
|
"adjust",
|
||||||
"add",
|
"add",
|
||||||
@@ -42,7 +43,23 @@ const config = {
|
|||||||
"arrow_back",
|
"arrow_back",
|
||||||
"arrow_back_ios_new",
|
"arrow_back_ios_new",
|
||||||
"save",
|
"save",
|
||||||
|
"step_over",
|
||||||
|
"step_into",
|
||||||
|
"step_out",
|
||||||
"settings_backup_restore",
|
"settings_backup_restore",
|
||||||
|
"sound_detection_loud_sound",
|
||||||
|
"ring_volume",
|
||||||
|
"wifi",
|
||||||
|
"power_settings_circle",
|
||||||
|
"graphic_eq",
|
||||||
|
"mail",
|
||||||
|
"calculate",
|
||||||
|
"open_in_browser",
|
||||||
|
"chevron_backward",
|
||||||
|
"chevron_forward",
|
||||||
|
"bookmark",
|
||||||
|
"drag_pan",
|
||||||
|
"markdown_copy",
|
||||||
"sort",
|
"sort",
|
||||||
"shopping_bag",
|
"shopping_bag",
|
||||||
"filter_list",
|
"filter_list",
|
||||||
@@ -66,16 +83,24 @@ const config = {
|
|||||||
"delete",
|
"delete",
|
||||||
"remove_selection",
|
"remove_selection",
|
||||||
"bolt",
|
"bolt",
|
||||||
|
"thunderstorm",
|
||||||
|
"join_inner",
|
||||||
|
"uppercase",
|
||||||
"undo",
|
"undo",
|
||||||
"redo",
|
"redo",
|
||||||
"replay",
|
"replay",
|
||||||
"reply",
|
"reply",
|
||||||
"navigate_before",
|
"navigate_before",
|
||||||
"navigate_next",
|
"navigate_next",
|
||||||
|
"library_add",
|
||||||
|
"reset_wrench",
|
||||||
|
"reset_settings",
|
||||||
|
"delete_sweep",
|
||||||
"print",
|
"print",
|
||||||
"restore_from_trash",
|
"restore_from_trash",
|
||||||
"history",
|
"history",
|
||||||
"history_toggle_off",
|
"history_toggle_off",
|
||||||
|
"text_to_speech",
|
||||||
"sentiment_satisfied",
|
"sentiment_satisfied",
|
||||||
"sentiment_dissatisfied",
|
"sentiment_dissatisfied",
|
||||||
"sentiment_very_satisfied",
|
"sentiment_very_satisfied",
|
||||||
@@ -89,6 +114,7 @@ const config = {
|
|||||||
"sentiment_sad",
|
"sentiment_sad",
|
||||||
"sentiment_content",
|
"sentiment_content",
|
||||||
"sentiment_worried",
|
"sentiment_worried",
|
||||||
|
"construction",
|
||||||
"timer",
|
"timer",
|
||||||
"target",
|
"target",
|
||||||
"download",
|
"download",
|
||||||
|
|||||||
90
package.json
90
package.json
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "charachorder-device-manager",
|
"name": "charachorder-device-manager",
|
||||||
"version": "2.2.2",
|
"version": "2.4.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.4",
|
"node": ">=22.14",
|
||||||
"pnpm": ">=9.4"
|
"pnpm": ">=10.7"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -34,62 +34,64 @@
|
|||||||
"typesafe-i18n": "typesafe-i18n"
|
"typesafe-i18n": "typesafe-i18n"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/autocomplete": "^6.18.2",
|
"@codemirror/autocomplete": "^6.18.6",
|
||||||
"@codemirror/commands": "^6.7.1",
|
"@codemirror/commands": "^6.8.1",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
"@codemirror/language": "^6.10.3",
|
"@codemirror/language": "^6.11.2",
|
||||||
"@codemirror/state": "^6.4.1",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/view": "^6.34.1",
|
"@codemirror/view": "^6.38.1",
|
||||||
"@fontsource-variable/material-symbols-rounded": "^5.1.3",
|
"@fontsource-variable/material-symbols-rounded": "^5.2.17",
|
||||||
"@fontsource-variable/noto-sans-mono": "^5.1.0",
|
"@fontsource-variable/noto-sans-mono": "^5.2.7",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@material/material-color-utilities": "^0.3.0",
|
"@material/material-color-utilities": "^0.3.0",
|
||||||
"@melt-ui/pp": "^0.3.2",
|
"@melt-ui/pp": "^0.3.2",
|
||||||
"@melt-ui/svelte": "^0.86.0",
|
"@melt-ui/svelte": "^0.86.6",
|
||||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
"@modyfi/vite-plugin-yaml": "^1.1.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.6",
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
"@sveltejs/kit": "^2.7.5",
|
"@sveltejs/kit": "^2.26.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^6.1.0",
|
||||||
"@tauri-apps/api": "^1.6.0",
|
"@tauri-apps/api": "^1.6.0",
|
||||||
"@tauri-apps/cli": "^1.6.0",
|
"@tauri-apps/cli": "^1.6.0",
|
||||||
"@types/dom-view-transitions": "^1.0.5",
|
"@types/dom-view-transitions": "^1.0.6",
|
||||||
"@types/flexsearch": "^0.7.6",
|
"@types/semver": "^7.7.0",
|
||||||
"@types/w3c-web-serial": "^1.0.7",
|
"@types/w3c-web-serial": "^1.0.8",
|
||||||
"@types/w3c-web-usb": "^1.0.10",
|
"@types/w3c-web-usb": "^1.0.10",
|
||||||
"@types/wicg-file-system-access": "^2023.10.5",
|
"@types/wicg-file-system-access": "^2023.10.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.6",
|
"@vite-pwa/sveltekit": "^1.0.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.21",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.2",
|
||||||
"cypress": "^13.13.2",
|
"cypress": "^14.5.3",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"esptool-js": "^0.4.7",
|
"esptool-js": "^0.5.6",
|
||||||
"flexsearch": "^0.7.43",
|
"flexsearch": "^0.8.205",
|
||||||
"fontkit": "^2.0.4",
|
"fontkit": "^2.0.4",
|
||||||
"glob": "^11.0.0",
|
"glob": "^11.0.3",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^26.1.0",
|
||||||
"matrix-js-sdk": "^34.9.0",
|
"matrix-js-sdk": "^37.12.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-svelte": "^3.2.7",
|
"prettier-plugin-css-order": "^2.1.2",
|
||||||
"rxjs": "^7.8.1",
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
"sass": "^1.80.6",
|
"rxjs": "^7.8.2",
|
||||||
|
"sass": "^1.89.2",
|
||||||
|
"semver": "^7.7.2",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"stylelint": "^16.10.0",
|
"stylelint": "^16.23.0",
|
||||||
"stylelint-config-clean-order": "^6.1.0",
|
"stylelint-config-clean-order": "^7.0.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": "^14.1.0",
|
"stylelint-config-recommended-scss": "^15.0.1",
|
||||||
"stylelint-config-standard-scss": "^13.1.0",
|
"stylelint-config-standard-scss": "^15.0.1",
|
||||||
"svelte": "5.1.9",
|
"svelte": "5.37.1",
|
||||||
"svelte-check": "^4.0.5",
|
"svelte-check": "^4.3.0",
|
||||||
"svelte-preprocess": "^6.0.3",
|
"svelte-preprocess": "^6.0.3",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"typesafe-i18n": "^5.26.2",
|
"typesafe-i18n": "^5.26.2",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^5.4.10",
|
"vite": "^7.0.6",
|
||||||
"vite-plugin-mkcert": "^1.17.6",
|
"vite-plugin-mkcert": "^1.17.8",
|
||||||
"vite-plugin-pwa": "^0.20.5",
|
"vite-plugin-pwa": "^1.0.2",
|
||||||
"vitest": "^2.1.4",
|
"vitest": "^3.2.4",
|
||||||
"web-serial-polyfill": "^1.0.15",
|
"web-serial-polyfill": "^1.0.15",
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
2741
pnpm-lock.yaml
generated
2741
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "2.2.2"
|
version = "2.4.0"
|
||||||
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,7 +6,7 @@
|
|||||||
"devPath": "http://localhost:5173",
|
"devPath": "http://localhost:5173",
|
||||||
"distDir": "../build"
|
"distDir": "../build"
|
||||||
},
|
},
|
||||||
"package": { "productName": "amacc1ng", "version": "2.2.2" },
|
"package": { "productName": "amacc1ng", "version": "2.4.0" },
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": { "all": false },
|
"allowlist": { "all": false },
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const de = {
|
|||||||
AUTO_BACKUP: "Auto-backup",
|
AUTO_BACKUP: "Auto-backup",
|
||||||
DISCLAIMER:
|
DISCLAIMER:
|
||||||
"Das Backup in diesem Browser gespeichert und bleibt nur auf diesem Computer.",
|
"Das Backup in diesem Browser gespeichert und bleibt nur auf diesem Computer.",
|
||||||
DOWNLOAD: "Alles",
|
DOWNLOAD: "Komplettes Profil",
|
||||||
RESTORE: "Wiederherstellen",
|
RESTORE: "Wiederherstellen",
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const en = {
|
|||||||
AUTO_BACKUP: "Auto-backup",
|
AUTO_BACKUP: "Auto-backup",
|
||||||
DISCLAIMER:
|
DISCLAIMER:
|
||||||
"Whenever you connect this device to browser, a backup is made locally and kept only on your computer.",
|
"Whenever you connect this device to browser, a backup is made locally and kept only on your computer.",
|
||||||
DOWNLOAD: "Everything",
|
DOWNLOAD: "Full profile",
|
||||||
RESTORE: "Restore",
|
RESTORE: "Restore",
|
||||||
},
|
},
|
||||||
sync: {
|
sync: {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { expoIn, expoOut } from "svelte/easing";
|
import { expoIn, expoOut } from "svelte/easing";
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
let { children, routeOrder }: { children: Snippet; routeOrder: string[]; direction: } =
|
let { children, routeOrder }: { children: Snippet; routeOrder: string[] } =
|
||||||
$props();
|
$props();
|
||||||
|
|
||||||
let inDirection = $state(0);
|
let inDirection = $state(0);
|
||||||
|
|||||||
@@ -1,497 +0,0 @@
|
|||||||
s + Dup,say
|
|
||||||
y + b,by
|
|
||||||
y + Dup,why
|
|
||||||
n + Dup,no
|
|
||||||
n + f,find
|
|
||||||
l + f,life
|
|
||||||
l + p,people
|
|
||||||
l + s + p,spell
|
|
||||||
l + n,line
|
|
||||||
l + n + p,plant
|
|
||||||
t + h,that
|
|
||||||
t + n + h,than
|
|
||||||
t + n + p,plant
|
|
||||||
t + l + f,left
|
|
||||||
a + f,after
|
|
||||||
a + d,add
|
|
||||||
a + d + f,had
|
|
||||||
a + h,has
|
|
||||||
a + s,as
|
|
||||||
a + s + h,has
|
|
||||||
a + y + d,day
|
|
||||||
a + y + s,say
|
|
||||||
a + n,an
|
|
||||||
a + n + d,and
|
|
||||||
a + n + d + f,hand
|
|
||||||
a + n + h,hand
|
|
||||||
a + n + y,any
|
|
||||||
a + l,all
|
|
||||||
a + l + d,land
|
|
||||||
a + l + p,plant
|
|
||||||
a + l + s,last
|
|
||||||
a + l + y + p,play
|
|
||||||
a + l + n + d,land
|
|
||||||
a + t,at
|
|
||||||
a + t + h,that
|
|
||||||
a + t + n + h,than
|
|
||||||
a + t + l + s,last
|
|
||||||
a + t + l + n + p,plant
|
|
||||||
- + ?,question
|
|
||||||
w + h,who
|
|
||||||
w + s,saw
|
|
||||||
w + y + h,why
|
|
||||||
w + t,without
|
|
||||||
w + t + h,watch
|
|
||||||
w + t + n,went
|
|
||||||
w + a,was
|
|
||||||
w + a + h,what
|
|
||||||
w + a + s,saw
|
|
||||||
w + a + y,way
|
|
||||||
w + a + y + Dup,away
|
|
||||||
w + a + n,want
|
|
||||||
w + a + l + f,walk
|
|
||||||
w + a + l + y,always
|
|
||||||
w + a + l + y + s,always
|
|
||||||
w + a + t,watch
|
|
||||||
w + a + t + h,watch
|
|
||||||
w + a + t + n,want
|
|
||||||
g + b,begin
|
|
||||||
g + Dup,question
|
|
||||||
g + h,here
|
|
||||||
g + p,page
|
|
||||||
g + l + n,long
|
|
||||||
g + t,get
|
|
||||||
k + q,quick
|
|
||||||
k + f,and
|
|
||||||
k + l + y + q,quickly
|
|
||||||
k + t + l,talk
|
|
||||||
k + a + b,back
|
|
||||||
k + a + s,ask
|
|
||||||
k + a + t + l,talk
|
|
||||||
k + w + a + l,walk
|
|
||||||
m + f,form
|
|
||||||
m + y,my
|
|
||||||
m + t + Dup,mountain
|
|
||||||
m + a + f,family
|
|
||||||
m + a + s,small
|
|
||||||
m + a + y,may
|
|
||||||
m + a + n,man
|
|
||||||
m + a + n + y,many
|
|
||||||
m + a + l,almost
|
|
||||||
m + a + l + s,small
|
|
||||||
m + a + l + s + Dup,small
|
|
||||||
c + b,because
|
|
||||||
c + Dup,sea
|
|
||||||
c + h,head
|
|
||||||
c + a + n,can
|
|
||||||
c + a + n + h,change
|
|
||||||
c + a + l,call
|
|
||||||
c + a + l + p,place
|
|
||||||
c + k + a + b,back
|
|
||||||
u + p,us
|
|
||||||
u + y,you
|
|
||||||
u + j,just
|
|
||||||
u + j + s,just
|
|
||||||
u + t + b,but
|
|
||||||
u + t + p,put
|
|
||||||
u + t + s + d,study
|
|
||||||
u + t + n,until
|
|
||||||
u + t + j + s,just
|
|
||||||
u + a + l,last
|
|
||||||
u + k + q,quick
|
|
||||||
u + k + l + y + q,quickly
|
|
||||||
u + m + h,much
|
|
||||||
u + m + n,number
|
|
||||||
u + m + t + s,must
|
|
||||||
u + m + c + h,much
|
|
||||||
u + c + s + h,such
|
|
||||||
u + c + t,cut
|
|
||||||
u + c + t + n,country
|
|
||||||
' + a + s,say
|
|
||||||
' + a + n,any
|
|
||||||
' + m + a + n,many
|
|
||||||
o + Dup,off
|
|
||||||
o + f,of
|
|
||||||
o + f + f,food
|
|
||||||
o + d,do
|
|
||||||
o + s,so
|
|
||||||
o + y + b,boy
|
|
||||||
o + n,on
|
|
||||||
o + n + s + Dup,soon
|
|
||||||
o + l,line
|
|
||||||
o + l + Dup,oil
|
|
||||||
o + l + d,old
|
|
||||||
o + l + y,only
|
|
||||||
o + l + n + y,only
|
|
||||||
o + t,to
|
|
||||||
o + t + b,both
|
|
||||||
o + t + Dup,too
|
|
||||||
o + t + s,stop
|
|
||||||
o + t + s + p,stop
|
|
||||||
o + t + n,not
|
|
||||||
o + t + n + f,often
|
|
||||||
o + t + n + d,don't
|
|
||||||
o + t + n + p,point
|
|
||||||
o + a + n + h,another
|
|
||||||
o + a + l,also
|
|
||||||
o + a + l + s,also
|
|
||||||
o + w,own
|
|
||||||
o + w + h,how
|
|
||||||
o + w + s + h,show
|
|
||||||
o + w + n,now
|
|
||||||
o + w + n + d,down
|
|
||||||
o + w + l + f,follow
|
|
||||||
o + w + t,two
|
|
||||||
o + g,go
|
|
||||||
o + g + Dup,good
|
|
||||||
o + g + n + s,song
|
|
||||||
o + g + l + n,long
|
|
||||||
o + g + t,got
|
|
||||||
o + g + a + l,along
|
|
||||||
o + g + a + l + n,along
|
|
||||||
o + k + b,book
|
|
||||||
o + k + n,know
|
|
||||||
o + k + l,look
|
|
||||||
o + k + t,took
|
|
||||||
o + k + t + Dup,took
|
|
||||||
o + k + w + n,know
|
|
||||||
o + v + a + b,above
|
|
||||||
o + v + k,move
|
|
||||||
o + m + s,some
|
|
||||||
o + m + t + s,most
|
|
||||||
o + m + a + l,almost
|
|
||||||
o + m + a + l + s,almost
|
|
||||||
o + m + a + t + l + s,almost
|
|
||||||
o + c + f,food
|
|
||||||
o + c + l + s + h,school
|
|
||||||
o + u,our
|
|
||||||
o + u + f,four
|
|
||||||
o + u + s + h,should
|
|
||||||
o + u + y,you
|
|
||||||
o + u + n + f,found
|
|
||||||
o + u + n + f + f,found
|
|
||||||
o + u + n + s,sound
|
|
||||||
o + u + n + s + d,sound
|
|
||||||
o + u + l + s + d + f,should
|
|
||||||
o + u + l + s + h,should
|
|
||||||
o + u + t,out
|
|
||||||
o + u + a + b,about
|
|
||||||
o + u + a + t + b,about
|
|
||||||
o + u + w + l + d,would
|
|
||||||
o + u + g + n + y,young
|
|
||||||
o + u + g + t + h,thought
|
|
||||||
o + u + m + t + n,mountain
|
|
||||||
o + u + m + t + n + Dup,mountain
|
|
||||||
o + u + c + l + d,could
|
|
||||||
o + ' + t + n + d,don't
|
|
||||||
o + o + l,oil
|
|
||||||
o + o + t + n,into
|
|
||||||
o + o + t + n + p,point
|
|
||||||
o + o + u + w + t + h,without
|
|
||||||
o + o + u + m + a + t + n,mountain
|
|
||||||
i + f,if
|
|
||||||
i + f + f,different
|
|
||||||
i + d,did
|
|
||||||
i + RH_Thumb_1_Center,different
|
|
||||||
i + s,is
|
|
||||||
i + s + d,side
|
|
||||||
i + s + h,his
|
|
||||||
i + n,in
|
|
||||||
i + n + f + f,find
|
|
||||||
i + l,list
|
|
||||||
i + l + n,line
|
|
||||||
i + t,it
|
|
||||||
i + t + s + Dup,still
|
|
||||||
i + t + s + h,this
|
|
||||||
i + t + n,into
|
|
||||||
i + t + l + s,list
|
|
||||||
i + t + l + s + Dup,still
|
|
||||||
i + a + s,said
|
|
||||||
i + a + s + d,said
|
|
||||||
i + a + n,animal
|
|
||||||
i + w + h,which
|
|
||||||
i + w + h + Dup,which
|
|
||||||
i + w + l,will
|
|
||||||
i + w + l + Dup,will
|
|
||||||
i + w + l + h,while
|
|
||||||
i + w + t + h,with
|
|
||||||
i + g,give
|
|
||||||
i + g + b,big
|
|
||||||
i + g + h,high
|
|
||||||
i + g + n + h,night
|
|
||||||
i + g + l + h,light
|
|
||||||
i + g + t + n + h,thing
|
|
||||||
i + g + t + l + h,light
|
|
||||||
i + g + a + n,again
|
|
||||||
i + g + a + n + Dup,again
|
|
||||||
i + k + n + d,kind
|
|
||||||
i + k + l,like
|
|
||||||
i + k + t + n + h,think
|
|
||||||
i + v + l,live
|
|
||||||
i + m + h,him
|
|
||||||
i + m + p,important
|
|
||||||
i + m + s,miss
|
|
||||||
i + m + s + Dup,miss
|
|
||||||
i + m + l,mile
|
|
||||||
i + m + t,time
|
|
||||||
i + m + t + h,might
|
|
||||||
i + m + a + n,animal
|
|
||||||
i + m + a + n + Dup,animal
|
|
||||||
i + m + a + l + n,another
|
|
||||||
i + m + g + t + h,might
|
|
||||||
i + c + p,picture
|
|
||||||
i + c + t + y,city
|
|
||||||
i + u + t + l + n,until
|
|
||||||
i + u + w + t + h,without
|
|
||||||
i + u + k + q,quick
|
|
||||||
i + u + k + l + y + q,quickly
|
|
||||||
i + u + c + k + q,quick
|
|
||||||
i + u + c + k + l + y + q,quickly
|
|
||||||
i + ' + t,it's
|
|
||||||
i + ' + t + s,it's
|
|
||||||
e + b,be
|
|
||||||
e + Dup,earth
|
|
||||||
e + x,example
|
|
||||||
e + f + b,before
|
|
||||||
e + h,he
|
|
||||||
e + h + Dup,here
|
|
||||||
e + s,state
|
|
||||||
e + s + Dup,see
|
|
||||||
e + s + h,she
|
|
||||||
e + y + Dup,eye
|
|
||||||
e + n,name
|
|
||||||
e + n + b,been
|
|
||||||
e + n + Dup,need
|
|
||||||
e + n + d,end
|
|
||||||
e + l + h,help
|
|
||||||
e + l + h + f,help
|
|
||||||
e + l + s + p,spell
|
|
||||||
e + t,the
|
|
||||||
e + t + Dup,eat
|
|
||||||
e + t + f,feet
|
|
||||||
e + t + h,there
|
|
||||||
e + t + s,set
|
|
||||||
e + t + s + h,these
|
|
||||||
e + t + y + h,they
|
|
||||||
e + t + n + x,next
|
|
||||||
e + t + n + h,then
|
|
||||||
e + t + l,let
|
|
||||||
e + t + l + Dup,tell
|
|
||||||
e + t + l + f,left
|
|
||||||
e + a,at
|
|
||||||
e + a + f,father
|
|
||||||
e + a + d + f,head
|
|
||||||
e + a + h,hear
|
|
||||||
e + a + p,paper
|
|
||||||
e + a + s,sea
|
|
||||||
e + a + y,year
|
|
||||||
e + a + n,name
|
|
||||||
e + a + t,eat
|
|
||||||
e + a + t + s + Dup,state
|
|
||||||
e + w,we
|
|
||||||
e + w + f,few
|
|
||||||
e + w + n,new
|
|
||||||
e + w + n + h,when
|
|
||||||
e + w + l,well
|
|
||||||
e + w + l + Dup,well
|
|
||||||
e + w + t + b,between
|
|
||||||
e + w + t + n,went
|
|
||||||
e + w + t + n + b,between
|
|
||||||
e + g + h,here
|
|
||||||
e + g + t,get
|
|
||||||
e + g + a + p,page
|
|
||||||
e + g + a + n + b,began
|
|
||||||
e + k,keep
|
|
||||||
e + k + p,keep
|
|
||||||
e + k + t,take
|
|
||||||
e + k + a + t,take
|
|
||||||
e + v + y,every
|
|
||||||
e + v + n,even
|
|
||||||
e + v + a + h,have
|
|
||||||
e + v + a + l,leave
|
|
||||||
e + v + a + l + Dup,leave
|
|
||||||
e + m,me
|
|
||||||
e + m + s,seem
|
|
||||||
e + m + s + Dup,seem
|
|
||||||
e + m + n,men
|
|
||||||
e + m + t + h,them
|
|
||||||
e + m + a,make
|
|
||||||
e + m + a + d,made
|
|
||||||
e + m + a + s,same
|
|
||||||
e + m + a + n,mean
|
|
||||||
e + m + a + l + p + x,example
|
|
||||||
e + m + c + a,came
|
|
||||||
e + c + s,second
|
|
||||||
e + c + t + n + s,sentence
|
|
||||||
e + c + a,came
|
|
||||||
e + c + a + f,face
|
|
||||||
e + c + a + h,each
|
|
||||||
e + c + a + l + p,place
|
|
||||||
e + LH_Thumb_1_Center + a,make
|
|
||||||
e + u + s,use
|
|
||||||
e + u + s + q,question
|
|
||||||
e + u + n + d,under
|
|
||||||
e + u + t + q,quite
|
|
||||||
e + u + c + a + s + b,because
|
|
||||||
e + o + p,open
|
|
||||||
e + o + s + d,does
|
|
||||||
e + o + n,one
|
|
||||||
e + o + n + p,open
|
|
||||||
e + o + l + b,below
|
|
||||||
e + o + l + h,hello
|
|
||||||
e + o + l + h + Dup,hello
|
|
||||||
e + o + l + p,people
|
|
||||||
e + o + t + f,often
|
|
||||||
e + o + t + h,other
|
|
||||||
e + o + t + s + h,those
|
|
||||||
e + o + t + n + f,often
|
|
||||||
e + o + w + l + b,below
|
|
||||||
e + o + g + t + h,together
|
|
||||||
e + o + v,over
|
|
||||||
e + o + v + a + b,above
|
|
||||||
e + o + v + k,move
|
|
||||||
e + o + m,move
|
|
||||||
e + o + m + h,home
|
|
||||||
e + o + m + s,some
|
|
||||||
e + o + m + t + s,sometime
|
|
||||||
e + o + m + t + s + h,something
|
|
||||||
e + o + m + g + t + n + s + h,something
|
|
||||||
e + o + m + c,come
|
|
||||||
e + o + c,come
|
|
||||||
e + o + c + n,once
|
|
||||||
e + o + c + n + s + d,second
|
|
||||||
e + o + c + l + s,close
|
|
||||||
e + o + u + s + h,house
|
|
||||||
e + o + u + n,enough
|
|
||||||
e + o + u + g + n + h,enough
|
|
||||||
e + i + s + d,side
|
|
||||||
e + i + l + f,life
|
|
||||||
e + i + l + n,line
|
|
||||||
e + i + t + q,quite
|
|
||||||
e + i + t + l,little
|
|
||||||
e + i + t + l + Dup,little
|
|
||||||
e + i + a + d,idea
|
|
||||||
e + i + w + l + h,while
|
|
||||||
e + i + w + t + h,white
|
|
||||||
e + i + g,give
|
|
||||||
e + i + g + p,give
|
|
||||||
e + i + g + n + b,begin
|
|
||||||
e + i + k + l,like
|
|
||||||
e + i + v + l,live
|
|
||||||
e + i + m + l,mile
|
|
||||||
e + i + m + t,time
|
|
||||||
e + i + u + t + q,quite
|
|
||||||
e + i + u + u + t + n + s + q,question
|
|
||||||
r + e + h,her
|
|
||||||
r + e + t + Dup,tree
|
|
||||||
r + e + t + h,there
|
|
||||||
r + e + t + h + Dup,three
|
|
||||||
r + e + t + l,letter
|
|
||||||
r + e + a,are
|
|
||||||
r + e + a + d,read
|
|
||||||
r + e + a + h,hear
|
|
||||||
r + e + a + p,paper
|
|
||||||
r + e + a + n,near
|
|
||||||
r + e + a + l + y,really
|
|
||||||
r + e + a + l + y + Dup,really
|
|
||||||
r + e + a + t + f,after
|
|
||||||
r + e + a + t + h,earth
|
|
||||||
r + e + a + t + RH_Thumb_1_Center,father
|
|
||||||
r + e + a + t + l,learn
|
|
||||||
r + e + w,were
|
|
||||||
r + e + w + Dup,were
|
|
||||||
r + e + w + h,where
|
|
||||||
r + e + w + a + n + s,answer
|
|
||||||
r + e + w + a + t,water
|
|
||||||
r + e + g + h,here
|
|
||||||
r + e + g + a + l,large
|
|
||||||
r + e + g + a + t,great
|
|
||||||
r + e + v + y,very
|
|
||||||
r + e + v + y + Dup,every
|
|
||||||
r + e + v + n,never
|
|
||||||
r + e + u + n + d,under
|
|
||||||
r + e + u + m + n + b,number
|
|
||||||
r + e + o + f + b,before
|
|
||||||
r + e + o + t + h,other
|
|
||||||
r + e + o + g + t + h,together
|
|
||||||
r + e + o + v,over
|
|
||||||
r + e + o + m,more
|
|
||||||
r + e + o + m + t + h,mother
|
|
||||||
r + e + i + t + h,their
|
|
||||||
r + e + i + t + n + f + f,different
|
|
||||||
r + e + i + v,river
|
|
||||||
r + e + i + c + l + d + f,children
|
|
||||||
r + e + i + u + c + t + p,picture
|
|
||||||
r + f,for
|
|
||||||
r + h,her
|
|
||||||
r + y,your
|
|
||||||
r + n,near
|
|
||||||
r + l,learn
|
|
||||||
r + t + Dup,tree
|
|
||||||
r + t + f,father
|
|
||||||
r + t + s + Dup,start
|
|
||||||
r + t + y,try
|
|
||||||
r + t + l,letter
|
|
||||||
r + t + l + Dup,letter
|
|
||||||
r + a,are
|
|
||||||
r + a + f,far
|
|
||||||
r + a + d,read
|
|
||||||
r + a + d + f,hard
|
|
||||||
r + a + h,hard
|
|
||||||
r + a + p,part
|
|
||||||
r + a + l + y,really
|
|
||||||
r + a + l + y + Dup,really
|
|
||||||
r + a + t + p,part
|
|
||||||
r + a + t + s + Dup,start
|
|
||||||
r + w,were
|
|
||||||
r + w + h,where
|
|
||||||
r + w + t,water
|
|
||||||
r + w + a + n + s,answer
|
|
||||||
r + g + t,great
|
|
||||||
r + g + a + l,large
|
|
||||||
r + v + y,very
|
|
||||||
r + v + n,never
|
|
||||||
r + v + l,later
|
|
||||||
r + m + f,form
|
|
||||||
r + c + a,car
|
|
||||||
r + c + a + y,carry
|
|
||||||
r + c + a + y + Dup,carry
|
|
||||||
r + u + n,run
|
|
||||||
r + u + t + h,through
|
|
||||||
r + u + t + n,turn
|
|
||||||
r + ' + t + s,story
|
|
||||||
r + o,or
|
|
||||||
r + o + f,for
|
|
||||||
r + o + t + y + s,story
|
|
||||||
r + o + a + n + h,another
|
|
||||||
r + o + w,work
|
|
||||||
r + o + w + d,word
|
|
||||||
r + o + w + l + d,world
|
|
||||||
r + o + g,grow
|
|
||||||
r + o + LeftDoubleClick,grow
|
|
||||||
r + o + k + w,work
|
|
||||||
r + o + m,more
|
|
||||||
r + o + m + f,from
|
|
||||||
r + o + m + t + h,mother
|
|
||||||
r + o + u,our
|
|
||||||
r + o + u + f,four
|
|
||||||
r + o + u + y,your
|
|
||||||
r + o + u + a,around
|
|
||||||
r + o + u + a + n,around
|
|
||||||
r + o + u + a + n + d,around
|
|
||||||
r + o + u + g,group
|
|
||||||
r + o + u + g + p,group
|
|
||||||
r + o + u + g + t + h,through
|
|
||||||
r + o + u + c + t + n,country
|
|
||||||
r + o + u + c + t + n + y,country
|
|
||||||
r + i + t + h,their
|
|
||||||
r + i + t + s + f,first
|
|
||||||
r + i + a,air
|
|
||||||
r + i + w + t,write
|
|
||||||
r + i + g + h,right
|
|
||||||
r + i + g + l,girl
|
|
||||||
r + i + g + t + h,right
|
|
||||||
r + i + v,river
|
|
||||||
r + i + v + Dup,river
|
|
||||||
r + i + m + t + n + p,important
|
|
||||||
r + i + c + l + h,children
|
|
||||||
|
3
src/lib/assets/keymaps/keymap.d.ts
vendored
3
src/lib/assets/keymaps/keymap.d.ts
vendored
@@ -16,4 +16,7 @@ export interface ActionInfo {
|
|||||||
variant: "left" | "right";
|
variant: "left" | "right";
|
||||||
variantOf: number;
|
variantOf: number;
|
||||||
keyCode: string;
|
keyCode: string;
|
||||||
|
printable?: boolean;
|
||||||
|
separator?: boolean;
|
||||||
|
breaking?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/lib/assets/layouts/t4g.yml
Normal file
10
src/lib/assets/layouts/t4g.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
name: T4G
|
||||||
|
col:
|
||||||
|
- row:
|
||||||
|
- switch: { e: 3, n: 5, w: 4, s: 6 }
|
||||||
|
- offset: [0.5, 0]
|
||||||
|
row:
|
||||||
|
- key: 2
|
||||||
|
- row:
|
||||||
|
- key: 0
|
||||||
|
- key: 1
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"GTM stands for Generative Text Menu and can be used to change your device's settings anywhere",
|
"GTM stands for Generative Text Menu and can be used to change your device's settings anywhere",
|
||||||
"Ambidextrous Throwover (aka Mirror Mode) is a mode designed for one-handed use",
|
"Ambidextrous Throwover (aka Mirror Mode) is a mode designed for one-handed use",
|
||||||
"Chentry stands for character entry, or typing letter by letter on a chording enabled device",
|
"Chentry stands for character entry, or typing letter by letter on a chording enabled device",
|
||||||
"Chord modifiers are hard-coded (as of now) and can be used in the English language to add conjucations and more",
|
"Chord modifiers are hard-coded (as of now) and can be used in the English language to add conjugations and more",
|
||||||
"You can use 'cursor warping' by adding arrow key actions to a chord, for example to chord '()' with the cursor in the middle of the brackets",
|
"You can use 'cursor warping' by adding arrow key actions to a chord, for example to chord '()' with the cursor in the middle of the brackets",
|
||||||
"An arpeggiate is a single key press that modifies the preceding chord. Modifiers can be arpeggiated",
|
"An arpeggiate is a single key press that modifies the preceding chord. Modifiers can be arpeggiated",
|
||||||
"Some actions are marked as a 'macro', which means the output is generated by a key sequence rather than a pure key press. Be cautious with those, as they can affect other keys when held together!",
|
"Some actions are marked as a 'macro', which means the output is generated by a key sequence rather than a pure key press. Be cautious with those, as they can affect other keys when held together!",
|
||||||
|
|||||||
@@ -1,118 +1,154 @@
|
|||||||
settings:
|
- name: spurring
|
||||||
0x1:
|
description: |
|
||||||
title: Enable Serial Header
|
"Chording only" mode which tells your device to output chords on a press
|
||||||
description: boolean 0 or 1, default is 0
|
rather than a press & release. It also enables you to jump from one
|
||||||
0x2:
|
chord to another without releasing everything and can be activated in
|
||||||
title: Enable Serial Logging
|
GTM or by chording both mirror keys. It can provide significant speed
|
||||||
description: boolean 0 or 1, default is 0
|
gains with chording, but also takes away the flexibility of character
|
||||||
0x3:
|
entry.
|
||||||
title: Enable Serial Debugging
|
items:
|
||||||
description: boolean 0 or 1, default is 0
|
- id: 0x41
|
||||||
0x4:
|
name: enable
|
||||||
title: Enable Serial Raw
|
range: [0, 1]
|
||||||
description: boolean 0 or 1, default is 0
|
- id: 0x43
|
||||||
0x5:
|
name: character counter timeout
|
||||||
title: Enable Serial Chord
|
range: [0, 240000]
|
||||||
description: boolean 0 or 1, default is 0
|
step: 1000
|
||||||
0x6:
|
scale: 0.001
|
||||||
title: Enable Serial Keyboard
|
unit: s
|
||||||
description: boolean 0 or 1, default is 0
|
- name: arpeggiates
|
||||||
0x7:
|
description: |
|
||||||
title: Enable Serial Mouse
|
Allows chord modifiers to be hit after instead of with a chord,
|
||||||
description: boolean 0 or 1, default is 0
|
and enables select keys to be placed before auto-spaces.
|
||||||
0x11:
|
items:
|
||||||
title: Enable USB HID Keyboard
|
- id: 0x51
|
||||||
description: boolean 0 or 1, default is 1
|
name: enable
|
||||||
0x12:
|
range: [0, 1]
|
||||||
title: Enable Character Entry
|
- id: 0x54
|
||||||
description: boolean 0 or 1
|
name: timeout
|
||||||
0x13:
|
range: [0, 2550]
|
||||||
title: GUI-CTRL Swap Mode
|
step: 10
|
||||||
description: boolean 0 or 1; 1 swaps keymap 0 and 1. (CCL only)
|
unit: ms
|
||||||
0x14:
|
- name: keyboard
|
||||||
title: Key Scan Duration
|
items:
|
||||||
description: scan rate described in milliseconds; default is 2ms = 500Hz
|
- id: 0x11
|
||||||
0x15:
|
name: enable
|
||||||
title: Key Debounce Press Duration
|
range: [0, 1]
|
||||||
description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite
|
- id: 0x12
|
||||||
0x16:
|
name: character entry
|
||||||
title: Key Debounce Release Duration
|
range: [0, 1]
|
||||||
description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite
|
- id: 0x13
|
||||||
0x17:
|
name: command option swap
|
||||||
title: Keyboard Output Character Microsecond Delays
|
range: [0, 1]
|
||||||
description: delay time in microseconds (one delay for press and again for release); default is 480us; max is 10240us; increments of 40us
|
description: |
|
||||||
0x21:
|
Swaps ⌥ and ⌘ to make transitioning between Mac and other systems easier.
|
||||||
title: Enable USB HID Mouse
|
- id: 0x14
|
||||||
description: boolean 0 or 1; default is 1
|
name: poll rate
|
||||||
0x22:
|
range: [0, 255]
|
||||||
title: Slow Mouse Speed
|
unit: Hz
|
||||||
description: pixels to move at the mouse poll rate; default for CC1 is 5 = 250px/s
|
inverse: 1000
|
||||||
0x23:
|
- id: 0x15
|
||||||
title: Fast Mouse Speed
|
name: debounce press
|
||||||
description: pixels to move at the mouse poll rate; default for CC1 is 25 = 1250px/s
|
range: [0, 255]
|
||||||
0x24:
|
unit: ms
|
||||||
title: Enable Active Mouse
|
- id: 0x16
|
||||||
description: boolean 0 or 1; moves mouse back and forth every 60s
|
name: debounce release
|
||||||
0x25:
|
range: [0, 255]
|
||||||
title: Mouse Scroll Speed
|
unit: ms
|
||||||
description: default is 1; polls at 1/4th the rate of the mouse move updates
|
- id: 0x17
|
||||||
0x26:
|
name: output delay
|
||||||
title: Mouse Poll Duration
|
range: [0, 10200]
|
||||||
description: poll rate described in milliseconds; default is 20ms = 50Hz
|
step: 40
|
||||||
0x31:
|
unit: µs
|
||||||
title: Enable Chording
|
- name: mouse
|
||||||
description: boolean 0 or 1
|
items:
|
||||||
0x32:
|
- id: 0x21
|
||||||
title: Enable Chording Character Counter Timeout
|
name: enable
|
||||||
description: boolean 0 or 1; default is 1
|
range: [0, 1]
|
||||||
0x33:
|
- id: 0x22
|
||||||
title: Chording Character Counter Timeout Timer
|
name: slow speed
|
||||||
description: 0-255 deciseconds; default is 40 or 4.0 seconds
|
range: [0, 255]
|
||||||
0x34:
|
unit: px
|
||||||
title: Chord Detection Press Tolerance(ms)
|
- id: 0x23
|
||||||
description: 1-50 milliseconds
|
name: fast speed
|
||||||
0x35:
|
range: [0, 255]
|
||||||
title: Chord Detection Release Tolerance(ms)
|
unit: px
|
||||||
description: 1-50 milliseconds
|
- id: 0x24
|
||||||
0x41:
|
name: caffeine
|
||||||
title: Enable Spurring
|
range: [0, 1]
|
||||||
description: boolean 0 or 1; default is 1
|
description: |
|
||||||
0x42:
|
Keeps computer alive by moving the mouse back and forth one pixel every 60s
|
||||||
title: Enable Spurring Character Counter Timeout
|
- id: 0x25
|
||||||
description: boolean 0 or 1; default is 1
|
name: scroll speed
|
||||||
0x43:
|
range: [0, 255]
|
||||||
title: Spurring Character Counter Timeout Timer
|
unit: pg
|
||||||
description: 0-255 seconds; default is 240
|
- id: 0x26
|
||||||
0x51:
|
name: poll rate
|
||||||
title: Enable Arpeggiates
|
range: [0, 255]
|
||||||
description: boolean 0 or 1; default is 1
|
unit: Hz
|
||||||
0x54:
|
inverse: 1000
|
||||||
title: Arpeggiate Tolerance
|
- name: chording
|
||||||
description: in milliseconds; default 800ms
|
items:
|
||||||
0x61:
|
- id: 0x31
|
||||||
title: Enable Compound Chording (coming soon)
|
name: enable
|
||||||
description: boolean 0 or 1; default is 0
|
range: [0, 1]
|
||||||
0x64:
|
- id: 0x33
|
||||||
title: Compound Tolerance
|
name: auto delete timeout
|
||||||
description: in milliseconds; default 1500ms
|
range: [0, 25500]
|
||||||
0x81:
|
step: 100
|
||||||
title: LED Brightness
|
- id: 0x34
|
||||||
description: 0-50 (CCL only); default is 5, which draws around 100 mA of current
|
name: press tolerance
|
||||||
0x82:
|
description: |
|
||||||
title: LED Color Code
|
Scales with the number of chord inputs.
|
||||||
description: Color Codes to be listed (CCL only)
|
range: [0, 255]
|
||||||
0x83:
|
unit: ms
|
||||||
title: Enable LED Key Highlight (coming soon)
|
- id: 0x35
|
||||||
description: boolean 0 or 1 (CCL only)
|
name: release tolerance
|
||||||
0x84:
|
description: |
|
||||||
title: Enable LEDs
|
Scales with the number of chord inputs.
|
||||||
description: boolean 0 or 1; default is 1 (CCL only)
|
range: [0, 255]
|
||||||
0x91:
|
unit: ms
|
||||||
title: Operating System
|
- name: leds
|
||||||
description: Operating system codes listed below
|
items:
|
||||||
0x92:
|
- id: 0x84
|
||||||
title: Enable Realtime Feedback
|
name: enable
|
||||||
description: boolean 0 or 1; default is 1
|
range: [0, 1]
|
||||||
0x93:
|
- id: 0x81
|
||||||
title: Enable CharaChorder Ready on startup
|
name: brightness
|
||||||
description: boolean 0 or 1; default is 1
|
range: [0, 50]
|
||||||
|
- id: 0x82
|
||||||
|
name: base color code
|
||||||
|
enum:
|
||||||
|
white: 0
|
||||||
|
red: 1
|
||||||
|
orange: 2
|
||||||
|
yellow: 3
|
||||||
|
charteuse: 4
|
||||||
|
green: 5
|
||||||
|
spring green: 6
|
||||||
|
cyan: 7
|
||||||
|
azure: 8
|
||||||
|
blue: 9
|
||||||
|
violet: 10
|
||||||
|
magenta: 11
|
||||||
|
rose: 12
|
||||||
|
rainbow: 13
|
||||||
|
- id: 0x83
|
||||||
|
name: highlight
|
||||||
|
range: [0, 1]
|
||||||
|
- name: misc
|
||||||
|
items:
|
||||||
|
- id: 0x91
|
||||||
|
name: operating system
|
||||||
|
enum:
|
||||||
|
windows: 0
|
||||||
|
mac: 1
|
||||||
|
linux: 2
|
||||||
|
ios: 3
|
||||||
|
android: 4
|
||||||
|
- id: 0x92
|
||||||
|
name: GTM realtime feedback
|
||||||
|
range: [0, 1]
|
||||||
|
- id: 0x93
|
||||||
|
name: startup message
|
||||||
|
range: [0, 1]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
settings,
|
settings,
|
||||||
} from "$lib/undo-redo.js";
|
} from "$lib/undo-redo.js";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { serialPort } from "../serial/connection";
|
import { activeProfile, serialPort } from "../serial/connection";
|
||||||
import { csvLayoutToJson, isCsvLayout } from "$lib/backup/compat/legacy-layout";
|
import { csvLayoutToJson, isCsvLayout } from "$lib/backup/compat/legacy-layout";
|
||||||
import { isCsvChords, csvChordsToJson } from "./compat/legacy-chords";
|
import { isCsvChords, csvChordsToJson } from "./compat/legacy-chords";
|
||||||
|
|
||||||
@@ -50,11 +50,9 @@ export function createLayoutBackup(): CharaLayoutFile {
|
|||||||
charaVersion: 1,
|
charaVersion: 1,
|
||||||
type: "layout",
|
type: "layout",
|
||||||
device: get(serialPort)?.device,
|
device: get(serialPort)?.device,
|
||||||
layout: get(layout).map((it) => it.map((it) => it.action)) as [
|
layout: (get(layout)[get(activeProfile)]?.map((it) =>
|
||||||
number[],
|
it.map((it) => it.action),
|
||||||
number[],
|
) ?? []) as [number[], number[], number[]],
|
||||||
number[],
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +68,7 @@ export function createSettingsBackup(): CharaSettingsFile {
|
|||||||
return {
|
return {
|
||||||
charaVersion: 1,
|
charaVersion: 1,
|
||||||
type: "settings",
|
type: "settings",
|
||||||
settings: get(settings).map((it) => it.value),
|
settings: get(settings)[get(activeProfile)]?.map((it) => it.value) ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,9 +95,11 @@ export function restoreFromFile(
|
|||||||
const recent = file.history[0];
|
const recent = file.history[0];
|
||||||
if (!recent) return;
|
if (!recent) return;
|
||||||
let backupDevice = recent[1].device;
|
let backupDevice = recent[1].device;
|
||||||
if (backupDevice === "TWO") backupDevice = "ONE";
|
if (backupDevice === "TWO" || backupDevice === "M4G")
|
||||||
|
backupDevice = "ONE";
|
||||||
let currentDevice = get(serialPort)?.device;
|
let currentDevice = get(serialPort)?.device;
|
||||||
if (currentDevice === "TWO") currentDevice = "ONE";
|
if (currentDevice === "TWO" || backupDevice === "M4G")
|
||||||
|
currentDevice = "ONE";
|
||||||
|
|
||||||
if (backupDevice !== currentDevice) {
|
if (backupDevice !== currentDevice) {
|
||||||
alert("Backup is incompatible with this device");
|
alert("Backup is incompatible with this device");
|
||||||
@@ -107,32 +107,32 @@ export function restoreFromFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
changes.update((changes) => {
|
changes.update((changes) => {
|
||||||
changes.push(
|
changes.push([
|
||||||
...getChangesFromChordFile(recent[0]),
|
...getChangesFromChordFile(recent[0]),
|
||||||
...getChangesFromLayoutFile(recent[1]),
|
...getChangesFromLayoutFile(recent[1]),
|
||||||
...getChangesFromSettingsFile(recent[2]),
|
...getChangesFromSettingsFile(recent[2]),
|
||||||
);
|
]);
|
||||||
return changes;
|
return changes;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "chords": {
|
case "chords": {
|
||||||
changes.update((changes) => {
|
changes.update((changes) => {
|
||||||
changes.push(...getChangesFromChordFile(file));
|
changes.push(getChangesFromChordFile(file));
|
||||||
return changes;
|
return changes;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "layout": {
|
case "layout": {
|
||||||
changes.update((changes) => {
|
changes.update((changes) => {
|
||||||
changes.push(...getChangesFromLayoutFile(file));
|
changes.push(getChangesFromLayoutFile(file));
|
||||||
return changes;
|
return changes;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "settings": {
|
case "settings": {
|
||||||
changes.update((changes) => {
|
changes.update((changes) => {
|
||||||
changes.push(...getChangesFromSettingsFile(file));
|
changes.push(getChangesFromSettingsFile(file));
|
||||||
return changes;
|
return changes;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -167,12 +167,13 @@ export function getChangesFromChordFile(file: CharaChordFile) {
|
|||||||
export function getChangesFromSettingsFile(file: CharaSettingsFile) {
|
export function getChangesFromSettingsFile(file: CharaSettingsFile) {
|
||||||
const changes: Change[] = [];
|
const changes: Change[] = [];
|
||||||
for (const [id, value] of file.settings.entries()) {
|
for (const [id, value] of file.settings.entries()) {
|
||||||
const setting = get(settings)[id];
|
const setting = get(settings)[get(activeProfile)]?.[id];
|
||||||
if (setting !== undefined && setting.value !== value) {
|
if (setting !== undefined && setting.value !== value) {
|
||||||
changes.push({
|
changes.push({
|
||||||
type: ChangeType.Setting,
|
type: ChangeType.Setting,
|
||||||
id,
|
id,
|
||||||
setting: value,
|
setting: value,
|
||||||
|
profile: get(activeProfile),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,12 +184,13 @@ export function getChangesFromLayoutFile(file: CharaLayoutFile) {
|
|||||||
const changes: Change[] = [];
|
const changes: Change[] = [];
|
||||||
for (const [layer, keys] of file.layout.entries()) {
|
for (const [layer, keys] of file.layout.entries()) {
|
||||||
for (const [id, action] of keys.entries()) {
|
for (const [id, action] of keys.entries()) {
|
||||||
if (get(layout)[layer]?.[id]?.action !== action) {
|
if (get(layout)[get(activeProfile)]?.[layer]?.[id]?.action !== action) {
|
||||||
changes.push({
|
changes.push({
|
||||||
type: ChangeType.Layout,
|
type: ChangeType.Layout,
|
||||||
layer,
|
layer,
|
||||||
id,
|
id,
|
||||||
action,
|
action,
|
||||||
|
profile: get(activeProfile),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/lib/ccos/attachment.ts
Normal file
26
src/lib/ccos/attachment.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { Attachment } from "svelte/attachments";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import { persistentWritable } from "$lib/storage";
|
||||||
|
|
||||||
|
export const emulatedCCOS = persistentWritable("emulatedCCOS", false);
|
||||||
|
|
||||||
|
export function ccosKeyInterceptor() {
|
||||||
|
return ((element: Window) => {
|
||||||
|
const ccos = browser
|
||||||
|
? import("./ccos").then((module) => module.fetchCCOS(".test"))
|
||||||
|
: Promise.resolve(undefined);
|
||||||
|
|
||||||
|
function onEvent(event: KeyboardEvent) {
|
||||||
|
ccos.then((it) => it?.handleKeyEvent(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
element.addEventListener("keydown", onEvent, true);
|
||||||
|
element.addEventListener("keyup", onEvent, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ccos.then((it) => it?.destroy());
|
||||||
|
element.removeEventListener("keydown", onEvent, true);
|
||||||
|
element.removeEventListener("keyup", onEvent, true);
|
||||||
|
};
|
||||||
|
}) satisfies Attachment<Window>;
|
||||||
|
}
|
||||||
37
src/lib/ccos/ccos-events.ts
Normal file
37
src/lib/ccos/ccos-events.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export interface CCOSInitEvent {
|
||||||
|
type: "init";
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CCOSKeyPressEvent {
|
||||||
|
type: "press";
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CCOSKeyReleaseEvent {
|
||||||
|
type: "release";
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CCOSSerialEvent {
|
||||||
|
type: "serial";
|
||||||
|
data: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CCOSInEvent =
|
||||||
|
| CCOSInitEvent
|
||||||
|
| CCOSKeyPressEvent
|
||||||
|
| CCOSKeyReleaseEvent
|
||||||
|
| CCOSSerialEvent;
|
||||||
|
|
||||||
|
export interface CCOSReportEvent {
|
||||||
|
type: "report";
|
||||||
|
modifiers: number;
|
||||||
|
keys: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CCOSReadyEvent {
|
||||||
|
type: "ready";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CCOSOutEvent = CCOSReportEvent | CCOSReadyEvent | CCOSSerialEvent;
|
||||||
111
src/lib/ccos/ccos-interop.ts
Normal file
111
src/lib/ccos/ccos-interop.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
export const KEYCODE_TO_SCANCODE = new Map<string, number | undefined>(
|
||||||
|
Object.entries({
|
||||||
|
KeyA: 0x04,
|
||||||
|
KeyB: 0x05,
|
||||||
|
KeyC: 0x06,
|
||||||
|
KeyD: 0x07,
|
||||||
|
KeyE: 0x08,
|
||||||
|
KeyF: 0x09,
|
||||||
|
KeyG: 0x0a,
|
||||||
|
KeyH: 0x0b,
|
||||||
|
KeyI: 0x0c,
|
||||||
|
KeyJ: 0x0d,
|
||||||
|
KeyK: 0x0e,
|
||||||
|
KeyL: 0x0f,
|
||||||
|
KeyM: 0x10,
|
||||||
|
KeyN: 0x11,
|
||||||
|
KeyO: 0x12,
|
||||||
|
KeyP: 0x13,
|
||||||
|
KeyQ: 0x14,
|
||||||
|
KeyR: 0x15,
|
||||||
|
KeyS: 0x16,
|
||||||
|
KeyT: 0x17,
|
||||||
|
KeyU: 0x18,
|
||||||
|
KeyV: 0x19,
|
||||||
|
KeyW: 0x1a,
|
||||||
|
KeyX: 0x1b,
|
||||||
|
KeyY: 0x1c,
|
||||||
|
KeyZ: 0x1d,
|
||||||
|
Digit1: 0x1e,
|
||||||
|
Digit2: 0x1f,
|
||||||
|
Digit3: 0x20,
|
||||||
|
Digit4: 0x21,
|
||||||
|
Digit5: 0x22,
|
||||||
|
Digit6: 0x23,
|
||||||
|
Digit7: 0x24,
|
||||||
|
Digit8: 0x25,
|
||||||
|
Digit9: 0x26,
|
||||||
|
Digit0: 0x27,
|
||||||
|
Enter: 0x28,
|
||||||
|
Escape: 0x29,
|
||||||
|
Backspace: 0x2a,
|
||||||
|
Tab: 0x2b,
|
||||||
|
Space: 0x2c,
|
||||||
|
Minus: 0x2d,
|
||||||
|
Equal: 0x2e,
|
||||||
|
BracketLeft: 0x2f,
|
||||||
|
BracketRight: 0x30,
|
||||||
|
Backslash: 0x31,
|
||||||
|
Semicolon: 0x33,
|
||||||
|
Quote: 0x34,
|
||||||
|
Backquote: 0x35,
|
||||||
|
Comma: 0x36,
|
||||||
|
Period: 0x37,
|
||||||
|
Slash: 0x38,
|
||||||
|
CapsLock: 0x39,
|
||||||
|
F1: 0x3a,
|
||||||
|
F2: 0x3b,
|
||||||
|
F3: 0x3c,
|
||||||
|
F4: 0x3d,
|
||||||
|
F5: 0x3e,
|
||||||
|
F6: 0x3f,
|
||||||
|
F7: 0x40,
|
||||||
|
F8: 0x41,
|
||||||
|
F9: 0x42,
|
||||||
|
F10: 0x43,
|
||||||
|
F11: 0x44,
|
||||||
|
F12: 0x45,
|
||||||
|
PrintScreen: 0x46,
|
||||||
|
ScrollLock: 0x47,
|
||||||
|
Pause: 0x48,
|
||||||
|
Insert: 0x49,
|
||||||
|
Home: 0x4a,
|
||||||
|
PageUp: 0x4b,
|
||||||
|
Delete: 0x4c,
|
||||||
|
End: 0x4d,
|
||||||
|
PageDown: 0x4e,
|
||||||
|
ArrowRight: 0x4f,
|
||||||
|
ArrowLeft: 0x50,
|
||||||
|
ArrowDown: 0x51,
|
||||||
|
ArrowUp: 0x52,
|
||||||
|
NumLock: 0x53,
|
||||||
|
NumpadDivide: 0x54,
|
||||||
|
NumpadMultiply: 0x55,
|
||||||
|
NumpadSubtract: 0x56,
|
||||||
|
NumpadAdd: 0x57,
|
||||||
|
NumpadEnter: 0x58,
|
||||||
|
Numpad1: 0x59,
|
||||||
|
Numpad2: 0x5a,
|
||||||
|
Numpad3: 0x5b,
|
||||||
|
Numpad4: 0x5c,
|
||||||
|
Numpad5: 0x5d,
|
||||||
|
Numpad6: 0x5e,
|
||||||
|
Numpad7: 0x5f,
|
||||||
|
Numpad8: 0x60,
|
||||||
|
Numpad9: 0x61,
|
||||||
|
Numpad0: 0x62,
|
||||||
|
NumpadDecimal: 0x63,
|
||||||
|
ControlLeft: 0xe0,
|
||||||
|
ShiftLeft: 0xe1,
|
||||||
|
AltLeft: 0xe2,
|
||||||
|
MetaLeft: 0xe3,
|
||||||
|
ControlRight: 0xe4,
|
||||||
|
ShiftRight: 0xe5,
|
||||||
|
AltRight: 0xe6,
|
||||||
|
MetaRight: 0xe7,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SCANCODE_TO_KEYCODE = new Map<number, string>(
|
||||||
|
KEYCODE_TO_SCANCODE.entries().map(([key, value]) => [value!, key]),
|
||||||
|
);
|
||||||
210
src/lib/ccos/ccos.ts
Normal file
210
src/lib/ccos/ccos.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { getMeta } from "$lib/meta/meta-storage";
|
||||||
|
import { connectable, from, multicast, Subject } from "rxjs";
|
||||||
|
import type {
|
||||||
|
CCOSInitEvent,
|
||||||
|
CCOSKeyPressEvent,
|
||||||
|
CCOSKeyReleaseEvent,
|
||||||
|
CCOSOutEvent,
|
||||||
|
} from "./ccos-events";
|
||||||
|
import { KEYCODE_TO_SCANCODE, SCANCODE_TO_KEYCODE } from "./ccos-interop";
|
||||||
|
|
||||||
|
const device = ".zero_wasm";
|
||||||
|
|
||||||
|
class CCOSKeyboardEvent extends KeyboardEvent {
|
||||||
|
constructor(...params: ConstructorParameters<typeof KeyboardEvent>) {
|
||||||
|
super(...params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MASK_CTRL = 0b0001_0001;
|
||||||
|
const MASK_SHIFT = 0b0010_0010;
|
||||||
|
const MASK_ALT = 0b0100_0100;
|
||||||
|
const MASK_ALT_GRAPH = 0b0000_0100;
|
||||||
|
const MASK_GUI = 0b1000_1000;
|
||||||
|
|
||||||
|
export class CCOS {
|
||||||
|
private readonly currKeys = new Set<number>();
|
||||||
|
|
||||||
|
private readonly layout = new Map<string, string>();
|
||||||
|
|
||||||
|
private readonly worker = new Worker("/ccos-worker.js", { type: "module" });
|
||||||
|
|
||||||
|
private ready = false;
|
||||||
|
|
||||||
|
private lastEvent?: KeyboardEvent;
|
||||||
|
|
||||||
|
private onKey(
|
||||||
|
type: ConstructorParameters<typeof KeyboardEvent>[0],
|
||||||
|
modifiers: number,
|
||||||
|
scanCode: number,
|
||||||
|
) {
|
||||||
|
if (!this.lastEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = SCANCODE_TO_KEYCODE.get(scanCode);
|
||||||
|
if (code === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const layoutKey = [code];
|
||||||
|
if (modifiers & MASK_SHIFT) {
|
||||||
|
layoutKey.push("Shift");
|
||||||
|
}
|
||||||
|
if (modifiers & MASK_ALT_GRAPH) {
|
||||||
|
layoutKey.push("AltGraph");
|
||||||
|
}
|
||||||
|
const key = this.layout.get(JSON.stringify(layoutKey)) ?? code;
|
||||||
|
|
||||||
|
const params: Required<KeyboardEventInit> = {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
location: this.lastEvent.location,
|
||||||
|
repeat: this.lastEvent.repeat,
|
||||||
|
detail: this.lastEvent.detail,
|
||||||
|
view: this.lastEvent.view,
|
||||||
|
isComposing: this.lastEvent.isComposing,
|
||||||
|
which: this.lastEvent.which,
|
||||||
|
composed: this.lastEvent.composed,
|
||||||
|
key,
|
||||||
|
code,
|
||||||
|
charCode: key.charCodeAt(0),
|
||||||
|
keyCode: this.lastEvent.keyCode,
|
||||||
|
shiftKey: (modifiers & MASK_SHIFT) !== 0,
|
||||||
|
ctrlKey: (modifiers & MASK_CTRL) !== 0,
|
||||||
|
metaKey: (modifiers & MASK_GUI) !== 0,
|
||||||
|
altKey: (modifiers & MASK_ALT) !== 0,
|
||||||
|
modifierAltGraph: (modifiers & MASK_ALT_GRAPH) !== 0,
|
||||||
|
modifierCapsLock: this.lastEvent.getModifierState("CapsLock"),
|
||||||
|
modifierFn: this.lastEvent.getModifierState("Fn"),
|
||||||
|
modifierFnLock: this.lastEvent.getModifierState("FnLock"),
|
||||||
|
modifierHyper: this.lastEvent.getModifierState("Hyper"),
|
||||||
|
modifierNumLock: this.lastEvent.getModifierState("NumLock"),
|
||||||
|
modifierSuper: (modifiers & MASK_GUI) !== 0,
|
||||||
|
modifierSymbol: this.lastEvent.getModifierState("Symbol"),
|
||||||
|
modifierSymbolLock: this.lastEvent.getModifierState("SymbolLock"),
|
||||||
|
modifierScrollLock: this.lastEvent.getModifierState("ScrollLock"),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.lastEvent.target?.dispatchEvent(new CCOSKeyboardEvent(type, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
private onReport(modifiers: number, keys: number[]) {
|
||||||
|
const nextKeys = new Set<number>(keys);
|
||||||
|
nextKeys.delete(0);
|
||||||
|
for (const key of this.currKeys) {
|
||||||
|
if (!nextKeys.has(key)) {
|
||||||
|
this.onKey("keyup", modifiers, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of nextKeys) {
|
||||||
|
if (!this.currKeys.has(key)) {
|
||||||
|
this.onKey("keydown", modifiers, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.currKeys.clear();
|
||||||
|
for (const key of keys) {
|
||||||
|
this.currKeys.add(key);
|
||||||
|
}
|
||||||
|
this.currKeys.delete(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private outStream = new Subject<number>();
|
||||||
|
|
||||||
|
private readonly buffer: number[] = [];
|
||||||
|
private readonly outStream = new WritableStream<number>({
|
||||||
|
start(controller) {},
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly readable = connectable()
|
||||||
|
readonly writable = new WritableStream<string>();
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
this.worker.addEventListener(
|
||||||
|
"message",
|
||||||
|
(event: MessageEvent<CCOSOutEvent>) => {
|
||||||
|
switch (event.data.type) {
|
||||||
|
case "ready": {
|
||||||
|
this.ready = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "report": {
|
||||||
|
this.onReport(event.data.modifiers, event.data.keys);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "serial": {
|
||||||
|
this.outStream.next(event.data.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
(navigator as any).keyboard
|
||||||
|
?.getLayoutMap()
|
||||||
|
?.then((it: Map<string, string>) =>
|
||||||
|
it.entries().forEach(([key, value]) => {
|
||||||
|
this.layout.set(JSON.stringify([key]), value);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.worker.postMessage({
|
||||||
|
type: "init",
|
||||||
|
url,
|
||||||
|
} satisfies CCOSInitEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy() {
|
||||||
|
this.worker.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleKeyEvent(event: KeyboardEvent) {
|
||||||
|
if (
|
||||||
|
event.target instanceof HTMLInputElement ||
|
||||||
|
event.target instanceof HTMLTextAreaElement
|
||||||
|
) {
|
||||||
|
console.error("CCOS does not support input elements");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.ready || event instanceof CCOSKeyboardEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
this.lastEvent = event;
|
||||||
|
|
||||||
|
const layoutKey = [event.code];
|
||||||
|
if (event.getModifierState("Shift")) {
|
||||||
|
layoutKey.push("Shift");
|
||||||
|
}
|
||||||
|
if (event.getModifierState("AltGraph")) {
|
||||||
|
layoutKey.push("AltGraph");
|
||||||
|
}
|
||||||
|
this.layout.set(JSON.stringify(layoutKey), event.key);
|
||||||
|
|
||||||
|
const scanCode = KEYCODE_TO_SCANCODE.get(event.code);
|
||||||
|
if (scanCode === undefined) return;
|
||||||
|
if (event.type === "keydown") {
|
||||||
|
this.worker.postMessage({
|
||||||
|
type: "press",
|
||||||
|
code: scanCode,
|
||||||
|
} satisfies CCOSKeyPressEvent);
|
||||||
|
} else {
|
||||||
|
this.worker.postMessage({
|
||||||
|
type: "release",
|
||||||
|
code: scanCode,
|
||||||
|
} satisfies CCOSKeyReleaseEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCCOS(
|
||||||
|
version = ".test",
|
||||||
|
fetch: typeof window.fetch = window.fetch,
|
||||||
|
): Promise<CCOS | undefined> {
|
||||||
|
const meta = await getMeta(device, version, fetch);
|
||||||
|
if (!meta?.update.js || !meta?.update.wasm) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CCOS(`${meta.path}/${meta.update.js}`);
|
||||||
|
}
|
||||||
@@ -113,15 +113,15 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg > :global(text) {
|
svg > :global(text) {
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
dominant-baseline: middle;
|
dominant-baseline: middle;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,24 +83,24 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
section {
|
section {
|
||||||
|
display: grid;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
display: grid;
|
|
||||||
height: 3em;
|
height: 3em;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating {
|
.rating {
|
||||||
font-weight: bold;
|
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile {
|
.tile {
|
||||||
|
border-radius: 0.1em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0.2em;
|
height: 0.2em;
|
||||||
border-radius: 0.1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
@@ -112,19 +112,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chord {
|
.chord {
|
||||||
will-change: transform, opacity, scale;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-inline-end: 1em;
|
|
||||||
padding-inline: 0.1em;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transition:
|
transition:
|
||||||
opacity 0.3s ease,
|
opacity 0.3s ease,
|
||||||
translate 0.3s ease,
|
translate 0.3s ease,
|
||||||
scale 0.3s ease;
|
scale 0.3s ease;
|
||||||
|
will-change: transform, opacity, scale;
|
||||||
|
margin-inline-end: 1em;
|
||||||
|
padding-inline: 0.1em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
20
src/lib/charrecorder/TrackText.svelte
Normal file
20
src/lib/charrecorder/TrackText.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import type { ReplayPlayer } from "./core/player";
|
||||||
|
import { TextPlugin } from "./core/plugins/text";
|
||||||
|
|
||||||
|
const player: { player: ReplayPlayer | undefined } = getContext("replay");
|
||||||
|
|
||||||
|
let { text = $bindable("") } = $props();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!player.player) return;
|
||||||
|
const tracker = new TextPlugin();
|
||||||
|
tracker.register(player.player);
|
||||||
|
const unsubscribe = tracker.subscribe((value) => {
|
||||||
|
text = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
20
src/lib/charrecorder/TrackWpm.svelte
Normal file
20
src/lib/charrecorder/TrackWpm.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import { WpmReplayPlugin } from "./core/plugins/wpm";
|
||||||
|
import type { ReplayPlayer } from "./core/player";
|
||||||
|
|
||||||
|
const player: { player: ReplayPlayer | undefined } = getContext("replay");
|
||||||
|
|
||||||
|
let { wpm = $bindable(0) } = $props();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!player.player) return;
|
||||||
|
const tracker = new WpmReplayPlugin();
|
||||||
|
tracker.register(player.player);
|
||||||
|
const unsubscribe = tracker.subscribe((value) => {
|
||||||
|
wpm = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -85,7 +85,6 @@ export class ChordsReplayPlugin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(this.tokens);
|
|
||||||
|
|
||||||
clearTimeout(this.timeout);
|
clearTimeout(this.timeout);
|
||||||
if (replay.stepper.held.size === 0) {
|
if (replay.stepper.held.size === 0) {
|
||||||
|
|||||||
23
src/lib/charrecorder/core/plugins/text.ts
Normal file
23
src/lib/charrecorder/core/plugins/text.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ReplayPlayer } from "../player";
|
||||||
|
import type { ReplayPlugin, StoreContract } from "../types";
|
||||||
|
|
||||||
|
export class TextPlugin implements StoreContract<string>, ReplayPlugin {
|
||||||
|
private subscribers = new Set<(value: string) => void>();
|
||||||
|
|
||||||
|
register(replay: ReplayPlayer) {
|
||||||
|
replay.subscribe(() => {
|
||||||
|
if (this.subscribers.size === 0) return;
|
||||||
|
const text = replay.stepper.text
|
||||||
|
.filter((it) => it.source !== "ghost")
|
||||||
|
.map((it) => it.text)
|
||||||
|
.join("");
|
||||||
|
for (const subscription of this.subscribers) {
|
||||||
|
subscription(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
subscribe(subscription: (value: string) => void) {
|
||||||
|
this.subscribers.add(subscription);
|
||||||
|
return () => this.subscribers.delete(subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ export class TextRenderer {
|
|||||||
);
|
);
|
||||||
this.cursorNode.setAttribute("x", "0");
|
this.cursorNode.setAttribute("x", "0");
|
||||||
this.cursorNode.setAttribute("y", "0");
|
this.cursorNode.setAttribute("y", "0");
|
||||||
|
this.cursorNode.setAttribute("class", "cursor");
|
||||||
this.svg.appendChild(this.cursorNode);
|
this.svg.appendChild(this.cursorNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,16 +37,16 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.avatar {
|
.avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-placeholder {
|
.avatar-placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,8 +61,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
overflow-y: auto;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
|
|||||||
73
src/lib/chat/MatrixRooms.svelte
Normal file
73
src/lib/chat/MatrixRooms.svelte
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Room } from "matrix-js-sdk";
|
||||||
|
import { matrixClient, currentRoomId } from "./chat";
|
||||||
|
|
||||||
|
let { rooms }: { rooms: Room[] } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="rooms">
|
||||||
|
{#each $matrixClient.getRooms() as room}
|
||||||
|
{@const avatar = room.getMxcAvatarUrl()}
|
||||||
|
<button
|
||||||
|
class:active={$currentRoomId === room.roomId}
|
||||||
|
class="room"
|
||||||
|
onclick={() => ($currentRoomId = room.roomId)}
|
||||||
|
>
|
||||||
|
{#if avatar}
|
||||||
|
<img
|
||||||
|
alt={room.name}
|
||||||
|
src={$matrixClient.mxcUrlToHttp(avatar, 16, 16)}
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div>#</div>
|
||||||
|
{/if}
|
||||||
|
<div>{room.name}</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#await $matrixClient.publicRooms()}
|
||||||
|
<div>Loading...</div>
|
||||||
|
{:then rooms}
|
||||||
|
{#each rooms.chunk as room}
|
||||||
|
<button class="room" onclick={() => ($currentRoomId = room.roomId)}>
|
||||||
|
<div>#</div>
|
||||||
|
<div>{room.name}</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{:catch error}
|
||||||
|
<div>{error.message}</div>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.rooms {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
padding-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
padding-block: 2px;
|
||||||
|
padding-block: 4px;
|
||||||
|
width: 100%;
|
||||||
|
height: unset;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--md-sys-color-primary-container);
|
||||||
|
color: var(--md-sys-color-on-primary-container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -177,22 +177,18 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
$border-radius: 16px;
|
$border-radius: 16px;
|
||||||
|
|
||||||
h2 {
|
|
||||||
height: min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
border: 1px solid var(--md-sys-color-outline);
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
|
border: 1px solid var(--md-sys-color-outline);
|
||||||
|
border-radius: $border-radius;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
border-radius: $border-radius;
|
|
||||||
|
|
||||||
text-wrap: wrap;
|
text-wrap: wrap;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -201,9 +197,9 @@
|
|||||||
|
|
||||||
.input-box {
|
.input-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding-block: 8px;
|
padding-block: 8px;
|
||||||
flex-shrink: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,39 +209,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timeline {
|
.timeline {
|
||||||
contain: content;
|
|
||||||
height: auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-x: hidden;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
contain: content;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
height: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
.back-to-present {
|
overflow-y: scroll;
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-controls {
|
|
||||||
position: sticky;
|
|
||||||
bottom: 0;
|
|
||||||
min-height: 16px;
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
transparent,
|
|
||||||
var(--md-sys-color-background)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
109
src/lib/chat/chat-rx.ts
Normal file
109
src/lib/chat/chat-rx.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { derived, writable, type Writable } from "svelte/store";
|
||||||
|
import type {
|
||||||
|
ClientEvent,
|
||||||
|
LoginResponse,
|
||||||
|
MatrixClient,
|
||||||
|
RoomMember,
|
||||||
|
} from "matrix-js-sdk";
|
||||||
|
import { persistentWritable } from "$lib/storage";
|
||||||
|
import {
|
||||||
|
themeFromSourceColor,
|
||||||
|
argbFromHex,
|
||||||
|
type CustomColorGroup,
|
||||||
|
} from "@material/material-color-utilities";
|
||||||
|
import type { UserTheme } from "$lib/preferences";
|
||||||
|
import { MatrixRx } from "./matrix-rx/client";
|
||||||
|
|
||||||
|
export const matrixClient: Writable<MatrixClient> = writable();
|
||||||
|
|
||||||
|
export const isLoggedIn: Writable<boolean> = writable(false);
|
||||||
|
|
||||||
|
export const matrix = derived(
|
||||||
|
[matrixClient, isLoggedIn],
|
||||||
|
([matrixClient, isLoggedIn]) =>
|
||||||
|
isLoggedIn ? new MatrixRx(matrixClient) : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const currentRoomId = persistentWritable<string | null>(
|
||||||
|
"currentRoomId",
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
function getStoredLogin(): LoginResponse | undefined {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem("matrix-login")!);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storeLogin(response: LoginResponse) {
|
||||||
|
localStorage.setItem("matrix-login", JSON.stringify(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initMatrixClient() {
|
||||||
|
const { createClient, IndexedDBStore, IndexedDBCryptoStore } = await import(
|
||||||
|
"matrix-js-sdk"
|
||||||
|
);
|
||||||
|
|
||||||
|
const storedLogin = getStoredLogin();
|
||||||
|
|
||||||
|
const store = new IndexedDBStore({
|
||||||
|
dbName: "matrix",
|
||||||
|
indexedDB: window.indexedDB,
|
||||||
|
});
|
||||||
|
const cryptoStore = new IndexedDBCryptoStore(
|
||||||
|
window.indexedDB,
|
||||||
|
"matrix-crypto",
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
baseUrl: import.meta.env.VITE_MATRIX_URL,
|
||||||
|
userId: storedLogin?.user_id,
|
||||||
|
accessToken: storedLogin?.access_token,
|
||||||
|
timelineSupport: true,
|
||||||
|
store,
|
||||||
|
cryptoStore,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("store");
|
||||||
|
await store.startup();
|
||||||
|
console.log("cryptoStore");
|
||||||
|
await cryptoStore.startup();
|
||||||
|
console.log("client");
|
||||||
|
await client.startClient();
|
||||||
|
client.once("sync" as ClientEvent.Sync, () => {
|
||||||
|
isLoggedIn.set(client.isLoggedIn());
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginToken = new URLSearchParams(window.location.search).get(
|
||||||
|
"loginToken",
|
||||||
|
);
|
||||||
|
if (loginToken) {
|
||||||
|
storeLogin(await client.loginWithToken(loginToken));
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
isLoggedIn.set(client.isLoggedIn());
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixClient.set(client);
|
||||||
|
console.log("done");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function memberColor(
|
||||||
|
member: RoomMember,
|
||||||
|
theme: UserTheme,
|
||||||
|
): CustomColorGroup {
|
||||||
|
let hash = 0;
|
||||||
|
member.userId.split("").forEach((char) => {
|
||||||
|
hash = char.charCodeAt(0) + ((hash << 5) - hash);
|
||||||
|
});
|
||||||
|
let color = "#";
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 0xff;
|
||||||
|
color += value.toString(16).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
return themeFromSourceColor(argbFromHex(theme.color), [
|
||||||
|
{ value: argbFromHex(color), name: "member", blend: true },
|
||||||
|
]).customColors.find((c) => c.color.name === "member")!;
|
||||||
|
}
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
import { derived, writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import type {
|
import type { MatrixClient, RoomMember } from "matrix-js-sdk";
|
||||||
ClientEvent,
|
|
||||||
LoginResponse,
|
|
||||||
MatrixClient,
|
|
||||||
RoomMember,
|
|
||||||
} from "matrix-js-sdk";
|
|
||||||
import { persistentWritable } from "$lib/storage";
|
import { persistentWritable } from "$lib/storage";
|
||||||
import {
|
import {
|
||||||
themeFromSourceColor,
|
themeFromSourceColor,
|
||||||
@@ -12,83 +7,14 @@ import {
|
|||||||
type CustomColorGroup,
|
type CustomColorGroup,
|
||||||
} from "@material/material-color-utilities";
|
} from "@material/material-color-utilities";
|
||||||
import type { UserTheme } from "$lib/preferences";
|
import type { UserTheme } from "$lib/preferences";
|
||||||
import { MatrixRx } from "./matrix-rx/client";
|
|
||||||
|
|
||||||
export const matrixClient: Writable<MatrixClient> = writable();
|
export const matrixClient: Writable<MatrixClient> = writable();
|
||||||
|
|
||||||
export const isLoggedIn: Writable<boolean> = writable(false);
|
|
||||||
|
|
||||||
export const matrix = derived(
|
|
||||||
[matrixClient, isLoggedIn],
|
|
||||||
([matrixClient, isLoggedIn]) =>
|
|
||||||
isLoggedIn ? new MatrixRx(matrixClient) : undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const currentRoomId = persistentWritable<string | null>(
|
export const currentRoomId = persistentWritable<string | null>(
|
||||||
"currentRoomId",
|
"currentRoomId",
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
function getStoredLogin(): LoginResponse | undefined {
|
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem("matrix-login")!);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function storeLogin(response: LoginResponse) {
|
|
||||||
localStorage.setItem("matrix-login", JSON.stringify(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initMatrixClient() {
|
|
||||||
const { createClient, IndexedDBStore, IndexedDBCryptoStore } = await import(
|
|
||||||
"matrix-js-sdk"
|
|
||||||
);
|
|
||||||
|
|
||||||
const storedLogin = getStoredLogin();
|
|
||||||
|
|
||||||
const store = new IndexedDBStore({
|
|
||||||
dbName: "matrix",
|
|
||||||
indexedDB: window.indexedDB,
|
|
||||||
});
|
|
||||||
const cryptoStore = new IndexedDBCryptoStore(
|
|
||||||
window.indexedDB,
|
|
||||||
"matrix-crypto",
|
|
||||||
);
|
|
||||||
|
|
||||||
const client = createClient({
|
|
||||||
baseUrl: import.meta.env.VITE_MATRIX_URL,
|
|
||||||
userId: storedLogin?.user_id,
|
|
||||||
accessToken: storedLogin?.access_token,
|
|
||||||
timelineSupport: true,
|
|
||||||
store,
|
|
||||||
cryptoStore,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("store");
|
|
||||||
await store.startup();
|
|
||||||
console.log("cryptoStore");
|
|
||||||
await cryptoStore.startup();
|
|
||||||
console.log("client");
|
|
||||||
await client.startClient();
|
|
||||||
client.once("sync" as ClientEvent.Sync, () => {
|
|
||||||
isLoggedIn.set(client.isLoggedIn());
|
|
||||||
});
|
|
||||||
|
|
||||||
const loginToken = new URLSearchParams(window.location.search).get(
|
|
||||||
"loginToken",
|
|
||||||
);
|
|
||||||
if (loginToken) {
|
|
||||||
storeLogin(await client.loginWithToken(loginToken));
|
|
||||||
window.history.replaceState({}, document.title, window.location.pathname);
|
|
||||||
isLoggedIn.set(client.isLoggedIn());
|
|
||||||
}
|
|
||||||
|
|
||||||
matrixClient.set(client);
|
|
||||||
console.log("done");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function memberColor(
|
export function memberColor(
|
||||||
member: RoomMember,
|
member: RoomMember,
|
||||||
theme: UserTheme,
|
theme: UserTheme,
|
||||||
|
|||||||
@@ -191,6 +191,14 @@
|
|||||||
onmouseout={() => (toolbarHover = false)}
|
onmouseout={() => (toolbarHover = false)}
|
||||||
onblur={() => (toolbarHover = false)}
|
onblur={() => (toolbarHover = false)}
|
||||||
>
|
>
|
||||||
|
{#if event.getType() === "m.room.message"}
|
||||||
|
{@const message = event.event.content?.["body"]}
|
||||||
|
<a
|
||||||
|
class="icon rocket"
|
||||||
|
href="/learn/sentence/?sentence={encodeURIComponent(message)}"
|
||||||
|
>rocket_launch</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
<button class="icon">add_reaction</button>
|
<button class="icon">add_reaction</button>
|
||||||
<button class="icon">reply</button>
|
<button class="icon">reply</button>
|
||||||
{#if event.event.content?.["m.replay"]}
|
{#if event.event.content?.["m.replay"]}
|
||||||
@@ -231,21 +239,37 @@
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes rocket {
|
||||||
|
0% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
transform: translate(4px, -4px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.icon.rocket {
|
||||||
|
animation: rocket 2s;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -26px;
|
top: -26px;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: var(--md-sys-color-secondary-container);
|
|
||||||
color: var(--md-sys-color-on-secondary-container);
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--md-sys-color-secondary-container);
|
||||||
|
padding: 4px;
|
||||||
|
color: var(--md-sys-color-on-secondary-container);
|
||||||
|
|
||||||
|
a,
|
||||||
button {
|
button {
|
||||||
font-size: 16px;
|
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,10 +289,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dot {
|
.dot {
|
||||||
|
animation: bounce 1s infinite;
|
||||||
|
border-radius: 50%;
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 50%;
|
|
||||||
animation: bounce 1s infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender,
|
.sender,
|
||||||
@@ -278,10 +302,10 @@
|
|||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
grid-area: avatar;
|
grid-area: avatar;
|
||||||
|
translate: 0 2px;
|
||||||
|
border-radius: 50%;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 50%;
|
|
||||||
translate: 0 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.avatar {
|
div.avatar {
|
||||||
@@ -298,18 +322,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reactions {
|
.reactions {
|
||||||
grid-area: reactions;
|
|
||||||
margin-top: 2px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
grid-area: reactions;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reaction {
|
.reaction {
|
||||||
border: 1px solid var(--md-sys-color-outline);
|
|
||||||
padding: 6px;
|
|
||||||
border-radius: 6px;
|
|
||||||
height: 24px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
border: 1px solid var(--md-sys-color-outline);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
height: 24px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
> .count {
|
> .count {
|
||||||
@@ -320,16 +344,16 @@
|
|||||||
.event {
|
.event {
|
||||||
display: grid;
|
display: grid;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-inline: 0.5em;
|
grid-template-columns: 32px 1fr auto;
|
||||||
margin-inline: 0.5em;
|
|
||||||
padding-block: 0.25em;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"avatar sender date"
|
"avatar sender date"
|
||||||
"avatar content content"
|
"avatar content content"
|
||||||
"none reactions reactions";
|
"none reactions reactions";
|
||||||
grid-template-columns: 32px 1fr auto;
|
margin-inline: 0.5em;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-inline: 0.5em;
|
||||||
|
padding-block: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -346,12 +370,12 @@
|
|||||||
|
|
||||||
.backdrop {
|
.backdrop {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
|
||||||
z-index: -1;
|
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
|
z-index: -1;
|
||||||
background: var(--md-sys-color-surface-variant);
|
inset: 0;
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -39,9 +39,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
border-radius: 8px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 16em;
|
max-height: 16em;
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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";
|
||||||
import { action as title } from "$lib/title";
|
|
||||||
import { osLayout } from "$lib/os-layout";
|
import { osLayout } from "$lib/os-layout";
|
||||||
|
import { tooltip } from "$lib/hover-popover";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
action,
|
action,
|
||||||
@@ -11,47 +11,56 @@
|
|||||||
|
|
||||||
let info = $derived(
|
let info = $derived(
|
||||||
typeof action === "number"
|
typeof action === "number"
|
||||||
? (KEYMAP_CODES.get(action) ?? { code: action })
|
? ($KEYMAP_CODES.get(action) ?? { code: action })
|
||||||
: action,
|
: action,
|
||||||
);
|
);
|
||||||
let dynamicMapping = $derived(info.keyCode && $osLayout.get(info.keyCode));
|
let dynamicMapping = $derived(info.keyCode && $osLayout.get(info.keyCode));
|
||||||
|
|
||||||
let tooltip = $derived(
|
let popover: HTMLElement | undefined = $state(undefined);
|
||||||
`<${info.id ?? `0x${info.code.toString(16)}`}> ` +
|
|
||||||
(info.title ?? "") +
|
|
||||||
(info.variant === "left"
|
|
||||||
? " (left)"
|
|
||||||
: info.variant === "right"
|
|
||||||
? " (right)"
|
|
||||||
: ""),
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#snippet popoverSnippet()}
|
||||||
|
<div bind:this={popover} popover="hint">
|
||||||
|
<{info.id ?? `0x${info.code.toString(16)}`}>
|
||||||
|
{#if info.title}
|
||||||
|
{info.title}
|
||||||
|
{/if}
|
||||||
|
{#if info.variant === "left"}
|
||||||
|
(Left)
|
||||||
|
{:else if info.variant === "right"}
|
||||||
|
(Right)
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
{#if display === "keys"}
|
{#if display === "keys"}
|
||||||
<kbd
|
<kbd
|
||||||
class:icon={!!info.icon}
|
class:icon={!!info.icon}
|
||||||
class:left={info.variant === "left"}
|
class:left={info.variant === "left"}
|
||||||
class:right={info.variant === "right"}
|
class:right={info.variant === "right"}
|
||||||
use:title={{ title: tooltip }}
|
{@attach tooltip(popover)}
|
||||||
>
|
>
|
||||||
{dynamicMapping ??
|
{dynamicMapping ??
|
||||||
info.icon ??
|
info.icon ??
|
||||||
info.display ??
|
info.display ??
|
||||||
info.id ??
|
info.id ??
|
||||||
`0x${info.code.toString(16)}`}
|
`0x${info.code.toString(16)}`}
|
||||||
|
{@render popoverSnippet()}
|
||||||
</kbd>
|
</kbd>
|
||||||
{:else if display === "inline-keys"}
|
{:else if display === "inline-keys"}
|
||||||
{#if !info.icon && dynamicMapping?.length === 1}
|
{#if !info.icon && dynamicMapping?.length === 1}
|
||||||
<span
|
<span
|
||||||
use:title={{ title: tooltip }}
|
{@attach tooltip(popover)}
|
||||||
class:left={info.variant === "left"}
|
class:left={info.variant === "left"}
|
||||||
class:right={info.variant === "right"}>{dynamicMapping}</span
|
class:right={info.variant === "right"}
|
||||||
|
>{dynamicMapping}{@render popoverSnippet()}</span
|
||||||
>
|
>
|
||||||
{:else if !info.icon && info.id?.length === 1}
|
{:else if !info.icon && info.id?.length === 1}
|
||||||
<span
|
<span
|
||||||
use:title={{ title: tooltip }}
|
{@attach tooltip(popover)}
|
||||||
class:left={info.variant === "left"}
|
class:left={info.variant === "left"}
|
||||||
class:right={info.variant === "right"}>{info.id}</span
|
class:right={info.variant === "right"}
|
||||||
|
>{info.id}{@render popoverSnippet()}</span
|
||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<kbd
|
<kbd
|
||||||
@@ -59,22 +68,22 @@
|
|||||||
class:left={info.variant === "left"}
|
class:left={info.variant === "left"}
|
||||||
class:right={info.variant === "right"}
|
class:right={info.variant === "right"}
|
||||||
class:icon={!!info.icon}
|
class:icon={!!info.icon}
|
||||||
use:title={{ title: tooltip }}
|
{@attach tooltip(popover)}
|
||||||
>
|
>
|
||||||
{dynamicMapping ??
|
{dynamicMapping ??
|
||||||
info.icon ??
|
info.icon ??
|
||||||
info.display ??
|
info.display ??
|
||||||
info.id ??
|
info.id ??
|
||||||
`0x${info.code.toString(16)}`}</kbd
|
`0x${info.code.toString(16)}`}{@render popoverSnippet()}</kbd
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
kbd:not(.inline-kbd) {
|
kbd:not(.inline-kbd) {
|
||||||
height: 24px;
|
|
||||||
padding-block: auto;
|
|
||||||
transition: color 250ms ease;
|
transition: color 250ms ease;
|
||||||
|
padding-block: auto;
|
||||||
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
@@ -84,17 +93,6 @@
|
|||||||
border-right-width: 3px;
|
border-right-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dynamic {
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 1px;
|
|
||||||
min-width: 8px;
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
|
|
||||||
&.inline {
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-kbd {
|
.inline-kbd {
|
||||||
margin-inline-end: 2px;
|
margin-inline-end: 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
$props();
|
$props();
|
||||||
|
|
||||||
let key = $derived(
|
let key = $derived(
|
||||||
(typeof id === "number" ? KEYMAP_CODES.get(id) ?? id : id) as
|
(typeof id === "number" ? ($KEYMAP_CODES.get(id) ?? id) : id) as
|
||||||
| number
|
| number
|
||||||
| KeyInfo,
|
| KeyInfo,
|
||||||
);
|
);
|
||||||
@@ -48,33 +48,33 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
button {
|
button {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
margin: 0;
|
|
||||||
padding: 8px;
|
|
||||||
|
|
||||||
font-family: "Noto Sans Mono", monospace;
|
font-family: "Noto Sans Mono", monospace;
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
@media not (forced-colors: active) {
|
@media not (forced-colors: active) {
|
||||||
color: inherit;
|
|
||||||
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
outline: none;
|
outline: none;
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (forced-colors: active) {
|
@media (forced-colors: active) {
|
||||||
border: 1px solid ButtonBorder;
|
|
||||||
margin-block: 4px;
|
margin-block: 4px;
|
||||||
|
border: 1px solid ButtonBorder;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ActiveText;
|
color: ActiveText;
|
||||||
@@ -86,8 +86,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
text-align: start;
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/lib/components/AnimatedNumber.svelte
Normal file
51
src/lib/components/AnimatedNumber.svelte
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fade, slide } from "svelte/transition";
|
||||||
|
|
||||||
|
let { value }: { value: number } = $props();
|
||||||
|
|
||||||
|
let digits: number[] = $derived(value.toString().split("").map(Number));
|
||||||
|
const nums = Array.from({ length: 10 }, (_, i) => i);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="digits" style:width="{digits.length}ch">
|
||||||
|
{#each digits as digit, i (digits.length - i)}
|
||||||
|
<div
|
||||||
|
class="digit-wrapper"
|
||||||
|
style:right="{digits.length - 1 - i}ch"
|
||||||
|
transition:fade
|
||||||
|
>
|
||||||
|
{#each nums as num (num)}
|
||||||
|
<div
|
||||||
|
class="digit"
|
||||||
|
style:transform="translateY({(digit - num) / 4}em)"
|
||||||
|
style:opacity={digit === num ? 1 : 0}
|
||||||
|
>
|
||||||
|
{num}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.digits {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
transition: width 500ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit-wrapper {
|
||||||
|
display: inline-grid;
|
||||||
|
width: 1ch;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit {
|
||||||
|
display: inline-block;
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
transition:
|
||||||
|
transform 500ms ease,
|
||||||
|
opacity 500ms ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
button {
|
button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--md-sys-color-on-background);
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--md-sys-color-on-background);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -33,63 +33,61 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
form {
|
form {
|
||||||
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
contain: strict;
|
contain: strict;
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
border-radius: 16px;
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--md-sys-color-on-secondary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
font-family: "Noto Sans Mono", monospace;
|
font-family: "Noto Sans Mono", monospace;
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset::before {
|
fieldset::before {
|
||||||
content: "$";
|
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
|
content: "$";
|
||||||
|
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
appearance: none;
|
||||||
margin-block-start: -16px;
|
margin-block-start: -16px;
|
||||||
|
border: none;
|
||||||
|
background: var(--md-sys-color-secondary);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
padding-block-start: 24px;
|
|
||||||
padding-inline-start: calc(8px + 1.5ch);
|
padding-inline-start: calc(8px + 1.5ch);
|
||||||
|
padding-block-start: 24px;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--md-sys-color-on-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
font-family: "Noto Sans Mono", monospace;
|
font-family: "Noto Sans Mono", monospace;
|
||||||
font-weight: 600;
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
|
|
||||||
appearance: none;
|
|
||||||
background: var(--md-sys-color-secondary);
|
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.io {
|
.io {
|
||||||
--scrollbar-color: var(--md-sys-color-secondary);
|
--scrollbar-color: var(--md-sys-color-secondary);
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
border-radius: 0 0 16px 16px;
|
||||||
|
|
||||||
overflow-y: auto;
|
background: var(--md-sys-color-secondary-container);
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
||||||
color: var(--md-sys-color-on-secondary-container);
|
overflow-y: auto;
|
||||||
|
|
||||||
background: var(--md-sys-color-secondary-container);
|
color: var(--md-sys-color-on-secondary-container);
|
||||||
border-radius: 0 0 16px 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
@@ -99,10 +97,10 @@
|
|||||||
fieldset {
|
fieldset {
|
||||||
all: unset;
|
all: unset;
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
|
||||||
transition: opacity 250ms ease;
|
transition: opacity 250ms ease;
|
||||||
@@ -113,16 +111,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.anchor {
|
.anchor {
|
||||||
overflow-anchor: auto;
|
|
||||||
height: 1px;
|
height: 1px;
|
||||||
|
overflow-anchor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
code,
|
code,
|
||||||
samp,
|
samp,
|
||||||
p {
|
p {
|
||||||
display: block;
|
display: block;
|
||||||
overflow-anchor: none;
|
|
||||||
margin-block: 0.15rem;
|
margin-block: 0.15rem;
|
||||||
|
overflow-anchor: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@@ -130,24 +128,24 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
margin-block-end: 1rem;
|
margin-block-end: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-secondary);
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
color: var(--md-sys-color-on-secondary);
|
||||||
|
|
||||||
background: var(--md-sys-color-secondary);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code::before {
|
code::before {
|
||||||
content: "> ";
|
|
||||||
margin-block-end: 0.25rem;
|
margin-block-end: 0.25rem;
|
||||||
font-weight: 900;
|
content: "> ";
|
||||||
color: var(--md-sys-color-primary);
|
color: var(--md-sys-color-primary);
|
||||||
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
color: var(--md-sys-color-background);
|
|
||||||
background: var(--md-sys-color-on-background);
|
background: var(--md-sys-color-on-background);
|
||||||
|
color: var(--md-sys-color-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
|
|
||||||
:global(kbd.icon) {
|
:global(kbd.icon) {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
font-size: inherit;
|
|
||||||
translate: 0 0.2em;
|
translate: 0 0.2em;
|
||||||
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
KEYMAP_CATEGORIES,
|
KEYMAP_CATEGORIES,
|
||||||
KEYMAP_CODES,
|
KEYMAP_CODES,
|
||||||
KEYMAP_IDS,
|
KEYMAP_IDS,
|
||||||
|
type KeyInfo,
|
||||||
} from "$lib/serial/keymap-codes";
|
} from "$lib/serial/keymap-codes";
|
||||||
import FlexSearch from "flexsearch";
|
import FlexSearch from "flexsearch";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import ActionListItem from "$lib/components/ActionListItem.svelte";
|
import ActionListItem from "$lib/components/ActionListItem.svelte";
|
||||||
import LL from "$i18n/i18n-svelte";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { action } from "$lib/title";
|
import { action } from "$lib/title";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
currentAction = undefined,
|
currentAction = undefined,
|
||||||
@@ -27,10 +29,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const index = new FlexSearch.Index({ tokenize: "full" });
|
const index = new FlexSearch.Index({ tokenize: "full" });
|
||||||
createIndex();
|
|
||||||
|
|
||||||
async function createIndex() {
|
$effect(() => {
|
||||||
for (const [, action] of KEYMAP_CODES) {
|
createIndex($KEYMAP_CODES);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createIndex(codes: Map<number, KeyInfo>) {
|
||||||
|
for (const [, action] of codes) {
|
||||||
await index?.addAsync(
|
await index?.addAsync(
|
||||||
action.code,
|
action.code,
|
||||||
`${action.title || ""} ${action.variant || ""} ${action.category} ${action.id || ""} ${
|
`${action.title || ""} ${action.variant || ""} ${action.category} ${action.id || ""} ${
|
||||||
@@ -42,7 +47,7 @@
|
|||||||
|
|
||||||
async function search() {
|
async function search() {
|
||||||
results = (await index!.searchAsync(searchBox.value)) as number[];
|
results = (await index!.searchAsync(searchBox.value)) as number[];
|
||||||
exact = KEYMAP_IDS.get(searchBox.value)?.code;
|
exact = get(KEYMAP_IDS).get(searchBox.value)?.code;
|
||||||
code = Number(searchBox.value);
|
code = Number(searchBox.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +87,7 @@
|
|||||||
|
|
||||||
let searchBox: HTMLInputElement;
|
let searchBox: HTMLInputElement;
|
||||||
let resultList: HTMLUListElement;
|
let resultList: HTMLUListElement;
|
||||||
let filter = $state(new Set<number>());
|
let filter: Set<number> | undefined = $state(undefined);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={keyboardNavigation} />
|
<svelte:window on:keydown={keyboardNavigation} />
|
||||||
@@ -127,7 +132,7 @@
|
|||||||
bind:group={filter}
|
bind:group={filter}
|
||||||
/></label
|
/></label
|
||||||
>
|
>
|
||||||
{#each KEYMAP_CATEGORIES as category}
|
{#each $KEYMAP_CATEGORIES as category}
|
||||||
<label
|
<label
|
||||||
>{category.name}<input
|
>{category.name}<input
|
||||||
name="category"
|
name="category"
|
||||||
@@ -167,7 +172,7 @@
|
|||||||
{#if filter !== undefined || results.length > 0}
|
{#if filter !== undefined || results.length > 0}
|
||||||
{@const resultValue =
|
{@const resultValue =
|
||||||
results.length === 0
|
results.length === 0
|
||||||
? Array.from(KEYMAP_CODES, ([it]) => it)
|
? Array.from($KEYMAP_CODES, ([it]) => it)
|
||||||
: results}
|
: results}
|
||||||
{#each filter ? resultValue.filter( (it) => filter.has(it), ) : resultValue as id (id)}
|
{#each filter ? resultValue.filter( (it) => filter.has(it), ) : resultValue as id (id)}
|
||||||
<li><ActionListItem {id} onclick={() => select(id)} /></li>
|
<li><ActionListItem {id} onclick={() => select(id)} /></li>
|
||||||
@@ -184,18 +189,17 @@
|
|||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
height: unset;
|
border: 1px solid currentcolor;
|
||||||
padding-block: 2px;
|
border-radius: 6px;
|
||||||
padding-inline: 4px;
|
padding-inline: 4px;
|
||||||
|
padding-block: 2px;
|
||||||
|
height: unset;
|
||||||
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
border: 1px solid currentcolor;
|
|
||||||
border-radius: 6px;
|
|
||||||
|
|
||||||
&:has(:checked) {
|
&:has(:checked) {
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
background: var(--md-sys-color-secondary);
|
background: var(--md-sys-color-secondary);
|
||||||
|
color: var(--md-sys-color-on-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
@@ -206,34 +210,33 @@
|
|||||||
|
|
||||||
dialog {
|
dialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
width: 100%;
|
border: none;
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
background: rgba(0 0 0 / 60%);
|
background: rgba(0 0 0 / 60%);
|
||||||
|
|
||||||
border: none;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside {
|
aside {
|
||||||
pointer-events: none;
|
opacity: 0.4;
|
||||||
|
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
|
|
||||||
opacity: 0.4;
|
|
||||||
border: 1px dashed var(--md-sys-color-outline);
|
border: 1px dashed var(--md-sys-color-outline);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
> h3 {
|
> h3 {
|
||||||
width: fit-content;
|
margin-inline-start: 16px;
|
||||||
margin-block-start: -13px;
|
margin-block-start: -13px;
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
margin-inline-start: 16px;
|
|
||||||
padding-inline: 8px;
|
|
||||||
|
|
||||||
background: var(--md-sys-color-background);
|
background: var(--md-sys-color-background);
|
||||||
|
padding-inline: 8px;
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-contrast: more) {
|
@media (prefers-contrast: more) {
|
||||||
@@ -248,26 +251,26 @@
|
|||||||
|
|
||||||
.search-row {
|
.search-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
margin-inline: 16px;
|
margin-inline: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
|
||||||
transform-origin: top left;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
transform-origin: top left;
|
||||||
|
border-radius: 16px;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-background);
|
||||||
|
|
||||||
width: calc(min(30cm, 90%));
|
width: calc(min(30cm, 90%));
|
||||||
height: calc(min(100% - 128px, 90%));
|
height: calc(min(100% - 128px, 90%));
|
||||||
|
|
||||||
color: var(--md-sys-color-on-background);
|
overflow: hidden;
|
||||||
|
|
||||||
background: var(--md-sys-color-background);
|
color: var(--md-sys-color-on-background);
|
||||||
border-radius: 16px;
|
|
||||||
|
|
||||||
@media (forced-colors: active) {
|
@media (forced-colors: active) {
|
||||||
border: 1px solid CanvasText;
|
border: 1px solid CanvasText;
|
||||||
@@ -275,39 +278,38 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
input[type="search"] {
|
input[type="search"] {
|
||||||
width: 100%;
|
transition: all 250ms ease;
|
||||||
height: 64px;
|
|
||||||
margin-block-end: 8px;
|
margin-block-end: 8px;
|
||||||
padding-inline: 16px;
|
|
||||||
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 16px;
|
|
||||||
color: currentcolor;
|
|
||||||
|
|
||||||
background: none;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid var(--md-sys-color-surface-variant);
|
border-bottom: 1px solid var(--md-sys-color-surface-variant);
|
||||||
|
|
||||||
transition: all 250ms ease;
|
background: none;
|
||||||
|
padding-inline: 16px;
|
||||||
|
width: 100%;
|
||||||
|
height: 64px;
|
||||||
|
color: currentcolor;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
font-family: inherit;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-bottom: 1px solid var(--md-sys-color-primary);
|
|
||||||
outline: none;
|
outline: none;
|
||||||
|
border-bottom: 1px solid var(--md-sys-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
--scrollbar-color: var(--md-sys-color-surface-variant);
|
--scrollbar-color: var(--md-sys-color-surface-variant);
|
||||||
|
|
||||||
scrollbar-gutter: both-edges stable;
|
|
||||||
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-inline: 4px;
|
padding-inline: 4px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
scrollbar-gutter: both-edges stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
@@ -317,27 +319,27 @@
|
|||||||
.exact {
|
.exact {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
width: 100%;
|
|
||||||
margin-block-start: 8px;
|
margin-block-start: 8px;
|
||||||
|
|
||||||
border: 1px solid var(--md-sys-color-primary);
|
border: 1px solid var(--md-sys-color-primary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
> i {
|
> i {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-primary);
|
||||||
|
|
||||||
padding-inline: 6px;
|
padding-inline: 6px;
|
||||||
|
|
||||||
color: var(--md-sys-color-on-primary);
|
color: var(--md-sys-color-on-primary);
|
||||||
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
border-radius: 0 0 8px 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (forced-colors: active) {
|
@media (forced-colors: active) {
|
||||||
|
|||||||
@@ -8,17 +8,16 @@
|
|||||||
import { dev } from "$app/environment";
|
import { dev } from "$app/environment";
|
||||||
import ActionSelector from "$lib/components/layout/ActionSelector.svelte";
|
import ActionSelector from "$lib/components/layout/ActionSelector.svelte";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import type { Writable } from "svelte/store";
|
|
||||||
import KeyboardKey from "$lib/components/layout/KeyboardKey.svelte";
|
import KeyboardKey from "$lib/components/layout/KeyboardKey.svelte";
|
||||||
import { getContext, mount, unmount } from "svelte";
|
import { getContext, mount, unmount } from "svelte";
|
||||||
import type { VisualLayoutConfig } from "./visual-layout.js";
|
import type { VisualLayoutConfig } from "./visual-layout.js";
|
||||||
import { changes, ChangeType, layout } from "$lib/undo-redo";
|
import { changes, ChangeType, layout } from "$lib/undo-redo";
|
||||||
import { fly } from "svelte/transition";
|
import { fly } from "svelte/transition";
|
||||||
import { expoOut } from "svelte/easing";
|
import { expoOut } from "svelte/easing";
|
||||||
|
import { activeLayer, activeProfile } from "$lib/serial/connection";
|
||||||
|
|
||||||
const { scale, margin, strokeWidth, fontSize, iconFontSize } =
|
const { scale, margin, strokeWidth, fontSize, iconFontSize } =
|
||||||
getContext<VisualLayoutConfig>("visual-layout-config");
|
getContext<VisualLayoutConfig>("visual-layout-config");
|
||||||
const activeLayer = getContext<Writable<number>>("active-layer");
|
|
||||||
|
|
||||||
if (dev) {
|
if (dev) {
|
||||||
// you have absolutely no idea what a difference this makes for performance
|
// you have absolutely no idea what a difference this makes for performance
|
||||||
@@ -125,8 +124,10 @@
|
|||||||
const keyInfo = layoutInfo.keys[index];
|
const keyInfo = layoutInfo.keys[index];
|
||||||
if (!keyInfo) return;
|
if (!keyInfo) return;
|
||||||
const clickedGroup = groupParent.children.item(index) as SVGGElement;
|
const clickedGroup = groupParent.children.item(index) as SVGGElement;
|
||||||
const nextAction = get(layout)[get(activeLayer)]?.[keyInfo.id];
|
const nextAction =
|
||||||
const currentAction = get(deviceLayout)[get(activeLayer)]?.[keyInfo.id];
|
get(layout)[get(activeProfile)]![get(activeLayer)]?.[keyInfo.id];
|
||||||
|
const currentAction =
|
||||||
|
get(deviceLayout)[get(activeProfile)]![get(activeLayer)]?.[keyInfo.id];
|
||||||
const component = mount(ActionSelector, {
|
const component = mount(ActionSelector, {
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: {
|
props: {
|
||||||
@@ -137,12 +138,15 @@
|
|||||||
},
|
},
|
||||||
onselect(action) {
|
onselect(action) {
|
||||||
changes.update((changes) => {
|
changes.update((changes) => {
|
||||||
changes.push({
|
changes.push([
|
||||||
type: ChangeType.Layout,
|
{
|
||||||
id: keyInfo.id,
|
type: ChangeType.Layout,
|
||||||
layer: get(activeLayer),
|
id: keyInfo.id,
|
||||||
action,
|
layer: get(activeLayer),
|
||||||
});
|
profile: get(activeProfile),
|
||||||
|
action,
|
||||||
|
},
|
||||||
|
]);
|
||||||
return changes;
|
return changes;
|
||||||
});
|
});
|
||||||
closed();
|
closed();
|
||||||
@@ -215,9 +219,9 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
svg {
|
svg {
|
||||||
overflow: visible;
|
|
||||||
grid-area: "d";
|
grid-area: "d";
|
||||||
width: calc(min(100%, 35cm));
|
width: calc(min(100%, 35cm));
|
||||||
max-height: calc(100% - 170px);
|
max-height: calc(100% - 170px);
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from "svelte";
|
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import type { VisualLayoutConfig } from "$lib/components/layout/visual-layout";
|
import type { VisualLayoutConfig } from "$lib/components/layout/visual-layout";
|
||||||
import type { CompiledLayoutKey } from "$lib/serialization/visual-layout";
|
import type { CompiledLayoutKey } from "$lib/serialization/visual-layout";
|
||||||
@@ -7,10 +6,14 @@
|
|||||||
import { osLayout } from "$lib/os-layout.js";
|
import { osLayout } from "$lib/os-layout.js";
|
||||||
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
|
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
|
||||||
import { action } from "$lib/title";
|
import { action } from "$lib/title";
|
||||||
|
import { activeProfile, activeLayer } from "$lib/serial/connection";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
const { fontSize, margin, inactiveOpacity, inactiveScale, iconFontSize } =
|
const { fontSize, margin, inactiveOpacity, inactiveScale, iconFontSize } =
|
||||||
getContext<VisualLayoutConfig>("visual-layout-config");
|
getContext<VisualLayoutConfig>("visual-layout-config");
|
||||||
const activeLayer = getContext<Writable<number>>("active-layer");
|
const currentAction = getContext<Writable<Set<number>> | undefined>(
|
||||||
|
"highlight-action",
|
||||||
|
);
|
||||||
|
|
||||||
let {
|
let {
|
||||||
key,
|
key,
|
||||||
@@ -30,12 +33,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each positions as position, layer}
|
{#each positions as position, layer}
|
||||||
{@const { action: actionId, isApplied } = $layout[layer]?.[key.id] ?? {
|
{@const { action: actionId, isApplied } = $layout[$activeProfile]?.[layer]?.[
|
||||||
|
key.id
|
||||||
|
] ?? {
|
||||||
action: 0,
|
action: 0,
|
||||||
isApplied: true,
|
isApplied: true,
|
||||||
}}
|
}}
|
||||||
{@const { code, icon, id, display, title, keyCode, variant } =
|
{@const { code, icon, id, display, title, keyCode, variant } =
|
||||||
KEYMAP_CODES.get(actionId) ?? { code: actionId }}
|
$KEYMAP_CODES.get(actionId) ?? { code: actionId }}
|
||||||
{@const dynamicMapping = keyCode && $osLayout.get(keyCode)}
|
{@const dynamicMapping = keyCode && $osLayout.get(keyCode)}
|
||||||
{@const tooltip =
|
{@const tooltip =
|
||||||
(title ?? id ?? `0x${code.toString(16)}`) +
|
(title ?? id ?? `0x${code.toString(16)}`) +
|
||||||
@@ -47,6 +52,7 @@
|
|||||||
]}
|
]}
|
||||||
{@const hasIcon = !dynamicMapping && !!icon}
|
{@const hasIcon = !dynamicMapping && !!icon}
|
||||||
<text
|
<text
|
||||||
|
class:hidden={$currentAction?.has(actionId) === false}
|
||||||
fill={isApplied ? "currentcolor" : "var(--md-sys-color-primary)"}
|
fill={isApplied ? "currentcolor" : "var(--md-sys-color-primary)"}
|
||||||
font-weight={isApplied ? "" : "bold"}
|
font-weight={isApplied ? "" : "bold"}
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
@@ -77,15 +83,15 @@
|
|||||||
$transition: 200ms;
|
$transition: 200ms;
|
||||||
|
|
||||||
text {
|
text {
|
||||||
will-change: translate, scale;
|
|
||||||
user-select: none;
|
|
||||||
transform-origin: center;
|
|
||||||
transform-box: fill-box;
|
transform-box: fill-box;
|
||||||
|
transform-origin: center;
|
||||||
transition:
|
transition:
|
||||||
fill #{$focus-transition} ease,
|
fill #{$focus-transition} ease,
|
||||||
opacity #{$transition} ease,
|
opacity #{$transition} ease,
|
||||||
translate #{$transition} ease,
|
translate #{$transition} ease,
|
||||||
scale #{$transition} ease;
|
scale #{$transition} ease;
|
||||||
|
will-change: translate, scale;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
@media (prefers-contrast: more) {
|
@media (prefers-contrast: more) {
|
||||||
--inactive-opacity: 0.8;
|
--inactive-opacity: 0.8;
|
||||||
@@ -96,4 +102,8 @@
|
|||||||
text:focus-within {
|
text:focus-within {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
text.hidden {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,11 +8,14 @@
|
|||||||
KeyboardEventHandler,
|
KeyboardEventHandler,
|
||||||
MouseEventHandler,
|
MouseEventHandler,
|
||||||
} from "svelte/elements";
|
} from "svelte/elements";
|
||||||
|
import { type Writable } from "svelte/store";
|
||||||
|
|
||||||
const { scale, margin, strokeWidth } = getContext<VisualLayoutConfig>(
|
const { scale, margin, strokeWidth } = getContext<VisualLayoutConfig>(
|
||||||
"visual-layout-config",
|
"visual-layout-config",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const highlight = getContext<Writable<Set<number>> | undefined>("highlight");
|
||||||
|
|
||||||
let {
|
let {
|
||||||
i,
|
i,
|
||||||
key,
|
key,
|
||||||
@@ -35,6 +38,8 @@
|
|||||||
|
|
||||||
<g
|
<g
|
||||||
class="key-group"
|
class="key-group"
|
||||||
|
class:highlight={$highlight?.has(key.id) === true}
|
||||||
|
class:faded={$highlight?.has(key.id) === false}
|
||||||
{onclick}
|
{onclick}
|
||||||
{onkeypress}
|
{onkeypress}
|
||||||
{onfocusin}
|
{onfocusin}
|
||||||
@@ -108,14 +113,14 @@
|
|||||||
$transition: 200ms;
|
$transition: 200ms;
|
||||||
|
|
||||||
rect {
|
rect {
|
||||||
transform-origin: center;
|
|
||||||
transform-box: fill-box;
|
transform-box: fill-box;
|
||||||
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
path,
|
path,
|
||||||
g {
|
g {
|
||||||
transform-origin: top left;
|
|
||||||
transform-box: fill-box;
|
transform-box: fill-box;
|
||||||
|
transform-origin: top left;
|
||||||
}
|
}
|
||||||
|
|
||||||
path,
|
path,
|
||||||
@@ -131,15 +136,17 @@
|
|||||||
stroke-opacity: 0.3;
|
stroke-opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.faded,
|
||||||
g:hover {
|
g:hover {
|
||||||
cursor: default;
|
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: opacity #{$transition} ease;
|
transition: opacity #{$transition} ease;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.highlight,
|
||||||
g:focus-within {
|
g:focus-within {
|
||||||
color: var(--md-sys-color-primary);
|
|
||||||
outline: none;
|
outline: none;
|
||||||
|
color: var(--md-sys-color-primary);
|
||||||
|
|
||||||
> path,
|
> path,
|
||||||
> rect {
|
> rect {
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { serialPort } from "$lib/serial/connection";
|
import { deviceMeta, serialPort } from "$lib/serial/connection";
|
||||||
import { action } from "$lib/title";
|
import { action } from "$lib/title";
|
||||||
import GenericLayout from "$lib/components/layout/GenericLayout.svelte";
|
import GenericLayout from "$lib/components/layout/GenericLayout.svelte";
|
||||||
import { getContext } from "svelte";
|
import { activeProfile, activeLayer } from "$lib/serial/connection";
|
||||||
import type { Writable } from "svelte/store";
|
|
||||||
import type { VisualLayout } from "$lib/serialization/visual-layout";
|
import type { VisualLayout } from "$lib/serialization/visual-layout";
|
||||||
import { fade } from "svelte/transition";
|
import { fade, fly } from "svelte/transition";
|
||||||
|
import { restoreFromFile } from "$lib/backup/backup";
|
||||||
let device = $derived($serialPort?.device);
|
|
||||||
const activeLayer = getContext<Writable<number>>("active-layer");
|
|
||||||
|
|
||||||
const layers = [
|
|
||||||
["Numeric Layer", "123", 1],
|
|
||||||
["Primary Layer", "abc", 0],
|
|
||||||
["Function Layer", "function", 2],
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const layouts = {
|
const layouts = {
|
||||||
ONE: () =>
|
ONE: () =>
|
||||||
@@ -41,23 +32,43 @@
|
|||||||
import("$lib/assets/layouts/m4gr.yml").then(
|
import("$lib/assets/layouts/m4gr.yml").then(
|
||||||
(it) => it.default as VisualLayout,
|
(it) => it.default as VisualLayout,
|
||||||
),
|
),
|
||||||
|
T4G: () =>
|
||||||
|
import("$lib/assets/layouts/t4g.yml").then(
|
||||||
|
(it) => it.default as VisualLayout,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{#if device}
|
{#if $serialPort}
|
||||||
{#await layouts[device]() then visualLayout}
|
{#await layouts[$serialPort.device]() then visualLayout}
|
||||||
<fieldset transition:fade>
|
<fieldset transition:fade>
|
||||||
{#each layers as [title, icon, value]}
|
<div class="layers">
|
||||||
|
{#each Array.from({ length: $serialPort.layerCount }, (_, i) => i) as layer}
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
onclick={() => ($activeLayer = layer)}
|
||||||
|
name="layer"
|
||||||
|
value={layer}
|
||||||
|
checked={$activeLayer === layer}
|
||||||
|
/>
|
||||||
|
{String.fromCodePoint(
|
||||||
|
"A".codePointAt(0)! + $activeProfile,
|
||||||
|
)}{layer + 1}
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if $deviceMeta?.factoryDefaults?.layout}
|
||||||
<button
|
<button
|
||||||
class="icon"
|
use:action={{ title: "Reset Layout" }}
|
||||||
use:action={{ title, shortcut: `alt+${value + 1}` }}
|
transition:fly={{ x: -8 }}
|
||||||
onclick={() => ($activeLayer = value)}
|
class="icon reset-layout"
|
||||||
class:active={$activeLayer === value}
|
onclick={() =>
|
||||||
|
restoreFromFile($deviceMeta!.factoryDefaults!.layout)}
|
||||||
|
>reset_wrench</button
|
||||||
>
|
>
|
||||||
{icon}
|
{/if}
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<GenericLayout {visualLayout} />
|
<GenericLayout {visualLayout} />
|
||||||
@@ -69,8 +80,8 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -78,62 +89,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
position: relative;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.icon {
|
.layers {
|
||||||
cursor: pointer;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
z-index: 1;
|
gap: 2px;
|
||||||
|
|
||||||
font-size: 24px;
|
margin-inline: auto;
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
|
||||||
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
transition: all 250ms ease;
|
|
||||||
|
|
||||||
&:nth-child(2) {
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
aspect-ratio: 1;
|
|
||||||
|
|
||||||
font-size: 32px;
|
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child,
|
|
||||||
&:last-child {
|
|
||||||
aspect-ratio: unset;
|
|
||||||
height: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-inline-end: -8px;
|
|
||||||
padding-inline: 4px 24px;
|
|
||||||
border-radius: 16px 0 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-inline-start: -8px;
|
|
||||||
padding-inline: 24px 4px;
|
|
||||||
border-radius: 0 16px 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background: var(--md-sys-color-tertiary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Dialog from "$lib/dialogs/Dialog.svelte";
|
import Dialog from "$lib/dialogs/Dialog.svelte";
|
||||||
import ActionString from "$lib/components/ActionString.svelte";
|
import type { Chord } from "$lib/serial/chord";
|
||||||
|
import ChordActionEdit from "../../routes/(app)/config/chords/ChordActionEdit.svelte";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
abortTitle,
|
abortTitle,
|
||||||
confirmTitle,
|
confirmTitle,
|
||||||
actions = [],
|
chord,
|
||||||
onabort,
|
onabort,
|
||||||
onconfirm,
|
onconfirm,
|
||||||
}: {
|
}: {
|
||||||
@@ -15,7 +16,7 @@
|
|||||||
message?: string;
|
message?: string;
|
||||||
abortTitle: string;
|
abortTitle: string;
|
||||||
confirmTitle: string;
|
confirmTitle: string;
|
||||||
actions: number[];
|
chord: Chord & { deleted: boolean };
|
||||||
onabort: () => void;
|
onabort: () => void;
|
||||||
onconfirm: () => void;
|
onconfirm: () => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
@@ -26,7 +27,20 @@
|
|||||||
{#if message}
|
{#if message}
|
||||||
<p>{@html message}</p>
|
<p>{@html message}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<p><ActionString {actions} /></p>
|
<p>
|
||||||
|
<ChordActionEdit
|
||||||
|
chord={{
|
||||||
|
...chord,
|
||||||
|
isApplied: false,
|
||||||
|
phraseChanged: false,
|
||||||
|
actionsChanged: false,
|
||||||
|
sortBy: "",
|
||||||
|
id: chord.actions,
|
||||||
|
}}
|
||||||
|
interactive={false}
|
||||||
|
onsubmit={() => {}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button onclick={onabort}>{abortTitle}</button>
|
<button onclick={onabort}>{abortTitle}</button>
|
||||||
<button class="primary" onclick={onconfirm}>{confirmTitle}</button>
|
<button class="primary" onclick={onconfirm}>{confirmTitle}</button>
|
||||||
|
|||||||
106
src/lib/dialogs/ConnectionFailed.svelte
Normal file
106
src/lib/dialogs/ConnectionFailed.svelte
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Dialog from "$lib/dialogs/Dialog.svelte";
|
||||||
|
import LL from "$i18n/i18n-svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
message,
|
||||||
|
onclose,
|
||||||
|
}: {
|
||||||
|
message: string;
|
||||||
|
onclose: () => void;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog>
|
||||||
|
{#if !navigator.serial}
|
||||||
|
<h1>Incompatible Browser</h1>
|
||||||
|
<p>Your browser does not support the Web Serial API.</p>
|
||||||
|
<p>Supported browsers are any Chromium based Browsers, such as</p>
|
||||||
|
<ul>
|
||||||
|
<li>Google Chrome</li>
|
||||||
|
<li>Microsoft Edge</li>
|
||||||
|
<li>Opera</li>
|
||||||
|
<li>Brave</li>
|
||||||
|
</ul>
|
||||||
|
{:else}
|
||||||
|
<h1>Connection Failed</h1>
|
||||||
|
<pre>{message}</pre>
|
||||||
|
<h2>Troubleshooting Steps</h2>
|
||||||
|
<ul>
|
||||||
|
{#if navigator.userAgent.includes("Linux")}
|
||||||
|
<li>
|
||||||
|
<p>{@html $LL.deviceManager.LINUX_PERMISSIONS()}</p>
|
||||||
|
<p>
|
||||||
|
In most cases you can simply follow the <a
|
||||||
|
target="_blank"
|
||||||
|
href="https://docs.arduino.cc/software/ide-v1/tutorials/Linux#please-read"
|
||||||
|
>Arduino Guide</a
|
||||||
|
> on serial port permissions.
|
||||||
|
</p>
|
||||||
|
<p>Special systems:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://wiki.archlinux.org/title/Arduino#Accessing_serial"
|
||||||
|
>Arch and Arch-based like Manjaro or EndeavourOS</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://gist.github.com/CMCDragonkai/d00201ec143c9f749fc49533034e5009?permalink_comment_id=4670311#gistcomment-4670311"
|
||||||
|
>NixOS</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://wiki.gentoo.org/wiki/Arduino#Grant_access_to_non-root_users"
|
||||||
|
>Gentoo</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
<li>
|
||||||
|
You device may be pre-CCOS. refer to <a
|
||||||
|
target="_blank"
|
||||||
|
href="https://docs.charachorder.com/CCOS.html#upgrade-to-ccos"
|
||||||
|
>Upgrade to CCOS</a
|
||||||
|
> on how to upgrade your device.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Some USB cables or hubs can cause issues, try directly connecting to a
|
||||||
|
port on your computer with the included cable.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="primary" onclick={onclose}>Close</button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
h1 {
|
||||||
|
color: var(--md-sys-color-error);
|
||||||
|
font-size: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
color: var(--md-sys-color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--md-sys-color-primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,15 +16,15 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
dialog {
|
dialog {
|
||||||
|
box-shadow: 0 0 48px rgba(0 0 0 / 60%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 38px;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-background);
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
max-width: 512px;
|
max-width: 512px;
|
||||||
|
|
||||||
color: var(--md-sys-color-on-background);
|
color: var(--md-sys-color-on-background);
|
||||||
|
|
||||||
background: var(--md-sys-color-background);
|
|
||||||
border: none;
|
|
||||||
border-radius: 38px;
|
|
||||||
box-shadow: 0 0 48px rgba(0 0 0 / 60%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog::backdrop {
|
dialog::backdrop {
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import ConfirmDialog from "$lib/dialogs/ConfirmDialog.svelte";
|
import ConfirmDialog from "$lib/dialogs/ConfirmDialog.svelte";
|
||||||
|
import { mount, unmount } from "svelte";
|
||||||
|
import type { Chord } from "$lib/serial/chord";
|
||||||
|
|
||||||
export async function askForConfirmation(
|
export async function askForConfirmation(
|
||||||
title: string,
|
title: string,
|
||||||
message: string,
|
message: string,
|
||||||
confirmTitle: string,
|
confirmTitle: string,
|
||||||
abortTitle: string,
|
abortTitle: string,
|
||||||
actions: number[],
|
chord: Chord,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const dialog = new ConfirmDialog({
|
let resolvePromise: (value: boolean) => void;
|
||||||
|
const resultPromise = new Promise<boolean>((resolve) => {
|
||||||
|
resolvePromise = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialog = mount(ConfirmDialog, {
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: {
|
props: {
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
confirmTitle,
|
confirmTitle,
|
||||||
abortTitle,
|
abortTitle,
|
||||||
actions,
|
chord,
|
||||||
|
onabort: () => resolvePromise(false),
|
||||||
|
onconfirm: () => resolvePromise(true),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let resolvePromise: (value: boolean) => void;
|
|
||||||
const resultPromise = new Promise<boolean>((resolve) => {
|
|
||||||
resolvePromise = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.$on("abort", () => resolvePromise(false));
|
|
||||||
dialog.$on("confirm", () => resolvePromise(true));
|
|
||||||
|
|
||||||
const result = await resultPromise;
|
const result = await resultPromise;
|
||||||
dialog.$destroy();
|
unmount(dialog);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/lib/dialogs/connection-failed-dialog.ts
Normal file
24
src/lib/dialogs/connection-failed-dialog.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import ConnectionFailed from "$lib/dialogs/ConnectionFailed.svelte";
|
||||||
|
import { mount, unmount } from "svelte";
|
||||||
|
|
||||||
|
export async function showConnectionFailedDialog(
|
||||||
|
message: string,
|
||||||
|
): Promise<void> {
|
||||||
|
let resolvePromise: (value: void) => void;
|
||||||
|
const resultPromise = new Promise<void>((resolve) => {
|
||||||
|
resolvePromise = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialog = mount(ConnectionFailed, {
|
||||||
|
target: document.body,
|
||||||
|
props: {
|
||||||
|
message,
|
||||||
|
onclose: () => resolvePromise(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await resultPromise;
|
||||||
|
unmount(dialog);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Material Symbols Rounded";
|
|
||||||
font-weight: 100 700;
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-weight: 100 700;
|
||||||
src: url("$lib/assets/icons.min.woff2") format("woff2");
|
src: url("$lib/assets/icons.min.woff2") format("woff2");
|
||||||
|
font-family: "Material Symbols Rounded";
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
direction: ltr;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
/* stylelint-disable-next-line */
|
/* stylelint-disable-next-line */
|
||||||
font-family: "Material Symbols Rounded";
|
font-family: "Material Symbols Rounded";
|
||||||
font-size: 24px;
|
|
||||||
font-feature-settings: "liga";
|
|
||||||
font-variation-settings:
|
font-variation-settings:
|
||||||
"FILL" var(--icon-fill, 0),
|
"FILL" var(--icon-fill, 0),
|
||||||
"wght" var(--icon-weigth, 400),
|
"wght" var(--icon-weigth, 400),
|
||||||
"GRAD" var(--icon-grade, 0);
|
"GRAD" var(--icon-grade, 0);
|
||||||
font-weight: normal;
|
font-feature-settings: "liga";
|
||||||
font-style: normal;
|
|
||||||
line-height: 1;
|
|
||||||
text-transform: none;
|
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
|
|
||||||
|
direction: ltr;
|
||||||
|
user-select: none;
|
||||||
|
text-transform: none;
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
transition: font-variation-settings 250ms ease;
|
transition: font-variation-settings 250ms ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,28 +66,30 @@
|
|||||||
|
|
||||||
/* noto-sans-mono-latin-ext-wght-normal */
|
/* noto-sans-mono-latin-ext-wght-normal */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Noto Sans Mono Variable";
|
|
||||||
font-weight: 100 900;
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-weight: 100 900;
|
||||||
font-stretch: 62.5% 100%;
|
font-stretch: 62.5% 100%;
|
||||||
src: url("@fontsource-variable/noto-sans-mono/files/noto-sans-mono-latin-ext-wght-normal.woff2")
|
src: url("@fontsource-variable/noto-sans-mono/files/noto-sans-mono-latin-ext-wght-normal.woff2")
|
||||||
format("woff2-variations");
|
format("woff2-variations");
|
||||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323,
|
font-family: "Noto Sans Mono Variable";
|
||||||
U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F,
|
font-display: swap;
|
||||||
|
unicode-range:
|
||||||
|
U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
|
||||||
|
U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F,
|
||||||
U+A720-A7FF;
|
U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* noto-sans-mono-latin-wght-normal */
|
/* noto-sans-mono-latin-wght-normal */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Noto Sans Mono";
|
|
||||||
font-weight: 100 900;
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-weight: 100 900;
|
||||||
font-stretch: 62.5% 100%;
|
font-stretch: 62.5% 100%;
|
||||||
src: url("@fontsource-variable/noto-sans-mono/files/noto-sans-mono-latin-wght-normal.woff2")
|
src: url("@fontsource-variable/noto-sans-mono/files/noto-sans-mono-latin-wght-normal.woff2")
|
||||||
format("woff2-variations");
|
format("woff2-variations");
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
font-family: "Noto Sans Mono";
|
||||||
U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F,
|
font-display: swap;
|
||||||
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||||
|
U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074,
|
||||||
|
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/lib/hover-popover.ts
Normal file
39
src/lib/hover-popover.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { Attachment } from "svelte/attachments";
|
||||||
|
|
||||||
|
export const hotkeys = new Map<string, HTMLElement>();
|
||||||
|
|
||||||
|
export function tooltip(
|
||||||
|
target: HTMLElement | undefined,
|
||||||
|
shortcut?: string,
|
||||||
|
): Attachment<HTMLElement> {
|
||||||
|
return (node: HTMLElement) => {
|
||||||
|
function show() {
|
||||||
|
if (!target) return;
|
||||||
|
target.showPopover({ source: node });
|
||||||
|
}
|
||||||
|
function hide() {
|
||||||
|
if (!target) return;
|
||||||
|
target.hidePopover();
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener("mouseenter", show);
|
||||||
|
node.addEventListener("focus", show);
|
||||||
|
node.addEventListener("mouseout", hide);
|
||||||
|
node.addEventListener("blur", hide);
|
||||||
|
|
||||||
|
if (shortcut && node instanceof HTMLElement) {
|
||||||
|
hotkeys.set(shortcut, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
node.removeEventListener("mouseenter", show);
|
||||||
|
node.removeEventListener("focus", show);
|
||||||
|
node.removeEventListener("mouseout", hide);
|
||||||
|
node.removeEventListener("blur", hide);
|
||||||
|
|
||||||
|
if (shortcut && node instanceof HTMLElement) {
|
||||||
|
hotkeys.delete(shortcut);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
150
src/lib/meta/meta-storage.ts
Normal file
150
src/lib/meta/meta-storage.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import type { RawVersionMeta, SettingsMeta, VersionMeta } from "./types/meta";
|
||||||
|
import type { Listing } from "./types/listing";
|
||||||
|
import type { KeymapCategory } from "./types/actions";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
|
let lock: Promise<void> | undefined = undefined;
|
||||||
|
|
||||||
|
export async function getMeta(
|
||||||
|
device: string,
|
||||||
|
version: string,
|
||||||
|
fetch: typeof window.fetch = window.fetch,
|
||||||
|
): Promise<VersionMeta> {
|
||||||
|
while (lock) await lock;
|
||||||
|
let resolveLock!: () => void;
|
||||||
|
lock = new Promise((resolve) => (resolveLock = resolve));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!browser) return fetchMeta(device, version, fetch);
|
||||||
|
|
||||||
|
const dbRequest = indexedDB.open("version-meta", 4);
|
||||||
|
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
||||||
|
dbRequest.onsuccess = () => resolve(dbRequest.result);
|
||||||
|
dbRequest.onerror = () => reject(dbRequest.error);
|
||||||
|
dbRequest.onupgradeneeded = () => {
|
||||||
|
const db = dbRequest.result;
|
||||||
|
if (db.objectStoreNames.contains("meta")) {
|
||||||
|
db.deleteObjectStore("meta");
|
||||||
|
}
|
||||||
|
db.createObjectStore("meta", { keyPath: ["device", "version"] });
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log("upgrading version meta db");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const readTransaction = db.transaction(["meta"], "readonly");
|
||||||
|
const store = readTransaction.objectStore("meta");
|
||||||
|
const itemRequest = store.get([device, version]);
|
||||||
|
const item = await new Promise<VersionMeta | undefined>((resolve) => {
|
||||||
|
itemRequest.onsuccess = () => resolve(itemRequest.result);
|
||||||
|
itemRequest.onerror = () => resolve(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (item) return item;
|
||||||
|
|
||||||
|
const meta = await fetchMeta(device, version);
|
||||||
|
|
||||||
|
const putTransaction = db.transaction(["meta"], "readwrite");
|
||||||
|
const putStore = putTransaction.objectStore("meta");
|
||||||
|
const putRequest = putStore.put(meta);
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
putRequest.onsuccess = () => resolve();
|
||||||
|
putRequest.onerror = () => reject(putRequest.error);
|
||||||
|
});
|
||||||
|
putTransaction.commit();
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
resolveLock();
|
||||||
|
lock = undefined;
|
||||||
|
}
|
||||||
|
return fetchMeta(device, version, fetch);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMeta(
|
||||||
|
device: string,
|
||||||
|
version: string,
|
||||||
|
fetch: typeof window.fetch = window.fetch,
|
||||||
|
): Promise<VersionMeta> {
|
||||||
|
const path = `${import.meta.env.VITE_FIRMWARE_URL}/${device}/${version}`;
|
||||||
|
const files: Listing[] = await fetch(`${path}/`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch(() => []);
|
||||||
|
const meta: Partial<RawVersionMeta> | undefined = files.some(
|
||||||
|
(entry) => entry.type === "file" && entry.name === "meta.json",
|
||||||
|
)
|
||||||
|
? await fetch(`${path}/meta.json`).then((res) => res.json())
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: meta?.version ?? version,
|
||||||
|
device: meta?.target ?? device,
|
||||||
|
date: new Date(meta?.git_date ?? files[0]?.mtime ?? ""),
|
||||||
|
path,
|
||||||
|
commit: meta?.git_commit ?? undefined,
|
||||||
|
dirty: meta?.git_is_dirty ?? false,
|
||||||
|
public: meta?.public_build ?? !version.includes("+"),
|
||||||
|
developmentBuild: (meta?.development_mode ?? 0) === 1,
|
||||||
|
factoryDefaults: meta?.factory_defaults
|
||||||
|
? {
|
||||||
|
layout: await fetch(`${path}/${meta.factory_defaults.layout}`).then(
|
||||||
|
(it) => it.json(),
|
||||||
|
),
|
||||||
|
settings: await fetch(
|
||||||
|
`${path}/${meta.factory_defaults.settings}`,
|
||||||
|
).then((it) => it.json()),
|
||||||
|
chords: Object.fromEntries(
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(meta.factory_defaults.chords).map(
|
||||||
|
async ([name, file]) => [
|
||||||
|
name,
|
||||||
|
await fetch(`${path}/${file}`).then((it) => it.json()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
settings: await (meta?.settings
|
||||||
|
? fetch(`${path}/${meta.settings}`).then((it) => it.json())
|
||||||
|
: import("$lib/assets/settings.yml")
|
||||||
|
.then((it) => (it as any).default)
|
||||||
|
.then((settings: SettingsMeta[]) => {
|
||||||
|
if (!device.startsWith("lite_")) {
|
||||||
|
settings = settings.filter((it) => it.name !== "leds");
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
})),
|
||||||
|
changelog: await (meta?.changelog
|
||||||
|
? fetch(`${path}/${meta.changelog}`).then((it) => it.json())
|
||||||
|
: {}),
|
||||||
|
actions: await (meta?.actions
|
||||||
|
? fetch(`${path}/${meta.actions}`).then((it) => it.json())
|
||||||
|
: Promise.all<KeymapCategory[]>(
|
||||||
|
Object.values(import.meta.glob("$lib/assets/keymaps/*.yml")).map(
|
||||||
|
async (load) => load().then((it) => (it as any).default),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
update: {
|
||||||
|
uf2:
|
||||||
|
meta?.update?.uf2 ??
|
||||||
|
files.find(
|
||||||
|
(entry) => entry.type === "file" && entry.name === "CURRENT.UF2",
|
||||||
|
)?.name ??
|
||||||
|
undefined,
|
||||||
|
ota:
|
||||||
|
meta?.update?.ota ??
|
||||||
|
files.find(
|
||||||
|
(entry) => entry.type === "file" && entry.name === "firmware.bin",
|
||||||
|
)?.name ??
|
||||||
|
undefined,
|
||||||
|
esptool: meta?.update?.esptool ?? undefined,
|
||||||
|
},
|
||||||
|
spiFlash: meta?.spi_flash ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
19
src/lib/meta/types/actions.ts
Normal file
19
src/lib/meta/types/actions.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export interface KeymapCategory {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon?: string;
|
||||||
|
display?: string;
|
||||||
|
type?: "unassigned";
|
||||||
|
actions: Record<number, Partial<ActionInfo>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionInfo {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
display: string;
|
||||||
|
description: string;
|
||||||
|
variant: "left" | "right";
|
||||||
|
variantOf: number;
|
||||||
|
keyCode: string;
|
||||||
|
}
|
||||||
108
src/lib/meta/types/meta.ts
Normal file
108
src/lib/meta/types/meta.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import type {
|
||||||
|
CharaChordFile,
|
||||||
|
CharaLayoutFile,
|
||||||
|
CharaSettingsFile,
|
||||||
|
} from "$lib/share/chara-file";
|
||||||
|
import type { KeymapCategory } from "./actions";
|
||||||
|
|
||||||
|
export interface SettingsMeta {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
items: SettingsItemMeta[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingsItemMeta {
|
||||||
|
id: number;
|
||||||
|
description?: string;
|
||||||
|
enum?: string[];
|
||||||
|
range: [number, number];
|
||||||
|
step?: number;
|
||||||
|
unit?: string;
|
||||||
|
inverse?: number;
|
||||||
|
scale?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangelogEntry {
|
||||||
|
summary: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Changelog {
|
||||||
|
features: ChangelogEntry[];
|
||||||
|
fixes: ChangelogEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RawVersionMeta {
|
||||||
|
version: string;
|
||||||
|
target: string;
|
||||||
|
git_commit: string;
|
||||||
|
git_is_dirty: boolean;
|
||||||
|
git_date: string;
|
||||||
|
public_build: boolean;
|
||||||
|
development_mode: number;
|
||||||
|
actions: string;
|
||||||
|
settings: string;
|
||||||
|
changelog: string;
|
||||||
|
factory_defaults: {
|
||||||
|
layout: string;
|
||||||
|
settings: string;
|
||||||
|
chords: Record<string, string>;
|
||||||
|
};
|
||||||
|
update: {
|
||||||
|
ota: string | null;
|
||||||
|
uf2: string | null;
|
||||||
|
esptool: EspToolData | null;
|
||||||
|
};
|
||||||
|
files: string[];
|
||||||
|
spi_flash: SPIFlashInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionMeta {
|
||||||
|
version: string;
|
||||||
|
device: string;
|
||||||
|
path: string;
|
||||||
|
date: Date;
|
||||||
|
public: boolean;
|
||||||
|
commit?: string;
|
||||||
|
dirty: boolean;
|
||||||
|
developmentBuild: boolean;
|
||||||
|
actions: KeymapCategory[];
|
||||||
|
settings: SettingsMeta[];
|
||||||
|
changelog: Changelog;
|
||||||
|
factoryDefaults?: {
|
||||||
|
layout: CharaLayoutFile;
|
||||||
|
settings: CharaSettingsFile;
|
||||||
|
chords: Record<string, CharaChordFile>;
|
||||||
|
};
|
||||||
|
update: {
|
||||||
|
ota?: string;
|
||||||
|
uf2?: string;
|
||||||
|
esptool?: EspToolData;
|
||||||
|
};
|
||||||
|
spiFlash?: SPIFlashInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SPIFlashInfo {
|
||||||
|
type: string;
|
||||||
|
size: string;
|
||||||
|
connection: SPIConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SPIConnection {
|
||||||
|
clk: number;
|
||||||
|
q: number;
|
||||||
|
d: number;
|
||||||
|
hd: number;
|
||||||
|
cs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EspToolData {
|
||||||
|
chip: string;
|
||||||
|
baud: string;
|
||||||
|
before: string;
|
||||||
|
after: string;
|
||||||
|
flash_mode: string;
|
||||||
|
flash_freq: string;
|
||||||
|
flash_size: string;
|
||||||
|
files: Record<string, string>;
|
||||||
|
}
|
||||||
@@ -69,5 +69,8 @@ export function hashChord(actions: number[]) {
|
|||||||
for (let i = 0; i < 16; i++) {
|
for (let i = 0; i < 16; i++) {
|
||||||
hash = Math.imul(hash ^ view.getUint8(i), 16777619);
|
hash = Math.imul(hash ^ view.getUint8(i), 16777619);
|
||||||
}
|
}
|
||||||
|
if ((hash & 0xff) === 0xff) {
|
||||||
|
hash ^= 0xff;
|
||||||
|
}
|
||||||
return hash & 0x3fff_ffff;
|
return hash & 0x3fff_ffff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import type { Writable } from "svelte/store";
|
|||||||
import type { CharaLayout } from "$lib/serialization/layout";
|
import type { CharaLayout } from "$lib/serialization/layout";
|
||||||
import { persistentWritable } from "$lib/storage";
|
import { persistentWritable } from "$lib/storage";
|
||||||
import { userPreferences } from "$lib/preferences";
|
import { userPreferences } from "$lib/preferences";
|
||||||
import settingInfo from "$lib/assets/settings.yml";
|
import { getMeta } from "$lib/meta/meta-storage";
|
||||||
|
import type { VersionMeta } from "$lib/meta/types/meta";
|
||||||
|
|
||||||
export const serialPort = writable<CharaDevice | undefined>();
|
export const serialPort = writable<CharaDevice | undefined>();
|
||||||
|
|
||||||
@@ -28,25 +29,30 @@ export const deviceChords = persistentWritable<Chord[]>(
|
|||||||
/**
|
/**
|
||||||
* Layout as read from the device
|
* Layout as read from the device
|
||||||
*/
|
*/
|
||||||
export const deviceLayout = persistentWritable<CharaLayout>(
|
export const deviceLayout = persistentWritable<CharaLayout[]>(
|
||||||
"layout",
|
"layout-profiles",
|
||||||
[[], [], []],
|
[],
|
||||||
() => get(userPreferences).backup,
|
() => get(userPreferences).backup,
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings as read from the device
|
* Settings as read from the device
|
||||||
*/
|
*/
|
||||||
export const deviceSettings = persistentWritable<number[]>(
|
export const deviceSettings = persistentWritable<number[][]>(
|
||||||
"device-settings",
|
"settings-profiles",
|
||||||
[],
|
[],
|
||||||
() => get(userPreferences).backup,
|
() => get(userPreferences).backup,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const activeProfile = persistentWritable<number>("active-profile", 0);
|
||||||
|
export const activeLayer = persistentWritable<number>("active-profile", 0);
|
||||||
|
|
||||||
export const syncStatus: Writable<
|
export const syncStatus: Writable<
|
||||||
"done" | "error" | "downloading" | "uploading"
|
"done" | "error" | "downloading" | "uploading"
|
||||||
> = writable("done");
|
> = writable("done");
|
||||||
|
|
||||||
|
export const deviceMeta = writable<VersionMeta | undefined>(undefined);
|
||||||
|
|
||||||
export interface ProgressInfo {
|
export interface ProgressInfo {
|
||||||
max: number;
|
max: number;
|
||||||
current: number;
|
current: number;
|
||||||
@@ -65,36 +71,61 @@ export async function initSerial(manual = false, withSync = true) {
|
|||||||
export async function sync() {
|
export async function sync() {
|
||||||
const device = get(serialPort);
|
const device = get(serialPort);
|
||||||
if (!device) return;
|
if (!device) return;
|
||||||
const chordCount = await device.getChordCount();
|
|
||||||
syncStatus.set("downloading");
|
syncStatus.set("downloading");
|
||||||
|
const meta = await getMeta(
|
||||||
|
`${device.device}_${device.chipset}`.toLowerCase(),
|
||||||
|
device.version.toString(),
|
||||||
|
);
|
||||||
|
deviceMeta.set(meta);
|
||||||
|
const chordCount = await device.getChordCount();
|
||||||
|
|
||||||
|
const maxSettings = meta.settings
|
||||||
|
.map((it) => it.items.length)
|
||||||
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
const max =
|
const max =
|
||||||
Object.keys(settingInfo["settings"]).length +
|
(maxSettings + device.keyCount * device.layerCount) * device.profileCount +
|
||||||
device.keyCount * 3 +
|
|
||||||
chordCount;
|
chordCount;
|
||||||
let current = 0;
|
let current = 0;
|
||||||
|
activeProfile.update((it) => Math.min(it, device.profileCount - 1));
|
||||||
|
activeLayer.update((it) => Math.min(it, device.layerCount - 1));
|
||||||
syncProgress.set({ max, current });
|
syncProgress.set({ max, current });
|
||||||
function progressTick() {
|
function progressTick() {
|
||||||
current++;
|
current++;
|
||||||
syncProgress.set({ max, current });
|
syncProgress.set({ max, current });
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedSettings: number[] = [];
|
const parsedSettings: number[][] = Array.from(
|
||||||
for (const key in settingInfo["settings"]) {
|
{ length: device.profileCount },
|
||||||
try {
|
() => [],
|
||||||
parsedSettings[Number.parseInt(key)] = await device.getSetting(
|
);
|
||||||
Number.parseInt(key),
|
for (const [profile, settings] of parsedSettings.entries()) {
|
||||||
);
|
for (const category of meta.settings) {
|
||||||
} catch {}
|
for (const setting of category.items) {
|
||||||
progressTick();
|
try {
|
||||||
|
settings[setting.id] = await device.getSetting(profile, setting.id);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
progressTick();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
deviceSettings.set(parsedSettings);
|
deviceSettings.set(parsedSettings);
|
||||||
|
|
||||||
const parsedLayout: CharaLayout = [[], [], []];
|
const parsedLayout: CharaLayout[] = Array.from(
|
||||||
for (let layer = 1; layer <= 3; layer++) {
|
{ length: device.profileCount },
|
||||||
for (let i = 0; i < device.keyCount; i++) {
|
() =>
|
||||||
parsedLayout[layer - 1]![i] = await device.getLayoutKey(layer, i);
|
Array.from({ length: device.layerCount }, () =>
|
||||||
progressTick();
|
Array.from({ length: device.keyCount }, () => 0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for (const [profile, layout] of parsedLayout.entries()) {
|
||||||
|
for (const [layer, keys] of layout.entries()) {
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
try {
|
||||||
|
keys[i] = await device.getLayoutKey(profile, layer + 1, i);
|
||||||
|
} catch {}
|
||||||
|
progressTick();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deviceLayout.set(parsedLayout);
|
deviceLayout.set(parsedLayout);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { LineBreakTransformer } from "$lib/serial/line-break-transformer";
|
import { LineBreakTransformer } from "$lib/serial/line-break-transformer";
|
||||||
import { serialLog } from "$lib/serial/connection";
|
import { serialLog } from "$lib/serial/connection";
|
||||||
import type { Chord } from "$lib/serial/chord";
|
import type { Chord } from "$lib/serial/chord";
|
||||||
import { SemVer } from "$lib/serial/sem-ver";
|
|
||||||
import {
|
import {
|
||||||
parseChordActions,
|
parseChordActions,
|
||||||
parsePhrase,
|
parsePhrase,
|
||||||
@@ -9,15 +8,18 @@ import {
|
|||||||
stringifyPhrase,
|
stringifyPhrase,
|
||||||
} from "$lib/serial/chord";
|
} from "$lib/serial/chord";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
|
||||||
|
import semverGte from "semver/functions/gte";
|
||||||
|
|
||||||
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
|
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
|
||||||
["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }],
|
["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }],
|
||||||
["TWO S3 (pre-production)", { usbProductId: 0x8252, usbVendorId: 0x303a }], // TODO: remove this after everyone has migrated
|
["TWO S3 (pre-production)", { usbProductId: 0x8252, usbVendorId: 0x303a }],
|
||||||
["TWO S3", { usbProductId: 0x8253, usbVendorId: 0x303a }],
|
["TWO S3", { usbProductId: 0x8253, usbVendorId: 0x303a }],
|
||||||
["LITE S2", { usbProductId: 33070, usbVendorId: 12346 }],
|
["LITE S2", { usbProductId: 0x812e, usbVendorId: 0x303a }],
|
||||||
["LITE M0", { usbProductId: 32796, usbVendorId: 9114 }],
|
["LITE M0", { usbProductId: 32796, usbVendorId: 9114 }],
|
||||||
["X", { usbProductId: 33163, usbVendorId: 12346 }],
|
["X", { usbProductId: 0x818b, usbVendorId: 0x303a }],
|
||||||
["M4G S3", { usbProductId: 4097, usbVendorId: 12346 }],
|
["M4G S3 (pre-production)", { usbProductId: 0x1001, usbVendorId: 0x303a }],
|
||||||
|
["M4G S3", { usbProductId: 0x829a, usbVendorId: 0x303a }],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const KEY_COUNTS = {
|
const KEY_COUNTS = {
|
||||||
@@ -27,6 +29,7 @@ const KEY_COUNTS = {
|
|||||||
X: 256,
|
X: 256,
|
||||||
M4G: 90,
|
M4G: 90,
|
||||||
M4GR: 90,
|
M4GR: 90,
|
||||||
|
T4G: 7,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -98,11 +101,13 @@ export class CharaDevice {
|
|||||||
private readonly suspendDebounce = 100;
|
private readonly suspendDebounce = 100;
|
||||||
private suspendDebounceId?: number;
|
private suspendDebounceId?: number;
|
||||||
|
|
||||||
version!: SemVer;
|
version!: string;
|
||||||
company!: "CHARACHORDER" | "FORGE";
|
company!: "CHARACHORDER" | "FORGE";
|
||||||
device!: "ONE" | "TWO" | "LITE" | "X" | "M4G";
|
device!: "ONE" | "TWO" | "LITE" | "X" | "M4G";
|
||||||
chipset!: "M0" | "S2" | "S3";
|
chipset!: "M0" | "S2" | "S3";
|
||||||
keyCount!: 90 | 67 | 256;
|
keyCount!: 90 | 67 | 256;
|
||||||
|
layerCount = 3;
|
||||||
|
profileCount = 1;
|
||||||
|
|
||||||
get portInfo() {
|
get portInfo() {
|
||||||
return this.port.getInfo();
|
return this.port.getInfo();
|
||||||
@@ -133,18 +138,20 @@ export class CharaDevice {
|
|||||||
});
|
});
|
||||||
await this.port.close();
|
await this.port.close();
|
||||||
|
|
||||||
this.version = new SemVer(
|
this.version = await this.send(1, ["VERSION"]).then(
|
||||||
await this.send(1, "VERSION").then(([version]) => version),
|
([version]) => version,
|
||||||
);
|
);
|
||||||
const [company, device, chipset] = await this.send(3, "ID");
|
if (semverGte(this.version, "2.2.0-beta.4")) {
|
||||||
|
this.profileCount = 3;
|
||||||
|
}
|
||||||
|
const [company, device, chipset] = await this.send(3, ["ID"]);
|
||||||
this.company = company as typeof this.company;
|
this.company = company as typeof this.company;
|
||||||
this.device = device as typeof this.device;
|
this.device = device as typeof this.device;
|
||||||
this.chipset = chipset as typeof this.chipset;
|
this.chipset = chipset as typeof this.chipset;
|
||||||
this.keyCount = KEY_COUNTS[this.device];
|
this.keyCount = KEY_COUNTS[this.device];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e);
|
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw e;
|
await showConnectionFailedDialog(String(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,9 +192,12 @@ export class CharaDevice {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async internalRead() {
|
private async internalRead(timeoutMs: number | undefined) {
|
||||||
try {
|
try {
|
||||||
const { value } = await timeout(this.reader.read(), 5000);
|
const { value } =
|
||||||
|
timeoutMs !== undefined
|
||||||
|
? await timeout(this.reader.read(), timeoutMs)
|
||||||
|
: await this.reader.read();
|
||||||
serialLog.update((it) => {
|
serialLog.update((it) => {
|
||||||
it.push({
|
it.push({
|
||||||
type: "output",
|
type: "output",
|
||||||
@@ -278,14 +288,15 @@ export class CharaDevice {
|
|||||||
*/
|
*/
|
||||||
async send<T extends number>(
|
async send<T extends number>(
|
||||||
expectedLength: T,
|
expectedLength: T,
|
||||||
...command: string[]
|
command: string[],
|
||||||
|
timeout: number | undefined = 5000,
|
||||||
): Promise<LengthArray<string, T>> {
|
): Promise<LengthArray<string, T>> {
|
||||||
return this.runWith(async (send, read) => {
|
return this.runWith(async (send, read) => {
|
||||||
await send(...command);
|
await send(...command);
|
||||||
const commandString = command
|
const commandString = command
|
||||||
.join(" ")
|
.join(" ")
|
||||||
.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
||||||
const readResult = await read();
|
const readResult = await read(timeout);
|
||||||
if (readResult === undefined) {
|
if (readResult === undefined) {
|
||||||
console.error("No response");
|
console.error("No response");
|
||||||
return Array(expectedLength).fill("NO_RESPONSE") as LengthArray<
|
return Array(expectedLength).fill("NO_RESPONSE") as LengthArray<
|
||||||
@@ -307,7 +318,7 @@ export class CharaDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getChordCount(): Promise<number> {
|
async getChordCount(): Promise<number> {
|
||||||
const [count] = await this.send(1, "CML C0");
|
const [count] = await this.send(1, ["CML", "C0"]);
|
||||||
return Number.parseInt(count);
|
return Number.parseInt(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +326,11 @@ export class CharaDevice {
|
|||||||
* Retrieves a chord by index
|
* Retrieves a chord by index
|
||||||
*/
|
*/
|
||||||
async getChord(index: number | number[]): Promise<Chord> {
|
async getChord(index: number | number[]): Promise<Chord> {
|
||||||
const [actions, phrase] = await this.send(2, `CML C1 ${index}`);
|
const [actions, phrase] = await this.send(2, [
|
||||||
|
"CML",
|
||||||
|
"C1",
|
||||||
|
index.toString(),
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
actions: parseChordActions(actions),
|
actions: parseChordActions(actions),
|
||||||
phrase: parsePhrase(phrase),
|
phrase: parsePhrase(phrase),
|
||||||
@@ -326,29 +341,30 @@ export class CharaDevice {
|
|||||||
* Retrieves the phrase for a set of actions
|
* Retrieves the phrase for a set of actions
|
||||||
*/
|
*/
|
||||||
async getChordPhrase(actions: number[]): Promise<number[] | undefined> {
|
async getChordPhrase(actions: number[]): Promise<number[] | undefined> {
|
||||||
const [phrase] = await this.send(
|
const [phrase] = await this.send(1, [
|
||||||
1,
|
"CML",
|
||||||
`CML C2 ${stringifyChordActions(actions)}`,
|
"C2",
|
||||||
);
|
stringifyChordActions(actions),
|
||||||
|
]);
|
||||||
return phrase === "2" ? undefined : parsePhrase(phrase);
|
return phrase === "2" ? undefined : parsePhrase(phrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setChord(chord: Chord) {
|
async setChord(chord: Chord) {
|
||||||
const [status] = await this.send(
|
const [status] = await this.send(1, [
|
||||||
1,
|
|
||||||
"CML",
|
"CML",
|
||||||
"C3",
|
"C3",
|
||||||
stringifyChordActions(chord.actions),
|
stringifyChordActions(chord.actions),
|
||||||
stringifyPhrase(chord.phrase),
|
stringifyPhrase(chord.phrase),
|
||||||
);
|
]);
|
||||||
if (status !== "0") console.error(`Failed with status ${status}`);
|
if (status !== "0") console.error(`Failed with status ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteChord(chord: Pick<Chord, "actions">) {
|
async deleteChord(chord: Pick<Chord, "actions">) {
|
||||||
const status = await this.send(
|
const status = await this.send(1, [
|
||||||
1,
|
"CML",
|
||||||
`CML C4 ${stringifyChordActions(chord.actions)}`,
|
"C4",
|
||||||
);
|
stringifyChordActions(chord.actions),
|
||||||
|
]);
|
||||||
if (status?.at(-1) !== "2" && status?.at(-1) !== "0")
|
if (status?.at(-1) !== "2" && status?.at(-1) !== "0")
|
||||||
throw new Error(`Failed with status ${status}`);
|
throw new Error(`Failed with status ${status}`);
|
||||||
}
|
}
|
||||||
@@ -359,8 +375,19 @@ export class CharaDevice {
|
|||||||
* @param id id of the key, refer to the individual device for where each key is
|
* @param id id of the key, refer to the individual device for where each key is
|
||||||
* @param action the assigned action id
|
* @param action the assigned action id
|
||||||
*/
|
*/
|
||||||
async setLayoutKey(layer: number, id: number, action: number) {
|
async setLayoutKey(
|
||||||
const [status] = await this.send(1, `VAR B4 A${layer} ${id} ${action}`);
|
profile: number,
|
||||||
|
layer: number,
|
||||||
|
id: number,
|
||||||
|
action: number,
|
||||||
|
) {
|
||||||
|
const [status] = await this.send(1, [
|
||||||
|
"VAR",
|
||||||
|
"B4",
|
||||||
|
`${String.fromCodePoint("A".codePointAt(0)! + profile)}${layer}`,
|
||||||
|
id.toString(),
|
||||||
|
action.toString(),
|
||||||
|
]);
|
||||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,8 +397,13 @@ export class CharaDevice {
|
|||||||
* @param id id of the key, refer to the individual device for where each key is
|
* @param id id of the key, refer to the individual device for where each key is
|
||||||
* @returns the assigned action id
|
* @returns the assigned action id
|
||||||
*/
|
*/
|
||||||
async getLayoutKey(layer: number, id: number) {
|
async getLayoutKey(profile: number, layer: number, id: number) {
|
||||||
const [position, status] = await this.send(2, `VAR B3 A${layer} ${id}`);
|
const [position, status] = await this.send(2, [
|
||||||
|
"VAR",
|
||||||
|
"B3",
|
||||||
|
`${String.fromCodePoint("A".codePointAt(0)! + profile)}${layer}`,
|
||||||
|
id.toString(),
|
||||||
|
]);
|
||||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||||
return Number(position);
|
return Number(position);
|
||||||
}
|
}
|
||||||
@@ -384,7 +416,7 @@ export class CharaDevice {
|
|||||||
* **This does not need to be called for chords**
|
* **This does not need to be called for chords**
|
||||||
*/
|
*/
|
||||||
async commit() {
|
async commit() {
|
||||||
const [status] = await this.send(1, "VAR B0");
|
const [status] = await this.send(1, ["VAR", "B0"]);
|
||||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,22 +426,25 @@ export class CharaDevice {
|
|||||||
* Settings are applied until the next reboot or loss of power.
|
* Settings are applied until the next reboot or loss of power.
|
||||||
* To permanently store the settings, you *must* call commit.
|
* To permanently store the settings, you *must* call commit.
|
||||||
*/
|
*/
|
||||||
async setSetting(id: number, value: number) {
|
async setSetting(profile: number, id: number, value: number) {
|
||||||
const [status] = await this.send(
|
const [status] = await this.send(1, [
|
||||||
1,
|
"VAR",
|
||||||
`VAR B2 ${id.toString(16).toUpperCase()} ${value}`,
|
"B2",
|
||||||
);
|
(id + profile * 0x100).toString(16).toUpperCase(),
|
||||||
|
value.toString(),
|
||||||
|
]);
|
||||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a setting from the device
|
* Retrieves a setting from the device
|
||||||
*/
|
*/
|
||||||
async getSetting(id: number): Promise<number> {
|
async getSetting(profile: number, id: number): Promise<number> {
|
||||||
const [value, status] = await this.send(
|
const [value, status] = await this.send(2, [
|
||||||
2,
|
"VAR",
|
||||||
`VAR B1 ${id.toString(16).toUpperCase()}`,
|
"B1",
|
||||||
);
|
(id + profile * 0x100).toString(16).toUpperCase(),
|
||||||
|
]);
|
||||||
if (status !== "0")
|
if (status !== "0")
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Setting "0x${id.toString(16)}" doesn't exist (Status code ${status})`,
|
`Setting "0x${id.toString(16)}" doesn't exist (Status code ${status})`,
|
||||||
@@ -421,14 +456,14 @@ export class CharaDevice {
|
|||||||
* Reboots the device
|
* Reboots the device
|
||||||
*/
|
*/
|
||||||
async reboot() {
|
async reboot() {
|
||||||
await this.send(0, "RST");
|
await this.send(0, ["RST"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reboots the device to the bootloader
|
* Reboots the device to the bootloader
|
||||||
*/
|
*/
|
||||||
async bootloader() {
|
async bootloader() {
|
||||||
await this.send(0, "RST BOOTLOADER");
|
await this.send(0, ["RST", "BOOTLOADER"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -437,7 +472,12 @@ export class CharaDevice {
|
|||||||
async reset(
|
async reset(
|
||||||
type: "FACTORY" | "PARAMS" | "KEYMAPS" | "STARTER" | "CLEARCML" | "FUNC",
|
type: "FACTORY" | "PARAMS" | "KEYMAPS" | "STARTER" | "CLEARCML" | "FUNC",
|
||||||
) {
|
) {
|
||||||
await this.send(0, `RST ${type}`);
|
await this.send(0, ["RST", type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryKey(): Promise<number> {
|
||||||
|
const [value] = await this.send(1, ["QRY", "KEY"], undefined);
|
||||||
|
return Number(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -446,13 +486,17 @@ export class CharaDevice {
|
|||||||
* This is useful for debugging when there is a suspected heap or stack issue.
|
* This is useful for debugging when there is a suspected heap or stack issue.
|
||||||
*/
|
*/
|
||||||
async getRamBytesAvailable(): Promise<number> {
|
async getRamBytesAvailable(): Promise<number> {
|
||||||
return Number(await this.send(1, "RAM").then(([bytes]) => bytes));
|
return Number(await this.send(1, ["RAM"]).then(([bytes]) => bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFirmware(file: File | Blob): Promise<void> {
|
async updateFirmware(
|
||||||
|
file: ArrayBuffer,
|
||||||
|
progress: (transferred: number, total: number) => void,
|
||||||
|
): Promise<void> {
|
||||||
while (this.lock) {
|
while (this.lock) {
|
||||||
await this.lock;
|
await this.lock;
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolveLock: (result: true) => void;
|
let resolveLock: (result: true) => void;
|
||||||
this.lock = new Promise<true>((resolve) => {
|
this.lock = new Promise<true>((resolve) => {
|
||||||
resolveLock = resolve;
|
resolveLock = resolve;
|
||||||
@@ -482,46 +526,46 @@ export class CharaDevice {
|
|||||||
});
|
});
|
||||||
return it;
|
return it;
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
writer.releaseLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the device to be ready
|
// Wait for the device to be ready
|
||||||
const signal = await this.reader.read();
|
const signal = await this.reader.read();
|
||||||
serialLog.update((it) => {
|
serialLog.update((it) => {
|
||||||
it.push({
|
it.push({
|
||||||
type: "output",
|
type: "output",
|
||||||
value: signal.value!.trim(),
|
value: signal.value!.trim(),
|
||||||
|
});
|
||||||
|
return it;
|
||||||
});
|
});
|
||||||
return it;
|
|
||||||
});
|
|
||||||
|
|
||||||
await file.stream().pipeTo(this.port.writable!);
|
const chunkSize = 128;
|
||||||
|
for (let i = 0; i < file.byteLength; i += chunkSize) {
|
||||||
|
const chunk = file.slice(i, i + chunkSize);
|
||||||
|
await writer.write(new Uint8Array(chunk));
|
||||||
|
progress(i + chunk.byteLength, file.byteLength);
|
||||||
|
}
|
||||||
|
|
||||||
serialLog.update((it) => {
|
serialLog.update((it) => {
|
||||||
it.push({
|
it.push({
|
||||||
type: "input",
|
type: "input",
|
||||||
value: `...${file.size} bytes`,
|
value: `...${file.byteLength} bytes`,
|
||||||
|
});
|
||||||
|
return it;
|
||||||
});
|
});
|
||||||
return it;
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = (await this.reader.read()).value!.trim();
|
const result = (await this.reader.read()).value!.trim();
|
||||||
serialLog.update((it) => {
|
serialLog.update((it) => {
|
||||||
it.push({
|
it.push({
|
||||||
type: "output",
|
type: "output",
|
||||||
value: result!,
|
value: result!,
|
||||||
|
});
|
||||||
|
return it;
|
||||||
});
|
});
|
||||||
return it;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result !== "OTA OK") {
|
if (result !== "OTA OK") {
|
||||||
throw new Error(result);
|
throw new Error(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
const writer2 = this.port.writable!.getWriter();
|
await writer.write(new TextEncoder().encode(`RST RESTART\r\n`));
|
||||||
try {
|
|
||||||
await writer2.write(new TextEncoder().encode(`RST RESTART\r\n`));
|
|
||||||
serialLog.update((it) => {
|
serialLog.update((it) => {
|
||||||
it.push({
|
it.push({
|
||||||
type: "input",
|
type: "input",
|
||||||
@@ -530,7 +574,7 @@ export class CharaDevice {
|
|||||||
return it;
|
return it;
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
writer2.releaseLock();
|
writer.releaseLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.suspend();
|
await this.suspend();
|
||||||
|
|||||||
@@ -1,38 +1,64 @@
|
|||||||
import type { ActionInfo, KeymapCategory } from "$lib/assets/keymaps/keymap";
|
import type { ActionInfo, KeymapCategory } from "$lib/assets/keymaps/keymap";
|
||||||
|
import { derived, type Readable } from "svelte/store";
|
||||||
|
import { deviceMeta } from "./connection";
|
||||||
|
|
||||||
export interface KeyInfo extends Partial<ActionInfo> {
|
export interface KeyInfo extends Partial<ActionInfo> {
|
||||||
code: number;
|
code: number;
|
||||||
category?: KeymapCategory;
|
category?: KeymapCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KEYMAP_CATEGORIES = (await Promise.all(
|
const fallbackActions = await Promise.all<KeymapCategory>(
|
||||||
Object.values(import.meta.glob("$lib/assets/keymaps/*.yml")).map(
|
Object.values(import.meta.glob("$lib/assets/keymaps/*.yml")).map(
|
||||||
async (load) => load().then((it) => (it as any).default),
|
async (load) => load().then((it) => (it as any).default),
|
||||||
),
|
),
|
||||||
)) as KeymapCategory[];
|
|
||||||
|
|
||||||
export const KEYMAP_CODES = new Map<number, KeyInfo>(
|
|
||||||
KEYMAP_CATEGORIES.flatMap((category) =>
|
|
||||||
Object.entries(category.actions).map(([code, action]) => [
|
|
||||||
Number(code),
|
|
||||||
{ ...action, code: Number(code), category },
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const KEYMAP_KEYCODES = new Map<string, number>(
|
export let KEYMAP_CATEGORIES: Readable<KeymapCategory[]> = derived(
|
||||||
KEYMAP_CATEGORIES.flatMap((category) =>
|
deviceMeta,
|
||||||
Object.entries(category.actions).map(
|
(deviceMeta) => deviceMeta?.actions ?? fallbackActions,
|
||||||
([code, action]) => [action.keyCode!, Number(code)] as const,
|
);
|
||||||
|
|
||||||
|
export const KEYMAP_CODES: Readable<Map<number, KeyInfo>> = derived(
|
||||||
|
KEYMAP_CATEGORIES,
|
||||||
|
(categories) =>
|
||||||
|
new Map<number, KeyInfo>(
|
||||||
|
categories.flatMap((category) =>
|
||||||
|
Object.entries(category.actions).map(([code, action]) => [
|
||||||
|
Number(code),
|
||||||
|
{ ...action, code: Number(code), category },
|
||||||
|
]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
).filter(([keyCode]) => keyCode !== undefined),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const KEYMAP_IDS = new Map<string, KeyInfo>(
|
export const KEYMAP_KEYCODES: Readable<Map<string, number>> = derived(
|
||||||
KEYMAP_CATEGORIES.flatMap((category) =>
|
KEYMAP_CATEGORIES,
|
||||||
Object.entries(category.actions).map(
|
(categories) =>
|
||||||
([code, action]) =>
|
new Map<string, number>(
|
||||||
[action.id!, { ...action, code: Number(code), category }] as const,
|
categories
|
||||||
|
.flatMap((category) =>
|
||||||
|
Object.entries(category.actions).map(
|
||||||
|
([code, action]) => [action.keyCode!, Number(code)] as const,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter(([keyCode]) => keyCode !== undefined),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const KEYMAP_IDS: Readable<Map<string, KeyInfo>> = derived(
|
||||||
|
KEYMAP_CATEGORIES,
|
||||||
|
(categories) =>
|
||||||
|
new Map<string, KeyInfo>(
|
||||||
|
categories
|
||||||
|
.flatMap((category) =>
|
||||||
|
Object.entries(category.actions).map(
|
||||||
|
([code, action]) =>
|
||||||
|
[
|
||||||
|
action.id!,
|
||||||
|
{ ...action, code: Number(code), category },
|
||||||
|
] as const,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter(([id]) => id !== undefined),
|
||||||
),
|
),
|
||||||
).filter(([id]) => id !== undefined),
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
export class SemVer {
|
|
||||||
major = 0;
|
|
||||||
minor = 0;
|
|
||||||
patch = 0;
|
|
||||||
preRelease?: string;
|
|
||||||
meta?: string;
|
|
||||||
|
|
||||||
constructor(versionString: string) {
|
|
||||||
const result =
|
|
||||||
/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+))?$/.exec(
|
|
||||||
versionString,
|
|
||||||
);
|
|
||||||
if (!result) {
|
|
||||||
console.error("Invalid version string:", versionString);
|
|
||||||
} else {
|
|
||||||
const [, major, minor, patch, preRelease, meta] = result;
|
|
||||||
this.major = Number.parseInt(major ?? "NaN");
|
|
||||||
this.minor = Number.parseInt(minor ?? "NaN");
|
|
||||||
this.patch = Number.parseInt(patch ?? "NaN");
|
|
||||||
if (preRelease) this.preRelease = preRelease;
|
|
||||||
if (meta) this.meta = meta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return (
|
|
||||||
`${this.major}.${this.minor}.${this.patch}` +
|
|
||||||
(this.preRelease ? `-${this.preRelease}` : "") +
|
|
||||||
(this.meta ? `+${this.meta}` : "")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,13 +4,10 @@ import { fromBase64, toBase64 } from "$lib/serialization/base64";
|
|||||||
export interface NewCharaLayout {
|
export interface NewCharaLayout {
|
||||||
charaLayoutVersion: 1;
|
charaLayoutVersion: 1;
|
||||||
device: "one" | "lite" | string;
|
device: "one" | "lite" | string;
|
||||||
/**
|
layers: number[][];
|
||||||
* Layers A1-A3, with numeric action codes on each
|
|
||||||
*/
|
|
||||||
layers: [number[], number[], number[]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharaLayout = [number[], number[], number[]];
|
export type CharaLayout = number[][];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize a layout into a micro package
|
* Serialize a layout into a micro package
|
||||||
|
|||||||
@@ -1,5 +1,88 @@
|
|||||||
import type { Action } from "svelte/action";
|
import type { Action } from "svelte/action";
|
||||||
import { changes, ChangeType, settings } from "$lib/undo-redo";
|
import { changes, ChangeType, settings } from "$lib/undo-redo";
|
||||||
|
import { activeProfile } from "./serial/connection";
|
||||||
|
import { combineLatest, map } from "rxjs";
|
||||||
|
import { fromReadable } from "./util/from-readable";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://gist.github.com/mjackson/5311256
|
||||||
|
*/
|
||||||
|
function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
|
||||||
|
r /= 255;
|
||||||
|
g /= 255;
|
||||||
|
b /= 255;
|
||||||
|
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
let h = 0;
|
||||||
|
const v = max;
|
||||||
|
|
||||||
|
const d = max - min;
|
||||||
|
const s = max == 0 ? 0 : d / max;
|
||||||
|
|
||||||
|
if (max == min) {
|
||||||
|
h = 0; // achromatic
|
||||||
|
} else {
|
||||||
|
switch (max) {
|
||||||
|
case r:
|
||||||
|
h = (g - b) / d + (g < b ? 6 : 0);
|
||||||
|
break;
|
||||||
|
case g:
|
||||||
|
h = (b - r) / d + 2;
|
||||||
|
break;
|
||||||
|
case b:
|
||||||
|
h = (r - g) / d + 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
h /= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [Math.floor(h * 0xffff), Math.floor(s * 0xff), Math.floor(v * 0xff)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://gist.github.com/mjackson/5311256
|
||||||
|
*/
|
||||||
|
function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
|
||||||
|
h /= 0xffff;
|
||||||
|
s /= 0xff;
|
||||||
|
v /= 0xff;
|
||||||
|
|
||||||
|
let r = 0;
|
||||||
|
let g = 0;
|
||||||
|
let b = 0;
|
||||||
|
|
||||||
|
const i = Math.floor(h * 6);
|
||||||
|
const f = h * 6 - i;
|
||||||
|
const p = v * (1 - s);
|
||||||
|
const q = v * (1 - f * s);
|
||||||
|
const t = v * (1 - (1 - f) * s);
|
||||||
|
|
||||||
|
switch (i % 6) {
|
||||||
|
case 0:
|
||||||
|
((r = v), (g = t), (b = p));
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
((r = q), (g = v), (b = p));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
((r = p), (g = v), (b = t));
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
((r = p), (g = q), (b = v));
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
((r = t), (g = p), (b = v));
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
((r = v), (g = p), (b = q));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [Math.floor(r * 0xff), Math.floor(g * 0xff), Math.floor(b * 0xff)];
|
||||||
|
}
|
||||||
|
|
||||||
export const setting: Action<
|
export const setting: Action<
|
||||||
HTMLInputElement | HTMLSelectElement,
|
HTMLInputElement | HTMLSelectElement,
|
||||||
@@ -9,7 +92,12 @@ export const setting: Action<
|
|||||||
{ id, inverse, scale },
|
{ id, inverse, scale },
|
||||||
) {
|
) {
|
||||||
node.setAttribute("disabled", "");
|
node.setAttribute("disabled", "");
|
||||||
const type = node.getAttribute("type") as "number" | "checkbox" | "range";
|
const type = node.getAttribute("type") as
|
||||||
|
| "number"
|
||||||
|
| "checkbox"
|
||||||
|
| "range"
|
||||||
|
| "color";
|
||||||
|
const isColor = type === "color";
|
||||||
const isNumeric =
|
const isNumeric =
|
||||||
type === "number" || type === "range" || node instanceof HTMLSelectElement;
|
type === "number" || type === "range" || node instanceof HTMLSelectElement;
|
||||||
const min = node.hasAttribute("min")
|
const min = node.hasAttribute("min")
|
||||||
@@ -19,36 +107,50 @@ export const setting: Action<
|
|||||||
? Number(node.getAttribute("max"))
|
? Number(node.getAttribute("max"))
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const unsubscribe = settings.subscribe(async (settings) => {
|
const subscription = combineLatest([
|
||||||
if (id in settings) {
|
fromReadable(settings),
|
||||||
const { value, isApplied } = settings[id]!;
|
fromReadable(activeProfile),
|
||||||
if (isNumeric) {
|
])
|
||||||
node.value = (
|
.pipe(map(([settings, profile]) => settings[profile]!))
|
||||||
inverse !== undefined
|
.subscribe(async (settings) => {
|
||||||
? inverse / value
|
if (id in settings) {
|
||||||
: scale !== undefined
|
const { value, isApplied } = settings[id]!;
|
||||||
? scale * value
|
if (isNumeric) {
|
||||||
: value
|
node.value = (
|
||||||
).toString();
|
inverse !== undefined
|
||||||
|
? inverse / value
|
||||||
|
: scale !== undefined
|
||||||
|
? scale * value
|
||||||
|
: value
|
||||||
|
).toString();
|
||||||
|
} else if (isColor) {
|
||||||
|
const rgb = hsvToRgb(
|
||||||
|
settings[id]!.value,
|
||||||
|
settings[id + 1]!.value,
|
||||||
|
settings[id + 2]!.value,
|
||||||
|
);
|
||||||
|
node.value = `#${rgb.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
||||||
|
} else {
|
||||||
|
node.checked = value !== 0;
|
||||||
|
}
|
||||||
|
if (isApplied) {
|
||||||
|
node.classList.remove("pending-changes");
|
||||||
|
} else {
|
||||||
|
node.classList.add("pending-changes");
|
||||||
|
}
|
||||||
|
node.removeAttribute("disabled");
|
||||||
} else {
|
} else {
|
||||||
node.checked = value !== 0;
|
node.setAttribute("disabled", "");
|
||||||
}
|
}
|
||||||
if (isApplied) {
|
});
|
||||||
node.classList.remove("pending-changes");
|
|
||||||
} else {
|
|
||||||
node.classList.add("pending-changes");
|
|
||||||
}
|
|
||||||
node.removeAttribute("disabled");
|
|
||||||
} else {
|
|
||||||
node.setAttribute("disabled", "");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function listener() {
|
async function listener() {
|
||||||
let value: number;
|
let value: number;
|
||||||
if (isNumeric) {
|
if (isNumeric) {
|
||||||
value = Number(node.value);
|
value = Number(node.value);
|
||||||
if (Number.isNaN(value)) return;
|
if (Number.isNaN(value)) return;
|
||||||
|
if (min !== undefined) value = Math.max(min, value);
|
||||||
|
if (max !== undefined) value = Math.min(max, value);
|
||||||
value = Math.floor(
|
value = Math.floor(
|
||||||
inverse !== undefined
|
inverse !== undefined
|
||||||
? inverse / value
|
? inverse / value
|
||||||
@@ -56,18 +158,36 @@ export const setting: Action<
|
|||||||
? value / scale
|
? value / scale
|
||||||
: value,
|
: value,
|
||||||
);
|
);
|
||||||
if (min !== undefined) value = Math.max(min, value);
|
} else if (isColor) {
|
||||||
if (max !== undefined) value = Math.min(max, value);
|
const r = parseInt(node.value.slice(1, 3), 16);
|
||||||
|
const g = parseInt(node.value.slice(3, 5), 16);
|
||||||
|
const b = parseInt(node.value.slice(5, 7), 16);
|
||||||
|
const hsv = rgbToHsv(r, g, b);
|
||||||
|
changes.update((changes) => {
|
||||||
|
changes.push(
|
||||||
|
hsv.map((value, i) => ({
|
||||||
|
type: ChangeType.Setting,
|
||||||
|
id: id + i,
|
||||||
|
setting: value,
|
||||||
|
profile: get(activeProfile),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return changes;
|
||||||
|
});
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
value = node.checked ? 1 : 0;
|
value = node.checked ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
changes.update((changes) => {
|
changes.update((changes) => {
|
||||||
changes.push({
|
changes.push([
|
||||||
type: ChangeType.Setting,
|
{
|
||||||
id: id,
|
type: ChangeType.Setting,
|
||||||
setting: value,
|
id: id,
|
||||||
});
|
setting: value,
|
||||||
|
profile: get(activeProfile),
|
||||||
|
},
|
||||||
|
]);
|
||||||
return changes;
|
return changes;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -77,7 +197,7 @@ export const setting: Action<
|
|||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
node.removeEventListener("change", listener);
|
node.removeEventListener("change", listener);
|
||||||
unsubscribe();
|
subscription.unsubscribe();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export function persistentWritable<T>(
|
|||||||
: writable(value);
|
: writable(value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
|
||||||
store = writable(value);
|
store = writable(value);
|
||||||
}
|
}
|
||||||
store.subscribe((value) => {
|
store.subscribe((value) => {
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
kbd {
|
kbd {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
height: 20px;
|
|
||||||
margin-block: 6px;
|
margin-block: 6px;
|
||||||
padding: 4px;
|
|
||||||
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: normal;
|
|
||||||
color: currentcolor;
|
|
||||||
|
|
||||||
border: 1px solid currentcolor;
|
border: 1px solid currentcolor;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
height: 20px;
|
||||||
|
color: currentcolor;
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
&.icon {
|
&.icon {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
@@ -21,8 +21,8 @@ kbd {
|
|||||||
|
|
||||||
&:has(> kbd) {
|
&:has(> kbd) {
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 0;
|
|
||||||
border: none;
|
border: none;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> kbd {
|
> kbd {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
h1 {
|
h1 {
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
font-size: 4rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--md-sys-color-secondary);
|
color: var(--md-sys-color-secondary);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 4rem;
|
||||||
}
|
}
|
||||||
|
|||||||
67
src/lib/style/elements/_popover.scss
Normal file
67
src/lib/style/elements/_popover.scss
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
$animation-duration: 150ms;
|
||||||
|
$translate: translateY(8px);
|
||||||
|
|
||||||
|
[popover] {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
transition:
|
||||||
|
transform $animation-duration ease,
|
||||||
|
opacity $animation-duration linear,
|
||||||
|
overlay $animation-duration allow-discrete,
|
||||||
|
display $animation-duration allow-discrete;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
inset: unset;
|
||||||
|
border: 1px solid var(--md-sys-color-outline);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--md-sys-color-surface);
|
||||||
|
padding: 8px;
|
||||||
|
color: var(--md-sys-color-on-surface);
|
||||||
|
font-weight: initial;
|
||||||
|
font-size: initial;
|
||||||
|
|
||||||
|
font-family: "Noto Sans Mono", monospace;
|
||||||
|
|
||||||
|
position-area: bottom span-all;
|
||||||
|
position-try-fallbacks:
|
||||||
|
top span-all,
|
||||||
|
bottom span-right,
|
||||||
|
top span-right,
|
||||||
|
bottom span-left,
|
||||||
|
top span-left;
|
||||||
|
|
||||||
|
position-visibility: no-overflow;
|
||||||
|
|
||||||
|
&:popover-open {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> h1:first-child,
|
||||||
|
h2:first-child,
|
||||||
|
h3:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[popover="auto"] {
|
||||||
|
transform: $translate;
|
||||||
|
}
|
||||||
|
|
||||||
|
[popover="hint"] {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@starting-style {
|
||||||
|
[popover]:popover-open {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[popover="auto"] {
|
||||||
|
transform: $translate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,29 +5,30 @@ a {
|
|||||||
a,
|
a,
|
||||||
label:has(input),
|
label:has(input),
|
||||||
button {
|
button {
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: all 250ms ease;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 32px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
padding-block: 8px;
|
||||||
|
|
||||||
width: max-content;
|
width: max-content;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
padding-block: 8px;
|
|
||||||
padding-inline: 16px;
|
|
||||||
|
|
||||||
font-family: inherit;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
|
font-family: inherit;
|
||||||
|
|
||||||
@media not (forced-colors: active) {
|
@media not (forced-colors: active) {
|
||||||
color: currentcolor;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: currentcolor;
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background: var(--md-sys-color-primary);
|
background: var(--md-sys-color-primary);
|
||||||
|
color: var(--md-sys-color-on-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,24 +37,19 @@ button {
|
|||||||
color: ButtonText;
|
color: ButtonText;
|
||||||
}
|
}
|
||||||
|
|
||||||
border-radius: 32px;
|
|
||||||
|
|
||||||
transition: all 250ms ease;
|
|
||||||
|
|
||||||
&.icon {
|
&.icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding-inline: 0;
|
||||||
|
padding-block: 0;
|
||||||
|
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
padding-block: 0;
|
|
||||||
padding-inline: 0;
|
|
||||||
|
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
@media (forced-colors: active) {
|
@media (forced-colors: active) {
|
||||||
padding: 2px;
|
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
|
padding: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +87,8 @@ button {
|
|||||||
}
|
}
|
||||||
&.active,
|
&.active,
|
||||||
&:active {
|
&:active {
|
||||||
color: SelectedItemText;
|
|
||||||
background: SelectedItem;
|
background: SelectedItem;
|
||||||
|
color: SelectedItemText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/lib/style/form/_radio.scss
Normal file
35
src/lib/style/form/_radio.scss
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
label:has(input[type="radio"]) {
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
transition: all 250ms ease;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
padding-inline: 12px;
|
||||||
|
|
||||||
|
aspect-ratio: unset;
|
||||||
|
height: 1.5em;
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
> input[type="radio"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-radius: 16px 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 16px 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(:checked) {
|
||||||
|
background: var(--md-sys-color-tertiary);
|
||||||
|
color: var(--md-sys-color-on-tertiary);
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,57 +3,55 @@ $border: 2px;
|
|||||||
$height: 1.5em;
|
$height: 1.5em;
|
||||||
|
|
||||||
label:has(input[type="checkbox"]) {
|
label:has(input[type="checkbox"]) {
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $padding;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: $padding;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
$width: calc($height * (5 / 3));
|
$width: calc($height * (5 / 3));
|
||||||
$diameter: calc($height - ((2 * $padding) + (2 * $border)));
|
$diameter: calc($height - ((2 * $padding) + (2 * $border)));
|
||||||
$radius: calc($diameter / 2);
|
$radius: calc($diameter / 2);
|
||||||
|
display: flex;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
overflow: hidden;
|
cursor: pointer;
|
||||||
display: flex;
|
|
||||||
|
outline: $border solid currentcolor;
|
||||||
|
outline-offset: calc(-1 * $border);
|
||||||
|
border-radius: calc($height / 2);
|
||||||
|
|
||||||
width: $width;
|
width: $width;
|
||||||
height: $height;
|
height: $height;
|
||||||
|
|
||||||
font-size: inherit;
|
overflow: hidden;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|
||||||
border-radius: calc($height / 2);
|
font-size: inherit;
|
||||||
outline: $border solid currentcolor;
|
|
||||||
outline-offset: calc(-1 * $border);
|
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
display: block;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc($padding + $border);
|
top: calc($padding + $border);
|
||||||
left: calc($padding + $border);
|
left: calc($padding + $border);
|
||||||
|
|
||||||
display: block;
|
transition: all 250ms ease;
|
||||||
|
|
||||||
width: $diameter;
|
|
||||||
height: $diameter;
|
|
||||||
|
|
||||||
border-radius: calc($radius);
|
|
||||||
outline-color: inherit;
|
outline-color: inherit;
|
||||||
outline-style: solid;
|
outline-style: solid;
|
||||||
outline-width: $radius;
|
outline-width: $radius;
|
||||||
outline-offset: calc(-1 * $radius);
|
outline-offset: calc(-1 * $radius);
|
||||||
|
border-radius: calc($radius);
|
||||||
|
|
||||||
transition: all 250ms ease;
|
width: $diameter;
|
||||||
|
height: $diameter;
|
||||||
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked::after {
|
&:checked::after {
|
||||||
@@ -62,4 +60,83 @@ label:has(input[type="checkbox"]) {
|
|||||||
outline-offset: calc($padding / 2);
|
outline-offset: calc($padding / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:has(span.icon) {
|
||||||
|
$line-width: 10%;
|
||||||
|
$side: calc(($line-width * 2) / sqrt(2));
|
||||||
|
$mid: calc($side / 2);
|
||||||
|
|
||||||
|
> input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span.icon {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
clip-path: polygon(
|
||||||
|
0% $side,
|
||||||
|
$mid $mid,
|
||||||
|
calc(100% - $mid) calc(100% - $mid),
|
||||||
|
calc(100% - $side) 100%,
|
||||||
|
0% 100%,
|
||||||
|
0% $side,
|
||||||
|
$side 0%,
|
||||||
|
100% calc(100% - $side),
|
||||||
|
calc(100% - $side) 100%,
|
||||||
|
calc(100% - $side) 100%,
|
||||||
|
100% calc(100% - $side),
|
||||||
|
100% calc(100% - $side),
|
||||||
|
100% 0%,
|
||||||
|
$side 0%
|
||||||
|
);
|
||||||
|
|
||||||
|
transition: all 250ms ease;
|
||||||
|
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
|
||||||
|
font-size: inherit;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0) rotate(45deg);
|
||||||
|
|
||||||
|
transition: all 250ms ease;
|
||||||
|
|
||||||
|
background-color: currentcolor;
|
||||||
|
|
||||||
|
width: calc(100% * sqrt(2));
|
||||||
|
height: $line-width;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(:checked) > span.icon {
|
||||||
|
clip-path: polygon(
|
||||||
|
0% $side,
|
||||||
|
$mid $mid,
|
||||||
|
calc(100% - $mid) calc(100% - $mid),
|
||||||
|
calc(100% - $side) 100%,
|
||||||
|
0% 100%,
|
||||||
|
0% $side,
|
||||||
|
$side 0%,
|
||||||
|
100% calc(100% - $side),
|
||||||
|
calc(100% - $side) 100%,
|
||||||
|
0% $side,
|
||||||
|
$side 0%,
|
||||||
|
100% calc(100% - $side),
|
||||||
|
100% 0%,
|
||||||
|
$side 0%
|
||||||
|
);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
transform: translate(-50%, 0) rotate(45deg) translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
@media not (forced-colors: active) {
|
@media not (forced-colors: active) {
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: var(--scrollbar-color, white);
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 250ms ease;
|
transition: all 250ms ease;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--scrollbar-color, white);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: brightness(120%);
|
filter: brightness(120%);
|
||||||
|
|||||||
@@ -1,35 +1,36 @@
|
|||||||
@import "./reset";
|
@use "reset";
|
||||||
|
|
||||||
@import "./form/button";
|
@use "form/button";
|
||||||
@import "./form/toggle";
|
@use "form/toggle";
|
||||||
@import "./form/checkbox";
|
@use "form/radio";
|
||||||
|
|
||||||
@import "./kbd";
|
@use "kbd";
|
||||||
@import "./print";
|
@use "print";
|
||||||
|
|
||||||
@import "./elements/h1";
|
@use "elements/h1";
|
||||||
|
@use "elements/popover";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-background);
|
||||||
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
margin: 0;
|
overflow: hidden;
|
||||||
|
|
||||||
font-family: "Noto Sans Mono", monospace;
|
|
||||||
color: var(--md-sys-color-on-background);
|
color: var(--md-sys-color-on-background);
|
||||||
|
|
||||||
background: var(--md-sys-color-background);
|
font-family: "Noto Sans Mono", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
contain: strict;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
contain: strict;
|
||||||
|
|
||||||
padding-inline: 16px;
|
padding-inline: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
$padding: 16px;
|
$padding: 16px;
|
||||||
|
|
||||||
.tippy-box[data-theme~="surface-variant"] {
|
.tippy-box[data-theme~="surface-variant"] {
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
|
||||||
background-color: var(--md-sys-color-surface-variant);
|
|
||||||
filter: drop-shadow(0 0 12px #000a);
|
filter: drop-shadow(0 0 12px #000a);
|
||||||
border-radius: calc(24px + $padding);
|
border-radius: calc(24px + $padding);
|
||||||
|
background-color: var(--md-sys-color-surface-variant);
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
|
||||||
.tippy-content {
|
.tippy-content {
|
||||||
padding: $padding;
|
padding: $padding;
|
||||||
@@ -24,10 +24,10 @@ $padding: 16px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (forced-colors: active) {
|
@media (forced-colors: active) {
|
||||||
color: CanvasText;
|
|
||||||
background-color: Canvas;
|
|
||||||
filter: none;
|
filter: none;
|
||||||
border: 1px solid CanvasText;
|
border: 1px solid CanvasText;
|
||||||
|
background-color: Canvas;
|
||||||
|
color: CanvasText;
|
||||||
|
|
||||||
> .tippy-arrow {
|
> .tippy-arrow {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -36,16 +36,16 @@ $padding: 16px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tippy-box[data-theme~="tooltip"] {
|
.tippy-box[data-theme~="tooltip"] {
|
||||||
color: var(--md-sys-color-on-background);
|
|
||||||
background-color: var(--md-sys-color-background);
|
|
||||||
border: 1px solid var(--md-sys-color-outline);
|
border: 1px solid var(--md-sys-color-outline);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
background-color: var(--md-sys-color-background);
|
||||||
|
color: var(--md-sys-color-on-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-box[data-theme~="search-completion"] {
|
.tippy-box[data-theme~="search-completion"] {
|
||||||
overflow: hidden;
|
|
||||||
filter: none;
|
filter: none;
|
||||||
border-radius: 0 0 16px 16px;
|
border-radius: 0 0 16px 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.tippy-content {
|
.tippy-content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import Tooltip from "$lib/components/Tooltip.svelte";
|
|||||||
|
|
||||||
export const hotkeys = new Map<string, HTMLElement>();
|
export const hotkeys = new Map<string, HTMLElement>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use `tooltip` instead.
|
||||||
|
*/
|
||||||
export const action: Action<Element, { title?: string; shortcut?: string }> = (
|
export const action: Action<Element, { title?: string; shortcut?: string }> = (
|
||||||
node: Element,
|
node: Element,
|
||||||
{ title, shortcut },
|
{ title, shortcut },
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface LayoutChange {
|
|||||||
id: number;
|
id: number;
|
||||||
layer: number;
|
layer: number;
|
||||||
action: number;
|
action: number;
|
||||||
|
profile?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChordChange {
|
export interface ChordChange {
|
||||||
@@ -33,6 +34,7 @@ export interface SettingChange {
|
|||||||
type: ChangeType.Setting;
|
type: ChangeType.Setting;
|
||||||
id: number;
|
id: number;
|
||||||
setting: number;
|
setting: number;
|
||||||
|
profile?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChangeInfo {
|
export interface ChangeInfo {
|
||||||
@@ -42,36 +44,46 @@ export interface ChangeInfo {
|
|||||||
|
|
||||||
export type Change = LayoutChange | ChordChange | SettingChange;
|
export type Change = LayoutChange | ChordChange | SettingChange;
|
||||||
|
|
||||||
export const changes = persistentWritable<Change[]>("changes", []);
|
export const changes = persistentWritable<Change[][]>("changes", []);
|
||||||
|
|
||||||
export interface Overlay {
|
export interface Overlay {
|
||||||
layout: [Map<number, number>, Map<number, number>, Map<number, number>];
|
layout: Array<Array<Map<number, number> | undefined> | undefined>;
|
||||||
chords: Map<string, Chord & { deleted: boolean }>;
|
chords: Map<string, Chord & { deleted: boolean }>;
|
||||||
settings: Map<number, number>;
|
settings: Array<Map<number, number> | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const overlay = derived(changes, (changes) => {
|
export const overlay = derived(changes, (changes) => {
|
||||||
const overlay: Overlay = {
|
const overlay: Overlay = {
|
||||||
layout: [new Map(), new Map(), new Map()],
|
layout: [],
|
||||||
chords: new Map(),
|
chords: new Map(),
|
||||||
settings: new Map(),
|
settings: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const changeset of changes) {
|
||||||
switch (change.type) {
|
for (const change of changeset) {
|
||||||
case ChangeType.Layout:
|
switch (change.type) {
|
||||||
overlay.layout[change.layer]?.set(change.id, change.action);
|
case ChangeType.Layout:
|
||||||
break;
|
change.profile ??= 0;
|
||||||
case ChangeType.Chord:
|
overlay.layout[change.profile] ??= [];
|
||||||
overlay.chords.set(JSON.stringify(change.id), {
|
overlay.layout[change.profile]![change.layer] ??= new Map();
|
||||||
actions: change.actions,
|
overlay.layout[change.profile]![change.layer]!.set(
|
||||||
phrase: change.phrase,
|
change.id,
|
||||||
deleted: change.deleted ?? false,
|
change.action,
|
||||||
});
|
);
|
||||||
break;
|
break;
|
||||||
case ChangeType.Setting:
|
case ChangeType.Chord:
|
||||||
overlay.settings.set(change.id, change.setting);
|
overlay.chords.set(JSON.stringify(change.id), {
|
||||||
break;
|
actions: change.actions,
|
||||||
|
phrase: change.phrase,
|
||||||
|
deleted: change.deleted ?? false,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ChangeType.Setting:
|
||||||
|
change.profile ??= 0;
|
||||||
|
overlay.settings[change.profile] ??= new Map();
|
||||||
|
overlay.settings[change.profile]!.set(change.id, change.setting);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,21 +92,25 @@ export const overlay = derived(changes, (changes) => {
|
|||||||
|
|
||||||
export const settings = derived(
|
export const settings = derived(
|
||||||
[overlay, deviceSettings],
|
[overlay, deviceSettings],
|
||||||
([overlay, settings]) =>
|
([overlay, profiles]) =>
|
||||||
settings.map<{ value: number } & ChangeInfo>((value, id) => ({
|
profiles.map((settings, profile) =>
|
||||||
value: overlay.settings.get(id) ?? value,
|
settings.map<{ value: number } & ChangeInfo>((value, id) => ({
|
||||||
isApplied: !overlay.settings.has(id),
|
value: overlay.settings[profile]?.get(id) ?? value,
|
||||||
})),
|
isApplied: !overlay.settings[profile]?.has(id),
|
||||||
|
})),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export type KeyInfo = { action: number } & ChangeInfo;
|
export type KeyInfo = { action: number } & ChangeInfo;
|
||||||
export const layout = derived([overlay, deviceLayout], ([overlay, layout]) =>
|
export const layout = derived([overlay, deviceLayout], ([overlay, profiles]) =>
|
||||||
layout.map(
|
profiles.map((layout, profile) =>
|
||||||
(actions, layer) =>
|
layout.map(
|
||||||
actions.map<KeyInfo>((action, id) => ({
|
(actions, layer) =>
|
||||||
action: overlay.layout[layer]?.get(id) ?? action,
|
actions.map<KeyInfo>((action, id) => ({
|
||||||
isApplied: !overlay.layout[layer]?.has(id),
|
action: overlay.layout[profile]?.[layer]?.get(id) ?? action,
|
||||||
})) as [KeyInfo, KeyInfo, KeyInfo],
|
isApplied: !overlay.layout[profile]?.[layer]?.has(id),
|
||||||
|
})) as [KeyInfo, KeyInfo, KeyInfo],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -107,57 +123,61 @@ export type ChordInfo = Chord &
|
|||||||
id: number[];
|
id: number[];
|
||||||
deleted: boolean;
|
deleted: boolean;
|
||||||
};
|
};
|
||||||
export const chords = derived([overlay, deviceChords], ([overlay, chords]) => {
|
export const chords = derived(
|
||||||
const newChords = new Set(overlay.chords.keys());
|
[overlay, deviceChords, KEYMAP_CODES],
|
||||||
|
([overlay, chords, codes]) => {
|
||||||
|
const newChords = new Set(overlay.chords.keys());
|
||||||
|
|
||||||
const changedChords = chords.map<ChordInfo>((chord) => {
|
const changedChords = chords.map<ChordInfo>((chord) => {
|
||||||
const id = JSON.stringify(chord.actions);
|
const id = JSON.stringify(chord.actions);
|
||||||
if (overlay.chords.has(id)) {
|
if (overlay.chords.has(id)) {
|
||||||
newChords.delete(id);
|
newChords.delete(id);
|
||||||
const changedChord = overlay.chords.get(id)!;
|
const changedChord = overlay.chords.get(id)!;
|
||||||
return {
|
return {
|
||||||
id: chord.actions,
|
id: chord.actions,
|
||||||
// use the old phrase for stable editing
|
// use the old phrase for stable editing
|
||||||
sortBy: chord.phrase.map((it) => KEYMAP_CODES.get(it)?.id ?? it).join(),
|
sortBy: chord.phrase.map((it) => codes.get(it)?.id ?? it).join(),
|
||||||
actions: changedChord.actions,
|
actions: changedChord.actions,
|
||||||
phrase: changedChord.phrase,
|
phrase: changedChord.phrase,
|
||||||
actionsChanged: id !== JSON.stringify(changedChord.actions),
|
actionsChanged: id !== JSON.stringify(changedChord.actions),
|
||||||
phraseChanged:
|
phraseChanged:
|
||||||
JSON.stringify(chord.phrase) !== JSON.stringify(changedChord.phrase),
|
JSON.stringify(chord.phrase) !==
|
||||||
isApplied: false,
|
JSON.stringify(changedChord.phrase),
|
||||||
deleted: changedChord.deleted,
|
isApplied: false,
|
||||||
};
|
deleted: changedChord.deleted,
|
||||||
} else {
|
};
|
||||||
return {
|
} else {
|
||||||
id: chord.actions,
|
return {
|
||||||
sortBy: chord.phrase.map((it) => KEYMAP_CODES.get(it)?.id ?? it).join(),
|
id: chord.actions,
|
||||||
actions: chord.actions,
|
sortBy: chord.phrase.map((it) => codes.get(it)?.id ?? it).join(),
|
||||||
phrase: chord.phrase,
|
actions: chord.actions,
|
||||||
phraseChanged: false,
|
phrase: chord.phrase,
|
||||||
actionsChanged: false,
|
phraseChanged: false,
|
||||||
isApplied: true,
|
actionsChanged: false,
|
||||||
deleted: false,
|
isApplied: true,
|
||||||
};
|
deleted: false,
|
||||||
}
|
};
|
||||||
});
|
}
|
||||||
for (const id of newChords) {
|
|
||||||
const chord = overlay.chords.get(id)!;
|
|
||||||
changedChords.push({
|
|
||||||
sortBy: "",
|
|
||||||
isApplied: false,
|
|
||||||
actionsChanged: true,
|
|
||||||
phraseChanged: false,
|
|
||||||
deleted: chord.deleted,
|
|
||||||
id: JSON.parse(id),
|
|
||||||
phrase: chord.phrase,
|
|
||||||
actions: chord.actions,
|
|
||||||
});
|
});
|
||||||
}
|
for (const id of newChords) {
|
||||||
|
const chord = overlay.chords.get(id)!;
|
||||||
|
changedChords.push({
|
||||||
|
sortBy: "",
|
||||||
|
isApplied: false,
|
||||||
|
actionsChanged: true,
|
||||||
|
phraseChanged: false,
|
||||||
|
deleted: chord.deleted,
|
||||||
|
id: JSON.parse(id),
|
||||||
|
phrase: chord.phrase,
|
||||||
|
actions: chord.actions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return changedChords.sort(({ sortBy: a }, { sortBy: b }) =>
|
return changedChords.sort(({ sortBy: a }, { sortBy: b }) =>
|
||||||
a.localeCompare(b),
|
a.localeCompare(b),
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const chordHashes = derived(
|
export const chordHashes = derived(
|
||||||
chords,
|
chords,
|
||||||
|
|||||||
47
src/lib/util/debounce.ts
Normal file
47
src/lib/util/debounce.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Creates a debounced function that delays invoking the provided function
|
||||||
|
* until after 'wait' milliseconds have elapsed since the last time it was
|
||||||
|
* invoked.
|
||||||
|
*
|
||||||
|
* I could use _.debounce(), but bringing dependency on lodash didn't feel
|
||||||
|
* justified yet.
|
||||||
|
*
|
||||||
|
* @param func The function to debounce
|
||||||
|
* @param wait The number of milliseconds to delay execution
|
||||||
|
* @returns A debounced version of the provided function
|
||||||
|
*/
|
||||||
|
function debounce<T extends (...args: any[]) => void>(
|
||||||
|
func: T,
|
||||||
|
wait: number,
|
||||||
|
): T & { cancel: () => void } {
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const debounced = function (
|
||||||
|
this: ThisParameterType<T>,
|
||||||
|
...args: Parameters<T>
|
||||||
|
): void {
|
||||||
|
const context = this;
|
||||||
|
|
||||||
|
const later = function () {
|
||||||
|
timeout = null;
|
||||||
|
func.apply(context, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
|
||||||
|
debounced.cancel = function () {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return debounced as T & { cancel: () => void };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default debounce;
|
||||||
10
src/lib/util/from-readable.ts
Normal file
10
src/lib/util/from-readable.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
import type { Readable } from "svelte/store";
|
||||||
|
|
||||||
|
export function fromReadable<T>(store: Readable<T>): Observable<T> {
|
||||||
|
return new Observable((subscriber) =>
|
||||||
|
store.subscribe((value) => {
|
||||||
|
subscriber.next(value);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
15
src/lib/util/shuffle.ts
Normal file
15
src/lib/util/shuffle.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
|
||||||
|
*/
|
||||||
|
export function shuffleInPlace<T>(array: T[]) {
|
||||||
|
for (let i = array.length - 1; i >= 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[array[i], array[j]] = [array[j]!, array[i]!];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shuffle<T>(array: T[]): T[] {
|
||||||
|
const result = [...array];
|
||||||
|
shuffleInPlace(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -131,14 +131,13 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.layout {
|
.layout {
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"sidebar main"
|
"sidebar main"
|
||||||
"sidebar footer";
|
"sidebar footer";
|
||||||
grid-template-columns: auto 1fr;
|
width: 100vw;
|
||||||
grid-template-rows: 1fr;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -26,17 +26,17 @@
|
|||||||
dialog {
|
dialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-error);
|
||||||
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
color: var(--md-sys-color-on-error);
|
color: var(--md-sys-color-on-error);
|
||||||
|
|
||||||
background: var(--md-sys-color-error);
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
max-width: 20cm;
|
max-width: 20cm;
|
||||||
}
|
}
|
||||||
@@ -54,8 +54,8 @@
|
|||||||
|
|
||||||
div > p {
|
div > p {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
syncStatus,
|
syncStatus,
|
||||||
} from "$lib/serial/connection";
|
} from "$lib/serial/connection";
|
||||||
import { fade, slide } from "svelte/transition";
|
import { fade, slide } from "svelte/transition";
|
||||||
|
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
|
||||||
|
|
||||||
let locale = $state(
|
let locale = $state(
|
||||||
(browser && (localStorage.getItem("locale") as Locales)) || detectLocale(),
|
(browser && (localStorage.getItem("locale") as Locales)) || detectLocale(),
|
||||||
@@ -52,9 +53,7 @@
|
|||||||
await initSerial(true);
|
await initSerial(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert(
|
await showConnectionFailedDialog(String(error));
|
||||||
"Connection failed. Is your device maybe pre-CCOS? Refer to the doc link in the bottom left for more information on your device.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,16 +173,11 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
select {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync-box {
|
.sync-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
@@ -192,14 +186,14 @@
|
|||||||
|
|
||||||
progress {
|
progress {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: -1;
|
right: 16px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
right: 16px;
|
z-index: -1;
|
||||||
overflow: hidden;
|
border-radius: 4px;
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 4px;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress::-webkit-progress-bar {
|
progress::-webkit-progress-bar {
|
||||||
@@ -211,32 +205,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
color: var(--md-sys-color-error);
|
|
||||||
gap: 8px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--md-sys-color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="color"] {
|
input[type="color"] {
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
inline-size: 20px;
|
inline-size: 20px;
|
||||||
block-size: 20px;
|
block-size: 20px;
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
overflow: hidden;
|
||||||
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
&::-webkit-color-swatch-wrapper {
|
&::-webkit-color-swatch-wrapper {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -248,16 +242,16 @@
|
|||||||
|
|
||||||
footer {
|
footer {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
width: 100%;
|
opacity: 0.4;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
padding-inline-end: 16px;
|
padding-inline-end: 16px;
|
||||||
padding-block-start: 0;
|
padding-block-start: 0;
|
||||||
|
|
||||||
opacity: 0.4;
|
width: 100%;
|
||||||
|
|
||||||
@media (prefers-contrast: more) {
|
@media (prefers-contrast: more) {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
@@ -270,8 +264,8 @@
|
|||||||
|
|
||||||
ul {
|
ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -294,13 +288,13 @@
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding-inline: 12px;
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
padding-inline: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
{ href: "/config/layout/", icon: "keyboard", title: "Layout" },
|
{ href: "/config/layout/", icon: "keyboard", title: "Layout" },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
// { href: "/learn", icon: "school", title: "Learn", wip: true },
|
|
||||||
{
|
{
|
||||||
href: import.meta.env.VITE_LEARN_URL,
|
href: import.meta.env.VITE_LEARN_URL,
|
||||||
icon: "school",
|
icon: "school",
|
||||||
@@ -26,8 +25,17 @@
|
|||||||
title: "Docs",
|
title: "Docs",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: "https://voicebox.charachorder.io/",
|
||||||
|
icon: "text_to_speech",
|
||||||
|
title: "Voicebox",
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
{ href: "/editor", icon: "edit_document", title: "Editor", wip: true },
|
{ href: "/editor", icon: "edit_document", title: "Editor", wip: true },
|
||||||
{ href: "https://chat.dev.charachorder.io", icon: "chat", title: "Chat", wip: true },
|
{ href: "/chat", icon: "chat", title: "Chat", wip: true },
|
||||||
|
{ href: "/learn", icon: "school", title: "Learn", wip: true },
|
||||||
],
|
],
|
||||||
/*[
|
/*[
|
||||||
{ href: "/plugin", icon: "code", title: "Plugin", wip: true },
|
{ href: "/plugin", icon: "code", title: "Plugin", wip: true },
|
||||||
@@ -73,15 +81,15 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.sidebar {
|
.sidebar {
|
||||||
margin: 8px;
|
|
||||||
padding-inline-end: 8px;
|
|
||||||
width: 64px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
grid-area: sidebar;
|
grid-area: sidebar;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 8px;
|
||||||
border-right: 1px solid var(--md-sys-color-outline);
|
border-right: 1px solid var(--md-sys-color-outline);
|
||||||
|
padding-inline-end: 8px;
|
||||||
|
width: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
@@ -96,18 +104,18 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
&.wip {
|
&.wip {
|
||||||
color: var(--md-sys-color-error);
|
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
color: var(--md-sys-color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 24px;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
transition: all 250ms ease;
|
transition: all 250ms ease;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
@@ -124,24 +132,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
border-radius: 50%;
|
||||||
background: var(--md-sys-color-primary);
|
background: var(--md-sys-color-primary);
|
||||||
color: var(--md-sys-color-on-primary);
|
color: var(--md-sys-color-on-primary);
|
||||||
border-radius: 50%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul + ul::before {
|
ul + ul::before {
|
||||||
content: "";
|
|
||||||
display: block;
|
display: block;
|
||||||
height: 1px;
|
|
||||||
background: var(--md-sys-color-outline);
|
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
|
background: var(--md-sys-color-outline);
|
||||||
|
height: 1px;
|
||||||
|
content: "";
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
@@ -42,10 +42,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
progress {
|
progress {
|
||||||
overflow: hidden;
|
border-radius: 4px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 4px;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress::-webkit-progress-bar {
|
progress::-webkit-progress-bar {
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
progress::-webkit-progress-value {
|
progress::-webkit-progress-value {
|
||||||
|
transition: width 2s ease;
|
||||||
background: var(--md-sys-color-primary);
|
background: var(--md-sys-color-primary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"CHARACHORDER ONE M0": {
|
|
||||||
"latest": "1.1.3",
|
|
||||||
"next": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,107 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { beforeNavigate } from "$app/navigation";
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
import { serialPort } from "$lib/serial/connection";
|
||||||
|
import { expoOut } from "svelte/easing";
|
||||||
|
import { slide } from "svelte/transition";
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
const animationDuration = 400;
|
||||||
|
const stagger = 80;
|
||||||
|
|
||||||
|
let targetDevice = $derived($page.params["device"]);
|
||||||
|
let version = $derived($page.params["version"]);
|
||||||
|
|
||||||
|
let currentDevice = $derived(
|
||||||
|
$serialPort
|
||||||
|
? `${$serialPort.device.toLowerCase()}_${$serialPort.chipset.toLowerCase()}`
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
let isCorrectDevice = $derived(
|
||||||
|
currentDevice ? currentDevice === targetDevice : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
let fullBack = $state(false);
|
||||||
|
|
||||||
|
beforeNavigate(({ from, to, cancel }) => {
|
||||||
|
fullBack = version !== undefined;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1><a href="/ccos">Firmware Updates</a></h1>
|
<h1>
|
||||||
|
<a class="inline-link" href="/ccos">CCOS</a>
|
||||||
|
{#if targetDevice !== undefined}
|
||||||
|
<div
|
||||||
|
class="uri-fragment"
|
||||||
|
transition:slide={{
|
||||||
|
axis: "x",
|
||||||
|
duration: animationDuration,
|
||||||
|
delay: fullBack ? stagger : 0,
|
||||||
|
easing: expoOut,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<a
|
||||||
|
href="/ccos/{targetDevice}"
|
||||||
|
class="device inline-link"
|
||||||
|
class:correct-device={isCorrectDevice === true}
|
||||||
|
class:incorrect-device={isCorrectDevice === false}>{targetDevice}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if version !== undefined}
|
||||||
|
<div
|
||||||
|
class="uri-fragment"
|
||||||
|
transition:slide={{
|
||||||
|
axis: "x",
|
||||||
|
duration: animationDuration,
|
||||||
|
easing: expoOut,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<em class="version">{version}</em>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</h1>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
h1 {
|
h1 {
|
||||||
|
display: flex;
|
||||||
margin-block: 1em;
|
margin-block: 1em;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 3em;
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uri-fragment {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
margin-inline: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
color: var(--md-sys-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-link {
|
||||||
|
display: inline;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.correct-device {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--md-sys-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.incorrect-device {
|
||||||
|
color: var(--md-sys-color-error);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
ul {
|
ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
list-style: none;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
@@ -38,13 +38,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
outline: 1px solid var(--md-sys-color-outline);
|
|
||||||
border-radius: 8px;
|
|
||||||
transition:
|
transition:
|
||||||
background-color 200ms ease,
|
background-color 200ms ease,
|
||||||
color 200ms ease,
|
color 200ms ease,
|
||||||
outline-offset 200ms ease,
|
outline-offset 200ms ease,
|
||||||
outline-color 200ms ease;
|
outline-color 200ms ease;
|
||||||
|
outline: 1px solid var(--md-sys-color-outline);
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes highlight {
|
@keyframes highlight {
|
||||||
@@ -71,9 +71,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
outline-width: 2px;
|
|
||||||
outline-color: var(--md-sys-color-primary);
|
|
||||||
animation: wiggle 500ms ease 2 alternate;
|
animation: wiggle 500ms ease 2 alternate;
|
||||||
|
outline-color: var(--md-sys-color-primary);
|
||||||
|
outline-width: 2px;
|
||||||
background-color: var(--md-sys-color-primary-container);
|
background-color: var(--md-sys-color-primary-container);
|
||||||
color: var(--md-sys-color-on-primary-container);
|
color: var(--md-sys-color-on-primary-container);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
import type { DirectoryListing } from "./listing";
|
import type { DirectoryListing } from "$lib/meta/types/listing";
|
||||||
|
|
||||||
export const load = (async ({ fetch }) => {
|
export const load = (async ({ fetch }) => {
|
||||||
const result = await fetch(import.meta.env.VITE_FIRMWARE_URL);
|
const result = await fetch(`${import.meta.env.VITE_FIRMWARE_URL}/`);
|
||||||
const data = await result.json();
|
const data = await result.json();
|
||||||
|
|
||||||
return { devices: data as DirectoryListing[] };
|
return { devices: data as DirectoryListing[] };
|
||||||
|
|||||||
@@ -38,21 +38,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
height: 2em;
|
|
||||||
overflow: hidden;
|
|
||||||
transition:
|
transition:
|
||||||
height 200ms ease,
|
height 200ms ease,
|
||||||
opacity 200ms ease;
|
opacity 200ms ease;
|
||||||
|
height: 2em;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
padding: 0;
|
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -64,8 +64,8 @@
|
|||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
|
|
||||||
em {
|
em {
|
||||||
font-style: normal;
|
|
||||||
color: var(--md-sys-color-primary);
|
color: var(--md-sys-color-primary);
|
||||||
|
font-style: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,13 +73,13 @@
|
|||||||
time {
|
time {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
&:before {
|
&:before {
|
||||||
content: "•";
|
|
||||||
padding-inline: 0.4ch;
|
padding-inline: 0.4ch;
|
||||||
|
content: "•";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.title:has(input:not(:checked)) ~ ul .pre-release {
|
div.title:has(input:not(:checked)) ~ ul .pre-release {
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
import type { DirectoryListing } from "../listing";
|
import type { DirectoryListing } from "$lib/meta/types/listing";
|
||||||
|
import { compare } from "semver";
|
||||||
|
|
||||||
export const load = (async ({ fetch, params }) => {
|
export const load = (async ({ fetch, params }) => {
|
||||||
const result = await fetch(
|
const result = await fetch(
|
||||||
@@ -9,7 +10,7 @@ export const load = (async ({ fetch, params }) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
versions: (data as DirectoryListing[]).sort((a, b) =>
|
versions: (data as DirectoryListing[]).sort((a, b) =>
|
||||||
b.name.localeCompare(a.name),
|
compare(b.name, a.name),
|
||||||
),
|
),
|
||||||
device: params.device,
|
device: params.device,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { downloadBackup } from "$lib/backup/backup";
|
import { downloadBackup } from "$lib/backup/backup";
|
||||||
import { initSerial, serialPort } from "$lib/serial/connection";
|
import { initSerial, serialPort } from "$lib/serial/connection";
|
||||||
import { fade, slide } from "svelte/transition";
|
import { fade, slide } from "svelte/transition";
|
||||||
|
import { lt as semverLt } from "semver";
|
||||||
import type { LoaderOptions, ESPLoader } from "esptool-js";
|
import type { LoaderOptions, ESPLoader } from "esptool-js";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
@@ -10,7 +11,14 @@
|
|||||||
let success = $state(false);
|
let success = $state(false);
|
||||||
let error = $state<Error | undefined>(undefined);
|
let error = $state<Error | undefined>(undefined);
|
||||||
|
|
||||||
|
let isTooOld = $derived(
|
||||||
|
$serialPort ? semverLt($serialPort.version, "2.0.0") : false,
|
||||||
|
);
|
||||||
|
|
||||||
|
let unsafeUpdate = $state(false);
|
||||||
|
|
||||||
let terminalOutput = $state("");
|
let terminalOutput = $state("");
|
||||||
|
let progress = $state(0);
|
||||||
|
|
||||||
let step = $state(0);
|
let step = $state(0);
|
||||||
let eraseAll = $state(false);
|
let eraseAll = $state(false);
|
||||||
@@ -25,10 +33,12 @@
|
|||||||
$serialPort = undefined;
|
$serialPort = undefined;
|
||||||
try {
|
try {
|
||||||
const file = await fetch(
|
const file = await fetch(
|
||||||
`${data.meta.path}/${data.meta.update.ota?.name}`,
|
`${data.meta.path}/${data.meta.update.ota}`,
|
||||||
).then((it) => it.blob());
|
).then((it) => it.arrayBuffer());
|
||||||
|
|
||||||
await port.updateFirmware(file);
|
await port.updateFirmware(file, (transferred, total) => {
|
||||||
|
progress = transferred / total;
|
||||||
|
});
|
||||||
|
|
||||||
success = true;
|
success = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -44,7 +54,7 @@
|
|||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
let isCorrectDevice = $derived(
|
let isCorrectDevice = $derived(
|
||||||
currentDevice ? currentDevice === data.meta.target : undefined,
|
currentDevice ? currentDevice === data.meta.device : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,11 +92,11 @@
|
|||||||
|
|
||||||
async function getFileSystem() {
|
async function getFileSystem() {
|
||||||
if (!data.meta.update.uf2) return;
|
if (!data.meta.update.uf2) return;
|
||||||
const uf2Promise = fetch(
|
const uf2Promise = fetch(`${data.meta.path}/${data.meta.update.uf2}`).then(
|
||||||
`${data.meta.path}/${data.meta.update.uf2.name}`,
|
(it) => it.blob(),
|
||||||
).then((it) => it.blob());
|
);
|
||||||
const handle = await window.showSaveFilePicker({
|
const handle = await window.showSaveFilePicker({
|
||||||
id: `${data.meta.target}-update`,
|
id: `${data.meta.device}-update`,
|
||||||
suggestedName: "CURRENT.UF2",
|
suggestedName: "CURRENT.UF2",
|
||||||
excludeAcceptAllOption: true,
|
excludeAcceptAllOption: true,
|
||||||
types: [
|
types: [
|
||||||
@@ -170,7 +180,7 @@
|
|||||||
const port = await navigator.serial.requestPort();
|
const port = await navigator.serial.requestPort();
|
||||||
try {
|
try {
|
||||||
console.log(data.meta);
|
console.log(data.meta);
|
||||||
const spiFlash = data.meta.spi_flash!;
|
const spiFlash = data.meta.spiFlash!;
|
||||||
espLoader = await connectEsp(port);
|
espLoader = await connectEsp(port);
|
||||||
|
|
||||||
/*espLoader.flashSpiAttach(
|
/*espLoader.flashSpiAttach(
|
||||||
@@ -187,29 +197,28 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>
|
{#if data.meta.update.ota && !data.meta.device.endsWith("m0")}
|
||||||
<a class="inline-link" href="/ccos">CCOS</a> /
|
|
||||||
<a
|
|
||||||
href="/ccos/{data.meta.target}"
|
|
||||||
class="device inline-link"
|
|
||||||
class:correct-device={isCorrectDevice === true}
|
|
||||||
class:incorrect-device={isCorrectDevice === false}>{data.meta.target}</a
|
|
||||||
>
|
|
||||||
/ <em class="version">{data.meta.version}</em>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{#if data.meta.update.ota && !data.meta.target.endsWith("m0")}
|
|
||||||
{@const buttonError = error || (!success && isCorrectDevice === false)}
|
{@const buttonError = error || (!success && isCorrectDevice === false)}
|
||||||
<section>
|
<section>
|
||||||
<button
|
<button
|
||||||
class="update-button"
|
class="update-button"
|
||||||
class:working
|
class:working={working && (progress <= 0 || progress >= 1)}
|
||||||
|
class:progress={working && progress > 0 && progress < 1}
|
||||||
|
style:--progress="{progress * 100}%"
|
||||||
class:primary={!buttonError}
|
class:primary={!buttonError}
|
||||||
class:error={buttonError}
|
class:error={buttonError}
|
||||||
disabled={working || $serialPort === undefined || !isCorrectDevice}
|
disabled={isTooOld ||
|
||||||
|
working ||
|
||||||
|
$serialPort === undefined ||
|
||||||
|
!isCorrectDevice}
|
||||||
onclick={update}>Apply Update</button
|
onclick={update}>Apply Update</button
|
||||||
>
|
>
|
||||||
{#if $serialPort && isCorrectDevice}
|
{#if isTooOld}
|
||||||
|
<div class="error" transition:slide>
|
||||||
|
Your device's firmware is too old to be updated via OTA. Follow the
|
||||||
|
instruction below to update it manually.
|
||||||
|
</div>
|
||||||
|
{:else if $serialPort && isCorrectDevice}
|
||||||
<div transition:slide>
|
<div transition:slide>
|
||||||
Your
|
Your
|
||||||
<b
|
<b
|
||||||
@@ -237,103 +246,164 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<h3>Manual Update</h3>
|
{#if !isTooOld}
|
||||||
|
<label class="unsafe-opt-in"
|
||||||
|
><input type="checkbox" /> Unsafe recovery options</label
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isCorrectDevice === false}
|
<div class="unsafe-updates">
|
||||||
<div transition:slide class="incorrect-device">
|
{#if isCorrectDevice === false}
|
||||||
These files are incompatible with your device
|
<div transition:slide class="incorrect-device">
|
||||||
</div>
|
These files are incompatible with your device
|
||||||
{/if}
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<button class="inline-button" onclick={connect}
|
|
||||||
><span class="icon">usb</span>Connect</button
|
|
||||||
>
|
|
||||||
your device
|
|
||||||
{#if step >= 1}
|
|
||||||
<span class="icon ok" transition:fade>check_circle</span>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class:faded={step < 1}>
|
|
||||||
Make a <button class="inline-button" onclick={backup}
|
|
||||||
><span class="icon">download</span>Backup</button
|
|
||||||
>
|
|
||||||
{#if step >= 2}
|
|
||||||
<span class="icon ok" transition:fade>check_circle</span>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class:faded={step < 2}>
|
|
||||||
Reboot to <button class="inline-button" onclick={bootloader}
|
|
||||||
><span class="icon">restart_alt</span>Bootloader</button
|
|
||||||
>
|
|
||||||
{#if step >= 3}
|
|
||||||
<span class="icon ok" transition:fade>check_circle</span>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class:faded={step < 3}>
|
|
||||||
Replace <button class="inline-button" onclick={getFileSystem}
|
|
||||||
><span class="icon">deployed_code_update</span>CURRENT.UF2</button
|
|
||||||
>
|
|
||||||
on the new drive
|
|
||||||
{#if step >= 4}
|
|
||||||
<span class="icon ok" transition:fade>check_circle</span>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{#if data.meta.update.esptool}
|
|
||||||
<section>
|
|
||||||
<h3>Factory Flash (WIP)</h3>
|
|
||||||
<p>
|
|
||||||
If everything else fails, you can go through the same process that is
|
|
||||||
being used in the factory.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
This will temporarily brick your device if the process is not done
|
|
||||||
completely or incorrectly.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="esp-buttons">
|
|
||||||
<button onclick={espBootloader}
|
|
||||||
><span class="icon">memory</span>ESP Bootloader</button
|
|
||||||
>
|
|
||||||
<button onclick={flashImages}
|
|
||||||
><span class="icon">developer_board</span>Flash Images</button
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
><input type="checkbox" id="erase" bind:checked={eraseAll} />Erase All</label
|
|
||||||
>
|
|
||||||
<button onclick={eraseSPI}
|
|
||||||
><span class="icon">developer_board</span>Erase SPI Flash</button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<pre>{terminalOutput}</pre>
|
<section>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<button class="inline-button" onclick={connect}
|
||||||
|
><span class="icon">usb</span>Connect</button
|
||||||
|
>
|
||||||
|
your device
|
||||||
|
{#if step >= 1}
|
||||||
|
<span class="icon ok" transition:fade>check_circle</span>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class:faded={step < 1}>
|
||||||
|
Make a <button class="inline-button" onclick={backup}
|
||||||
|
><span class="icon">download</span>Backup</button
|
||||||
|
>
|
||||||
|
{#if step >= 2}
|
||||||
|
<span class="icon ok" transition:fade>check_circle</span>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class:faded={step < 2}>
|
||||||
|
Reboot to <button class="inline-button" onclick={bootloader}
|
||||||
|
><span class="icon">restart_alt</span>Bootloader</button
|
||||||
|
>
|
||||||
|
{#if step >= 3}
|
||||||
|
<span class="icon ok" transition:fade>check_circle</span>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class:faded={step < 3}>
|
||||||
|
Replace <button class="inline-button" onclick={getFileSystem}
|
||||||
|
><span class="icon">deployed_code_update</span>CURRENT.UF2</button
|
||||||
|
>
|
||||||
|
on the new drive
|
||||||
|
{#if step >= 4}
|
||||||
|
<span class="icon ok" transition:fade>check_circle</span>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
|
||||||
|
{#if false && data.meta.update.esptool}
|
||||||
|
<section>
|
||||||
|
<h3>Factory Flash (WIP)</h3>
|
||||||
|
<p>
|
||||||
|
If everything else fails, you can go through the same process that is
|
||||||
|
being used in the factory.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This will temporarily brick your device if the process is not done
|
||||||
|
completely or incorrectly.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="esp-buttons">
|
||||||
|
<button onclick={espBootloader}
|
||||||
|
><span class="icon">memory</span>ESP Bootloader</button
|
||||||
|
>
|
||||||
|
<button onclick={flashImages}
|
||||||
|
><span class="icon">developer_board</span>Flash Images</button
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input type="checkbox" id="erase" bind:checked={eraseAll} />Erase
|
||||||
|
All</label
|
||||||
|
>
|
||||||
|
<button onclick={eraseSPI}
|
||||||
|
><span class="icon">developer_board</span>Erase SPI Flash</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre>{terminalOutput}</pre>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="changelog">
|
||||||
|
<h2>Changelog</h2>
|
||||||
|
{#if data.meta.changelog.features}
|
||||||
|
<h3>Features</h3>
|
||||||
|
<ul>
|
||||||
|
{#each data.meta.changelog.features as feature}
|
||||||
|
<li>
|
||||||
|
<b>{@html feature.summary}</b>
|
||||||
|
{@html feature.description}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
{#if data.meta.changelog.fixes}
|
||||||
|
<h3>Fixes</h3>
|
||||||
|
<ul>
|
||||||
|
{#each data.meta.changelog.fixes as fix}
|
||||||
|
<li>
|
||||||
|
<b>{@html fix.summary}</b>
|
||||||
|
{@html fix.description}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
h2 > em {
|
.changelog:empty {
|
||||||
font-style: normal;
|
display: none;
|
||||||
transition: color 200ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
.changelog ul {
|
||||||
margin-block-start: 4em;
|
padding-inline-start: 0em;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog li {
|
||||||
|
margin-block: 0.2em;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog b {
|
||||||
|
display: inline-block;
|
||||||
|
translate: -0.5em -0.2em;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--md-sys-color-tertiary-container);
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
color: var(--md-sys-color-on-tertiary-container);
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unsafe-opt-in {
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-block: 1em;
|
||||||
|
font-size: 0.7em;
|
||||||
|
|
||||||
|
& + .unsafe-updates {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(input:checked) + .unsafe-updates {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
color: var(--md-sys-color-primary);
|
color: var(--md-sys-color-primary);
|
||||||
}
|
}
|
||||||
@@ -370,22 +440,22 @@
|
|||||||
|
|
||||||
button.inline-button {
|
button.inline-button {
|
||||||
display: inline;
|
display: inline;
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
height: unset;
|
height: unset;
|
||||||
font-size: inherit;
|
|
||||||
color: var(--md-sys-color-primary);
|
color: var(--md-sys-color-primary);
|
||||||
|
font-size: inherit;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
font-size: 1.2em;
|
|
||||||
translate: 0 0.1em;
|
translate: 0 0.1em;
|
||||||
padding-inline-end: 0.2em;
|
padding-inline-end: 0.2em;
|
||||||
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.ok {
|
.icon.ok {
|
||||||
font-size: 1.2em;
|
|
||||||
translate: 0 0.1em;
|
translate: 0 0.1em;
|
||||||
|
font-size: 1.2em;
|
||||||
--icon-fill: 1;
|
--icon-fill: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,17 +464,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button.update-button {
|
button.update-button {
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 42px;
|
|
||||||
|
|
||||||
border: 2px solid currentcolor;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
outline: 2px dashed currentcolor;
|
|
||||||
outline-offset: 4px;
|
|
||||||
|
|
||||||
background: var(--md-sys-color-background);
|
|
||||||
transition:
|
transition:
|
||||||
border 200ms ease,
|
border 200ms ease,
|
||||||
color 200ms ease;
|
color 200ms ease;
|
||||||
@@ -412,68 +472,55 @@
|
|||||||
margin: 6px;
|
margin: 6px;
|
||||||
margin-block: 16px;
|
margin-block: 16px;
|
||||||
|
|
||||||
|
outline: 2px dashed currentcolor;
|
||||||
|
outline-offset: 4px;
|
||||||
|
|
||||||
|
border: 2px solid currentcolor;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-background);
|
||||||
|
height: 42px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
color: var(--md-sys-color-primary);
|
|
||||||
background: none;
|
background: none;
|
||||||
|
color: var(--md-sys-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.progress,
|
||||||
&.working {
|
&.working {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.working::before {
|
&.working::before {
|
||||||
z-index: -1;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
border-radius: 8px;
|
||||||
background: var(--md-sys-color-background);
|
background: var(--md-sys-color-background);
|
||||||
width: calc(100% - 4px);
|
width: calc(100% - 4px);
|
||||||
height: calc(100% - 4px);
|
height: calc(100% - 4px);
|
||||||
border-radius: 8px;
|
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
&.working::after {
|
&.working::after {
|
||||||
z-index: -2;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: "";
|
z-index: -2;
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
animation: rotate 1s ease-out forwards infinite;
|
animation: rotate 1s ease-out forwards infinite;
|
||||||
height: 30%;
|
background: var(--md-sys-color-primary);
|
||||||
width: 120%;
|
width: 120%;
|
||||||
}
|
height: 30%;
|
||||||
}
|
content: "";
|
||||||
|
|
||||||
hr {
|
|
||||||
color: var(--md-sys-color-outline);
|
|
||||||
margin-block: 3em;
|
|
||||||
margin-inline: 5em;
|
|
||||||
border-style: dashed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.files {
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
padding: 0;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a[download] {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
border: 1px solid var(--md-sys-color-outline);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
height: auto;
|
|
||||||
|
|
||||||
.size {
|
|
||||||
font-size: 0.8em;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
&.progress::after {
|
||||||
padding-inline-start: 0.4em;
|
position: absolute;
|
||||||
grid-column: 2;
|
left: 0;
|
||||||
grid-row: 1 / span 2;
|
opacity: 0.2;
|
||||||
|
z-index: -2;
|
||||||
|
background: var(--md-sys-color-primary);
|
||||||
|
width: var(--progress);
|
||||||
|
height: 100%;
|
||||||
|
content: "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,20 +528,6 @@
|
|||||||
color: var(--md-sys-color-secondary);
|
color: var(--md-sys-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.device {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-link {
|
|
||||||
display: inline;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.correct-device {
|
|
||||||
color: var(--md-sys-color-primary);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incorrect-device {
|
.incorrect-device {
|
||||||
color: var(--md-sys-color-error);
|
color: var(--md-sys-color-error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,14 @@
|
|||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
import type { FileListing, Listing } from "../../listing";
|
import { getMeta } from "$lib/meta/meta-storage";
|
||||||
import type { VersionMeta } from "./meta";
|
import { error } from "@sveltejs/kit";
|
||||||
|
|
||||||
export const load = (async ({ fetch, params }) => {
|
export const load = (async ({ fetch, params }) => {
|
||||||
const result = await fetch(
|
const meta = await getMeta(params.device, params.version, fetch);
|
||||||
`${import.meta.env.VITE_FIRMWARE_URL}/${params.device}/${params.version}/`,
|
if (meta === undefined) {
|
||||||
);
|
error(
|
||||||
const data: Listing[] = await result.json();
|
404,
|
||||||
const meta: VersionMeta | undefined = data.some(
|
`The version ${params.version} for device ${params.device} does not exist.`,
|
||||||
(entry) => entry.type === "file" && entry.name === "meta.json",
|
);
|
||||||
)
|
}
|
||||||
? await fetch(
|
return { meta };
|
||||||
`${import.meta.env.VITE_FIRMWARE_URL}/${params.device}/${params.version}/meta.json`,
|
|
||||||
).then((res) => res.json())
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
meta: {
|
|
||||||
version: meta?.version ?? params.version,
|
|
||||||
target: meta?.target ?? params.device,
|
|
||||||
path: `${import.meta.env.VITE_FIRMWARE_URL}${params.device}/${params.version}`,
|
|
||||||
git_commit: meta?.git_commit ?? "",
|
|
||||||
git_is_dirty: meta?.git_is_dirty ?? false,
|
|
||||||
git_date: meta?.git_date ?? data[0]?.mtime ?? "",
|
|
||||||
public_build: meta?.public_build ?? !params.version.startsWith("."),
|
|
||||||
development_mode: meta?.development_mode ?? 0,
|
|
||||||
update: {
|
|
||||||
uf2:
|
|
||||||
(data.find(
|
|
||||||
(entry) =>
|
|
||||||
entry.type === "file" &&
|
|
||||||
entry.name === (meta?.update?.uf2 ?? "CURRENT.UF2"),
|
|
||||||
) as FileListing) ?? undefined,
|
|
||||||
ota:
|
|
||||||
data.find(
|
|
||||||
(entry) =>
|
|
||||||
entry.type === "file" &&
|
|
||||||
entry.name === (meta?.update?.ota ?? "firmware.bin"),
|
|
||||||
) ?? undefined,
|
|
||||||
esptool: meta?.update?.esptool ?? undefined,
|
|
||||||
},
|
|
||||||
files: data.filter(
|
|
||||||
(entry) =>
|
|
||||||
entry.type === "file" && (!meta?.files || entry.name in meta.files),
|
|
||||||
) as FileListing[],
|
|
||||||
spi_flash: meta?.spi_flash ?? undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}) satisfies PageLoad;
|
}) satisfies PageLoad;
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
export interface VersionMeta {
|
|
||||||
version: string;
|
|
||||||
target: string;
|
|
||||||
git_commit: string;
|
|
||||||
git_is_dirty: boolean;
|
|
||||||
git_date: string;
|
|
||||||
public_build: boolean;
|
|
||||||
development_mode: number;
|
|
||||||
update: {
|
|
||||||
ota: string | null;
|
|
||||||
uf2: string | null;
|
|
||||||
esptool: EspToolData | null;
|
|
||||||
};
|
|
||||||
files: string[];
|
|
||||||
spi_flash: SPIFlashInfo | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SPIFlashInfo {
|
|
||||||
type: string;
|
|
||||||
size: string;
|
|
||||||
connection: SPIConnection;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SPIConnection {
|
|
||||||
clk: number;
|
|
||||||
q: number;
|
|
||||||
d: number;
|
|
||||||
hd: number;
|
|
||||||
cs: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EspToolData {
|
|
||||||
chip: string;
|
|
||||||
baud: string;
|
|
||||||
before: string;
|
|
||||||
after: string;
|
|
||||||
flash_mode: string;
|
|
||||||
flash_freq: string;
|
|
||||||
flash_size: string;
|
|
||||||
files: Record<string, string>;
|
|
||||||
}
|
|
||||||
92
src/routes/(app)/chat-rx/+page.svelte
Normal file
92
src/routes/(app)/chat-rx/+page.svelte
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { initMatrixClient, isLoggedIn, matrix } from "$lib/chat/chat-rx";
|
||||||
|
import { flip } from "svelte/animate";
|
||||||
|
import { slide } from "svelte/transition";
|
||||||
|
import Login from "./Login.svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (browser) {
|
||||||
|
await initMatrixClient();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
|
||||||
|
let spaces = $derived($matrix?.topLevelSpaces$);
|
||||||
|
|
||||||
|
function spaceShort(name: string) {
|
||||||
|
return name
|
||||||
|
.split(" ")
|
||||||
|
.map((it) => it[0])
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $isLoggedIn}
|
||||||
|
<div class="layout">
|
||||||
|
<nav class="spaces">
|
||||||
|
<a href="/chat/chats" class="icon chats">chat</a>
|
||||||
|
<hr />
|
||||||
|
{#if $spaces}
|
||||||
|
<ul>
|
||||||
|
{#each $spaces as space (space.roomId)}
|
||||||
|
<li animate:flip transition:slide>
|
||||||
|
<a class="space" href="/chat/space/{space.roomId}">
|
||||||
|
{spaceShort(space.name)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
<button class="icon">add</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Login />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
width: 60%;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chats {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user