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);