diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..fec7d91
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+# editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = crlf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.svg]
+insert_final_newline = false
diff --git a/.github/workflows/cmake-single-platform.yml b/.github/workflows/cmake-single-platform.yml
index 6b17b09..a7bf624 100644
--- a/.github/workflows/cmake-single-platform.yml
+++ b/.github/workflows/cmake-single-platform.yml
@@ -16,6 +16,22 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
+ - name: Setup node.js
+ uses: actions/setup-node@v4.0.1
+ with:
+ node-version: '20.x'
+
+ - name: Install npm dependencies
+ run: |
+ cd frontend
+ npm cache clear -f
+ npm install
+
+ - name: Build frontend
+ run: |
+ cd frontend
+ npm run build
+
- name: Install dependencies
run: |
sudo apt-get update
@@ -43,4 +59,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: pico-gb-printer-firmware
- path: build/pico_gb_printer.uf2
\ No newline at end of file
+ path: build/pico_gb_printer.uf2
diff --git a/.gitignore b/.gitignore
index e43f7c7..4a467e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
.vscode
+.idea
build
unused
gen_build.bat
+fs/
+frontend/node_modules
diff --git a/fs/index.html b/frontend/index.html
similarity index 76%
rename from fs/index.html
rename to frontend/index.html
index ebb6403..8259072 100644
--- a/fs/index.html
+++ b/frontend/index.html
@@ -1,4 +1,4 @@
-
+
Pico Game Boy Printer
@@ -6,6 +6,7 @@
+
@@ -17,24 +18,16 @@
-
-
-
-
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..c99ea89
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,1109 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "devDependencies": {
+ "ofetch": "^1.4.1",
+ "typescript": "~5.6.2",
+ "vite": "^6.0.5"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
+ "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
+ "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
+ "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
+ "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
+ "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
+ "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
+ "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
+ "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
+ "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
+ "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
+ "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
+ "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
+ "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
+ "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
+ "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
+ "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
+ "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
+ "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
+ "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
+ "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
+ "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
+ "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
+ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz",
+ "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz",
+ "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz",
+ "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz",
+ "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz",
+ "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz",
+ "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz",
+ "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz",
+ "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz",
+ "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz",
+ "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz",
+ "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz",
+ "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz",
+ "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz",
+ "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz",
+ "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz",
+ "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz",
+ "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz",
+ "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz",
+ "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "22.10.7",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
+ "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.20.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/destr": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz",
+ "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==",
+ "dev": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
+ "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.24.2",
+ "@esbuild/android-arm": "0.24.2",
+ "@esbuild/android-arm64": "0.24.2",
+ "@esbuild/android-x64": "0.24.2",
+ "@esbuild/darwin-arm64": "0.24.2",
+ "@esbuild/darwin-x64": "0.24.2",
+ "@esbuild/freebsd-arm64": "0.24.2",
+ "@esbuild/freebsd-x64": "0.24.2",
+ "@esbuild/linux-arm": "0.24.2",
+ "@esbuild/linux-arm64": "0.24.2",
+ "@esbuild/linux-ia32": "0.24.2",
+ "@esbuild/linux-loong64": "0.24.2",
+ "@esbuild/linux-mips64el": "0.24.2",
+ "@esbuild/linux-ppc64": "0.24.2",
+ "@esbuild/linux-riscv64": "0.24.2",
+ "@esbuild/linux-s390x": "0.24.2",
+ "@esbuild/linux-x64": "0.24.2",
+ "@esbuild/netbsd-arm64": "0.24.2",
+ "@esbuild/netbsd-x64": "0.24.2",
+ "@esbuild/openbsd-arm64": "0.24.2",
+ "@esbuild/openbsd-x64": "0.24.2",
+ "@esbuild/sunos-x64": "0.24.2",
+ "@esbuild/win32-arm64": "0.24.2",
+ "@esbuild/win32-ia32": "0.24.2",
+ "@esbuild/win32-x64": "0.24.2"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-fetch-native": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz",
+ "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==",
+ "dev": true
+ },
+ "node_modules/ofetch": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz",
+ "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==",
+ "dev": true,
+ "dependencies": {
+ "destr": "^2.0.3",
+ "node-fetch-native": "^1.6.4",
+ "ufo": "^1.5.4"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true
+ },
+ "node_modules/postcss": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
+ "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.30.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz",
+ "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.30.1",
+ "@rollup/rollup-android-arm64": "4.30.1",
+ "@rollup/rollup-darwin-arm64": "4.30.1",
+ "@rollup/rollup-darwin-x64": "4.30.1",
+ "@rollup/rollup-freebsd-arm64": "4.30.1",
+ "@rollup/rollup-freebsd-x64": "4.30.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.30.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.30.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.30.1",
+ "@rollup/rollup-linux-arm64-musl": "4.30.1",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.30.1",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.30.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.30.1",
+ "@rollup/rollup-linux-x64-gnu": "4.30.1",
+ "@rollup/rollup-linux-x64-musl": "4.30.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.30.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.30.1",
+ "@rollup/rollup-win32-x64-msvc": "4.30.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/source-map-support/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.37.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
+ "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.8.2",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/ufo": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
+ "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
+ "dev": true
+ },
+ "node_modules/undici-types": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "dev": true,
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/vite": {
+ "version": "6.0.7",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz",
+ "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.24.2",
+ "postcss": "^8.4.49",
+ "rollup": "^4.23.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..593ddb0
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build"
+ },
+ "devDependencies": {
+ "ofetch": "^1.4.1",
+ "typescript": "~5.6.2",
+ "vite": "^6.0.5"
+ }
+}
diff --git a/fs/android-chrome-192x192.png b/frontend/public/android-chrome-192x192.png
similarity index 100%
rename from fs/android-chrome-192x192.png
rename to frontend/public/android-chrome-192x192.png
diff --git a/fs/apple-touch-icon.png b/frontend/public/apple-touch-icon.png
similarity index 100%
rename from fs/apple-touch-icon.png
rename to frontend/public/apple-touch-icon.png
diff --git a/fs/browserconfig.xml b/frontend/public/browserconfig.xml
similarity index 100%
rename from fs/browserconfig.xml
rename to frontend/public/browserconfig.xml
diff --git a/fs/favicon-16x16.png b/frontend/public/favicon-16x16.png
similarity index 100%
rename from fs/favicon-16x16.png
rename to frontend/public/favicon-16x16.png
diff --git a/fs/favicon-32x32.png b/frontend/public/favicon-32x32.png
similarity index 100%
rename from fs/favicon-32x32.png
rename to frontend/public/favicon-32x32.png
diff --git a/fs/favicon.ico b/frontend/public/favicon.ico
similarity index 100%
rename from fs/favicon.ico
rename to frontend/public/favicon.ico
diff --git a/fs/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png b/frontend/public/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png
similarity index 100%
rename from fs/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png
rename to frontend/public/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png
diff --git a/fs/iPhone_14_Pro_Max__iPhone_14_Max__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png b/frontend/public/iPhone_14_Pro_Max__iPhone_14_Max__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png
similarity index 100%
rename from fs/iPhone_14_Pro_Max__iPhone_14_Max__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png
rename to frontend/public/iPhone_14_Pro_Max__iPhone_14_Max__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png
diff --git a/fs/iPhone_14_Pro__iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png b/frontend/public/iPhone_14_Pro__iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png
similarity index 100%
rename from fs/iPhone_14_Pro__iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png
rename to frontend/public/iPhone_14_Pro__iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png
diff --git a/fs/safari-pinned-tab.svg b/frontend/public/safari-pinned-tab.svg
similarity index 100%
rename from fs/safari-pinned-tab.svg
rename to frontend/public/safari-pinned-tab.svg
diff --git a/fs/site.webmanifest b/frontend/public/site.webmanifest
similarity index 100%
rename from fs/site.webmanifest
rename to frontend/public/site.webmanifest
diff --git a/frontend/src/consts.ts b/frontend/src/consts.ts
new file mode 100644
index 0000000..0850e47
--- /dev/null
+++ b/frontend/src/consts.ts
@@ -0,0 +1,20 @@
+// Api URLs
+export const OPTIONS = "/options";
+export const DOWNLOAD = "/download";
+export const RESET = "/reset";
+export const RESET_USB_BOOT = "/reset_usb_boot";
+export const STATUS_FILE = "/status.json"
+export const LIST_FILE = "/list.json"
+
+// Printer Commands
+export const COMMAND_INIT = 0x01;
+export const COMMAND_PRINT = 0x02;
+export const COMMAND_DATA = 0x04;
+export const COMMAND_TRANSFER = 0x10;
+
+// Image specific
+export const PRINTER_WIDTH = 20;
+export const CAMERA_WIDTH = 16;
+export const TILE_SIZE = 0x10;
+export const TILE_HEIGHT = 8;
+export const TILE_WIDTH = 8;
diff --git a/frontend/src/functions/appendCanvasToGallery.ts b/frontend/src/functions/appendCanvasToGallery.ts
new file mode 100644
index 0000000..0b1bd46
--- /dev/null
+++ b/frontend/src/functions/appendCanvasToGallery.ts
@@ -0,0 +1,48 @@
+import { updateButtons } from "./updateButtons.js";
+import { downloadImage } from "./saveImage.js";
+
+const gallery = document.getElementById("gallery") as HTMLDivElement;
+
+export const appendCanvasToGallery = (canvas: HTMLCanvasElement, timestamp?: number): boolean => {
+ if (canvas.height > 1) {
+ const imageContainer = document.createElement("label");
+ imageContainer.classList.add("gallery-image");
+
+ const img = new Image();
+ img.src = canvas.toDataURL();
+ imageContainer.appendChild(img);
+
+ if (timestamp) {
+ imageContainer.dataset.timestamp = timestamp.toString(10);
+ }
+
+ const input = document.createElement("input");
+ input.setAttribute("type", "checkbox");
+
+ input.addEventListener("change", function() {
+ if (input.checked) {
+ imageContainer.classList.add('marked-for-action');
+ } else {
+ imageContainer.classList.remove('marked-for-action');
+ }
+
+ updateButtons();
+ });
+
+ imageContainer.appendChild(input);
+
+ const btn = document.createElement("button");
+ btn.textContent = "Save";
+ btn.addEventListener("click", function () {
+ downloadImage(img);
+ });
+ imageContainer.appendChild(btn);
+
+ gallery.appendChild(imageContainer);
+ updateButtons();
+
+ return true;
+ }
+
+ return false;
+}
diff --git a/frontend/src/functions/database.ts b/frontend/src/functions/database.ts
new file mode 100644
index 0000000..c097fd1
--- /dev/null
+++ b/frontend/src/functions/database.ts
@@ -0,0 +1,67 @@
+export interface DownloadData {
+ timestamp: number,
+ data: Uint8Array,
+}
+
+export interface DbAccess {
+ add: (dlData: DownloadData) => Promise,
+ delete: (timestamp: number) => Promise,
+ getAll: () => Promise
+}
+
+export const initDb = async (): Promise => {
+ const dbName = "pico_printer";
+
+ return new Promise((resolve) => {
+ const request = indexedDB.open(dbName, 2);
+
+ request.onerror = (event) => {
+ console.error(event);
+ };
+
+ request.onsuccess = () => {
+ resolve({
+ add: async (dlData: DownloadData) => {
+ return new Promise((resolve) => {
+ const objectStore = request.result.transaction('downloads', 'readwrite').objectStore('downloads')
+ const rq = objectStore.add({
+ timestamp: dlData.timestamp,
+ data: dlData.data.join(',')
+ });
+
+ rq.onsuccess = () => resolve()
+ })
+ },
+ getAll: async () => {
+ return new Promise((resolve) => {
+ const objectStore = request.result.transaction('downloads').objectStore('downloads')
+ const rq = objectStore.getAll()
+
+ rq.onsuccess = (ev) => {
+ // @ts-ignore
+ const stored = ev.target?.result.map(({ timestamp, data }): DownloadData => ({
+ timestamp,
+ data: new Uint8Array(data.split(',').map((i: string) => parseInt(i, 10))),
+ }));
+ resolve(stored);
+ };
+ })
+ },
+ delete: async (timestamp: number) => {
+ return new Promise((resolve) => {
+ const objectStore = request.result.transaction('downloads', 'readwrite').objectStore('downloads')
+ const rq = objectStore.delete(timestamp)
+
+ rq.onsuccess = () => resolve()
+ })
+ }
+ });
+ }
+
+ request.onupgradeneeded = () => {
+ console.log('onupgradeneeded');
+ const objectStore = request.result.createObjectStore("downloads", { keyPath: "timestamp" });
+ objectStore.createIndex("timestamp", "timestamp", { unique: false });
+ };
+ })
+}
diff --git a/frontend/src/functions/decode.ts b/frontend/src/functions/decode.ts
new file mode 100644
index 0000000..461c2f2
--- /dev/null
+++ b/frontend/src/functions/decode.ts
@@ -0,0 +1,36 @@
+
+export const decode = (
+ is_compressed: boolean,
+ source: Uint8Array,
+ source_size: number,
+ source_data_len: number,
+ source_ptr: number,
+ dest: Uint8Array,
+ dest_ptr: number,
+) => {
+ if (source_ptr + source_data_len <= source_size) {
+ if (is_compressed) {
+ const stop = source_ptr + source_data_len;
+ while (source_ptr < stop) {
+ const tag = source[source_ptr++];
+ if (tag & 0x80) {
+ const data = source[source_ptr++];
+ for (let i = 0; i < ((tag & 0x7f) + 2); i++) {
+ dest[dest_ptr++] = data;
+ }
+ } else {
+ for (let i = 0; i < (tag + 1); i++) {
+ dest[dest_ptr++] = source[source_ptr++];
+ }
+ }
+ }
+ return dest_ptr;
+ } else {
+ for (let i = 0; i < source_data_len; i++) {
+ dest[dest_ptr++] = source[source_ptr++];
+ }
+ return dest_ptr;
+ }
+ }
+ return dest_ptr;
+}
diff --git a/frontend/src/functions/getCameraImage.ts b/frontend/src/functions/getCameraImage.ts
new file mode 100644
index 0000000..f9a850b
--- /dev/null
+++ b/frontend/src/functions/getCameraImage.ts
@@ -0,0 +1,93 @@
+import {
+ CAMERA_WIDTH,
+ COMMAND_DATA,
+ COMMAND_INIT,
+ COMMAND_PRINT,
+ COMMAND_TRANSFER,
+ PRINTER_WIDTH
+} from "../consts.js";
+import { appendCanvasToGallery } from "./appendCanvasToGallery.js";
+import { resetCanvas } from "./resetCanvas.js";
+import {decode} from "./decode.js";
+import {render} from "./render.js";
+import type { DownloadData } from "./database.js";
+
+export async function getCameraImage(canvas: HTMLCanvasElement, dlData: DownloadData): Promise {
+ const resData = dlData.data;
+ const data_size = resData.byteLength;
+ let rendered = 0;
+
+ const processed_data = new Uint8Array(Math.max(1024*1024, data_size));
+
+ resetCanvas(canvas);
+
+ let buffer_start = 0;
+ let ptr = 0;
+ let idx = 0;
+ let len = 0;
+ while (idx < data_size) {
+ const command = resData[idx++];
+ switch(command) {
+ case COMMAND_INIT:
+ break;
+ case COMMAND_PRINT: {
+ if ((len = resData[idx++] | (resData[idx++] << 8)) != 4) {
+ idx = data_size;
+ break;
+ }
+
+ // @ts-ignore
+ let sheets = resData[idx++];
+ let margins = resData[idx++];
+ let palette = resData[idx++];
+ let exposure = Math.min(0xFF, 0x80 + resData[idx++]);
+
+ palette = (palette) ? palette : 0xE4;
+
+ if (render(canvas, processed_data, buffer_start, ptr, PRINTER_WIDTH, margins, palette, exposure)) {
+ if (appendCanvasToGallery(canvas, dlData.timestamp)) {
+ rendered += 1;
+ }
+ resetCanvas(canvas);
+ }
+ buffer_start = ptr;
+
+ break;
+ }
+
+ case COMMAND_TRANSFER: {
+ len = resData[idx++] | (resData[idx++] << 8);
+ let current_image_start = ptr;
+ ptr = decode(false, resData, data_size, len, idx, processed_data, ptr);
+ idx += len;
+ render(canvas, processed_data, current_image_start, ptr, CAMERA_WIDTH, 0x03, 0xE4, 0xFF);
+ if (appendCanvasToGallery(canvas, dlData.timestamp)) {
+ rendered += 1;
+ }
+ resetCanvas(canvas);
+ buffer_start = ptr;
+ break;
+ }
+
+ case COMMAND_DATA: {
+ const compression = !!resData[idx++];
+ len = resData[idx++] | (resData[idx++] << 8);
+ ptr = decode(compression, resData, data_size, len, idx, processed_data, ptr);
+ idx += len;
+ break;
+ }
+ default:
+ idx = data_size;
+ break;
+ }
+ }
+
+ if (canvas.height > 1) {
+ if (appendCanvasToGallery(canvas, dlData.timestamp)) {
+ rendered += 1;
+ }
+ resetCanvas(canvas);
+ }
+
+ return rendered > 0;
+}
diff --git a/frontend/src/functions/initButtons.ts b/frontend/src/functions/initButtons.ts
new file mode 100644
index 0000000..2ac96cf
--- /dev/null
+++ b/frontend/src/functions/initButtons.ts
@@ -0,0 +1,122 @@
+import { updateButtons } from "./updateButtons.js";
+import { appendCanvasToGallery } from "./appendCanvasToGallery.js";
+import { DbAccess } from "./database.js";
+
+const gallery = document.getElementById("gallery") as HTMLDivElement;
+// const getImageBtn = document.getElementById("get_image_btn") as HTMLButtonElement;
+// const tearBtn = document.getElementById("tear_btn") as HTMLButtonElement;
+const deleteSelectedBtn = document.getElementById("delete_selected_btn") as HTMLButtonElement;
+const selectAllBtn = document.getElementById("select_all_btn") as HTMLButtonElement;
+const averageSelectedBtn = document.getElementById("average_selected_btn") as HTMLButtonElement;
+
+export const initButtons = (store: DbAccess) => {
+ selectAllBtn.addEventListener("click", function () {
+ const items = gallery.children;
+ const markedItems = gallery.querySelectorAll('.marked-for-action');
+
+ const unselect = markedItems.length === items.length;
+
+ if (items.length != 0) {
+ [...items].forEach(item => {
+ const checkbox = item.querySelector("input") as HTMLInputElement;
+ checkbox.checked = !unselect;
+ item.classList[unselect ? 'remove' : 'add']('marked-for-action');
+ });
+ }
+
+ updateButtons();
+ });
+
+ deleteSelectedBtn.addEventListener("click", function () {
+ const items = gallery.children;
+ for (let i = items.length - 1; i >= 0; i--) {
+ const item = items[i] as HTMLDivElement;
+ if (item.classList.contains('marked-for-action')) {
+ const imageTime = item.dataset.timestamp;
+ if (imageTime) {
+ store.delete(parseInt(imageTime, 10))
+ }
+ item.remove();
+ }
+ }
+
+ updateButtons();
+ });
+
+ averageSelectedBtn.addEventListener("click", function() {
+ const items = gallery.querySelectorAll('.marked-for-action');
+
+ if (items.length < 2) {
+ return;
+ }
+
+ const avgCanvas = document.createElement('canvas');
+ const avgCtx = avgCanvas.getContext('2d', { willReadFrequently: true }) as CanvasRenderingContext2D;
+
+ const tmpCanvas = document.createElement('canvas');
+ const tmpCtx = tmpCanvas.getContext('2d', { willReadFrequently: true }) as CanvasRenderingContext2D;
+
+ // Verify that image dimensions are the same
+ const firstImg = items[0].querySelector("img");
+ if (!firstImg) return;
+
+ const tmpW = firstImg.width;
+ const tmpH = firstImg.height;
+ for (let i = 1; i < items.length; i++) {
+ const img = items[i].querySelector("img");
+ if (!img) return;
+
+ if (tmpW != img.width || tmpH != img.height) {
+ alert("Image dimensions should be the same to do an average");
+ return;
+ }
+ }
+
+ tmpCanvas.width = tmpW;
+ tmpCanvas.height = tmpH;
+
+ avgCanvas.width = tmpW;
+ avgCanvas.height = tmpH;
+
+ const sumImgData = [];
+ const avgImgData = avgCtx.createImageData(avgCanvas.width, avgCanvas.height);
+ let selectedItems = 0;
+ // Generate average image
+ for (let i = items.length - 1; i >= 0; i--) {
+ if (items[i].classList.contains('marked-for-action')) {
+ selectedItems++;
+ const item = items[i];
+ const img = item.querySelector("img");
+ if (!img) return;
+ tmpCtx.drawImage(img,0,0);
+ const tmpImgData = tmpCtx.getImageData(0, 0, tmpCanvas.width, tmpCanvas.height);
+ for (let j = 0; j < tmpImgData.data.length; j += 1) {
+ if (!sumImgData[j]) {
+ sumImgData.push(0);
+ }
+ sumImgData[j] += tmpImgData.data[j];
+ }
+ }
+ }
+ for (let i = 0; i < avgImgData.data.length; i += 1) {
+ avgImgData.data[i] = (sumImgData[i] / selectedItems);
+ }
+ avgCtx.putImageData(avgImgData, 0, 0);
+ appendCanvasToGallery(avgCanvas);
+ });
+
+
+ // tearBtn.addEventListener("click", async function () {
+ // fetch(resetPath)
+ // .then((response) => {
+ // return response.json();
+ // })
+ // .then((data) => {
+ // console.log(data);
+ // if (data.result != "ok") return;
+ // getImageBtn.click();
+ // });
+ // });
+
+ updateButtons();
+}
diff --git a/frontend/src/functions/render.ts b/frontend/src/functions/render.ts
new file mode 100644
index 0000000..3d4403a
--- /dev/null
+++ b/frontend/src/functions/render.ts
@@ -0,0 +1,51 @@
+import {TILE_HEIGHT, TILE_SIZE} from "../consts.js";
+import {resizeCanvas} from "./resizeCanvas.js";
+
+export const render = (
+ canvas: HTMLCanvasElement,
+ image_data: Uint8Array,
+ image_start: number,
+ image_end: number,
+ image_tile_width: number,
+ margin: number,
+ palette: number,
+ exposure: number,
+): boolean => {
+ const pal = new Uint8Array(4);
+ pal[0] = ((exposure * ((palette >> 0) & 0x03)) / 3) >> 0;
+ pal[1] = ((exposure * ((palette >> 2) & 0x03)) / 3) >> 0;
+ pal[2] = ((exposure * ((palette >> 4) & 0x03)) / 3) >> 0;
+ pal[3] = ((exposure * ((palette >> 6) & 0x03)) / 3) >> 0;
+
+ let tile_y = ((canvas.height / TILE_HEIGHT) >> 0);
+ let tile_x = 0;
+
+ resizeCanvas(canvas, (image_tile_width * 8), ((canvas.height >> 3) << 3) + ((Math.max(0, image_end - image_start) / (TILE_SIZE * image_tile_width)) >> 0) * 8)
+
+ if (canvas.width * canvas.height !== 0) {
+ const ctx = canvas.getContext("2d", { willReadFrequently: true }) as CanvasRenderingContext2D;
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const writeData = imageData.data;
+ for (let i = image_start; i < image_end; ) {
+ for (let t = 0; t < 8; t++) {
+ let b1 = image_data[i++];
+ let b2 = image_data[i++];
+ for (let b = 0; b < 8; b++) {
+ let offset = (((tile_y << 3) + t) * canvas.width + (tile_x << 3) + b) << 2;
+ let color_index = ((b1 >> (7 - b)) & 1) | (((b2 >> (7 - b)) & 1) << 1);
+
+ writeData[offset] = writeData[offset + 1] = writeData[offset + 2] = 0xFF - pal[color_index];
+ writeData[offset + 3] = 0xff;
+ }
+ }
+ tile_x += 1;
+ if (tile_x >= image_tile_width) {
+ tile_x = 0;
+ tile_y++;
+ }
+ }
+ ctx.putImageData(imageData, 0, 0);
+ }
+
+ return ((margin & 0x0f) != 0);
+}
diff --git a/frontend/src/functions/resetCanvas.ts b/frontend/src/functions/resetCanvas.ts
new file mode 100644
index 0000000..f10b4b1
--- /dev/null
+++ b/frontend/src/functions/resetCanvas.ts
@@ -0,0 +1,5 @@
+
+export const resetCanvas = (canvas: HTMLCanvasElement) => {
+ canvas.height = 1;
+ canvas.width = 1;
+}
diff --git a/frontend/src/functions/resizeCanvas.ts b/frontend/src/functions/resizeCanvas.ts
new file mode 100644
index 0000000..975e911
--- /dev/null
+++ b/frontend/src/functions/resizeCanvas.ts
@@ -0,0 +1,7 @@
+export const resizeCanvas = (canvas: HTMLCanvasElement, new_w: number, new_h: number) => {
+ const ctx = canvas.getContext("2d", { willReadFrequently: true }) as CanvasRenderingContext2D;
+ let temp = ctx.getImageData(0, 0, canvas.width, canvas.height)
+ canvas.width = new_w;
+ canvas.height = new_h;
+ ctx.putImageData(temp, 0, 0);
+}
diff --git a/frontend/src/functions/saveImage.ts b/frontend/src/functions/saveImage.ts
new file mode 100644
index 0000000..feb005a
--- /dev/null
+++ b/frontend/src/functions/saveImage.ts
@@ -0,0 +1,37 @@
+
+const today = (date: Date, delim: string): string => {
+ return ((date.getDate() < 10)?"0":"") + date.getDate() + delim + (((date.getMonth()+1) < 10)?"0":"") + (date.getMonth()+1) + delim + date.getFullYear();
+}
+
+const timeNow = (date: Date, delim: string): string => {
+ return ((date.getHours() < 10)?"0":"") + date.getHours() + delim + ((date.getMinutes() < 10)?"0":"") + date.getMinutes() + delim + ((date.getSeconds() < 10)?"0":"") + date.getSeconds();
+}
+
+const format = (str: string, ...rest: string[]): string => {
+ var formatted = str;
+ for (var i = 0; i < rest.length; i++) {
+ var regexp = new RegExp('\\{'+i+'\\}', 'gi');
+ formatted = formatted.replace(regexp, rest[i]);
+ }
+ return formatted;
+};
+
+
+export const downloadImage = async (image: HTMLImageElement) => {
+ var datetime = new Date();
+ const file_name = format("image_{0}_{1}.png", today(datetime, "-"), timeNow(datetime, "-"));
+ // Fallback to simple download
+ const xhr = new XMLHttpRequest();
+ xhr.responseType = "blob";
+ xhr.onload = function () {
+ const a = document.createElement("a");
+ a.href = window.URL.createObjectURL(xhr.response);
+ a.download = file_name;
+ a.style.display = "none";
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ };
+ xhr.open("GET", image.src);
+ xhr.send();
+}
diff --git a/frontend/src/functions/updateButtons.ts b/frontend/src/functions/updateButtons.ts
new file mode 100644
index 0000000..87014f2
--- /dev/null
+++ b/frontend/src/functions/updateButtons.ts
@@ -0,0 +1,11 @@
+const gallery = document.getElementById("gallery") as HTMLDivElement;
+const deleteSelectedBtn = document.getElementById("delete_selected_btn") as HTMLButtonElement;
+const averageSelectedBtn = document.getElementById("average_selected_btn") as HTMLButtonElement;
+const selectAllBtn = document.getElementById("select_all_btn") as HTMLButtonElement;
+
+export const updateButtons = () => {
+ const numSelectedItems = document.querySelectorAll('.marked-for-action').length;
+ selectAllBtn.disabled = !gallery.children.length;
+ deleteSelectedBtn.disabled = !numSelectedItems;
+ averageSelectedBtn.disabled = numSelectedItems < 2;
+}
diff --git a/frontend/src/index.ts b/frontend/src/index.ts
new file mode 100644
index 0000000..26474a4
--- /dev/null
+++ b/frontend/src/index.ts
@@ -0,0 +1,85 @@
+import { ofetch } from 'ofetch';
+import { DOWNLOAD, STATUS_FILE } from './consts.js';
+import { getCameraImage } from "./functions/getCameraImage.js";
+import { initButtons } from "./functions/initButtons.js";
+import { initDb } from "./functions/database.js";
+
+const MAX_POLL_DELAY = 2000;
+const BASIC_POLL_DELAY = 10;
+
+interface StatusResponse {
+ options: {
+ debug: 'on' | 'off'
+ },
+ result: string,
+ status: {
+ last_size: number,
+ total_files: number,
+ },
+ system: {
+ fast: boolean,
+ },
+}
+
+(async () => {
+
+ const store = await initDb();
+ const workingCanvas = document.createElement('canvas');
+
+ const indicator = document.querySelector('.indicator') as HTMLSpanElement;
+ let pollDelay = BASIC_POLL_DELAY;
+
+ const pollDownload = async () => {
+ try {
+ const status = await ofetch(STATUS_FILE, { method: 'GET', timeout: 1000 });
+ indicator.classList.remove('red');
+ indicator.classList.add('green');
+ indicator.title = 'Device is connected';
+
+ if (status.status.total_files > 0) {
+ const downloadResponse = await ofetch.raw(DOWNLOAD, {
+ method: 'GET',
+ timeout: 1000,
+ responseType: 'arrayBuffer',
+ ignoreResponseError: true,
+ });
+
+ if (downloadResponse.status === 200 && downloadResponse._data?.byteLength) {
+ const dlData = {
+ timestamp: Date.now(),
+ data: new Uint8Array(downloadResponse._data),
+ };
+
+ store.add(dlData);
+
+ getCameraImage(workingCanvas, dlData);
+
+ pollDelay = BASIC_POLL_DELAY;
+ } else { // 404 case
+ pollDelay = Math.min(MAX_POLL_DELAY, Math.ceil(pollDelay * 1.5));
+ }
+ } else {
+ pollDelay = Math.min(MAX_POLL_DELAY, Math.ceil(pollDelay * 1.5));
+ }
+ } catch { // network error case
+ pollDelay = Math.min(MAX_POLL_DELAY, Math.ceil(pollDelay * 5));
+ indicator.classList.add('red');
+ indicator.classList.remove('green');
+ indicator.title = 'Device is disconnected';
+ }
+
+ window.setTimeout(pollDownload, pollDelay);
+ };
+
+ const all = await store.getAll();
+
+ for (const dlData of all) {
+ const validImage = await getCameraImage(workingCanvas, dlData);
+ if (!validImage) {
+ store.delete(dlData.timestamp);
+ }
+ }
+
+ pollDownload();
+ initButtons(store);
+})();
diff --git a/frontend/src/style.css b/frontend/src/style.css
new file mode 100644
index 0000000..274508a
--- /dev/null
+++ b/frontend/src/style.css
@@ -0,0 +1,115 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+ text-align: center;
+}
+
+header {
+ margin-bottom: 24px;
+}
+
+button {
+ border-radius: 4px;
+ border: none;
+ padding: 8px 16px;
+ font-size: 1rem;
+ background: #b7b7b7;
+}
+
+button:not(:disabled):hover {
+ cursor: pointer;
+ background: #a0a0a0;
+}
+
+h1 {
+ font-size: 2rem;
+ position: relative;
+}
+
+:root { --grid-gap: 16px;--num-cols: 1; }
+@media screen and (min-width: 450px) { :root { --num-cols: 2; } }
+@media screen and (min-width: 640px) { :root { --num-cols: 3; } }
+@media screen and (min-width: 830px) { :root { --num-cols: 4; } }
+@media screen and (min-width: 1020px) { :root { --num-cols: 5; } }
+@media screen and (min-width: 1210px) { :root { --num-cols: 6; } }
+@media screen and (min-width: 1400px) { :root { --num-cols: 7; } }
+
+.gallery {
+ width: calc(calc(var(--num-cols) * 176px) + calc(var(--num-cols) * var(--grid-gap)) - var(--grid-gap));
+ grid-template-columns: repeat(var(--num-cols), 176px);
+ display: grid;
+ grid-column-gap: var(--grid-gap);
+ grid-row-gap: var(--grid-gap);
+ margin: 0 auto;
+}
+
+.gallery-image {
+ padding: 8px;
+ background-color: #eeeeee;
+ border-radius: 8px;
+ border: 1px solid #d0d0d0;
+ box-shadow: 0 7px 6px -6px #404040;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.gallery-image:hover {
+ background-color: #dedede;
+}
+
+.gallery-image button {
+ width: 100%;
+}
+
+.gallery-image input {
+ display: none;
+}
+
+.marked-for-action {
+ background-color: #d0d0d0;
+ border-color: #707070;
+ border-width: 2px;
+ margin: -1px;
+}
+
+.marked-for-action:hover {
+ background-color: #c2c2c2;
+}
+
+.indicator {
+ --color-main: #c2c2c2;
+ --color-light: #e7e7e7;
+ --color-dark: #424242;
+
+ top: 16px;
+ right: 16px;
+ position: absolute;
+ width: 18px;
+ height: 18px;
+ background-color: var(--color-main);
+ border-radius: 50%;
+ margin-left: 24px;
+ box-shadow:
+ inset 5px 8px 6px -3px var(--color-light),
+ 0 0 0 1.3px var(--color-dark),
+ 0 0 5px 5px var(--color-light);
+}
+
+.indicator.green {
+ --color-main: #00c200;
+ --color-light: #9cff9c;
+ --color-dark: #064406;
+}
+
+.indicator.red {
+ --color-main: #c20000;
+ --color-light: #ff9c9c;
+ --color-dark: #440606;
+}
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..a4883f2
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..e6941c1
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,37 @@
+import { defineConfig } from 'vite';
+import { $fetch } from 'ofetch';
+
+export default defineConfig({
+ build: {
+ outDir: '../fs/',
+ emptyOutDir: true,
+ },
+ plugins: [
+ {
+ name: 'custom-fetch-proxy',
+ configureServer(server) {
+ server.middlewares.use(async (req, res, next) => {
+ const targetUrl = 'http://192.168.7.1';
+ if (['/list.json', '/status.json', '/download'].includes(req.url)) {
+ try {
+ const url = `${targetUrl}${req.url}`;
+ const targetResponse = await $fetch.raw(url, { method: 'GET', responseType: 'arrayBuffer', ignoreResponseError: true });
+ res.statusCode = targetResponse.status;
+ targetResponse.headers.forEach((value, name) => res.setHeader(name, value));
+ res.end(new Uint8Array(targetResponse._data));
+ } catch (error) {
+ console.error('Fetch proxy error:', error);
+ res.statusCode = 500;
+ res.end('Internal Server Error');
+ }
+ } else {
+ next();
+ }
+ });
+ },
+ },
+ ],
+ server: {
+ port: 3000,
+ },
+});
diff --git a/fs/consts.js b/fs/consts.js
deleted file mode 100644
index 572a799..0000000
--- a/fs/consts.js
+++ /dev/null
@@ -1,18 +0,0 @@
-// Api URLs
-export const OPTIONS = "/options";
-export const DOWNLOAD = "/download";
-export const RESET = "/reset";
-export const RESET_USB_BOOT = "/reset_usb_boot";
-export const STATUS_FILE = "/status.json";
-export const LIST_FILE = "/list.json";
-// Printer Commands
-export const COMMAND_INIT = 0x01;
-export const COMMAND_PRINT = 0x02;
-export const COMMAND_DATA = 0x04;
-export const COMMAND_TRANSFER = 0x10;
-// Image specific
-export const PRINTER_WIDTH = 20;
-export const CAMERA_WIDTH = 16;
-export const TILE_SIZE = 0x10;
-export const TILE_HEIGHT = 8;
-export const TILE_WIDTH = 8;
diff --git a/fs/functions/appendCanvasToGallery.js b/fs/functions/appendCanvasToGallery.js
deleted file mode 100644
index f424008..0000000
--- a/fs/functions/appendCanvasToGallery.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { updateButtons } from "./updateButtons.js";
-import { downloadImage } from "./saveImage.js";
-const gallery = document.getElementById("gallery");
-export const appendCanvasToGallery = (canvas, timestamp) => {
- if (canvas.height > 1) {
- const imageContainer = document.createElement("div");
- imageContainer.classList.add("gallery-image");
- const img = new Image();
- img.src = canvas.toDataURL();
- imageContainer.appendChild(img);
- imageContainer.appendChild(document.createElement("br"));
- if (timestamp) {
- imageContainer.dataset.timestamp = timestamp.toString(10);
- }
- const input = document.createElement("input");
- input.setAttribute("type", "checkbox");
- input.addEventListener("change", function () {
- if (input.checked) {
- imageContainer.classList.add('marked-for-action');
- }
- else {
- imageContainer.classList.remove('marked-for-action');
- }
- updateButtons();
- });
- imageContainer.appendChild(input);
- const btn = document.createElement("button");
- btn.textContent = "Save";
- btn.addEventListener("click", function () {
- downloadImage(img);
- });
- imageContainer.appendChild(btn);
- gallery.appendChild(imageContainer);
- updateButtons();
- }
-};
diff --git a/fs/functions/database.js b/fs/functions/database.js
deleted file mode 100644
index 2a70d4f..0000000
--- a/fs/functions/database.js
+++ /dev/null
@@ -1,49 +0,0 @@
-export const initDb = async () => {
- const dbName = "pico_printer";
- return new Promise((resolve) => {
- const request = indexedDB.open(dbName, 2);
- request.onerror = (event) => {
- console.error(event);
- };
- request.onsuccess = () => {
- resolve({
- add: async (dlData) => {
- return new Promise((resolve) => {
- const objectStore = request.result.transaction('downloads', 'readwrite').objectStore('downloads');
- const rq = objectStore.add({
- timestamp: dlData.timestamp,
- data: dlData.data.join(',')
- });
- rq.onsuccess = () => resolve();
- });
- },
- getAll: async () => {
- return new Promise((resolve) => {
- const objectStore = request.result.transaction('downloads').objectStore('downloads');
- const rq = objectStore.getAll();
- rq.onsuccess = (ev) => {
- // @ts-ignore
- const stored = ev.target?.result.map(({ timestamp, data }) => ({
- timestamp,
- data: new Uint8Array(data.split(',').map((i) => parseInt(i, 10))),
- }));
- resolve(stored);
- };
- });
- },
- delete: async (timestamp) => {
- return new Promise((resolve) => {
- const objectStore = request.result.transaction('downloads', 'readwrite').objectStore('downloads');
- const rq = objectStore.delete(timestamp);
- rq.onsuccess = () => resolve();
- });
- }
- });
- };
- request.onupgradeneeded = () => {
- console.log('onupgradeneeded');
- const objectStore = request.result.createObjectStore("downloads", { keyPath: "timestamp" });
- objectStore.createIndex("timestamp", "timestamp", { unique: false });
- };
- });
-};
diff --git a/fs/functions/decode.js b/fs/functions/decode.js
deleted file mode 100644
index 376c9c4..0000000
--- a/fs/functions/decode.js
+++ /dev/null
@@ -1,29 +0,0 @@
-export const decode = (is_compressed, source, source_size, source_data_len, source_ptr, dest, dest_ptr) => {
- if (source_ptr + source_data_len <= source_size) {
- if (is_compressed) {
- const stop = source_ptr + source_data_len;
- while (source_ptr < stop) {
- const tag = source[source_ptr++];
- if (tag & 0x80) {
- const data = source[source_ptr++];
- for (let i = 0; i < ((tag & 0x7f) + 2); i++) {
- dest[dest_ptr++] = data;
- }
- }
- else {
- for (let i = 0; i < (tag + 1); i++) {
- dest[dest_ptr++] = source[source_ptr++];
- }
- }
- }
- return dest_ptr;
- }
- else {
- for (let i = 0; i < source_data_len; i++) {
- dest[dest_ptr++] = source[source_ptr++];
- }
- return dest_ptr;
- }
- }
- return dest_ptr;
-};
diff --git a/fs/functions/getCameraImage.js b/fs/functions/getCameraImage.js
deleted file mode 100644
index 1cab749..0000000
--- a/fs/functions/getCameraImage.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { CAMERA_WIDTH, COMMAND_DATA, COMMAND_INIT, COMMAND_PRINT, COMMAND_TRANSFER, PRINTER_WIDTH } from "../consts.js";
-import { appendCanvasToGallery } from "./appendCanvasToGallery.js";
-import { resetCanvas } from "./resetCanvas.js";
-import { decode } from "./decode.js";
-import { render } from "./render.js";
-export async function getCameraImage(canvas, dlData) {
- const resData = dlData.data;
- const data_size = resData.byteLength;
- const processed_data = new Uint8Array(Math.max(1024 * 1024, data_size));
- resetCanvas(canvas);
- let buffer_start = 0;
- let ptr = 0;
- let idx = 0;
- let len = 0;
- while (idx < data_size) {
- const command = resData[idx++];
- switch (command) {
- case COMMAND_INIT:
- break;
- case COMMAND_PRINT: {
- if ((len = resData[idx++] | (resData[idx++] << 8)) != 4) {
- idx = data_size;
- break;
- }
- let sheets = resData[idx++];
- let margins = resData[idx++];
- let palette = resData[idx++];
- let exposure = Math.min(0xFF, 0x80 + resData[idx++]);
- palette = (palette) ? palette : 0xE4;
- if (render(canvas, processed_data, buffer_start, ptr, PRINTER_WIDTH, sheets, margins, palette, exposure)) {
- appendCanvasToGallery(canvas, dlData.timestamp);
- resetCanvas(canvas);
- }
- buffer_start = ptr;
- break;
- }
- case COMMAND_TRANSFER: {
- len = resData[idx++] | (resData[idx++] << 8);
- let current_image_start = ptr;
- ptr = decode(false, resData, data_size, len, idx, processed_data, ptr);
- idx += len;
- render(canvas, processed_data, current_image_start, ptr, CAMERA_WIDTH, 1, 0x03, 0xE4, 0xFF);
- appendCanvasToGallery(canvas, dlData.timestamp);
- resetCanvas(canvas);
- buffer_start = ptr;
- break;
- }
- case COMMAND_DATA: {
- const compression = !!resData[idx++];
- len = resData[idx++] | (resData[idx++] << 8);
- ptr = decode(compression, resData, data_size, len, idx, processed_data, ptr);
- idx += len;
- break;
- }
- default:
- idx = data_size;
- break;
- }
- }
- if (canvas.height > 1) {
- appendCanvasToGallery(canvas, dlData.timestamp);
- resetCanvas(canvas);
- }
-}
diff --git a/fs/functions/initButtons.js b/fs/functions/initButtons.js
deleted file mode 100644
index ffd53d9..0000000
--- a/fs/functions/initButtons.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import { updateButtons } from "./updateButtons.js";
-import { appendCanvasToGallery } from "./appendCanvasToGallery.js";
-const gallery = document.getElementById("gallery");
-// const getImageBtn = document.getElementById("get_image_btn") as HTMLButtonElement;
-// const tearBtn = document.getElementById("tear_btn") as HTMLButtonElement;
-const deleteSelectedBtn = document.getElementById("delete_selected_btn");
-const selectAllBtn = document.getElementById("select_all_btn");
-const averageSelectedBtn = document.getElementById("average_selected_btn");
-export const initButtons = (store) => {
- selectAllBtn.addEventListener("click", function () {
- var items = gallery.children;
- if (items.length != 0) {
- [...items].forEach(item => {
- const checkbox = item.querySelector("input");
- checkbox.checked = true;
- item.classList.add('marked-for-action');
- });
- }
- updateButtons();
- });
- deleteSelectedBtn.addEventListener("click", function () {
- const items = gallery.children;
- for (let i = items.length - 1; i >= 0; i--) {
- const item = items[i];
- if (item.classList.contains('marked-for-action')) {
- const imageTime = item.dataset.timestamp;
- if (imageTime) {
- store.delete(parseInt(imageTime, 10));
- }
- item.remove();
- }
- }
- updateButtons();
- });
- averageSelectedBtn.addEventListener("click", function () {
- const items = gallery.children;
- const avgCanvas = document.createElement('canvas');
- const avgCtx = avgCanvas.getContext('2d', { willReadFrequently: true });
- const tmpCanvas = document.createElement('canvas');
- const tmpCtx = tmpCanvas.getContext('2d', { willReadFrequently: true });
- // Verify that image dimensions are the same
- const firstImg = items[0].querySelector("img");
- if (!firstImg)
- return;
- const tmpW = firstImg.width;
- const tmpH = firstImg.height;
- for (let i = 1; i < items.length; i++) {
- const img = items[i].querySelector("img");
- if (!img)
- return;
- if (tmpW != img.width || tmpH != img.height) {
- alert("Image dimensions should be the same to do an average");
- return;
- }
- }
- tmpCanvas.width = tmpW;
- tmpCanvas.height = tmpH;
- avgCanvas.width = tmpW;
- avgCanvas.height = tmpH;
- const sumImgData = [];
- const avgImgData = avgCtx.createImageData(avgCanvas.width, avgCanvas.height);
- let selectedItems = 0;
- // Generate average image
- for (let i = items.length - 1; i >= 0; i--) {
- if (items[i].classList.contains('marked-for-action')) {
- selectedItems++;
- const item = items[i];
- const img = item.querySelector("img");
- if (!img)
- return;
- tmpCtx.drawImage(img, 0, 0);
- const tmpImgData = tmpCtx.getImageData(0, 0, tmpCanvas.width, tmpCanvas.height);
- for (let j = 0; j < tmpImgData.data.length; j += 1) {
- if (!sumImgData[j]) {
- sumImgData.push(0);
- }
- sumImgData[j] += tmpImgData.data[j];
- }
- }
- }
- for (let i = 0; i < avgImgData.data.length; i += 1) {
- avgImgData.data[i] = (sumImgData[i] / selectedItems);
- }
- avgCtx.putImageData(avgImgData, 0, 0);
- appendCanvasToGallery(avgCanvas);
- });
- // tearBtn.addEventListener("click", async function () {
- // fetch(resetPath)
- // .then((response) => {
- // return response.json();
- // })
- // .then((data) => {
- // console.log(data);
- // if (data.result != "ok") return;
- // getImageBtn.click();
- // });
- // });
- updateButtons();
-};
diff --git a/fs/functions/render.js b/fs/functions/render.js
deleted file mode 100644
index f82db47..0000000
--- a/fs/functions/render.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { TILE_HEIGHT, TILE_SIZE } from "../consts.js";
-import { resizeCanvas } from "./resizeCanvas.js";
-export const render = (canvas, image_data, image_start, image_end, image_tile_width, sheets, margin, palette, exposure) => {
- const pal = new Uint8Array(4);
- pal[0] = ((exposure * ((palette >> 0) & 0x03)) / 3) >> 0;
- pal[1] = ((exposure * ((palette >> 2) & 0x03)) / 3) >> 0;
- pal[2] = ((exposure * ((palette >> 4) & 0x03)) / 3) >> 0;
- pal[3] = ((exposure * ((palette >> 6) & 0x03)) / 3) >> 0;
- let tile_y = ((canvas.height / TILE_HEIGHT) >> 0);
- let tile_x = 0;
- resizeCanvas(canvas, (image_tile_width * 8), ((canvas.height >> 3) << 3) + ((Math.max(0, image_end - image_start) / (TILE_SIZE * image_tile_width)) >> 0) * 8);
- if (canvas.width * canvas.height !== 0) {
- const ctx = canvas.getContext("2d", { willReadFrequently: true });
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
- const writeData = imageData.data;
- for (let i = image_start; i < image_end;) {
- for (let t = 0; t < 8; t++) {
- let b1 = image_data[i++];
- let b2 = image_data[i++];
- for (let b = 0; b < 8; b++) {
- let offset = (((tile_y << 3) + t) * canvas.width + (tile_x << 3) + b) << 2;
- let color_index = ((b1 >> (7 - b)) & 1) | (((b2 >> (7 - b)) & 1) << 1);
- writeData[offset] = writeData[offset + 1] = writeData[offset + 2] = 0xFF - pal[color_index];
- writeData[offset + 3] = 0xff;
- }
- }
- tile_x += 1;
- if (tile_x >= image_tile_width) {
- tile_x = 0;
- tile_y++;
- }
- }
- ctx.putImageData(imageData, 0, 0);
- }
- return ((margin & 0x0f) != 0);
-};
diff --git a/fs/functions/resetCanvas.js b/fs/functions/resetCanvas.js
deleted file mode 100644
index 8d03481..0000000
--- a/fs/functions/resetCanvas.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export const resetCanvas = (canvas) => {
- canvas.height = 1;
- canvas.width = 1;
-};
diff --git a/fs/functions/resizeCanvas.js b/fs/functions/resizeCanvas.js
deleted file mode 100644
index ffa4fbe..0000000
--- a/fs/functions/resizeCanvas.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export const resizeCanvas = (canvas, new_w, new_h) => {
- const ctx = canvas.getContext("2d", { willReadFrequently: true });
- let temp = ctx.getImageData(0, 0, canvas.width, canvas.height);
- canvas.width = new_w;
- canvas.height = new_h;
- ctx.putImageData(temp, 0, 0);
-};
diff --git a/fs/functions/saveImage.js b/fs/functions/saveImage.js
deleted file mode 100644
index d7775cd..0000000
--- a/fs/functions/saveImage.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const today = (date, delim) => {
- return ((date.getDate() < 10) ? "0" : "") + date.getDate() + delim + (((date.getMonth() + 1) < 10) ? "0" : "") + (date.getMonth() + 1) + delim + date.getFullYear();
-};
-const timeNow = (date, delim) => {
- return ((date.getHours() < 10) ? "0" : "") + date.getHours() + delim + ((date.getMinutes() < 10) ? "0" : "") + date.getMinutes() + delim + ((date.getSeconds() < 10) ? "0" : "") + date.getSeconds();
-};
-const format = (str, ...rest) => {
- var formatted = str;
- for (var i = 0; i < rest.length; i++) {
- var regexp = new RegExp('\\{' + i + '\\}', 'gi');
- formatted = formatted.replace(regexp, rest[i]);
- }
- return formatted;
-};
-export const downloadImage = async (image) => {
- var datetime = new Date();
- const file_name = format("image_{0}_{1}.png", today(datetime, "-"), timeNow(datetime, "-"));
- // Fallback to simple download
- const xhr = new XMLHttpRequest();
- xhr.responseType = "blob";
- xhr.onload = function () {
- const a = document.createElement("a");
- a.href = window.URL.createObjectURL(xhr.response);
- a.download = file_name;
- a.style.display = "none";
- document.body.appendChild(a);
- a.click();
- a.remove();
- };
- xhr.open("GET", image.src);
- xhr.send();
-};
diff --git a/fs/functions/updateButtons.js b/fs/functions/updateButtons.js
deleted file mode 100644
index 101fbd2..0000000
--- a/fs/functions/updateButtons.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const gallery = document.getElementById("gallery");
-const deleteSelectedBtn = document.getElementById("delete_selected_btn");
-const averageSelectedBtn = document.getElementById("average_selected_btn");
-const selectAllBtn = document.getElementById("select_all_btn");
-export const updateButtons = () => {
- const hasSelectedItems = document.querySelectorAll('.marked-for-action').length;
- selectAllBtn.disabled = !gallery.children.length;
- deleteSelectedBtn.disabled = !hasSelectedItems;
- averageSelectedBtn.disabled = !hasSelectedItems;
-};
diff --git a/fs/index.js b/fs/index.js
deleted file mode 100644
index 2c91fe0..0000000
--- a/fs/index.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { DOWNLOAD } from "./consts.js";
-import { getCameraImage } from "./functions/getCameraImage.js";
-import { initButtons } from "./functions/initButtons.js";
-import { initDb } from "./functions/database.js";
-const STATUS_POLL_DELAY = 1000;
-const STATUS_POLL_NEXT = 10;
-const store = await initDb();
-const workingCanvas = document.createElement('canvas');
-const getStatus = async () => {
- try {
- const downloadResponse = await fetch(DOWNLOAD);
- if (downloadResponse.status == 200) {
- const downloadBody = await downloadResponse.blob();
- const downloadBuffer = await downloadBody.arrayBuffer();
- const downloadData = new Uint8Array(downloadBuffer);
- const dlData = {
- timestamp: Date.now(),
- data: downloadData,
- };
- store.add(dlData);
- getCameraImage(workingCanvas, dlData);
- const to = window.setTimeout(getStatus, STATUS_POLL_NEXT);
- } else {
- const to = window.setTimeout(getStatus, STATUS_POLL_DELAY);
- }
- } catch(e) {
- const to = window.setTimeout(getStatus, STATUS_POLL_DELAY);
- }
-};
-const all = await store.getAll();
-all.forEach((dlData) => {
- console.log(dlData.data.byteLength);
- getCameraImage(workingCanvas, dlData);
-});
-getStatus();
-initButtons(store);