From 5f5280d4de1637a9f987d2245a748d5b6af7f30c Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 25 Mar 2024 14:04:42 +0100 Subject: [PATCH] Reimpliment UI with React --- .vscode/settings.json | 3 + package-lock.json | 884 +++++++++++++++++- package.json | 14 +- src/App.ts | 25 +- src/ConfigSection.ts | 70 -- src/ControlPanel.ts | 77 -- src/ControlSection.ts | 325 ------- src/FileImportDialog.ts | 107 --- src/PlaybackSection.ts | 166 ---- src/assets/arrow.svg | 3 + src/{ => core}/AudioPlayer.ts | 0 src/{ => core}/AudioWorker.ts | 0 src/{ => core}/AudioWorkerComms.ts | 0 src/{ => core}/PianoRenderer.ts | 1 - src/{ => core}/Queue.ts | 0 src/{ => core}/Renderer.ts | 0 src/{ => core}/StateManager.ts | 0 src/{ => core}/SynthState.ts | 0 src/{ => core}/TimelineRenderer.ts | 0 src/export/ExportDialog.ts | 178 ---- src/export/ExportManager.ts | 38 +- src/export/Exporter.ts | 6 +- src/export/ProgressStatus.ts | 43 - src/export/wave/WaveExporter.ts | 4 +- .../wavePerChannel/WavePerChannelExporter.ts | 4 +- src/index.html | 84 +- src/style.css | 278 ------ src/ui/Collapsible.tsx | 42 + src/ui/ConfigGrid.tsx | 183 ++++ src/ui/ConfigSection.tsx | 63 ++ src/ui/ControlPanel.tsx | 72 ++ src/ui/Dialog.tsx | 25 + src/ui/ExportButton.tsx | 147 +++ src/ui/FileSection.tsx | 12 + src/ui/Helpers.ts | 20 + src/ui/NDSImportButton.tsx | 139 +++ src/ui/NumberSpinner.tsx | 13 + src/ui/PlaybackSection.tsx | 114 +++ src/ui/ReactRoot.tsx | 31 + src/ui/styles/Collapsible.module.css | 43 + src/ui/styles/ConfigGrid.module.css | 17 + src/ui/styles/ControlPanel.module.css | 29 + src/ui/styles/Dialog.module.css | 11 + src/ui/styles/ExportButton.module.css | 23 + src/ui/styles/Global.css | 77 ++ src/ui/styles/NDSImportButton.module.css | 17 + src/ui/styles/NumberSpinner.module.css | 9 + src/ui/styles/Panel.module.css | 10 + src/ui/styles/PlaybackSection.module.css | 22 + tsconfig.json | 15 +- 50 files changed, 2057 insertions(+), 1387 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 src/ConfigSection.ts delete mode 100644 src/ControlPanel.ts delete mode 100644 src/ControlSection.ts delete mode 100644 src/FileImportDialog.ts delete mode 100644 src/PlaybackSection.ts create mode 100644 src/assets/arrow.svg rename src/{ => core}/AudioPlayer.ts (100%) rename src/{ => core}/AudioWorker.ts (100%) rename src/{ => core}/AudioWorkerComms.ts (100%) rename src/{ => core}/PianoRenderer.ts (98%) rename src/{ => core}/Queue.ts (100%) rename src/{ => core}/Renderer.ts (100%) rename src/{ => core}/StateManager.ts (100%) rename src/{ => core}/SynthState.ts (100%) rename src/{ => core}/TimelineRenderer.ts (100%) delete mode 100644 src/export/ExportDialog.ts delete mode 100644 src/export/ProgressStatus.ts delete mode 100644 src/style.css create mode 100644 src/ui/Collapsible.tsx create mode 100644 src/ui/ConfigGrid.tsx create mode 100644 src/ui/ConfigSection.tsx create mode 100644 src/ui/ControlPanel.tsx create mode 100644 src/ui/Dialog.tsx create mode 100644 src/ui/ExportButton.tsx create mode 100644 src/ui/FileSection.tsx create mode 100644 src/ui/Helpers.ts create mode 100644 src/ui/NDSImportButton.tsx create mode 100644 src/ui/NumberSpinner.tsx create mode 100644 src/ui/PlaybackSection.tsx create mode 100644 src/ui/ReactRoot.tsx create mode 100644 src/ui/styles/Collapsible.module.css create mode 100644 src/ui/styles/ConfigGrid.module.css create mode 100644 src/ui/styles/ControlPanel.module.css create mode 100644 src/ui/styles/Dialog.module.css create mode 100644 src/ui/styles/ExportButton.module.css create mode 100644 src/ui/styles/Global.css create mode 100644 src/ui/styles/NDSImportButton.module.css create mode 100644 src/ui/styles/NumberSpinner.module.css create mode 100644 src/ui/styles/Panel.module.css create mode 100644 src/ui/styles/PlaybackSection.module.css diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..63662bf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib" +} diff --git a/package-lock.json b/package-lock.json index 931d1a3..8f4fe70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,13 +6,27 @@ "": { "name": "nitro-play", "license": "GPL-3.0-or-later", + "dependencies": { + "nitro-fs": "^1.1.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, "devDependencies": { "@parcel/service-worker": "^2.12.0", - "nitro-fs": "^1.1.1", + "@types/react": "^18.2.67", + "@types/react-dom": "^18.2.22", "parcel": "^2.12.0", - "prettier": "3.2.5" + "prettier": "3.2.5", + "process": "^0.11.10", + "typescript-plugin-css-modules": "^5.1.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", + "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", + "dev": true + }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", @@ -2094,6 +2108,56 @@ "node": ">=10.13.0" } }, + "node_modules/@types/postcss-modules-local-by-default": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.2.tgz", + "integrity": "sha512-CtYCcD+L+trB3reJPny+bKWKMzPfxEyQpKIwit7kErnOexf5/faaGpkFy4I5AwbV4hp1sk7/aTg0tt0B67VkLQ==", + "dev": true, + "dependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/@types/postcss-modules-scope": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/postcss-modules-scope/-/postcss-modules-scope-3.0.4.tgz", + "integrity": "sha512-//ygSisVq9kVI0sqx3UPLzWIMCmtSVrzdljtuaAEJtGoGnpjBikZ2sXO5MpH9SnWX9HRfXxHifDAXcQjupWnIQ==", + "dev": true, + "dependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.67", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.67.tgz", + "integrity": "sha512-vkIE2vTIMHQ/xL0rgmuoECBCkZFZeHr49HeWSc24AptMbNRo7pwSBvj73rlJJs9fGKj0koS+V7kQB1jHS0uCgw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.22", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz", + "integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "dev": true + }, "node_modules/abortcontroller-polyfill": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz", @@ -2115,12 +2179,31 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, "node_modules/base-x": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", @@ -2130,12 +2213,34 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -2225,6 +2330,30 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -2270,6 +2399,24 @@ "node": ">= 10" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -2404,6 +2551,18 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csso": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", @@ -2443,6 +2602,29 @@ "optional": true, "peer": true }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -2552,6 +2734,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2591,6 +2786,26 @@ "node": ">=8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "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/get-port": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", @@ -2600,6 +2815,38 @@ "node": ">=6" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -2615,6 +2862,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "optional": true + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2690,6 +2944,50 @@ "entities": "^3.0.1" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2706,12 +3004,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2748,11 +3074,16 @@ "node": ">=0.12.0" } }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -2784,6 +3115,32 @@ "node": ">=6" } }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, "node_modules/lightningcss": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.24.0.tgz", @@ -2991,6 +3348,15 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -3028,6 +3394,23 @@ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", "dev": true }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -3040,6 +3423,30 @@ "node": ">=10" } }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -3061,6 +3468,46 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/msgpackr": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.1.tgz", @@ -3104,11 +3551,45 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "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/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, "node_modules/nitro-fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nitro-fs/-/nitro-fs-1.1.1.tgz", - "integrity": "sha512-WS4eqWS3li//ygz7KKWlLqSHE1gIGlJLrRGNk5+5yw/2e31UJrI7MYsLmDgL2Taqt6S4qZbldstdFdX4xL9bAA==", - "dev": true + "integrity": "sha512-WS4eqWS3li//ygz7KKWlLqSHE1gIGlJLrRGNk5+5yw/2e31UJrI7MYsLmDgL2Taqt6S4qZbldstdFdX4xL9bAA==" }, "node_modules/node-addon-api": { "version": "7.1.0", @@ -3148,6 +3629,15 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -3166,6 +3656,15 @@ "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", "dev": true }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/ordered-binary": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.1.tgz", @@ -3234,6 +3733,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -3261,6 +3778,130 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "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.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -3331,6 +3972,45 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/react-error-overlay": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", @@ -3346,12 +4026,30 @@ "node": ">=0.10.0" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "dev": true }, + "node_modules/reserved-words": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz", + "integrity": "sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==", + "dev": true + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3381,6 +4079,44 @@ } ] }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "optional": true + }, + "node_modules/sass": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.72.0.tgz", + "integrity": "sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "dev": true + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -3406,12 +4142,10 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3435,6 +4169,46 @@ "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", "dev": true }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/stylus": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.62.0.tgz", + "integrity": "sha512-v3YCf31atbwJQIMtPNX8hcQ+okD4NQaTuKGUWfII8eaqn+3otrbttGL1zSMZAAtiPsBztQnujVBugg/cXFUpyg==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "~4.3.1", + "debug": "^4.3.2", + "glob": "^7.1.6", + "sax": "~1.3.0", + "source-map": "^0.7.3" + }, + "bin": { + "stylus": "bin/stylus" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://opencollective.com/stylus" + } + }, + "node_modules/stylus/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3504,6 +4278,20 @@ "node": ">=8.0" } }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -3522,6 +4310,59 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typescript": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-plugin-css-modules": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/typescript-plugin-css-modules/-/typescript-plugin-css-modules-5.1.0.tgz", + "integrity": "sha512-6h+sLBa4l+XYSTn/31vZHd/1c3SvAbLpobY6FxDiUOHJQG1eD9Gh3eCs12+Eqc+TCOAdxcO+zAPvUq0jBfdciw==", + "dev": true, + "dependencies": { + "@types/postcss-modules-local-by-default": "^4.0.2", + "@types/postcss-modules-scope": "^3.0.4", + "dotenv": "^16.4.2", + "icss-utils": "^5.1.0", + "less": "^4.2.0", + "lodash.camelcase": "^4.3.0", + "postcss": "^8.4.35", + "postcss-load-config": "^3.1.4", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", + "reserved-words": "^0.1.2", + "sass": "^1.70.0", + "source-map-js": "^1.0.2", + "stylus": "^0.62.0", + "tsconfig-paths": "^4.2.0" + }, + "peerDependencies": { + "typescript": ">=4.0.0" + } + }, + "node_modules/typescript-plugin-css-modules/node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -3552,6 +4393,12 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, "node_modules/utility-types": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", @@ -3567,11 +4414,26 @@ "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", "dev": true }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } } } } diff --git a/package.json b/package.json index cb53230..a75dde9 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,21 @@ "build": "parcel build --public-url ./", "format": "prettier . --write" }, - "author": "", + "author": "DanielPXL", "license": "GPL-3.0-or-later", + "dependencies": { + "nitro-fs": "^1.1.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, "devDependencies": { "@parcel/service-worker": "^2.12.0", - "nitro-fs": "^1.1.1", + "@types/react": "^18.2.67", + "@types/react-dom": "^18.2.22", "parcel": "^2.12.0", - "prettier": "3.2.5" + "prettier": "3.2.5", + "process": "^0.11.10", + "typescript-plugin-css-modules": "^5.1.0" }, "@parcel/runtime-js": { "splitManifestThreshold": 100000000 diff --git a/src/App.ts b/src/App.ts index 4cdff08..1dc73c3 100644 --- a/src/App.ts +++ b/src/App.ts @@ -1,11 +1,10 @@ import * as ServiceWorkerComms from "./ServiceWorkerComms"; -import * as AudioWorkerComms from "./AudioWorkerComms"; -import * as ControlPanel from "./ControlPanel"; -import * as AudioPlayer from "./AudioPlayer"; -import * as StateManager from "./StateManager"; -import * as Renderer from "./Renderer"; -import * as ExportManager from "./export/ExportManager"; -import { SynthState } from "./SynthState"; +import * as AudioWorkerComms from "./core/AudioWorkerComms"; +import * as AudioPlayer from "./core/AudioPlayer"; +import * as StateManager from "./core/StateManager"; +import * as Renderer from "./core/Renderer"; +import { SynthState } from "./core/SynthState"; +import * as ReactRoot from "./ui/ReactRoot"; const targetBufferHealth = 3.0; let acceptBuffers = false; @@ -23,7 +22,10 @@ AudioWorkerComms.on("pcm", (data: Float32Array[]) => { } }); -async function load() { +async function load(seq: string) { + stop(); + await AudioWorkerComms.call("loadSeq", { name: seq }); + acceptBuffers = true; const s = await AudioWorkerComms.call("tickSeconds", { seconds: targetBufferHealth - AudioPlayer.getBufferHealth() @@ -49,10 +51,6 @@ function start() { } setInterval(() => { - // document.title = `Buffer health: ${AudioPlayer.getBufferHealth()}`; - // document.title = `States: ${StateManager.statesQueue.array().length}`; - // document.title = `Time: ${AudioPlayer.getTime()}`; - // document.title = `Top time: ${StateManager.topTime()}`; StateManager.discardStates(StateManager.topTime() - targetBufferHealth * 2); }, 200); @@ -66,5 +64,4 @@ function stop() { ServiceWorkerComms.init(); AudioWorkerComms.init(); Renderer.init(); -ExportManager.init(); -ControlPanel.init(load, start, stop); +ReactRoot.init(load, start); diff --git a/src/ConfigSection.ts b/src/ConfigSection.ts deleted file mode 100644 index 5d62d28..0000000 --- a/src/ConfigSection.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ControlSection, ControlSectionEntry } from "./ControlSection"; -import * as Renderer from "./Renderer"; - -export function init() { - const configSection = document.getElementById("config") as HTMLDivElement; - - for (const configGroup of schema) { - new ControlSection( - configSection, - configGroup.title, - "config", - configGroup.entries - ); - } -} - -const schema: ConfigSchema = [ - { - title: "Appearance", - entries: [ - { - text: "Align notes to piano", - id: "alignNotesToPiano", - type: "checkbox", - default: true, - update: (value) => { - Renderer.alignNotesToPiano(value); - } - }, - { - text: "Piano position", - id: "pianoPosition", - type: "slider", - default: 0.7, - min: 0, - max: 1, - forceRange: true, - update: (value) => { - Renderer.setPianoPosition(value); - } - }, - { - text: "Piano height", - id: "pianoHeight", - type: "slider", - default: 0.1, - min: 0, - max: 0.8, - update: (value) => { - Renderer.setPianoHeight(value); - } - }, - { - text: "Piano range", - id: "pianoRange", - type: "minmax", - default: [0, 119], - integer: true, - update: (value) => { - Renderer.setPianoRange(value); - } - } - ] - } -]; - -type ConfigSchema = { - title: string; - entries: ControlSectionEntry[]; -}[]; diff --git a/src/ControlPanel.ts b/src/ControlPanel.ts deleted file mode 100644 index 57583e0..0000000 --- a/src/ControlPanel.ts +++ /dev/null @@ -1,77 +0,0 @@ -import * as AudioWorkerComms from "./AudioWorkerComms"; -import * as PlaybackSection from "./PlaybackSection"; -import * as FileImportDialog from "./FileImportDialog"; -import * as ExportDialog from "./export/ExportDialog"; -import * as ConfigSection from "./ConfigSection"; - -export function init( - load: () => Promise, - play: () => void, - stop: () => void -) { - const controlPanel = document.getElementById( - "controlPanel" - ) as HTMLDivElement; - const openButton = document.getElementById( - "openButton" - ) as HTMLImageElement; - const closeButton = document.getElementById( - "closeButton" - ) as HTMLButtonElement; - const ndsFileImportButton = document.getElementById( - "ndsFileImportButton" - ) as HTMLButtonElement; - const exportButton = document.getElementById( - "exportButton" - ) as HTMLButtonElement; - - PlaybackSection.init( - async () => { - ndsFileImportButton.disabled = true; - exportButton.disabled = true; - await load(); - ndsFileImportButton.disabled = false; - exportButton.disabled = false; - }, - () => { - ndsFileImportButton.disabled = true; - exportButton.disabled = true; - play(); - }, - () => { - ndsFileImportButton.disabled = false; - exportButton.disabled = false; - stop(); - } - ); - - FileImportDialog.init(async () => { - const symbols = await AudioWorkerComms.call("getSeqSymbols"); - PlaybackSection.enable(symbols); - }); - - ndsFileImportButton.addEventListener("click", () => { - exportButton.disabled = true; - PlaybackSection.disable(); - FileImportDialog.show(); - }); - - ExportDialog.init(); - - exportButton.disabled = true; - exportButton.addEventListener("click", () => { - ExportDialog.showDialog(); - }); - - ConfigSection.init(); - - openButton.style.display = "none"; - openButton.addEventListener("click", () => { - controlPanel.style.display = "block"; - }); - - closeButton.addEventListener("click", () => { - controlPanel.style.display = "none"; - openButton.style.display = "block"; - }); -} diff --git a/src/ControlSection.ts b/src/ControlSection.ts deleted file mode 100644 index cc4d4f8..0000000 --- a/src/ControlSection.ts +++ /dev/null @@ -1,325 +0,0 @@ -export const storagePrefix = "nitro-play"; - -export type ControlSectionEntry = { - text: string; - id: string; -} & ( - | { - type: "checkbox"; - default: boolean; - update?: (value: boolean) => void; - } - | { - type: "slider"; - default: number; - min: number; - max: number; - integer?: boolean; - forceRange?: boolean; - update?: (value: number) => void; - } - | { - type: "minmax"; - default: [number, number]; - integer?: boolean; - update?: (value: [number, number]) => void; - } - | { - type: "select"; - default: string; - options: string[]; - update?: (value: string) => void; - } -); - -export class ControlSection { - constructor( - parent: HTMLElement, - title: string, - storageTag: string, - entries: ControlSectionEntry[] - ) { - this.parent = parent; - this.storageTag = storageTag; - - this.createGrid(title, entries); - } - - private parent: HTMLElement; - private storageTag: string; - private values: Map = new Map(); - - public get(id: string): any { - return this.values.get(id); - } - - private createGrid(title: string, entries: ControlSectionEntry[]) { - const groupDiv = document.createElement("div"); - - const titleElement = document.createElement("h3"); - titleElement.textContent = title; - groupDiv.appendChild(titleElement); - - const gridDiv = document.createElement("div"); - gridDiv.classList.add("controlGroupGrid"); - gridDiv.style.gridTemplateRows = `repeat(${entries.length + 1}, auto)`; - - for (let i = 0; i < entries.length; i++) { - this.createEntry(entries[i], gridDiv, i); - } - - groupDiv.appendChild(gridDiv); - this.parent.appendChild(groupDiv); - } - - private createEntry( - entry: ControlSectionEntry, - parent: HTMLElement, - index: number - ) { - const text = document.createElement("div"); - text.textContent = entry.text; - text.style.gridColumn = "1"; - text.style.gridRow = (index + 1).toString(); - parent.appendChild(text); - - const controlDiv = document.createElement("div"); - controlDiv.style.gridColumn = "2"; - controlDiv.style.gridRow = (index + 1).toString(); - controlDiv.classList.add("controlEntry"); - - parent.appendChild(controlDiv); - - function createResetButton(click: () => void) { - const resetButton = document.createElement("button"); - resetButton.textContent = "⟲"; - resetButton.onclick = click; - resetButton.style.gridColumn = "3"; - resetButton.style.gridRow = (index + 1).toString(); - parent.appendChild(resetButton); - return resetButton; - } - - const key = `${storagePrefix}_${this.storageTag}_${entry.id}`; - const storedValue = localStorage.getItem(key); - - function storeValue(value: string | number | boolean) { - localStorage.setItem(key, value.toString()); - } - - // This is bad code but I don't care - // We need to copy this so we can use it in the function - const thisObject = this; - function updateEntry(value: any) { - thisObject.values.set(entry.id, value); - - if (entry.update) { - entry.update(value as never); - } - } - - switch (entry.type) { - case "checkbox": { - const checkbox = document.createElement("input"); - - createResetButton(() => { - checkbox.checked = entry.default; - checkbox.dispatchEvent(new Event("change")); - }); - - checkbox.type = "checkbox"; - checkbox.checked = - storedValue !== null - ? storedValue === "true" - : entry.default; - checkbox.onchange = () => { - storeValue(checkbox.checked); - updateEntry(checkbox.checked); - }; - - controlDiv.appendChild(checkbox); - updateEntry( - storedValue !== null - ? storedValue === "true" - : entry.default - ); - break; - } - - case "slider": { - const slider = document.createElement("input"); - const numberInput = document.createElement("input"); - - createResetButton(() => { - numberInput.value = entry.default.toString(); - numberInput.dispatchEvent(new Event("input")); - }); - - slider.type = "range"; - slider.min = entry.min.toString(); - slider.max = entry.max.toString(); - slider.step = entry.integer - ? "1" - : ((entry.max - entry.min) / 100).toString(); - slider.value = - storedValue !== null - ? storedValue - : entry.default.toString(); - slider.oninput = () => { - let value = entry.integer - ? Math.round(slider.valueAsNumber) - : slider.valueAsNumber; - - if (entry.forceRange) { - if (value < entry.min) { - value = entry.min; - } else if (value > entry.max) { - value = entry.max; - } - } - - numberInput.valueAsNumber = value; - storeValue(value); - updateEntry(value); - }; - - numberInput.type = "number"; - numberInput.value = - storedValue !== null - ? storedValue - : entry.default.toString(); - numberInput.step = entry.integer ? "1" : "0.01"; - if (entry.forceRange) { - numberInput.min = entry.min.toString(); - numberInput.max = entry.max.toString(); - } - - numberInput.oninput = () => { - let value = entry.integer - ? Math.round(numberInput.valueAsNumber) - : numberInput.valueAsNumber; - - if (entry.forceRange) { - if (value < entry.min) { - value = entry.min; - numberInput.valueAsNumber = value; - } else if (value > entry.max) { - value = entry.max; - numberInput.valueAsNumber = value; - } - } - - slider.valueAsNumber = value; - storeValue(value); - updateEntry(value); - }; - - controlDiv.appendChild(slider); - controlDiv.appendChild(numberInput); - - updateEntry( - storedValue !== null ? +storedValue : entry.default - ); - - break; - } - - case "minmax": { - const minInput = document.createElement("input"); - const maxInput = document.createElement("input"); - - createResetButton(() => { - minInput.value = entry.default[0].toString(); - minInput.dispatchEvent(new Event("input")); - maxInput.value = entry.default[1].toString(); - maxInput.dispatchEvent(new Event("input")); - }); - - function oninput() { - // Thanks TypeScript (if we don't do this, entry.integer doesn't exist...) - if (entry.type !== "minmax") { - return; - } - - let value: [number, number]; - if (entry.integer) { - value = [ - Math.round(minInput.valueAsNumber), - Math.round(maxInput.valueAsNumber) - ]; - } else { - value = [ - minInput.valueAsNumber, - maxInput.valueAsNumber - ]; - } - - storeValue(JSON.stringify(value)); - updateEntry(value); - } - - minInput.type = "number"; - minInput.value = - storedValue !== null - ? JSON.parse(storedValue)[0] - : entry.default[0].toString(); - minInput.step = entry.integer ? "1" : "0.01"; - minInput.oninput = () => { - if (minInput.valueAsNumber > maxInput.valueAsNumber) { - maxInput.valueAsNumber = minInput.valueAsNumber; - } - oninput(); - }; - - maxInput.type = "number"; - maxInput.value = - storedValue !== null - ? JSON.parse(storedValue)[1] - : entry.default[1].toString(); - maxInput.step = entry.integer ? "1" : "0.01"; - maxInput.oninput = () => { - if (maxInput.valueAsNumber < minInput.valueAsNumber) { - minInput.valueAsNumber = maxInput.valueAsNumber; - } - oninput(); - }; - - controlDiv.appendChild(document.createTextNode("Min:")); - controlDiv.appendChild(minInput); - controlDiv.appendChild(document.createTextNode("Max:")); - controlDiv.appendChild(maxInput); - - oninput(); - - break; - } - - case "select": { - const select = document.createElement("select"); - - createResetButton(() => { - select.value = entry.default; - select.dispatchEvent(new Event("change")); - }); - - for (const option of entry.options) { - const optionElement = document.createElement("option"); - optionElement.value = option; - optionElement.textContent = option; - select.appendChild(optionElement); - } - - select.value = - storedValue !== null ? storedValue : entry.default; - select.onchange = () => { - storeValue(select.value); - updateEntry(select.value); - }; - - controlDiv.appendChild(select); - updateEntry(storedValue !== null ? storedValue : entry.default); - break; - } - } - } -} diff --git a/src/FileImportDialog.ts b/src/FileImportDialog.ts deleted file mode 100644 index 9aa0036..0000000 --- a/src/FileImportDialog.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as AudioWorkerComms from "./AudioWorkerComms"; - -let dialog: HTMLDialogElement; -let fileInput: HTMLInputElement; -let fileName: HTMLSpanElement; -let fileStatus: HTMLSpanElement; -let sdatSelect: HTMLSelectElement; -let sdatStatus: HTMLSpanElement; -let importButton: HTMLButtonElement; - -let buffer: ArrayBuffer | null; - -export function init(useSdatCallback: () => void) { - dialog = document.getElementById( - "ndsFileImportDialog" - ) as HTMLDialogElement; - fileInput = document.getElementById("ndsFileInput") as HTMLInputElement; - fileName = document.getElementById("ndsFileName") as HTMLSpanElement; - fileStatus = document.getElementById("ndsImportStatus") as HTMLSpanElement; - sdatSelect = document.getElementById("ndsSdatSelect") as HTMLSelectElement; - sdatStatus = document.getElementById("ndsSdatStatus") as HTMLSpanElement; - importButton = document.getElementById( - "ndsImportButton" - ) as HTMLButtonElement; - - fileInput.addEventListener("change", async () => { - fileName.textContent = fileInput.files![0].name; - sdatSelect.innerHTML = ""; - sdatSelect.disabled = true; - sdatStatus.textContent = ""; - importButton.disabled = true; - fileStatus.textContent = "⌛"; - - let possibleSdats: string[]; - try { - buffer = await fileInput.files![0].arrayBuffer(); - possibleSdats = await AudioWorkerComms.call("parseNds", buffer); - fileStatus.textContent = "✅"; - } catch (err) { - fileStatus.textContent = "❌"; - console.error(err); - buffer = null; - sdatSelect.disabled = true; - alert(err); - return; - } - - displaySdatOptions(possibleSdats); - sdatSelect.dispatchEvent(new Event("change")); - }); - - sdatSelect.addEventListener("change", async () => { - importButton.disabled = true; - sdatStatus.textContent = "⌛"; - const sdatPath = sdatSelect.value; - - try { - const numSequences = await AudioWorkerComms.call("checkSdat", { - rom: buffer, - path: sdatPath - }); - sdatStatus.textContent = `✅ ${numSequences} sequences`; - } catch (err) { - sdatStatus.textContent = "❌"; - console.error(err); - alert(err); - return; - } - - importButton.disabled = false; - }); - - importButton.addEventListener("click", async () => { - fileInput.disabled = true; - sdatSelect.disabled = true; - importButton.disabled = true; - - await AudioWorkerComms.call("useSdat", { - rom: buffer, - path: sdatSelect.value - }); - useSdatCallback(); - - dialog.close(); - }); -} - -export function show() { - fileInput.disabled = false; - dialog.showModal(); - - // Call the change event when there is already a file selected - if (fileInput.files && fileInput.files.length > 0) { - fileInput.dispatchEvent(new Event("change")); - } -} - -function displaySdatOptions(sdats: string[]) { - for (const sdat of sdats) { - const option = document.createElement("option"); - option.value = sdat; - option.textContent = sdat; - sdatSelect.appendChild(option); - } - - sdatSelect.disabled = false; -} diff --git a/src/PlaybackSection.ts b/src/PlaybackSection.ts deleted file mode 100644 index cd39232..0000000 --- a/src/PlaybackSection.ts +++ /dev/null @@ -1,166 +0,0 @@ -import * as AudioWorkerComms from "./AudioWorkerComms"; -import * as AudioPlayer from "./AudioPlayer"; -import { storagePrefix } from "./ControlSection"; - -const speaker0 = new URL("./assets/speaker0.svg", import.meta.url).href; -const speaker1 = new URL("./assets/speaker1.svg", import.meta.url).href; -const speaker2 = new URL("./assets/speaker2.svg", import.meta.url).href; -const speaker3 = new URL("./assets/speaker3.svg", import.meta.url).href; - -let seqLeftButton: HTMLButtonElement; -let seqSelect: HTMLSelectElement; -let seqRightButton: HTMLButtonElement; -let seqPlayButton: HTMLButtonElement; -let seqStopButton: HTMLButtonElement; -let speakerIcon: HTMLImageElement; -let volumeSlider: HTMLInputElement; - -let volumeBeforeMute = 0; - -export function init( - load: () => Promise, - play: () => void, - stop: () => void -) { - seqLeftButton = document.getElementById( - "seqLeftButton" - ) as HTMLButtonElement; - seqSelect = document.getElementById("seqSelect") as HTMLSelectElement; - seqRightButton = document.getElementById( - "seqRightButton" - ) as HTMLButtonElement; - seqPlayButton = document.getElementById( - "seqPlayButton" - ) as HTMLButtonElement; - seqStopButton = document.getElementById( - "seqStopButton" - ) as HTMLButtonElement; - speakerIcon = document.getElementById("speakerIcon") as HTMLImageElement; - volumeSlider = document.getElementById("volumeSlider") as HTMLInputElement; - - disable(); - - async function loadSeq() { - seqPlayButton.disabled = true; - seqStopButton.disabled = true; - seqPlayButton.disabled = true; - seqLeftButton.disabled = true; - seqSelect.disabled = true; - seqRightButton.disabled = true; - - stop(); - const seq = seqSelect.value; - await AudioWorkerComms.call("loadSeq", { name: seq }); - await load(); - - seqPlayButton.disabled = false; - seqSelect.disabled = false; - - if (seqSelect.selectedIndex === 0) { - seqLeftButton.disabled = true; - } else { - seqLeftButton.disabled = false; - } - - if (seqSelect.selectedIndex === seqSelect.options.length - 1) { - seqRightButton.disabled = true; - } else { - seqRightButton.disabled = false; - } - } - - seqLeftButton.addEventListener("click", () => { - seqSelect.selectedIndex = Math.max(0, seqSelect.selectedIndex - 1); - seqSelect.dispatchEvent(new Event("change")); - }); - - seqRightButton.addEventListener("click", () => { - seqSelect.selectedIndex = Math.min( - seqSelect.options.length - 1, - seqSelect.selectedIndex + 1 - ); - seqSelect.dispatchEvent(new Event("change")); - }); - - seqSelect.addEventListener("change", async () => { - await loadSeq(); - }); - - seqPlayButton.addEventListener("click", () => { - seqPlayButton.disabled = true; - seqLeftButton.disabled = true; - seqSelect.disabled = true; - seqRightButton.disabled = true; - - play(); - - document.title = `NitroPlay 🎵 ${seqSelect.value}`; - - seqStopButton.disabled = false; - }); - - seqStopButton.addEventListener("click", async () => { - document.title = "NitroPlay"; - stop(); - await loadSeq(); - }); - - volumeSlider.addEventListener("input", () => { - changeSpeakerIcon(); - localStorage.setItem(storagePrefix + "_volume", volumeSlider.value); - AudioPlayer.setVolume((volumeSlider.valueAsNumber / 100) * 10); - }); - - speakerIcon.addEventListener("click", () => { - if (volumeSlider.valueAsNumber === 0) { - volumeSlider.valueAsNumber = volumeBeforeMute; - } else { - volumeBeforeMute = volumeSlider.valueAsNumber; - volumeSlider.valueAsNumber = 0; - } - volumeSlider.dispatchEvent(new Event("input")); - }); - - const storedVolume = localStorage.getItem(storagePrefix + "_volume"); - volumeSlider.value = storedVolume !== null ? storedVolume : "100"; - volumeSlider.dispatchEvent(new Event("input")); -} - -export function enable(seqSymbols: string[]) { - seqLeftButton.disabled = false; - seqSelect.disabled = false; - seqRightButton.disabled = false; - - seqSelect.innerHTML = ""; - for (const symbol of seqSymbols) { - const option = document.createElement("option"); - option.value = symbol; - option.textContent = symbol; - seqSelect.appendChild(option); - } - - seqSelect.dispatchEvent(new Event("change")); -} - -export function disable() { - seqLeftButton.disabled = true; - seqSelect.disabled = true; - seqRightButton.disabled = true; - seqPlayButton.disabled = true; - seqStopButton.disabled = true; - - seqSelect.innerHTML = ""; -} - -function changeSpeakerIcon() { - const volume = volumeSlider.valueAsNumber; - if (volume === 0) { - speakerIcon.src = speaker0; - } else if (volume < 33) { - speakerIcon.src = speaker1; - } else if (volume < 66) { - speakerIcon.src = speaker2; - } else { - speakerIcon.src = speaker3; - } -} diff --git a/src/assets/arrow.svg b/src/assets/arrow.svg new file mode 100644 index 0000000..8033b6f --- /dev/null +++ b/src/assets/arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/AudioPlayer.ts b/src/core/AudioPlayer.ts similarity index 100% rename from src/AudioPlayer.ts rename to src/core/AudioPlayer.ts diff --git a/src/AudioWorker.ts b/src/core/AudioWorker.ts similarity index 100% rename from src/AudioWorker.ts rename to src/core/AudioWorker.ts diff --git a/src/AudioWorkerComms.ts b/src/core/AudioWorkerComms.ts similarity index 100% rename from src/AudioWorkerComms.ts rename to src/core/AudioWorkerComms.ts diff --git a/src/PianoRenderer.ts b/src/core/PianoRenderer.ts similarity index 98% rename from src/PianoRenderer.ts rename to src/core/PianoRenderer.ts index 6ff279a..c52ed43 100644 --- a/src/PianoRenderer.ts +++ b/src/core/PianoRenderer.ts @@ -1,5 +1,4 @@ import { Audio } from "nitro-fs"; -import { SynthState } from "./SynthState"; let pianoCanvas: HTMLCanvasElement; let pianoCtx: CanvasRenderingContext2D; diff --git a/src/Queue.ts b/src/core/Queue.ts similarity index 100% rename from src/Queue.ts rename to src/core/Queue.ts diff --git a/src/Renderer.ts b/src/core/Renderer.ts similarity index 100% rename from src/Renderer.ts rename to src/core/Renderer.ts diff --git a/src/StateManager.ts b/src/core/StateManager.ts similarity index 100% rename from src/StateManager.ts rename to src/core/StateManager.ts diff --git a/src/SynthState.ts b/src/core/SynthState.ts similarity index 100% rename from src/SynthState.ts rename to src/core/SynthState.ts diff --git a/src/TimelineRenderer.ts b/src/core/TimelineRenderer.ts similarity index 100% rename from src/TimelineRenderer.ts rename to src/core/TimelineRenderer.ts diff --git a/src/export/ExportDialog.ts b/src/export/ExportDialog.ts deleted file mode 100644 index d231b82..0000000 --- a/src/export/ExportDialog.ts +++ /dev/null @@ -1,178 +0,0 @@ -import * as AudioWorkerComms from "../AudioWorkerComms"; -import { ControlSection, ControlSectionEntry } from "../ControlSection"; -import * as ProgressStatus from "./ProgressStatus"; -import { exporters, prepareStreamExport } from "./ExportManager"; - -let exportDialog: HTMLDialogElement; -let exportConfigContainer: HTMLDivElement; -let exportStartContainer: HTMLDivElement; -let exportStartButton: HTMLAnchorElement; - -let commonControls: ControlSection; -const exportControls: (ControlSection | null)[] = []; -const exportControlSections: HTMLElement[] = []; - -const commonControlsSchema: ControlSectionEntry[] = [ - { - type: "select", - text: "Export As", - id: "exportAs", - default: "Wave", - options: exporters.map((e) => e.name), - update(value) { - const index = exporters.findIndex((e) => e.name === value); - - for (let i = 0; i < exportControls.length; i++) { - if (exportControls[i]) { - exportControlSections[i].style.display = - i === index ? "" : "none"; - } - } - } - }, - { - type: "slider", - text: "Sample Rate", - id: "sampleRate", - min: 2000, - max: 96000, - default: 48000 - }, - { - type: "slider", - text: "Seconds", - id: "seconds", - min: 1, - max: 600, - default: 60 - }, - { - type: "checkbox", - text: "Compress (as .gz)", - id: "compress", - default: true - } -]; - -export function init() { - exportDialog = document.getElementById("exportDialog") as HTMLDialogElement; - exportDialog.addEventListener("cancel", (e) => { - e.preventDefault(); - }); - - exportConfigContainer = document.getElementById( - "exportConfigContainer" - ) as HTMLDivElement; - exportStartContainer = document.getElementById( - "exportStartContainer" - ) as HTMLDivElement; - - document - .getElementById("exportCancelButton")! - .addEventListener("click", () => { - exportDialog.close(); - }); - - document - .getElementById("exportContinueButton")! - .addEventListener("click", async () => { - await continueExport(); - }); - - // Create the control sections for each exporter first so that they can be controlled by the common controls - const exportControlsSection = exportDialog.querySelector( - "#exportControls" - ) as HTMLElement; - for (const exporter of exporters) { - const section = document.createElement("section"); - exportControlsSection.appendChild(section); - - if (exporter.configSchema) { - const controls = new ControlSection( - section, - exporter.name, - "export_" + exporter.storageTag, - exporter.configSchema - ); - exportControls.push(controls); - } else { - exportControls.push(null); - } - - exportControlSections.push(section); - } - - const commonControlsSection = exportDialog.querySelector( - "#exportCommonControls" - ) as HTMLElement; - commonControls = new ControlSection( - commonControlsSection, - "Export", - "export_common", - commonControlsSchema - ); - - // Second part of the dialog - exportStartButton = document.getElementById( - "exportStartButton" - ) as HTMLAnchorElement; - ProgressStatus.init(); - - exportStartButton.addEventListener("click", () => { - exportStartButton.style.display = "none"; - ProgressStatus.show(); - }); -} - -export function showDialog() { - exportDialog.showModal(); - exportStartButton.setAttribute("disabled", "true"); - exportStartButton.style.display = ""; -} - -export function close() { - exportDialog.close(); - exportConfigContainer.style.display = ""; - exportStartContainer.style.display = "none"; - ProgressStatus.hide(); -} - -export function enableStartButton(url: string, filename: string) { - exportStartButton.removeAttribute("disabled"); - exportStartButton.href = url; - - // For some reason, setting the download attribute prevents the download from - // being intercepted by the service worker in Chromium. And not setting it does - // some weird stuff in Safari. So we only set it if we're not in Chromium. - const isChromium = "chrome" in window; - if (!isChromium) { - exportStartButton.download = filename; - } - - exportStartButton.style.display = ""; -} - -async function continueExport() { - exportConfigContainer.style.display = "none"; - exportStartContainer.style.display = "flex"; - - const exporterName = commonControls.get("exportAs"); - const exporterIndex = exporters.findIndex((e) => e.name === exporterName); - - const seqName = await AudioWorkerComms.call("getCurrentSeqSymbol"); - const sampleRate = commonControls.get("sampleRate"); - const seconds = commonControls.get("seconds"); - const compress = commonControls.get("compress"); - const configSection = exportControls[exporterIndex]; - - ProgressStatus.reset(seconds); - - await prepareStreamExport( - exporterIndex, - sampleRate, - seconds, - compress, - seqName, - configSection - ); -} diff --git a/src/export/ExportManager.ts b/src/export/ExportManager.ts index f2a8f61..374333b 100644 --- a/src/export/ExportManager.ts +++ b/src/export/ExportManager.ts @@ -1,24 +1,10 @@ import { StreamSourceController } from "./Exporter"; import * as ServiceWorkerComms from "../ServiceWorkerComms"; -import * as ExportDialog from "./ExportDialog"; -import * as ProgressStatus from "./ProgressStatus"; -import { ControlSection } from "../ControlSection"; import { waveExporter } from "./wave/WaveExporter"; import { wavePerChannelExporter } from "./wavePerChannel/WavePerChannelExporter"; export const exporters = [waveExporter, wavePerChannelExporter]; -export function init() { - ServiceWorkerComms.on("streamReady", (data) => { - ExportDialog.enableStartButton(data.data.url, data.data.filename); - - ServiceWorkerComms.send("callResponse", { - id: data.id, - data: null - }); - }); -} - // TODO: This whole ServiceWorker stuff could be replaced with the // File System Access API (which is only available in Chromium). // Maybe check if it's available and use it instead. @@ -30,13 +16,14 @@ export async function prepareStreamExport( seconds: number, compress: boolean, seqName: string, - configSection: ControlSection | null + onReady: (url: string, filename: string) => void, + onProgress: (amount: number) => void, + onClose: () => void ) { const stream = exporters[exporterIndex].getStream( sampleRate, seconds, - ProgressStatus.update, - configSection + onProgress ); await ServiceWorkerComms.ready; @@ -49,7 +36,7 @@ export async function prepareStreamExport( queue.push(buf); }, close() { - ExportDialog.close(); + onClose(); shouldClose = true; } }; @@ -74,7 +61,7 @@ export async function prepareStreamExport( queue.push(buf); }, close() { - ExportDialog.close(); + onClose(); shouldClose = true; } }; @@ -97,7 +84,7 @@ export async function prepareStreamExport( ServiceWorkerComms.off("streamPull"); ServiceWorkerComms.off("streamCancel"); - ExportDialog.close(); + onClose(); ServiceWorkerComms.send("callResponse", { id: callData.id, @@ -110,6 +97,17 @@ export async function prepareStreamExport( filename += ".gz"; } + ServiceWorkerComms.on("streamReady", (data) => { + ServiceWorkerComms.off("streamReady"); + + onReady(data.data.url, data.data.filename); + + ServiceWorkerComms.send("callResponse", { + id: data.id, + data: null + }); + }); + ServiceWorkerComms.send("setStream", { filename, compress, diff --git a/src/export/Exporter.ts b/src/export/Exporter.ts index f2cacbe..1b623ef 100644 --- a/src/export/Exporter.ts +++ b/src/export/Exporter.ts @@ -1,5 +1,3 @@ -import { ControlSection, ControlSectionEntry } from "../ControlSection"; - // The better way to do this would be to just send a ReadableStream to the service worker, // which it then downloads. But since Safari doesn't support transfering ReadableStreams, // we have to do it like this, unfortunately. @@ -20,11 +18,9 @@ export interface Exporter { storageTag: string; mimeType: string; fileExtension: string; - configSchema?: ControlSectionEntry[]; getStream( sampleRate: number, seconds: number, - onProgress: (amount: number) => void, - config: ControlSection | null + onProgress: (amount: number) => void ): StreamSource; } diff --git a/src/export/ProgressStatus.ts b/src/export/ProgressStatus.ts deleted file mode 100644 index 14489e7..0000000 --- a/src/export/ProgressStatus.ts +++ /dev/null @@ -1,43 +0,0 @@ -let parentDiv: HTMLDivElement; -let progressBar: HTMLProgressElement; -let progressPercentage: HTMLSpanElement; -let progressAbsolute: HTMLSpanElement; - -let fullLength: number; - -export function init() { - parentDiv = document.getElementById("exportProgress") as HTMLDivElement; - progressBar = document.getElementById( - "exportProgressBar" - ) as HTMLProgressElement; - progressPercentage = document.getElementById( - "exportProgressPercentage" - ) as HTMLSpanElement; - progressAbsolute = document.getElementById( - "exportProgressAbsolute" - ) as HTMLSpanElement; - - hide(); -} - -export function show() { - parentDiv.style.display = ""; -} - -export function hide() { - parentDiv.style.display = "none"; -} - -export function reset(_fullLength: number) { - fullLength = _fullLength; - progressBar.value = 0; - progressPercentage.textContent = "0%"; - progressAbsolute.textContent = "0s / " + fullLength.toFixed(2) + "s"; -} - -export function update(amount: number) { - progressBar.value = amount; - progressPercentage.textContent = (amount * 100).toFixed(2) + "%"; - progressAbsolute.textContent = - (amount * fullLength).toFixed(2) + "s / " + fullLength.toFixed(2) + "s"; -} diff --git a/src/export/wave/WaveExporter.ts b/src/export/wave/WaveExporter.ts index 6318225..2daee63 100644 --- a/src/export/wave/WaveExporter.ts +++ b/src/export/wave/WaveExporter.ts @@ -1,5 +1,5 @@ import { Exporter, StreamSource } from "../Exporter"; -import * as AudioWorkerComms from "../../AudioWorkerComms"; +import * as AudioWorkerComms from "../../core/AudioWorkerComms"; import * as WaveFile from "./WaveFile"; export const waveExporter: Exporter = { @@ -7,7 +7,7 @@ export const waveExporter: Exporter = { storageTag: "wave", mimeType: "audio/wav", fileExtension: "wav", - getStream(sampleRate, seconds, onProgress, config) { + getStream(sampleRate, seconds, onProgress) { const numSamples = Math.floor(sampleRate * seconds); let i = 0; diff --git a/src/export/wavePerChannel/WavePerChannelExporter.ts b/src/export/wavePerChannel/WavePerChannelExporter.ts index 3e582f5..b12c6bd 100644 --- a/src/export/wavePerChannel/WavePerChannelExporter.ts +++ b/src/export/wavePerChannel/WavePerChannelExporter.ts @@ -1,5 +1,5 @@ import { Exporter, StreamSource } from "../Exporter"; -import * as AudioWorkerComms from "../../AudioWorkerComms"; +import * as AudioWorkerComms from "../../core/AudioWorkerComms"; import * as WaveFile from "../wave/WaveFile"; import { TarFile } from "./TarFile"; @@ -18,7 +18,7 @@ export const wavePerChannelExporter: Exporter = { storageTag: "wavePerChannel", mimeType: "application/x-tar", fileExtension: "tar", - getStream(sampleRate, seconds, onProgress, config) { + getStream(sampleRate, seconds, onProgress) { const numSamples = Math.floor(sampleRate * seconds); let i = 0; let tracksToExport: number[]; diff --git a/src/index.html b/src/index.html index 88473c4..4417e72 100644 --- a/src/index.html +++ b/src/index.html @@ -4,91 +4,11 @@ NitroPlay - + - Open Controls -
- - -
-

File

- - - - -
- - - - -
- - - - - -
- - -
-
-
-
-
-
- - -
-
- -
- Start Export -
- - - -
-
-
-
- -
-

Playback

-
- - - -
- -
- - -
- -
- Volume - -
-
- -
-

Configuration

-
-
+
diff --git a/src/style.css b/src/style.css deleted file mode 100644 index 4979af3..0000000 --- a/src/style.css +++ /dev/null @@ -1,278 +0,0 @@ -body { - background-color: black; - color: rgb(230, 230, 230); - font-family: sans-serif; - margin: 0; - overflow: hidden; - height: 100%; -} - -dialog { - background-color: rgb(40, 40, 40); - border: 1px solid rgb(80, 80, 80); - border-radius: 4px; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); - color: rgb(230, 230, 230); -} - -dialog > * { - margin: 8px; -} - -dialog::backdrop { - background-color: rgba(0, 0, 0, 0.6); -} - -#controlPanel { - position: absolute; - z-index: 100; - top: 20px; - left: 20px; - background-color: rgb(40, 40, 40); - border: 1px solid rgb(80, 80, 80); - padding: 12px; - border-radius: 8px; - overflow: auto; - height: 50%; -} - -#controlPanel > * { - margin-bottom: 16px; -} - -#controlPanel > *:last-child { - margin-bottom: 0; -} - -#openButton { - position: absolute; - top: 32px; - left: 32px; - width: 40px; - height: 40px; - z-index: 100; - cursor: pointer; -} - -#closeButton { - position: absolute; - top: 12px; - right: 12px; -} - -section { - display: flex; - flex-direction: column; - gap: 8px; -} - -h2, -h3 { - margin: 0; - padding-bottom: 8px; -} - -button, -#exportStartButton { - background-color: rgb(80, 80, 80); - border: 1px solid rgb(80, 80, 80); - border-radius: 4px; - color: rgb(230, 230, 230); - cursor: pointer; - font-size: 16px; - padding: 8px; -} - -button:hover, -#exportStartButton:hover { - background-color: rgb(100, 100, 100); -} - -button:active, -#exportStartButton { - background-color: rgb(60, 60, 60); -} - -button:disabled, -#exportStartButton[disabled="true"] { - background-color: rgb(60, 60, 60); - color: rgb(150, 150, 150); - cursor: default; -} - -#exportStartButton { - text-decoration: none; -} - -select { - background-color: rgb(80, 80, 80); - border: 1px solid rgb(80, 80, 80); - border-radius: 4px; - color: rgb(230, 230, 230); - font-size: 16px; - padding: 8px; - cursor: pointer; -} - -select:hover { - background-color: rgb(100, 100, 100); -} - -select:active { - background-color: rgb(60, 60, 60); -} - -select:disabled { - background-color: rgb(60, 60, 60); - color: rgb(150, 150, 150); - cursor: default; -} - -span { - display: inline-block; - margin: 4px; -} - -#importForm > input { - display: none; -} - -#importForm > label { - background-color: rgb(80, 80, 80); - border: 1px solid rgb(80, 80, 80); - border-radius: 4px; - color: rgb(230, 230, 230); - cursor: pointer; - font-size: 16px; - padding: 8px; -} - -#importForm > label:hover { - background-color: rgb(100, 100, 100); -} - -#importForm > label:active { - background-color: rgb(60, 60, 60); -} - -#ndsImportButton { - float: right; -} - -#exportConfigContainer { - min-width: 600px; - display: flex; - flex-direction: column; - justify-content: space-between; -} - -#exportControls { - overflow-y: scroll; -} - -#exportMenuButtons { - display: flex; - justify-content: space-between; -} - -#exportStartContainer { - display: none; - justify-content: center; -} - -#exportProgress { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -progress { - width: 100%; - height: 20px; - padding-bottom: 8px; -} - -.playbackControls { - display: flex; - align-items: center; - justify-content: start; - gap: 4px; -} - -.volumeBar { - display: flex; - align-items: center; - justify-content: space-between; -} - -.volumeBar > * { - margin-right: 8px; - white-space: nowrap; -} - -.volumeBar > *:last-child { - margin-right: 0; -} - -#speakerIcon { - height: 40px; - width: 40px; - cursor: pointer; -} - -.controlGroupGrid { - display: grid; - grid-template-columns: 1fr 2fr auto; - align-items: center; - grid-gap: 4px; -} - -.controlEntry { - width: 100%; - display: flex; - flex-direction: row; - align-items: center; - justify-content: right; - gap: 4px; -} - -input[type="number"] { - background-color: rgb(80, 80, 80); - border: 1px solid rgb(80, 80, 80); - border-radius: 4px; - color: rgb(230, 230, 230); - font-size: 16px; - padding: 8px; - width: 80px; -} - -input[type="range"] { - cursor: pointer; - width: 100%; -} - -input[type="checkbox"] { - cursor: pointer; -} - -#viewer { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -canvas { - position: absolute; - left: 0%; -} - -#pianoCanvas { - z-index: 5; -} - -#pianoOverlayCanvas { - z-index: 10; -} diff --git a/src/ui/Collapsible.tsx b/src/ui/Collapsible.tsx new file mode 100644 index 0000000..1680b52 --- /dev/null +++ b/src/ui/Collapsible.tsx @@ -0,0 +1,42 @@ +import { useState } from "react"; +import * as classes from "./styles/Collapsible.module.css"; + +// Thanks Luna for this snippet! https://github.com/DJLuna441 + +export function Collapsible({ title, children }) { + const [isCollapsed, setCollapsed] = useState(true); + + return ( +
+

setCollapsed(!isCollapsed)} + className={classes.title} + > + + + + {title} +

+
+
+
+ {children} +
+
+
+
+ ); +} diff --git a/src/ui/ConfigGrid.tsx b/src/ui/ConfigGrid.tsx new file mode 100644 index 0000000..ac493a2 --- /dev/null +++ b/src/ui/ConfigGrid.tsx @@ -0,0 +1,183 @@ +import { useEffect } from "react"; +import { useStorage } from "./Helpers"; +import * as classes from "./styles/ConfigGrid.module.css"; +import { NumberSpinner } from "./NumberSpinner"; + +export function ConfigGrid({ children }) { + return
{children}
; +} + +export function ConfigGridItem({ label, children, onReset }) { + return ( + <> +
{label}
+
{children}
+ + + ); +} + +export function ConfigGridCheckbox({ + label, + storageTag, + defaultValue, + onChange +}) { + const [checked, setChecked] = useStorage(storageTag, defaultValue); + + useEffect(() => { + onChange(checked); + }, [checked]); + + return ( + { + setChecked(defaultValue); + }} + > + { + setChecked(e.target.checked); + }} + /> + + ); +} + +export function ConfigGridNumber({ + label, + storageTag, + defaultValue, + min, + max, + step, + forceRange, + onChange +}) { + const [value, setValue] = useStorage(storageTag, defaultValue); + + useEffect(() => { + onChange(value); + }, [value]); + + useEffect(() => { + if (forceRange) { + setValue(Math.min(max, Math.max(min, value))); + } + }, [min, max]); + + return ( + { + setValue(defaultValue); + }} + > + setValue(parseFloat(e.target.value))} + /> + { + if (forceRange) { + value = Math.min(max, Math.max(min, value)); + } + + setValue(value); + }} + /> + + ); +} + +export function ConfigGridMinMax({ + label, + storageTag, + defaultValue, + step, + onChange +}) { + const [value, setValue] = useStorage<[number, number]>( + storageTag, + defaultValue + ); + + useEffect(() => { + onChange(value); + }, [value]); + + return ( + { + setValue(defaultValue); + }} + > + Min: + { + if (n > value[1]) { + setValue([n, n]); + } else { + setValue([n, value[1]]); + } + }} + step={step} + /> + Max: + { + if (n < value[0]) { + setValue([n, n]); + } else { + setValue([value[0], n]); + } + }} + step={step} + /> + + ); +} + +export function ConfigGridSelect({ + label, + storageTag, + defaultValue, + options, + onChange +}) { + const [value, setValue] = useStorage(storageTag, defaultValue); + + useEffect(() => { + onChange(value); + }, [value]); + + return ( + { + setValue(defaultValue); + }} + > + + + ); +} diff --git a/src/ui/ConfigSection.tsx b/src/ui/ConfigSection.tsx new file mode 100644 index 0000000..0737691 --- /dev/null +++ b/src/ui/ConfigSection.tsx @@ -0,0 +1,63 @@ +import { + ConfigGrid, + ConfigGridCheckbox, + ConfigGridMinMax, + ConfigGridNumber +} from "./ConfigGrid"; +import * as Renderer from "../core/Renderer"; +import { Collapsible } from "./Collapsible"; + +export function ConfigSection() { + return ( + <> +

Configuration

+
+ + + + Renderer.alignNotesToPiano(checked) + } + /> + + Renderer.setPianoPosition(value) + } + /> + + Renderer.setPianoHeight(value) + } + /> + + Renderer.setPianoRange(value) + } + /> + + +
+ + ); +} diff --git a/src/ui/ControlPanel.tsx b/src/ui/ControlPanel.tsx new file mode 100644 index 0000000..96a877e --- /dev/null +++ b/src/ui/ControlPanel.tsx @@ -0,0 +1,72 @@ +import { useState } from "react"; +import * as panelClasses from "./styles/Panel.module.css"; +import * as classes from "./styles/ControlPanel.module.css"; +import { FileSection } from "./FileSection"; +import { PlaybackSection } from "./PlaybackSection"; +import * as AudioWorkerComms from "../core/AudioWorkerComms"; +import { ConfigSection } from "./ConfigSection"; + +export function ControlPanel({ show, load, start, onClose }) { + const [seqs, setSeqs] = useState([]); + const [selectedIndex, setSelectedIndex] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); + + return ( +
+ +
+ 0} + fileChosen={() => { + setSeqs([]); + + AudioWorkerComms.call("getSeqSymbols").then((s) => { + setSeqs(s); + setSelectedIndex(0); + setIsLoading(true); + load(s[0]).then(() => { + setIsLoading(false); + }); + }); + }} + /> +
+
+ { + setSelectedIndex(index); + setIsLoading(true); + load(seqs[index]).then(() => { + setIsLoading(false); + }); + }} + onPlay={() => { + setIsPlaying(true); + start(seqs[selectedIndex]); + }} + onStop={() => { + setIsPlaying(false); + // Loading stops playback + setIsLoading(true); + load(seqs[selectedIndex]).then(() => { + setIsLoading(false); + }); + }} + /> +
+
+ +
+
+ ); +} diff --git a/src/ui/Dialog.tsx b/src/ui/Dialog.tsx new file mode 100644 index 0000000..d7454be --- /dev/null +++ b/src/ui/Dialog.tsx @@ -0,0 +1,25 @@ +import { useEffect, useRef } from "react"; +import * as panelClasses from "./styles/Panel.module.css"; +import * as classes from "./styles/Dialog.module.css"; + +export function Dialog({ show, children }) { + const dialog = useRef(); + + useEffect(() => { + if (show) { + dialog.current.showModal(); + } else { + dialog.current.close(); + } + }, [show]); + + return ( + e.preventDefault()} + > + {children} + + ); +} diff --git a/src/ui/ExportButton.tsx b/src/ui/ExportButton.tsx new file mode 100644 index 0000000..734c915 --- /dev/null +++ b/src/ui/ExportButton.tsx @@ -0,0 +1,147 @@ +import { useState } from "react"; +import { Dialog } from "./Dialog"; +import { + ConfigGrid, + ConfigGridCheckbox, + ConfigGridNumber, + ConfigGridSelect +} from "./ConfigGrid"; +import * as classes from "./styles/ExportButton.module.css"; +import * as ExportManager from "../export/ExportManager"; +import * as AudioWorkerComms from "../core/AudioWorkerComms"; + +export function ExportButton({ disabled }) { + const [showDialog, setShowDialog] = useState(false); + const [page, setPage] = useState<"config" | "start" | "progress">("config"); + const [url, setUrl] = useState(null); + const [filename, setFilename] = useState(null); + const [progress, setProgress] = useState(0); + + return ( + <> + + + {page === "config" ? ( + { + setUrl(url); + setFilename(filename); + setPage("start"); + }} + onProgress={(amount) => setProgress(amount)} + onClose={() => setShowDialog(false)} + /> + ) : page === "start" ? ( + setPage("progress")} + > + Start Export + + ) : ( +
+ + {(progress * 100).toFixed(2)}% +
+ )} +
+ + ); +} + +function ConfigPage({ onReady, onProgress, onClose }) { + const [exportAs, setExportAs] = useState("Wave"); + const [sampleRate, setSampleRate] = useState(48000); + const [seconds, setSeconds] = useState(60); + const [compress, setCompress] = useState(false); + const [exportLoading, setExportLoading] = useState(false); + + return ( + <> + + e.name)} + onChange={(value: string) => setExportAs(value)} + /> + setSampleRate(value)} + /> + setSeconds(value)} + /> + setCompress(checked)} + /> + +
+ + +
+ + ); +} diff --git a/src/ui/FileSection.tsx b/src/ui/FileSection.tsx new file mode 100644 index 0000000..fded455 --- /dev/null +++ b/src/ui/FileSection.tsx @@ -0,0 +1,12 @@ +import { ExportButton } from "./ExportButton"; +import { NDSImportButton } from "./NDSImportButton"; + +export function FileSection({ disabled, seqLoaded, fileChosen }) { + return ( + <> +

File

+ + + + ); +} diff --git a/src/ui/Helpers.ts b/src/ui/Helpers.ts new file mode 100644 index 0000000..2cc3ce2 --- /dev/null +++ b/src/ui/Helpers.ts @@ -0,0 +1,20 @@ +import { useState, useEffect } from "react"; + +export const storagePrefix = "nitro-play"; + +export function useStorage( + key: string, + defaultValue: any +): [T, React.Dispatch>] { + const k = `${storagePrefix}_${key}`; + const [value, setValue] = useState(() => { + const storedValue = localStorage.getItem(k); + return storedValue !== null ? JSON.parse(storedValue) : defaultValue; + }); + + useEffect(() => { + localStorage.setItem(k, JSON.stringify(value)); + }, [key, value]); + + return [value, setValue]; +} diff --git a/src/ui/NDSImportButton.tsx b/src/ui/NDSImportButton.tsx new file mode 100644 index 0000000..11bee4f --- /dev/null +++ b/src/ui/NDSImportButton.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from "react"; +import { Dialog } from "./Dialog"; +import * as classes from "./styles/NDSImportButton.module.css"; +import * as AudioWorkerComms from "../core/AudioWorkerComms"; + +export function NDSImportButton({ disabled, fileChosen }) { + const [show, setShow] = useState(false); + const [file, setFile] = useState(null); + const [fileStatus, setFileStatus] = useState(""); + const [possibleSdats, setPossibleSdats] = useState([]); + const [selectedSdat, setSelectedSdat] = useState(null); + const [sdatStatus, setSdatStatus] = useState(""); + const [sdatIsImportable, setSdatIsImportable] = useState(false); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (!file) { + setFileStatus(""); + return; + } + + setSelectedSdat(null); + setFileStatus("⌛"); + setLoading(true); + + file.arrayBuffer().then((buffer) => { + AudioWorkerComms.call("parseNds", buffer) + .then((possibleSdats) => { + setFileStatus(`✅ ${possibleSdats.length} SDATs found`); + setPossibleSdats(possibleSdats); + setSelectedSdat(possibleSdats[0]); + setLoading(false); + }) + .catch((err) => { + console.error(err); + setFileStatus(`❌ Error: ${err.message}`); + setPossibleSdats([]); + setSelectedSdat(null); + setLoading(false); + }); + }); + }, [file]); + + useEffect(() => { + setSdatIsImportable(false); + setSdatStatus(""); + if (!selectedSdat) { + return; + } + + setSdatStatus("⌛"); + setLoading(true); + + file.arrayBuffer().then((buffer) => { + AudioWorkerComms.call("checkSdat", { + rom: buffer, + path: selectedSdat + }) + .then((numSequences) => { + setSdatStatus(`✅ ${numSequences} sequences`); + setSdatIsImportable(true); + setLoading(false); + }) + .catch((err) => { + console.error(err); + setSdatStatus(`❌ Error: ${err.message}`); + setLoading(false); + }); + }); + }, [selectedSdat]); + + return ( + <> + + +
+
+ + { + if (e.target.files.length > 0) { + setFile(e.target.files[0]); + } + }} + > + {file ? ( + <> + {file.name} + {fileStatus} + + ) : null} +
+ +
+ + {sdatStatus} + +
+
+
+ + ); +} diff --git a/src/ui/NumberSpinner.tsx b/src/ui/NumberSpinner.tsx new file mode 100644 index 0000000..4629c3f --- /dev/null +++ b/src/ui/NumberSpinner.tsx @@ -0,0 +1,13 @@ +import * as classes from "./styles/NumberSpinner.module.css"; + +export function NumberSpinner({ value, step, onChange }) { + return ( + onChange(parseFloat(e.target.value))} + > + ); +} diff --git a/src/ui/PlaybackSection.tsx b/src/ui/PlaybackSection.tsx new file mode 100644 index 0000000..73f7906 --- /dev/null +++ b/src/ui/PlaybackSection.tsx @@ -0,0 +1,114 @@ +import { useEffect, useState } from "react"; +import * as classes from "./styles/PlaybackSection.module.css"; +import * as AudioPlayer from "../core/AudioPlayer"; +import { useStorage } from "./Helpers"; + +export function PlaybackSection({ + loading, + seqs, + selectedIndex, + onSelect, + onPlay, + onStop +}) { + const [volume, setVolume] = useStorage("volume", 30); + const [isMuted, setIsMuted] = useStorage("isMuted", false); + const [isPlaying, setIsPlaying] = useState(false); + const hasSeq = seqs.length > 0; + + useEffect(() => { + if (isMuted) { + AudioPlayer.setVolume(0); + } else { + AudioPlayer.setVolume(volume / 10); + } + }, [volume, isMuted]); + + return ( + <> +

Playback

+
+ + + +
+
+ + +
+ +
+ setIsMuted(!isMuted)} + /> + { + setIsMuted(false); + setVolume(parseInt(e.target.value)); + }} + /> +
+ + ); +} + +function getSpeakerIcon(volume: number) { + if (volume === 0) { + return new URL("../assets/speaker0.svg", import.meta.url).href; + } else if (volume < 33) { + return new URL("../assets/speaker1.svg", import.meta.url).href; + } else if (volume < 66) { + return new URL("../assets/speaker2.svg", import.meta.url).href; + } else { + return new URL("../assets/speaker3.svg", import.meta.url).href; + } +} diff --git a/src/ui/ReactRoot.tsx b/src/ui/ReactRoot.tsx new file mode 100644 index 0000000..d8324dd --- /dev/null +++ b/src/ui/ReactRoot.tsx @@ -0,0 +1,31 @@ +import { useState } from "react"; +import { createRoot } from "react-dom/client"; +import { ControlPanel } from "./ControlPanel"; +import * as classes from "./styles/ControlPanel.module.css"; + +export function init(load: (seq: string) => void, start: () => void) { + const reactRoot = createRoot(document.getElementById("reactRoot")!); + reactRoot.render(); +} + +function Root({ load, start }) { + const [controlPanelOpen, setControlPanelOpen] = useState(true); + + return ( + <> + setControlPanelOpen(false)} + /> + {!controlPanelOpen && ( + setControlPanelOpen(true)} + > + )} + + ); +} diff --git a/src/ui/styles/Collapsible.module.css b/src/ui/styles/Collapsible.module.css new file mode 100644 index 0000000..b574efd --- /dev/null +++ b/src/ui/styles/Collapsible.module.css @@ -0,0 +1,43 @@ +.collapsibleSection { + display: flex; + flex-direction: column; + gap: 8px; +} + +.title { + padding: 0px 0px 8px 0px; + margin: 0px; + cursor: pointer; + display: flex; + align-content: center; + gap: 8px; + user-select: none; +} + +.wrapper { + display: flex; +} + +.wrapper .collapsible { + transition: all 0.3s ease; + max-height: 0; + overflow: hidden; +} + +.wrapper .collapsed { + max-height: 100%; +} + +.arrow { + transform: rotate(0deg); + width: 21px; + height: 15px; + transition: all 0.3s ease; +} + +.arrowCollapsed { + transform: rotate(180deg); + width: 21px; + height: 15px; + transition: all 0.3s ease; +} diff --git a/src/ui/styles/ConfigGrid.module.css b/src/ui/styles/ConfigGrid.module.css new file mode 100644 index 0000000..d67e1e5 --- /dev/null +++ b/src/ui/styles/ConfigGrid.module.css @@ -0,0 +1,17 @@ +.grid { + display: grid; + grid-template-columns: 1fr 2fr auto; + gap: 4px; + align-items: center; +} + +.gridItem { + display: flex; + align-items: center; + gap: 4px; + justify-content: flex-end; +} + +.slider { + width: 100%; +} diff --git a/src/ui/styles/ControlPanel.module.css b/src/ui/styles/ControlPanel.module.css new file mode 100644 index 0000000..52b19d0 --- /dev/null +++ b/src/ui/styles/ControlPanel.module.css @@ -0,0 +1,29 @@ +.openButton { + position: absolute; + top: 32px; + left: 32px; + width: 40px; + height: 40px; + cursor: pointer; +} + +.closeButton { + position: absolute; + top: 20px; + right: 20px; + box-shadow: 0 0 4px black; +} + +.controlPanel { + display: flex; + flex-direction: column; + gap: 16px; + height: 50%; + overflow: auto; +} + +.section { + display: flex; + flex-direction: column; + gap: 8px; +} diff --git a/src/ui/styles/Dialog.module.css b/src/ui/styles/Dialog.module.css new file mode 100644 index 0000000..d73da4a --- /dev/null +++ b/src/ui/styles/Dialog.module.css @@ -0,0 +1,11 @@ +.dialog { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0; +} + +.dialog::backdrop { + background-color: rgba(0, 0, 0, 0.6); +} diff --git a/src/ui/styles/ExportButton.module.css b/src/ui/styles/ExportButton.module.css new file mode 100644 index 0000000..f6f24cd --- /dev/null +++ b/src/ui/styles/ExportButton.module.css @@ -0,0 +1,23 @@ +.menuButtons { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 8px; +} + +.startButton { + text-decoration: none; +} + +.progressContainer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.progress { + width: 200px; + height: 20px; + padding-bottom: 8px; +} diff --git a/src/ui/styles/Global.css b/src/ui/styles/Global.css new file mode 100644 index 0000000..43d46e9 --- /dev/null +++ b/src/ui/styles/Global.css @@ -0,0 +1,77 @@ +body { + background-color: black; + color: rgb(230, 230, 230); + font-family: sans-serif; + margin: 0; + overflow: hidden; + height: 100%; +} + +#reactRoot { + z-index: 100; + position: absolute; + top: 0; + left: 0; + height: 100%; +} + +#viewer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +canvas { + position: absolute; + left: 0%; +} + +#pianoCanvas { + z-index: 5; +} + +#pianoOverlayCanvas { + z-index: 10; +} + +button, +select, +.button { + background-color: rgb(80, 80, 80); + border: 1px solid rgb(80, 80, 80); + border-radius: 4px; + color: rgb(230, 230, 230); + font-size: 16px; + padding: 8px; + cursor: pointer; + user-select: none; +} + +button:hover, +select:hover, +.button:hover { + background-color: rgb(100, 100, 100); +} + +button:active, +select:active, +.button:active { + background-color: rgb(60, 60, 60); +} + +button:disabled, +select:disabled, +.button:disabled, +.button.disabled { + background-color: rgb(60, 60, 60); + color: rgb(150, 150, 150); + cursor: default; +} + +h1, +h2, +h3 { + margin: 0px 0px 8px 0px; +} diff --git a/src/ui/styles/NDSImportButton.module.css b/src/ui/styles/NDSImportButton.module.css new file mode 100644 index 0000000..56d15e4 --- /dev/null +++ b/src/ui/styles/NDSImportButton.module.css @@ -0,0 +1,17 @@ +.container { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.input { + display: none; +} diff --git a/src/ui/styles/NumberSpinner.module.css b/src/ui/styles/NumberSpinner.module.css new file mode 100644 index 0000000..2e2bac9 --- /dev/null +++ b/src/ui/styles/NumberSpinner.module.css @@ -0,0 +1,9 @@ +.input { + color: #e6e6e6; + background-color: #505050; + border: 1px solid #505050; + border-radius: 4px; + width: 80px; + padding: 8px; + font-size: 16px; +} diff --git a/src/ui/styles/Panel.module.css b/src/ui/styles/Panel.module.css new file mode 100644 index 0000000..e4f5ea5 --- /dev/null +++ b/src/ui/styles/Panel.module.css @@ -0,0 +1,10 @@ +.panel { + color: rgb(230, 230, 230); + background-color: rgb(40, 40, 40); + box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); + border: 1px solid rgb(80, 80, 80); + border-radius: 8px; + padding: 12px; + margin: 8px; + width: max-content; +} diff --git a/src/ui/styles/PlaybackSection.module.css b/src/ui/styles/PlaybackSection.module.css new file mode 100644 index 0000000..8cdaf92 --- /dev/null +++ b/src/ui/styles/PlaybackSection.module.css @@ -0,0 +1,22 @@ +.controlRow { + display: flex; + align-items: center; + gap: 4px; +} + +.volumeBarContainer { + display: flex; + align-items: center; + justify-content: space-between; +} + +.volumeIcon { + width: 40px; + height: 40px; + cursor: pointer; +} + +.volumeBar { + width: 100%; + cursor: pointer; +} diff --git a/tsconfig.json b/tsconfig.json index b6374ca..9e91927 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,15 @@ { "compilerOptions": { - "lib": ["ES2015", "WebWorker"] - }, - "include": ["src/AudioWorker.ts", "src/ServiceWorker.ts"] + "lib": ["ES2015", "DOM", "WebWorker"], + "module": "ES2020", + "target": "ES2015", + "plugins": [ + { + "name": "typescript-plugin-css-modules" + } + ], + "jsx": "preserve", + "esModuleInterop": true, + "moduleResolution": "Node" + } }