diff --git a/package-lock.json b/package-lock.json index e6e7fc3..14295d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,12 +34,12 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@vitejs/plugin-react": "6.1.1", - "eslint": "^10.11.0", + "eslint": "^10.12.0", "eslint-plugin-react-hooks": "^7.1.1", - "globals": "^17.12.0", + "globals": "^17.13.0", "qrcode": "^1.5.4", - "vite": "^8.3.1", - "vite-plugin-pwa": "^1.3.0" + "vite": "^8.3.2", + "vite-plugin-pwa": "^2.0.0" } }, "../automat": { @@ -72,7 +72,7 @@ "@vitejs/plugin-react": "6.1.1", "react": "^19.3.0", "react-dom": "^19.3.0", - "vite": "^8.3.1" + "vite": "^8.3.2" }, "peerDependencies": { "@emotion/react": "^11.14.0", @@ -2931,9 +2931,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", - "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", "dev": true, "license": "MIT", "funding": { @@ -2951,9 +2951,9 @@ } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz", - "integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", "cpu": [ "arm" ], @@ -2968,9 +2968,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz", - "integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", "cpu": [ "arm64" ], @@ -2985,9 +2985,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz", - "integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", "cpu": [ "arm64" ], @@ -3002,9 +3002,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz", - "integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", "cpu": [ "x64" ], @@ -3019,9 +3019,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz", - "integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", "cpu": [ "x64" ], @@ -3036,9 +3036,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz", - "integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", "cpu": [ "arm" ], @@ -3053,9 +3053,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz", - "integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", "cpu": [ "arm64" ], @@ -3073,9 +3073,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz", - "integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", "cpu": [ "arm64" ], @@ -3093,9 +3093,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz", - "integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", "cpu": [ "ppc64" ], @@ -3113,9 +3113,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz", - "integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", "cpu": [ "s390x" ], @@ -3133,9 +3133,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz", - "integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", "cpu": [ "x64" ], @@ -3153,9 +3153,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz", - "integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", "cpu": [ "x64" ], @@ -3173,9 +3173,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz", - "integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", "cpu": [ "arm64" ], @@ -3190,9 +3190,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz", - "integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", "cpu": [ "arm64" ], @@ -3207,9 +3207,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz", - "integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", "cpu": [ "x64" ], @@ -4926,9 +4926,9 @@ } }, "node_modules/eslint": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", - "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz", + "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==", "dev": true, "license": "MIT", "workspaces": [ @@ -5528,9 +5528,9 @@ } }, "node_modules/globals": { - "version": "17.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", - "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", "dev": true, "license": "MIT", "engines": { @@ -7666,13 +7666,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", - "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.150.0", + "@oxc-project/types": "=0.152.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -7682,21 +7682,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.9", - "@rolldown/binding-android-arm64": "1.2.9", - "@rolldown/binding-darwin-arm64": "1.2.9", - "@rolldown/binding-darwin-x64": "1.2.9", - "@rolldown/binding-freebsd-x64": "1.2.9", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", - "@rolldown/binding-linux-arm64-gnu": "1.2.9", - "@rolldown/binding-linux-arm64-musl": "1.2.9", - "@rolldown/binding-linux-ppc64-gnu": "1.2.9", - "@rolldown/binding-linux-s390x-gnu": "1.2.9", - "@rolldown/binding-linux-x64-gnu": "1.2.9", - "@rolldown/binding-linux-x64-musl": "1.2.9", - "@rolldown/binding-openharmony-arm64": "1.2.9", - "@rolldown/binding-win32-arm64-msvc": "1.2.9", - "@rolldown/binding-win32-x64-msvc": "1.2.9" + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" } }, "node_modules/rollup": { @@ -8736,16 +8736,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", - "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", - "rolldown": "~1.2.9", + "rolldown": "~1.2.11", "tinyglobby": "^0.2.17" }, "bin": { @@ -8814,9 +8814,9 @@ } }, "node_modules/vite-plugin-pwa": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.3.0.tgz", - "integrity": "sha512-c5kMgN+ITrOtHXp8PAtk2uOIEea6XjP/unCGxOWWBzQ6qa65qj/awHg0wf+QF9E/2u9vh86LqxPwzEPNbM2r5A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-2.0.0.tgz", + "integrity": "sha512-6G9dsPKT9QKYpeTuTLJM28zZL3HsXUTUM/QUZBEgQk2jbfrPJDq2i8gTewAKqi1c9Mt6MiidrL4SFI+HthiQMg==", "dev": true, "license": "MIT", "dependencies": { @@ -8827,13 +8827,13 @@ "workbox-window": "^7.4.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "@vite-pwa/assets-generator": "^1.0.0", + "@vite-pwa/assets-generator": "^1.0.0 || ^2.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "workbox-build": "^7.4.1", "workbox-window": "^7.4.1" diff --git a/package.json b/package.json index 05f4685..e99dfde 100644 --- a/package.json +++ b/package.json @@ -41,12 +41,12 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@vitejs/plugin-react": "6.1.1", - "eslint": "^10.11.0", + "eslint": "^10.12.0", "eslint-plugin-react-hooks": "^7.1.1", - "globals": "^17.12.0", + "globals": "^17.13.0", "qrcode": "^1.5.4", - "vite": "^8.3.1", - "vite-plugin-pwa": "^1.3.0" + "vite": "^8.3.2", + "vite-plugin-pwa": "^2.0.0" }, "allowScripts": { "canvas@3.2.3": true, diff --git a/src/automats/categoryAutomat.js b/src/automats/categoryAutomat.js index 188d8a7..8726dd7 100644 --- a/src/automats/categoryAutomat.js +++ b/src/automats/categoryAutomat.js @@ -34,6 +34,36 @@ const initialState = { }; let categoryAutomat; +let _treeLoadingPromise = null; + +function waitForCategoryTree() { + if (categoryAutomat.state.treeStatus === 'succeeded') { + return Promise.resolve(categoryAutomat.state); + } + return new Promise((resolve, reject) => { + let unsubCat = null; + let unsubAuth = null; + const cleanup = () => { + unsubCat?.(); + unsubAuth?.(); + }; + unsubCat = categoryAutomat.subscribe((state) => { + if (state.treeStatus === 'succeeded') { + cleanup(); + resolve(state); + } else if (state.treeStatus === 'failed') { + cleanup(); + reject(new Error(state.error || 'Fehler beim Laden des Kategoriebaums')); + } + }); + unsubAuth = authAutomat.subscribe((auth) => { + if (auth.status === 'anon') { + cleanup(); + reject(new Error('Nicht authentifiziert.')); + } + }); + }); +} /** * KeyedAutomat holding per-route category views indexed by slug path. @@ -50,7 +80,10 @@ export const categoryViews = new KeyedAutomat( // Ensure tree is loaded before resolving slug -> categoryId if (categoryAutomat.state.treeStatus !== 'succeeded') { - await categoryAutomat.actions.loadTree(); + if (categoryAutomat.state.treeStatus === 'idle') { + void categoryAutomat.actions.loadTree(); + } + await waitForCategoryTree(); } const { categories, rootCategoryId } = categoryAutomat.state; @@ -117,31 +150,43 @@ export function disposeAllCategoryViewAutomats() { const actions = { /** Load the category tree for the currently selected company. */ async loadTree() { - const company = companyAutomat.state.current; + let company = companyAutomat.state.current; + if (!company && companyAutomat.state.status === 'idle') { + await companyAutomat.actions.load(); + company = companyAutomat.state.current; + } if (!company) return; const { id: companyId, rootCategoryId: companyRootId } = company; if (!companyRootId) return; + if (_treeLoadingPromise) return _treeLoadingPromise; + categoryAutomat.setState({ treeStatus: 'loading', error: null }); - try { - const data = await categoryApi.getTree(); - const parsed = parseCategoryTreeResponse(data, companyRootId); - categoryAutomat.setState({ - treeStatus: 'succeeded', - companyId, - languageId: parsed.languageId, - categories: parsed.categories, - rootCategoryId: parsed.rootCategoryId, - error: null, - }); - refreshLoadedCategoryViewChildren(parsed.categories); - } catch (err) { - categoryAutomat.setState({ - treeStatus: 'failed', - error: err?.message ?? String(err), - }); - throw err; - } + _treeLoadingPromise = (async () => { + try { + const data = await categoryApi.getTree(); + const parsed = parseCategoryTreeResponse(data, companyRootId); + categoryAutomat.setState({ + treeStatus: 'succeeded', + companyId, + languageId: parsed.languageId, + categories: parsed.categories, + rootCategoryId: parsed.rootCategoryId, + error: null, + }); + refreshLoadedCategoryViewChildren(parsed.categories); + } catch (err) { + categoryAutomat.setState({ + treeStatus: 'failed', + error: err?.message ?? String(err), + }); + throw err; + } + })().finally(() => { + _treeLoadingPromise = null; + }); + + return _treeLoadingPromise; }, /** Toggle rename/edit mode for the category grid. */ @@ -429,8 +474,11 @@ companyAutomat.subscribe((company) => { const id = company.current?.id ?? null; const rootCategoryId = company.current?.rootCategoryId ?? null; if (id && rootCategoryId && id !== _lastCompanyId) { + const isCompanySwitch = _lastCompanyId !== null; _lastCompanyId = id; - disposeAllCategoryViewAutomats(); + if (isCompanySwitch) { + disposeAllCategoryViewAutomats(); + } void categoryAutomat.actions.loadTree(); } if (!id && _lastCompanyId) { diff --git a/src/components/article/ThumbHashImage.jsx b/src/components/article/ThumbHashImage.jsx index 4214bee..b73a18c 100644 --- a/src/components/article/ThumbHashImage.jsx +++ b/src/components/article/ThumbHashImage.jsx @@ -1,6 +1,48 @@ import { useEffect, useRef } from 'react'; import Box from '@mui/material/Box'; -import { thumbHashToRGBA } from 'thumbhash'; +import { thumbHashToRGBA, thumbHashToDataURL } from 'thumbhash'; + +const dataUrlCache = (globalThis.__THUMB_DATA_URL_CACHE__ ??= new Map()); +const loadedImageHashes = (globalThis.__LOADED_IMAGE_HASHES__ ??= new Set()); +export const imageCache = (globalThis.__IMAGE_CACHE__ ??= new Map()); + +export function isImageLoaded(fullHash) { + if (!fullHash) return false; + return loadedImageHashes.has(fullHash) || imageCache.has(fullHash); +} + +export function markImageLoaded(fullHash, img) { + if (!fullHash) return; + loadedImageHashes.add(fullHash); + if (img) { + imageCache.set(fullHash, img); + } +} + +export function getCachedImage(fullHash) { + if (!fullHash) return null; + const img = imageCache.get(fullHash); + if (img && img.complete && img.naturalWidth) { + return img; + } + return null; +} + +export function getThumbHashDataUrl(thumbHash) { + if (!thumbHash || typeof thumbHash !== 'string') return null; + let url = dataUrlCache.get(thumbHash); + if (!url) { + try { + const bytes = Uint8Array.from(atob(thumbHash), (c) => c.charCodeAt(0)); + url = thumbHashToDataURL(bytes); + dataUrlCache.set(thumbHash, url); + } catch (err) { + console.warn('[ThumbHashImage] thumbHashToDataURL failed:', err); + return null; + } + } + return url; +} export function decodeThumbHash(thumbHash) { if (!thumbHash || typeof thumbHash !== 'string') return null; diff --git a/src/components/tiles/ArticleTile.jsx b/src/components/tiles/ArticleTile.jsx index 65b383a..9ee8527 100644 --- a/src/components/tiles/ArticleTile.jsx +++ b/src/components/tiles/ArticleTile.jsx @@ -6,7 +6,7 @@ import Typography from '@mui/material/Typography'; import { Tile, tileBaseSx } from 'dashgui/tiles'; import { formatPrice } from '../../utils/price'; import { articleSku } from '../article/articleRoute'; -import { decodeThumbHash } from '../article/ThumbHashImage'; +import { getThumbHashDataUrl, getCachedImage, imageCache } from '../article/ThumbHashImage'; import { BrandLabel } from '../brand'; import { thumb } from '../../api'; @@ -100,12 +100,22 @@ class ArticleTileBackdrop extends PureComponent { img = null; observer = null; + constructor(props) { + super(props); + this.thumbUrl = getThumbHashDataUrl(props.thumbHash); + this.img = getCachedImage(props.fullHash); + } + componentDidMount() { this.loadImage(); } componentDidUpdate(prevProps) { + if (prevProps.thumbHash !== this.props.thumbHash) { + this.thumbUrl = getThumbHashDataUrl(this.props.thumbHash); + } if (prevProps.fullHash !== this.props.fullHash) { + this.img = getCachedImage(this.props.fullHash); this.loadImage(); } } @@ -118,8 +128,15 @@ class ArticleTileBackdrop extends PureComponent { loadImage() { const { fullHash } = this.props; if (fullHash) { + const cached = getCachedImage(fullHash); + if (cached) { + this.img = cached; + this.paint(); + return; + } const img = new Image(); img.onload = () => { + imageCache.set(fullHash, img); if (this.img === img) { this.paint(); } @@ -127,15 +144,18 @@ class ArticleTileBackdrop extends PureComponent { img.src = thumb.url(fullHash); this.img = img; if (img.complete && img.naturalWidth) { + imageCache.set(fullHash, img); this.paint(); return; } } - this.paint(); } setCanvas = (canvas) => { this.canvasRef = canvas; + if (!this.img && this.props.fullHash) { + this.img = getCachedImage(this.props.fullHash); + } if (canvas && typeof ResizeObserver !== 'undefined') { this.observer?.disconnect(); this.observer = new ResizeObserver(() => this.paint()); @@ -157,8 +177,9 @@ class ArticleTileBackdrop extends PureComponent { const ctx = canvas.getContext('2d'); if (!ctx) return; - const img = this.img; + const img = this.img || getCachedImage(this.props.fullHash); if (img && img.complete && img.naturalWidth && img.naturalHeight) { + this.img = img; drawContainedWithStretchedEdges( ctx, img, @@ -167,41 +188,49 @@ class ArticleTileBackdrop extends PureComponent { dstW, dstH, ); - return; - } - - const { thumbHash } = this.props; - if (thumbHash) { - const decoded = decodeThumbHash(thumbHash); - if (decoded) { - const { w, h, rgba } = decoded; - const temp = document.createElement('canvas'); - temp.width = w; - temp.height = h; - temp.getContext('2d').putImageData( - new ImageData(new Uint8ClampedArray(rgba), w, h), - 0, - 0, - ); - drawContainedWithStretchedEdges(ctx, temp, w, h, dstW, dstH); - } } }; render() { + const hasCachedImage = Boolean(this.img && this.img.complete && this.img.naturalWidth); return ( + > + {!hasCachedImage && this.thumbUrl ? ( + + ) : null} + + ); } } diff --git a/src/components/tiles/CategoryTile.jsx b/src/components/tiles/CategoryTile.jsx index 04a03f1..552ca35 100644 --- a/src/components/tiles/CategoryTile.jsx +++ b/src/components/tiles/CategoryTile.jsx @@ -9,7 +9,7 @@ import SettingsIcon from '@mui/icons-material/Settings'; import PointOfSaleIcon from '@mui/icons-material/PointOfSale'; import PhotoCameraIcon from '@mui/icons-material/PhotoCamera'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; -import ThumbHashImage from '../article/ThumbHashImage'; +import { getThumbHashDataUrl, getCachedImage, imageCache } from '../article/ThumbHashImage'; import { thumb } from '../../api'; import { tileBaseSx } from 'dashgui/tiles'; @@ -195,36 +195,56 @@ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds, * ThumbHash is shown immediately as a placeholder; the thumb fades in once loaded. */ class CategoryBackdrop extends PureComponent { - state = { thumbLoaded: false }; + constructor(props) { + super(props); + this.thumbUrl = getThumbHashDataUrl(props.thumbHash); + this.isCached = Boolean(getCachedImage(props.fullHash)); + } componentDidUpdate(prevProps) { + if (prevProps.thumbHash !== this.props.thumbHash) { + this.thumbUrl = getThumbHashDataUrl(this.props.thumbHash); + } if (prevProps.fullHash !== this.props.fullHash) { - this.setState({ thumbLoaded: false }); + this.isCached = Boolean(getCachedImage(this.props.fullHash)); + this.forceUpdate(); } } - handleThumbLoad = () => { - this.setState({ thumbLoaded: true }); - }; - - handleThumbError = () => { - this.setState({ thumbLoaded: false }); + handleLoad = (e) => { + if (this.props.fullHash && e.target) { + imageCache.set(this.props.fullHash, e.target); + } }; render() { - const { thumbHash, fullHash } = this.props; - const { thumbLoaded } = this.state; - if (!thumbHash && !fullHash) return null; + const { fullHash } = this.props; + if (!this.thumbUrl && !fullHash) return null; + const isCached = this.isCached || Boolean(getCachedImage(fullHash)); return ( - {thumbHash && !thumbLoaded ? : null} - {fullHash ? ( + {!isCached && this.thumbUrl ? ( + ) : null} + {fullHash ? ( + ) : null} diff --git a/src/nav.js b/src/nav.js index c8ddd6b..d013db5 100644 --- a/src/nav.js +++ b/src/nav.js @@ -7,6 +7,7 @@ import PeopleIcon from '@mui/icons-material/People'; import InfoIcon from '@mui/icons-material/Info'; import ViewListIcon from '@mui/icons-material/ViewList'; import CategoryIcon from '@mui/icons-material/Category'; +import GridViewIcon from '@mui/icons-material/GridView'; import WarehouseIcon from '@mui/icons-material/Warehouse'; import CopyrightIcon from '@mui/icons-material/Copyright'; import ListAltIcon from '@mui/icons-material/ListAlt'; @@ -16,7 +17,6 @@ import StorefrontIcon from '@mui/icons-material/Storefront'; import RequestQuoteIcon from '@mui/icons-material/RequestQuote'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import LabelIcon from '@mui/icons-material/Label'; -import TuneIcon from '@mui/icons-material/Tune'; import HistoryIcon from '@mui/icons-material/History'; import PublicIcon from '@mui/icons-material/Public'; import BarChartIcon from '@mui/icons-material/BarChart'; @@ -142,7 +142,7 @@ export const SUB_NAV = { segment: 'kategorien', label: 'Kategorieen', title: 'Kategorien', - icon: CategoryIcon, + icon: GridViewIcon, help: 'Hierarchischer Kategoriebaum zur Sortimentsgliederung', }, { @@ -167,7 +167,7 @@ export const SUB_NAV = { segment: 'merkmale', label: 'Merkmale', title: 'Merkmale & Attribute', - icon: TuneIcon, + icon: CategoryIcon, help: 'Globale Merkmale, Attribute und Werte verwalten', }, ], diff --git a/src/routes/artikel/kategorien.jsx b/src/routes/artikel/kategorien.jsx index 0a9abe4..d466848 100644 --- a/src/routes/artikel/kategorien.jsx +++ b/src/routes/artikel/kategorien.jsx @@ -238,11 +238,22 @@ class ArtikelKategorienPage extends PureComponent { routeError: view?.error ?? null, }); this.subscribeViewHandle(); - } else if (resolvedId !== resolvedCategoryId) { - this.setState({ - resolvedCategoryId: resolvedId, - childCategories: getCategoryChildren(categories, resolvedId), - }); + } else { + const view = this.handle?.state; + const updates = {}; + if (resolvedId !== resolvedCategoryId) { + updates.resolvedCategoryId = resolvedId; + updates.childCategories = view?.data?.childCategories ?? getCategoryChildren(categories, resolvedId); + } + if (view?.data?.articles && view.data.articles !== this.state.articles) { + updates.articles = view.data.articles; + } + if (Object.keys(updates).length > 0) { + this.setState(updates); + } + if (!this.handle?.loaded && !this.handle?.loading) { + void this.handle?.load().catch(() => {}); + } } };