diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 3e19fe0..772202d 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -8,69 +8,69 @@ jobs: runs-on: ubuntu-20.04 steps: - - name: Checkout - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up NodeJS 20 + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install JS dependencies + run: | + npm i -g pnpm + pnpm install + + - name: Build Frontend + run: | + pnpm run build + + - name: Package Release + run: | + mkdir "SDH-CssLoader" + cp *.py "./SDH-CssLoader" + cp *.json "./SDH-CssLoader" + cp LICENSE "./SDH-CssLoader" + cp README.md "./SDH-CssLoader" + cp -r dist "./SDH-CssLoader" + mkdir upload + mv "./SDH-CssLoader" ./upload + + - name: Upload package artifact + uses: actions/upload-artifact@v3 + with: + name: SDH-CSSLoader-Decky + path: ./upload - - name: Set up NodeJS 18 - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: Install JS dependencies - run: | - npm i -g pnpm - pnpm install - - - name: Build Frontend - run: | - pnpm run build - - - name: Package Release - run: | - mkdir "SDH-CssLoader" - cp *.py "./SDH-CssLoader" - cp *.json "./SDH-CssLoader" - cp LICENSE "./SDH-CssLoader" - cp README.md "./SDH-CssLoader" - cp -r dist "./SDH-CssLoader" - mkdir upload - mv "./SDH-CssLoader" ./upload - - - name: Upload package artifact - uses: actions/upload-artifact@v3 - with: - name: SDH-CSSLoader-Decky - path: ./upload - build-standalone-win: name: Build SDH-CSSLoader Standalone for Windows runs-on: windows-2022 steps: - - name: Checkout - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 - - name: Set up Python 3.10.2 - uses: actions/setup-python@v4 - with: - python-version: "3.10.2" + - name: Set up Python 3.10.2 + uses: actions/setup-python@v4 + with: + python-version: "3.10.2" - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install pyinstaller==5.5 - pip install -r requirements.txt + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller==5.5 + pip install -r requirements.txt - - name: Build Python Backend - run: pyinstaller --noconfirm --onefile --add-data "./assets;/assets" --name "CssLoader-Standalone" ./main.py ./css_win_tray.py + - name: Build Python Backend + run: pyinstaller --noconfirm --onefile --add-data "./assets;/assets" --name "CssLoader-Standalone" ./main.py ./css_win_tray.py - - name: Build Python Backend Headless - run: pyinstaller --noconfirm --noconsole --onefile --add-data "./assets;/assets" --name "CssLoader-Standalone-Headless" ./main.py ./css_win_tray.py + - name: Build Python Backend Headless + run: pyinstaller --noconfirm --noconsole --onefile --add-data "./assets;/assets" --name "CssLoader-Standalone-Headless" ./main.py ./css_win_tray.py - - name: Upload package artifact - uses: actions/upload-artifact@v3 - with: - name: SDH-CSSLoader-Win-Standalone - path: | - ./dist/CssLoader-Standalone.exe - ./dist/CssLoader-Standalone-Headless.exe + - name: Upload package artifact + uses: actions/upload-artifact@v3 + with: + name: SDH-CSSLoader-Win-Standalone + path: | + ./dist/CssLoader-Standalone.exe + ./dist/CssLoader-Standalone-Headless.exe diff --git a/commit-styles-macos.sh b/commit-styles-macos.sh new file mode 100755 index 0000000..67ef9b1 --- /dev/null +++ b/commit-styles-macos.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# TODO: NOT FINISHED + +# This uses macos' sed, which is different from the one in linux + +css_file_path="$(dirname "$0")/src/styles/styles.css" +css_as_string_path="$(dirname "$0")/src/styles/styles-as-string.ts" + +# Read the content of the CSS file, ignoring lines that start with '/*' +css_content=$(sed '/^\s*\/\*/d' "$css_file_path") + +export_header_line_number=$(grep -n "export const styles" $css_as_string_path | cut -d: -f1) +echo export_header_line_number: $export_header_line_number \ No newline at end of file diff --git a/main.py b/main.py index 06897a0..3165fad 100644 --- a/main.py +++ b/main.py @@ -23,7 +23,7 @@ try: if setting_redirect_logs(): - import decky_plugin + import decky except: pass @@ -217,7 +217,8 @@ async def run(self): count = 0 while count < 5: try: - task = asyncio.create_task(Plugin._main(Plugin)) + instance = Plugin() + task = asyncio.create_task(instance._main()) await asyncio.shield(task) except asyncio.CancelledError as e: print(str(e)) diff --git a/package.json b/package.json index 75619c3..d616242 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "SDH-CssLoader", - "version": "2.1.2", + "type": "module", + "version": "2.2.0", "description": "A css loader", "scripts": { "build": "shx rm -rf dist && rollup -c", @@ -18,34 +19,33 @@ "steam-deck", "deck" ], - "author": "Jonas Dellinger ", + "author": "Deckthemes", "license": "GPL-2.0-or-later", "bugs": { "url": "https://github.com/SteamDeckHomebrew/decky-plugin-template/issues" }, "homepage": "https://github.com/SteamDeckHomebrew/decky-plugin-template#readme", "devDependencies": { - "@rollup/plugin-commonjs": "^21.1.0", - "@rollup/plugin-json": "^4.1.0", - "@rollup/plugin-node-resolve": "^13.2.1", - "@rollup/plugin-replace": "^4.0.0", - "@rollup/plugin-typescript": "^8.3.2", + "@rollup/plugin-alias": "^5.1.0", "@types/color": "^3.0.3", "@types/lodash": "^4.14.191", "@types/react": "16.14.0", "@types/webpack": "^5.28.0", - "rollup": "^2.70.2", - "rollup-plugin-import-assets": "^1.1.1", - "rollup-plugin-styles": "^4.0.0", + "rollup": "^4.24.4", "shx": "^0.3.4", "tslib": "^2.4.0", - "typescript": "^4.6.4" + "typescript": "^5.6.3" }, "dependencies": { + "@decky/api": "^1.0.6", + "@decky/rollup": "^1.0.1", + "@decky/ui": "^4.4.0", + "clsx": "^2.1.1", "color": "^4.2.3", - "decky-frontend-lib": "^3.25.0", "lodash": "^4.17.21", - "react-icons": "^4.12.0" + "react-icons": "^4.12.0", + "tailwind-merge": "^2.3.0", + "zustand": "^4.5.2" }, "pnpm": { "peerDependencyRules": { @@ -54,5 +54,6 @@ "react-dom" ] } - } + }, + "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1" } diff --git a/plugin.json b/plugin.json index 71f3629..9fb71d0 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,7 @@ { "name": "CSS Loader", "author": "DeckThemes", + "api_version": 1, "flags": [], "publish": { "tags": ["style"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04219d2..1214686 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,240 +8,367 @@ importers: .: dependencies: + '@decky/api': + specifier: ^1.0.6 + version: 1.0.6 + '@decky/rollup': + specifier: ^1.0.1 + version: 1.0.1 + '@decky/ui': + specifier: ^4.4.0 + version: 4.4.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 color: specifier: ^4.2.3 version: 4.2.3 - decky-frontend-lib: - specifier: ^3.25.0 - version: 3.25.0 lodash: specifier: ^4.17.21 version: 4.17.21 react-icons: specifier: ^4.12.0 - version: 4.12.0 + version: 4.12.0(react@18.3.1) + tailwind-merge: + specifier: ^2.3.0 + version: 2.3.0 + zustand: + specifier: ^4.5.2 + version: 4.5.2(@types/react@16.14.0)(react@18.3.1) devDependencies: - '@rollup/plugin-commonjs': - specifier: ^21.1.0 - version: 21.1.0(rollup@2.75.6) - '@rollup/plugin-json': - specifier: ^4.1.0 - version: 4.1.0(rollup@2.75.6) - '@rollup/plugin-node-resolve': - specifier: ^13.2.1 - version: 13.3.0(rollup@2.75.6) - '@rollup/plugin-replace': - specifier: ^4.0.0 - version: 4.0.0(rollup@2.75.6) - '@rollup/plugin-typescript': - specifier: ^8.3.2 - version: 8.3.3(rollup@2.75.6)(tslib@2.4.0)(typescript@4.7.3) + '@rollup/plugin-alias': + specifier: ^5.1.0 + version: 5.1.0(rollup@4.24.4) '@types/color': specifier: ^3.0.3 version: 3.0.6 '@types/lodash': specifier: ^4.14.191 - version: 4.14.201 + version: 4.17.4 '@types/react': specifier: 16.14.0 version: 16.14.0 '@types/webpack': specifier: ^5.28.0 - version: 5.28.0 + version: 5.28.5 rollup: - specifier: ^2.70.2 - version: 2.75.6 - rollup-plugin-import-assets: - specifier: ^1.1.1 - version: 1.1.1(rollup@2.75.6) - rollup-plugin-styles: - specifier: ^4.0.0 - version: 4.0.0(rollup@2.75.6) + specifier: ^4.24.4 + version: 4.24.4 shx: specifier: ^0.3.4 version: 0.3.4 tslib: specifier: ^2.4.0 - version: 2.4.0 + version: 2.6.2 typescript: - specifier: ^4.6.4 - version: 4.7.3 + specifier: ^5.6.3 + version: 5.6.3 packages: - '@babel/code-frame@7.22.13': - resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} + '@babel/runtime@7.24.5': + resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.22.20': - resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} - engines: {node: '>=6.9.0'} + '@decky/api@1.0.6': + resolution: {integrity: sha512-pacO2qvAin7ZoB9AnCgQbevQS+6Wiy0t1C6QVEJPCWeQQdEgJaUm3KSeRjh2KNqsjKHsbV0j72Pv4X5Q44Lr7Q==} - '@babel/highlight@7.22.20': - resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} - engines: {node: '>=6.9.0'} + '@decky/rollup@1.0.1': + resolution: {integrity: sha512-dx1VJwD7ul14PA/aZvOwAfY/GujHzqZJ+MFb4OIUVi63/z4KWMSuZrK6QWo0S4LrNW3RzB3ua6LT0WcJaNY9gw==} + + '@decky/ui@4.4.0': + resolution: {integrity: sha512-w6hSoEdWQyXdvSk8cv4cGDJQ6/xoRx9LYrdNjTGPPj8hRmBcoRlThgZ+9vchmNsAJuOymwQ5hQXqHN6Y86eoOQ==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} - '@jridgewell/gen-mapping@0.3.1': - resolution: {integrity: sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==} + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} - '@jridgewell/resolve-uri@3.0.7': - resolution: {integrity: sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==} + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.1.1': - resolution: {integrity: sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==} + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.2': - resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==} + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} - '@jridgewell/sourcemap-codec@1.4.13': - resolution: {integrity: sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==} + '@jridgewell/sourcemap-codec@1.4.15': + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - '@jridgewell/trace-mapping@0.3.13': - resolution: {integrity: sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==} + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@rollup/plugin-commonjs@21.1.0': - resolution: {integrity: sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA==} - engines: {node: '>= 8.0.0'} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/plugin-alias@5.1.0': + resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==} + engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^2.38.3 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true - '@rollup/plugin-json@4.1.0': - resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==} + '@rollup/plugin-commonjs@26.0.1': + resolution: {integrity: sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} peerDependencies: - rollup: ^1.20.0 || ^2.0.0 + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true - '@rollup/plugin-node-resolve@13.3.0': - resolution: {integrity: sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==} - engines: {node: '>= 10.0.0'} + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^2.42.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true - '@rollup/plugin-replace@4.0.0': - resolution: {integrity: sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g==} + '@rollup/plugin-node-resolve@15.2.3': + resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} + engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0 || ^2.0.0 + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true - '@rollup/plugin-typescript@8.3.3': - resolution: {integrity: sha512-55L9SyiYu3r/JtqdjhwcwaECXP7JeJ9h1Sg1VWRJKIutla2MdZQodTgcCNybXLMCnqpNLEhS2vGENww98L1npg==} - engines: {node: '>=8.0.0'} + '@rollup/plugin-replace@5.0.7': + resolution: {integrity: sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==} + engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^2.14.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-typescript@11.1.6': + resolution: {integrity: sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.14.0||^3.0.0||^4.0.0 tslib: '*' typescript: '>=3.7.0' peerDependenciesMeta: + rollup: + optional: true tslib: optional: true - '@rollup/pluginutils@3.1.0': - resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} - engines: {node: '>= 8.0.0'} + '@rollup/pluginutils@5.1.0': + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true - '@rollup/pluginutils@4.2.1': - resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} - engines: {node: '>= 8.0.0'} + '@rollup/rollup-android-arm-eabi@4.24.4': + resolution: {integrity: sha512-jfUJrFct/hTA0XDM5p/htWKoNNTbDLY0KRwEt6pyOA6k2fmk0WVwl65PdUdJZgzGEHWx+49LilkcSaumQRyNQw==} + cpu: [arm] + os: [android] - '@trysound/sax@0.2.0': - resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} - engines: {node: '>=10.13.0'} + '@rollup/rollup-android-arm64@4.24.4': + resolution: {integrity: sha512-j4nrEO6nHU1nZUuCfRKoCcvh7PIywQPUCBa2UsootTHvTHIoIu2BzueInGJhhvQO/2FTRdNYpf63xsgEqH9IhA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.24.4': + resolution: {integrity: sha512-GmU/QgGtBTeraKyldC7cDVVvAJEOr3dFLKneez/n7BvX57UdhOqDsVwzU7UOnYA7AAOt+Xb26lk79PldDHgMIQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.24.4': + resolution: {integrity: sha512-N6oDBiZCBKlwYcsEPXGDE4g9RoxZLK6vT98M8111cW7VsVJFpNEqvJeIPfsCzbf0XEakPslh72X0gnlMi4Ddgg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.24.4': + resolution: {integrity: sha512-py5oNShCCjCyjWXCZNrRGRpjWsF0ic8f4ieBNra5buQz0O/U6mMXCpC1LvrHuhJsNPgRt36tSYMidGzZiJF6mw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.24.4': + resolution: {integrity: sha512-L7VVVW9FCnTTp4i7KrmHeDsDvjB4++KOBENYtNYAiYl96jeBThFfhP6HVxL74v4SiZEVDH/1ILscR5U9S4ms4g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.24.4': + resolution: {integrity: sha512-10ICosOwYChROdQoQo589N5idQIisxjaFE/PAnX2i0Zr84mY0k9zul1ArH0rnJ/fpgiqfu13TFZR5A5YJLOYZA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.24.4': + resolution: {integrity: sha512-ySAfWs69LYC7QhRDZNKqNhz2UKN8LDfbKSMAEtoEI0jitwfAG2iZwVqGACJT+kfYvvz3/JgsLlcBP+WWoKCLcw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.24.4': + resolution: {integrity: sha512-uHYJ0HNOI6pGEeZ/5mgm5arNVTI0nLlmrbdph+pGXpC9tFHFDQmDMOEqkmUObRfosJqpU8RliYoGz06qSdtcjg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.24.4': + resolution: {integrity: sha512-38yiWLemQf7aLHDgTg85fh3hW9stJ0Muk7+s6tIkSUOMmi4Xbv5pH/5Bofnsb6spIwD5FJiR+jg71f0CH5OzoA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.24.4': + resolution: {integrity: sha512-q73XUPnkwt9ZNF2xRS4fvneSuaHw2BXuV5rI4cw0fWYVIWIBeDZX7c7FWhFQPNTnE24172K30I+dViWRVD9TwA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.24.4': + resolution: {integrity: sha512-Aie/TbmQi6UXokJqDZdmTJuZBCU3QBDA8oTKRGtd4ABi/nHgXICulfg1KI6n9/koDsiDbvHAiQO3YAUNa/7BCw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.24.4': + resolution: {integrity: sha512-P8MPErVO/y8ohWSP9JY7lLQ8+YMHfTI4bAdtCi3pC2hTeqFJco2jYspzOzTUB8hwUWIIu1xwOrJE11nP+0JFAQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.24.4': + resolution: {integrity: sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.24.4': + resolution: {integrity: sha512-VJYl4xSl/wqG2D5xTYncVWW+26ICV4wubwN9Gs5NrqhJtayikwCXzPL8GDsLnaLU3WwhQ8W02IinYSFJfyo34Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.24.4': + resolution: {integrity: sha512-ku2GvtPwQfCqoPFIJCqZ8o7bJcj+Y54cZSr43hHca6jLwAiCbZdBUOrqE6y29QFajNAzzpIOwsckaTFmN6/8TA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.24.4': + resolution: {integrity: sha512-V3nCe+eTt/W6UYNr/wGvO1fLpHUrnlirlypZfKCT1fG6hWfqhPgQV/K/mRBXBpxc0eKLIF18pIOFVPh0mqHjlg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.24.4': + resolution: {integrity: sha512-LTw1Dfd0mBIEqUVCxbvTE/LLo+9ZxVC9k99v1v4ahg9Aak6FpqOfNu5kRkeTAn0wphoC4JU7No1/rL+bBCEwhg==} + cpu: [x64] + os: [win32] '@types/color-convert@2.0.3': resolution: {integrity: sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==} - '@types/color-name@1.1.3': - resolution: {integrity: sha512-87W6MJCKZYDhLAx/J1ikW8niMvmGRyY+rpUxWpL1cO7F8Uu5CHuQoFv+R0/L5pgNdW4jTyda42kv60uwVIPjLw==} + '@types/color-name@1.1.4': + resolution: {integrity: sha512-hulKeREDdLFesGQjl96+4aoJSHY5b2GRjagzzcqCfIrWhe5vkCqIvrLbqzBaI1q94Vg8DNJZZqTR5ocdWmWclg==} '@types/color@3.0.6': resolution: {integrity: sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A==} - '@types/cssnano@5.1.0': - resolution: {integrity: sha512-ikR+18UpFGgvaWSur4og6SJYF/6QEYHXvrIt36dp81p1MG3cAPTYDMBJGeyWa3LCnqEbgNMHKRb+FP0NrXtoWQ==} - deprecated: This is a stub types definition. cssnano provides its own type definitions, so you do not need this installed. + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - '@types/eslint-scope@3.7.3': - resolution: {integrity: sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==} + '@types/eslint@8.56.10': + resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} - '@types/eslint@8.4.3': - resolution: {integrity: sha512-YP1S7YJRMPs+7KZKDb9G63n8YejIwW9BALq7a5j2+H4yl6iOv9CB29edho+cuFRrvmJbbaH2yiVChKLJVysDGw==} + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/estree@0.0.39': - resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/estree@0.0.51': - resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} - '@types/json-schema@7.0.11': - resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/lodash@4.14.201': - resolution: {integrity: sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ==} + '@types/lodash@4.17.4': + resolution: {integrity: sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==} - '@types/node@17.0.42': - resolution: {integrity: sha512-Q5BPGyGKcvQgAMbsr7qEGN/kIPN6zZecYYABeTDBizOsau+2NMdSVTar9UQw21A2+JyA2KRNDYaYrPB0Rpk2oQ==} + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - '@types/parse-json@4.0.2': - resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/node@20.12.12': + resolution: {integrity: sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==} - '@types/prop-types@15.7.5': - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + '@types/prop-types@15.7.12': + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} '@types/react@16.14.0': resolution: {integrity: sha512-jJjHo1uOe+NENRIBvF46tJimUvPnmbQ41Ax0pEm7pRvhPg+wuj8VMOHHiMvaGmZRzRrCtm7KnL5OOE/6kHPK8w==} - '@types/resolve@1.17.1': - resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/webpack@5.28.0': - resolution: {integrity: sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==} + '@types/webpack@5.28.5': + resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} - '@webassemblyjs/ast@1.11.1': - resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} + '@webassemblyjs/ast@1.12.1': + resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} - '@webassemblyjs/floating-point-hex-parser@1.11.1': - resolution: {integrity: sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==} + '@webassemblyjs/floating-point-hex-parser@1.11.6': + resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} - '@webassemblyjs/helper-api-error@1.11.1': - resolution: {integrity: sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==} + '@webassemblyjs/helper-api-error@1.11.6': + resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} - '@webassemblyjs/helper-buffer@1.11.1': - resolution: {integrity: sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==} + '@webassemblyjs/helper-buffer@1.12.1': + resolution: {integrity: sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==} - '@webassemblyjs/helper-numbers@1.11.1': - resolution: {integrity: sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==} + '@webassemblyjs/helper-numbers@1.11.6': + resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} - '@webassemblyjs/helper-wasm-bytecode@1.11.1': - resolution: {integrity: sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==} + '@webassemblyjs/helper-wasm-bytecode@1.11.6': + resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} - '@webassemblyjs/helper-wasm-section@1.11.1': - resolution: {integrity: sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==} + '@webassemblyjs/helper-wasm-section@1.12.1': + resolution: {integrity: sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==} - '@webassemblyjs/ieee754@1.11.1': - resolution: {integrity: sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==} + '@webassemblyjs/ieee754@1.11.6': + resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} - '@webassemblyjs/leb128@1.11.1': - resolution: {integrity: sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==} + '@webassemblyjs/leb128@1.11.6': + resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} - '@webassemblyjs/utf8@1.11.1': - resolution: {integrity: sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==} + '@webassemblyjs/utf8@1.11.6': + resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} - '@webassemblyjs/wasm-edit@1.11.1': - resolution: {integrity: sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==} + '@webassemblyjs/wasm-edit@1.12.1': + resolution: {integrity: sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==} - '@webassemblyjs/wasm-gen@1.11.1': - resolution: {integrity: sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==} + '@webassemblyjs/wasm-gen@1.12.1': + resolution: {integrity: sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==} - '@webassemblyjs/wasm-opt@1.11.1': - resolution: {integrity: sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==} + '@webassemblyjs/wasm-opt@1.12.1': + resolution: {integrity: sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==} - '@webassemblyjs/wasm-parser@1.11.1': - resolution: {integrity: sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==} + '@webassemblyjs/wasm-parser@1.12.1': + resolution: {integrity: sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==} - '@webassemblyjs/wast-printer@1.11.1': - resolution: {integrity: sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==} + '@webassemblyjs/wast-printer@1.12.1': + resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -249,16 +376,20 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - acorn-import-assertions@1.8.0: - resolution: {integrity: sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==} + acorn-import-assertions@1.9.0: + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} peerDependencies: acorn: ^8 - acorn@8.7.1: - resolution: {integrity: sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==} + acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} hasBin: true + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -267,21 +398,41 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - browserslist@4.22.1: - resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.23.0: + resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -292,37 +443,25 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - caniuse-api@3.0.0: - resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - - caniuse-lite@1.0.30001352: - resolution: {integrity: sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA==} - - caniuse-lite@1.0.30001562: - resolution: {integrity: sha512-kfte3Hym//51EdX4239i+Rmp20EsLIYGdPkERegTgU19hQWCRhsRFGKHTliUlsry53tv17K7n077Kqa0WJU4ng==} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} + caniuse-lite@1.0.30001620: + resolution: {integrity: sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==} chrome-trace-event@1.0.3: resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} engines: {node: '>=6.0'} - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -333,121 +472,57 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} - colord@2.9.3: - resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} - commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - cosmiconfig@7.1.0: - resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} - engines: {node: '>=10'} - - css-declaration-sorter@6.4.1: - resolution: {integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==} - engines: {node: ^10 || ^12 || >=14} - peerDependencies: - postcss: ^8.0.9 + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} - css-select@4.3.0: - resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} - - css-tree@1.1.3: - resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} - engines: {node: '>=8.0.0'} - - css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - cssnano-preset-default@5.2.14: - resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - cssnano-utils@3.1.0: - resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - cssnano@5.1.15: - resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - csso@4.2.0: - resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} - engines: {node: '>=8.0.0'} - - csstype@3.1.0: - resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==} - - decky-frontend-lib@3.25.0: - resolution: {integrity: sha512-2lBoHS2AIRmuluq/bGdHBz+uyToQE7k3K/vDq1MQbDZ4eC+8CGDuh2T8yZOj3D0yjGP2MdikNNAWPA9Z5l2qDg==} - - decode-uri-component@0.2.2: - resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} - engines: {node: '>=0.10'} - - deepmerge@4.2.2: - resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - dom-serializer@1.4.1: - resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + del@5.1.0: + resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} + engines: {node: '>=8'} - domhandler@4.3.1: - resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} - engines: {node: '>= 4'} + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} - domutils@2.8.0: - resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.4.587: - resolution: {integrity: sha512-RyJX0q/zOkAoefZhB9XHghGeATVP0Q3mwA253XD/zj2OeXc+JZB9pCaEv6R578JUYaWM9PRhye0kXvd/V1cQ3Q==} + electron-to-chromium@1.4.775: + resolution: {integrity: sha512-JpOfl1aNAiZ88wFzjPczTLwYIoPIsij8S9/XQH9lqMpiJOf23kxea68B8wje4f68t4rOIq4Bh+vP4I65njiJBw==} - enhanced-resolve@5.9.3: - resolution: {integrity: sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==} - engines: {node: '>=10.13.0'} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + enhanced-resolve@5.16.1: + resolution: {integrity: sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==} + engines: {node: '>=10.13.0'} - es-module-lexer@0.9.3: - resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==} + es-module-lexer@1.5.3: + resolution: {integrity: sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==} - escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -467,14 +542,11 @@ packages: estree-walker@0.6.1: resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} - estree-walker@1.0.1: - resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} - estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} @@ -483,16 +555,23 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - filter-obj@1.1.0: - resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} - engines: {node: '>=0.10.0'} + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + foreground-child@3.2.1: + resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} + engines: {node: '>=14'} fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -502,42 +581,50 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + glob@10.4.1: + resolution: {integrity: sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==} + engines: {node: '>=16 || 14 >=14.18'} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - graceful-fs@4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + globby@10.0.2: + resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} + engines: {node: '>=8'} - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has@1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} - icss-utils@5.1.0: - resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 + ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -546,25 +633,60 @@ packages: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-builtin-module@3.1.0: - resolution: {integrity: sha512-OV7JjAgOTfAFJmHZLvpSTb4qi0nIILDV1gWPYDnDJUTNFM5aGlRAhk4QcT8i7TuAleeEV5Fdkqn3t4mS+Q11fg==} + is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} - is-core-module@2.9.0: - resolution: {integrity: sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==} + is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-cwd@2.2.0: + resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} + engines: {node: '>=6'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + is-reference@3.0.2: + resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} + + is-what@5.0.2: + resolution: {integrity: sha512-vI7Ui0qzNQ2ClDZd0bC7uqRk3T1imbX5cZODmVlqqdqiwmSIUX3CNSiRgFjFMJ987sVCMSa7xZeEDtpJduPg4A==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.0: + resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} + engines: {node: '>=14'} + jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} @@ -578,38 +700,39 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - - lodash.uniq@4.5.0: - resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - magic-string@0.25.9: - resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.2.2: + resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} + engines: {node: 14 || >=16.14} + + magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} - mdn-data@2.0.14: - resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + merge-anything@6.0.2: + resolution: {integrity: sha512-U8x6DL/YVudOcf82B6hd8GFg+6gF6hEHYwzqdo67GrH6vnDZ5YBq6BYX3hHWyCnG3CcqJDB1a9tj9fzMI3RL9Q==} + engines: {node: '>=18'} merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -621,271 +744,62 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimist@1.2.6: - resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} + minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-releases@2.0.13: - resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} - - normalize-url@6.1.0: - resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} - engines: {node: '>=10'} - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - p-finally@1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} - engines: {node: '>=4'} - - p-queue@6.6.2: - resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} - engines: {node: '>=8'} - - p-timeout@3.2.0: - resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} - engines: {node: '>=8'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + p-map@3.0.0: + resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} engines: {node: '>=8'} path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - postcss-calc@8.2.4: - resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} - peerDependencies: - postcss: ^8.2.2 - - postcss-colormin@5.3.1: - resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-convert-values@5.1.3: - resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-comments@5.1.2: - resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-duplicates@5.1.0: - resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-empty@5.1.1: - resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-overridden@5.1.0: - resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-merge-longhand@5.1.7: - resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-merge-rules@5.1.4: - resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-font-values@5.1.0: - resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-gradients@5.1.1: - resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-params@5.1.4: - resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-selectors@5.2.1: - resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-modules-extract-imports@3.0.0: - resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-local-by-default@4.0.3: - resolution: {integrity: sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-scope@3.0.0: - resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-values@4.0.0: - resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-normalize-charset@5.1.0: - resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-display-values@5.1.0: - resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-positions@5.1.1: - resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-repeat-style@5.1.1: - resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-string@5.1.0: - resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-timing-functions@5.1.0: - resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-unicode@5.1.1: - resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-url@5.1.0: - resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-whitespace@5.1.1: - resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-ordered-values@5.1.3: - resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-reduce-initial@5.1.2: - resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-reduce-transforms@5.1.0: - resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-selector-parser@6.0.13: - resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} - engines: {node: '>=4'} - - postcss-svgo@5.1.0: - resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-unique-selectors@5.1.1: - resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} - - punycode@2.1.1: - resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - query-string@7.1.3: - resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} - engines: {node: '>=6'} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -894,50 +808,73 @@ packages: resolution: {integrity: sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==} peerDependencies: react: '*' - peerDependenciesMeta: - react: - optional: true + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} rechoir@0.6.2: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - resolve@1.22.0: - resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==} + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rollup-plugin-delete@2.0.0: + resolution: {integrity: sha512-/VpLMtDy+8wwRlDANuYmDa9ss/knGsAgrDhM+tEwB1npHwNu4DYNmDfUL55csse/GHs9Q+SMT/rw9uiaZ3pnzA==} + engines: {node: '>=10'} + + rollup-plugin-external-globals@0.11.0: + resolution: {integrity: sha512-LR+sH2WkgWMPxsA5o5rT7uW7BeWXSeygLe60QQi9qoN/ufaCuHDaVOIbndIkqDPnZt/wZugJh5DCzkZFdSWlLQ==} + peerDependencies: + rollup: ^2.25.0 || ^3.3.0 || ^4.1.4 + rollup-plugin-import-assets@1.1.1: resolution: {integrity: sha512-u5zJwOjguTf2N+wETq2weNKGvNkuVc1UX/fPgg215p5xPvGOaI6/BTc024E9brvFjSQTfIYqgvwogQdipknu1g==} peerDependencies: rollup: '>=1.9.0' - rollup-plugin-styles@4.0.0: - resolution: {integrity: sha512-A2K2sao84OsTmDxXG83JTCdXWrmgvQkkI38XDat46rdtpGMRm9tSYqeCdlwwGDJF4kKIafhV1mUidqu8MxUGig==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - rollup: ^2.63.0 - rollup-pluginutils@2.8.2: resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} - rollup@2.75.6: - resolution: {integrity: sha512-OEf0TgpC9vU6WGROJIk1JA3LR5vk/yvqlzxqdrE2CzzXnqKXNzbAwlWUXis8RS3ZPe7LAq+YUxsRa0l3r27MLA==} - engines: {node: '>=10.0.0'} + rollup@4.24.4: + resolution: {integrity: sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - schema-utils@3.1.1: - resolution: {integrity: sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==} + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} - serialize-javascript@6.0.0: - resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} shelljs@0.8.5: resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} @@ -949,12 +886,20 @@ packages: engines: {node: '>=6'} hasBin: true + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -963,30 +908,21 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - sourcemap-codec@1.4.8: - resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - - split-on-first@1.1.0: - resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} - engines: {node: '>=6'} - - stable@0.1.8: - resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} - 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' + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} - strict-uri-encode@2.0.0: - resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} - engines: {node: '>=4'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} - stylehacks@5.1.1: - resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} supports-color@8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} @@ -996,17 +932,15 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svgo@2.8.0: - resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} - engines: {node: '>=10.13.0'} - hasBin: true + tailwind-merge@2.3.0: + resolution: {integrity: sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==} tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} - terser-webpack-plugin@5.3.3: - resolution: {integrity: sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==} + terser-webpack-plugin@5.3.10: + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -1021,25 +955,31 @@ packages: uglify-js: optional: true - terser@5.14.1: - resolution: {integrity: sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==} + terser@5.31.0: + resolution: {integrity: sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==} engines: {node: '>=10'} hasBin: true - tslib@2.4.0: - resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - typescript@4.7.3: - resolution: {integrity: sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==} - engines: {node: '>=4.2.0'} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} hasBin: true - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - update-browserslist-db@1.0.13: - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + update-browserslist-db@1.0.16: + resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -1050,19 +990,21 @@ packages: url-join@4.0.1: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + use-sync-external-store@1.2.0: + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 - watchpack@2.4.0: - resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + watchpack@2.4.1: + resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==} engines: {node: '>=10.13.0'} webpack-sources@3.2.3: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} - webpack@5.73.0: - resolution: {integrity: sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==} + webpack@5.91.0: + resolution: {integrity: sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -1071,249 +1013,369 @@ packages: webpack-cli: optional: true + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} + zustand@4.5.2: + resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + +snapshots: + + '@babel/runtime@7.24.5': + dependencies: + regenerator-runtime: 0.14.1 + + '@decky/api@1.0.6': {} + + '@decky/rollup@1.0.1': + dependencies: + '@rollup/plugin-commonjs': 26.0.1(rollup@4.24.4) + '@rollup/plugin-json': 6.1.0(rollup@4.24.4) + '@rollup/plugin-node-resolve': 15.2.3(rollup@4.24.4) + '@rollup/plugin-replace': 5.0.7(rollup@4.24.4) + '@rollup/plugin-typescript': 11.1.6(rollup@4.24.4)(tslib@2.8.1)(typescript@5.6.3) + merge-anything: 6.0.2 + rollup: 4.24.4 + rollup-plugin-delete: 2.0.0 + rollup-plugin-external-globals: 0.11.0(rollup@4.24.4) + rollup-plugin-import-assets: 1.1.1(rollup@4.24.4) + tslib: 2.8.1 + typescript: 5.6.3 + + '@decky/ui@4.4.0': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} -snapshots: + '@jridgewell/set-array@1.2.1': {} - '@babel/code-frame@7.22.13': + '@jridgewell/source-map@0.3.6': dependencies: - '@babel/highlight': 7.22.20 - chalk: 2.4.2 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 - '@babel/helper-validator-identifier@7.22.20': {} + '@jridgewell/sourcemap-codec@1.4.15': {} - '@babel/highlight@7.22.20': + '@jridgewell/trace-mapping@0.3.25': dependencies: - '@babel/helper-validator-identifier': 7.22.20 - chalk: 2.4.2 - js-tokens: 4.0.0 + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/gen-mapping@0.3.1': + '@nodelib/fs.scandir@2.1.5': dependencies: - '@jridgewell/set-array': 1.1.1 - '@jridgewell/sourcemap-codec': 1.4.13 - '@jridgewell/trace-mapping': 0.3.13 + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 - '@jridgewell/resolve-uri@3.0.7': {} + '@nodelib/fs.stat@2.0.5': {} - '@jridgewell/set-array@1.1.1': {} - - '@jridgewell/source-map@0.3.2': + '@nodelib/fs.walk@1.2.8': dependencies: - '@jridgewell/gen-mapping': 0.3.1 - '@jridgewell/trace-mapping': 0.3.13 + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 - '@jridgewell/sourcemap-codec@1.4.13': {} + '@pkgjs/parseargs@0.11.0': + optional: true - '@jridgewell/trace-mapping@0.3.13': + '@rollup/plugin-alias@5.1.0(rollup@4.24.4)': dependencies: - '@jridgewell/resolve-uri': 3.0.7 - '@jridgewell/sourcemap-codec': 1.4.13 + slash: 4.0.0 + optionalDependencies: + rollup: 4.24.4 - '@rollup/plugin-commonjs@21.1.0(rollup@2.75.6)': + '@rollup/plugin-commonjs@26.0.1(rollup@4.24.4)': dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.75.6) + '@rollup/pluginutils': 5.1.0(rollup@4.24.4) commondir: 1.0.1 estree-walker: 2.0.2 - glob: 7.2.3 + glob: 10.4.1 is-reference: 1.2.1 - magic-string: 0.25.9 - resolve: 1.22.0 - rollup: 2.75.6 + magic-string: 0.30.10 + optionalDependencies: + rollup: 4.24.4 - '@rollup/plugin-json@4.1.0(rollup@2.75.6)': + '@rollup/plugin-json@6.1.0(rollup@4.24.4)': dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.75.6) - rollup: 2.75.6 + '@rollup/pluginutils': 5.1.0(rollup@4.24.4) + optionalDependencies: + rollup: 4.24.4 - '@rollup/plugin-node-resolve@13.3.0(rollup@2.75.6)': + '@rollup/plugin-node-resolve@15.2.3(rollup@4.24.4)': dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.75.6) - '@types/resolve': 1.17.1 - deepmerge: 4.2.2 - is-builtin-module: 3.1.0 + '@rollup/pluginutils': 5.1.0(rollup@4.24.4) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-builtin-module: 3.2.1 is-module: 1.0.0 - resolve: 1.22.0 - rollup: 2.75.6 - - '@rollup/plugin-replace@4.0.0(rollup@2.75.6)': - dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.75.6) - magic-string: 0.25.9 - rollup: 2.75.6 + resolve: 1.22.8 + optionalDependencies: + rollup: 4.24.4 - '@rollup/plugin-typescript@8.3.3(rollup@2.75.6)(tslib@2.4.0)(typescript@4.7.3)': + '@rollup/plugin-replace@5.0.7(rollup@4.24.4)': dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.75.6) - resolve: 1.22.0 - rollup: 2.75.6 - tslib: 2.4.0 - typescript: 4.7.3 + '@rollup/pluginutils': 5.1.0(rollup@4.24.4) + magic-string: 0.30.10 + optionalDependencies: + rollup: 4.24.4 - '@rollup/pluginutils@3.1.0(rollup@2.75.6)': + '@rollup/plugin-typescript@11.1.6(rollup@4.24.4)(tslib@2.8.1)(typescript@5.6.3)': dependencies: - '@types/estree': 0.0.39 - estree-walker: 1.0.1 - picomatch: 2.3.1 - rollup: 2.75.6 + '@rollup/pluginutils': 5.1.0(rollup@4.24.4) + resolve: 1.22.8 + typescript: 5.6.3 + optionalDependencies: + rollup: 4.24.4 + tslib: 2.8.1 - '@rollup/pluginutils@4.2.1': + '@rollup/pluginutils@5.1.0(rollup@4.24.4)': dependencies: + '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 + optionalDependencies: + rollup: 4.24.4 + + '@rollup/rollup-android-arm-eabi@4.24.4': + optional: true + + '@rollup/rollup-android-arm64@4.24.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.24.4': + optional: true + + '@rollup/rollup-darwin-x64@4.24.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.24.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.24.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.24.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.24.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.24.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.24.4': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.24.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.24.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.24.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.24.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.24.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.24.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.24.4': + optional: true - '@trysound/sax@0.2.0': {} + '@rollup/rollup-win32-x64-msvc@4.24.4': + optional: true '@types/color-convert@2.0.3': dependencies: - '@types/color-name': 1.1.3 + '@types/color-name': 1.1.4 - '@types/color-name@1.1.3': {} + '@types/color-name@1.1.4': {} '@types/color@3.0.6': dependencies: '@types/color-convert': 2.0.3 - '@types/cssnano@5.1.0(postcss@8.4.31)': + '@types/eslint-scope@3.7.7': dependencies: - cssnano: 5.1.15(postcss@8.4.31) - transitivePeerDependencies: - - postcss + '@types/eslint': 8.56.10 + '@types/estree': 1.0.5 - '@types/eslint-scope@3.7.3': + '@types/eslint@8.56.10': dependencies: - '@types/eslint': 8.4.3 - '@types/estree': 0.0.51 + '@types/estree': 1.0.5 + '@types/json-schema': 7.0.15 - '@types/eslint@8.4.3': - dependencies: - '@types/estree': 0.0.51 - '@types/json-schema': 7.0.11 + '@types/estree@1.0.5': {} - '@types/estree@0.0.39': {} + '@types/estree@1.0.6': {} - '@types/estree@0.0.51': {} + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 20.12.12 - '@types/json-schema@7.0.11': {} + '@types/json-schema@7.0.15': {} - '@types/lodash@4.14.201': {} + '@types/lodash@4.17.4': {} - '@types/node@17.0.42': {} + '@types/minimatch@5.1.2': {} - '@types/parse-json@4.0.2': {} + '@types/node@20.12.12': + dependencies: + undici-types: 5.26.5 - '@types/prop-types@15.7.5': {} + '@types/prop-types@15.7.12': {} '@types/react@16.14.0': dependencies: - '@types/prop-types': 15.7.5 - csstype: 3.1.0 + '@types/prop-types': 15.7.12 + csstype: 3.1.3 - '@types/resolve@1.17.1': - dependencies: - '@types/node': 17.0.42 + '@types/resolve@1.20.2': {} - '@types/webpack@5.28.0': + '@types/webpack@5.28.5': dependencies: - '@types/node': 17.0.42 + '@types/node': 20.12.12 tapable: 2.2.1 - webpack: 5.73.0 + webpack: 5.91.0 transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js - webpack-cli - '@webassemblyjs/ast@1.11.1': + '@webassemblyjs/ast@1.12.1': dependencies: - '@webassemblyjs/helper-numbers': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 + '@webassemblyjs/helper-numbers': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 - '@webassemblyjs/floating-point-hex-parser@1.11.1': {} + '@webassemblyjs/floating-point-hex-parser@1.11.6': {} - '@webassemblyjs/helper-api-error@1.11.1': {} + '@webassemblyjs/helper-api-error@1.11.6': {} - '@webassemblyjs/helper-buffer@1.11.1': {} + '@webassemblyjs/helper-buffer@1.12.1': {} - '@webassemblyjs/helper-numbers@1.11.1': + '@webassemblyjs/helper-numbers@1.11.6': dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.11.1 - '@webassemblyjs/helper-api-error': 1.11.1 + '@webassemblyjs/floating-point-hex-parser': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 '@xtuc/long': 4.2.2 - '@webassemblyjs/helper-wasm-bytecode@1.11.1': {} + '@webassemblyjs/helper-wasm-bytecode@1.11.6': {} - '@webassemblyjs/helper-wasm-section@1.11.1': + '@webassemblyjs/helper-wasm-section@1.12.1': dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-buffer': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - '@webassemblyjs/wasm-gen': 1.11.1 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-buffer': 1.12.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/wasm-gen': 1.12.1 - '@webassemblyjs/ieee754@1.11.1': + '@webassemblyjs/ieee754@1.11.6': dependencies: '@xtuc/ieee754': 1.2.0 - '@webassemblyjs/leb128@1.11.1': + '@webassemblyjs/leb128@1.11.6': dependencies: '@xtuc/long': 4.2.2 - '@webassemblyjs/utf8@1.11.1': {} + '@webassemblyjs/utf8@1.11.6': {} - '@webassemblyjs/wasm-edit@1.11.1': + '@webassemblyjs/wasm-edit@1.12.1': dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-buffer': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - '@webassemblyjs/helper-wasm-section': 1.11.1 - '@webassemblyjs/wasm-gen': 1.11.1 - '@webassemblyjs/wasm-opt': 1.11.1 - '@webassemblyjs/wasm-parser': 1.11.1 - '@webassemblyjs/wast-printer': 1.11.1 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-buffer': 1.12.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/helper-wasm-section': 1.12.1 + '@webassemblyjs/wasm-gen': 1.12.1 + '@webassemblyjs/wasm-opt': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + '@webassemblyjs/wast-printer': 1.12.1 - '@webassemblyjs/wasm-gen@1.11.1': + '@webassemblyjs/wasm-gen@1.12.1': dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - '@webassemblyjs/ieee754': 1.11.1 - '@webassemblyjs/leb128': 1.11.1 - '@webassemblyjs/utf8': 1.11.1 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 - '@webassemblyjs/wasm-opt@1.11.1': + '@webassemblyjs/wasm-opt@1.12.1': dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-buffer': 1.11.1 - '@webassemblyjs/wasm-gen': 1.11.1 - '@webassemblyjs/wasm-parser': 1.11.1 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-buffer': 1.12.1 + '@webassemblyjs/wasm-gen': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 - '@webassemblyjs/wasm-parser@1.11.1': + '@webassemblyjs/wasm-parser@1.12.1': dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-api-error': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - '@webassemblyjs/ieee754': 1.11.1 - '@webassemblyjs/leb128': 1.11.1 - '@webassemblyjs/utf8': 1.11.1 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-api-error': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 - '@webassemblyjs/wast-printer@1.11.1': + '@webassemblyjs/wast-printer@1.12.1': dependencies: - '@webassemblyjs/ast': 1.11.1 + '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} - acorn-import-assertions@1.8.0(acorn@8.7.1): + acorn-import-assertions@1.9.0(acorn@8.11.3): dependencies: - acorn: 8.7.1 + acorn: 8.11.3 + + acorn@8.11.3: {} - acorn@8.7.1: {} + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 ajv-keywords@3.5.2(ajv@6.12.6): dependencies: @@ -1326,61 +1388,56 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ansi-styles@3.2.1: + ansi-regex@5.0.1: {} + + ansi-regex@6.0.1: {} + + ansi-styles@4.3.0: dependencies: - color-convert: 1.9.3 + color-convert: 2.0.1 - balanced-match@1.0.2: {} + ansi-styles@6.2.1: {} + + array-union@2.1.0: {} - boolbase@1.0.0: {} + balanced-match@1.0.2: {} brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - browserslist@4.22.1: + brace-expansion@2.0.1: dependencies: - caniuse-lite: 1.0.30001562 - electron-to-chromium: 1.4.587 - node-releases: 2.0.13 - update-browserslist-db: 1.0.13(browserslist@4.22.1) - - buffer-from@1.1.2: {} - - builtin-modules@3.3.0: {} + balanced-match: 1.0.2 - callsites@3.1.0: {} + braces@3.0.3: + dependencies: + fill-range: 7.1.1 - caniuse-api@3.0.0: + browserslist@4.23.0: dependencies: - browserslist: 4.22.1 - caniuse-lite: 1.0.30001352 - lodash.memoize: 4.1.2 - lodash.uniq: 4.5.0 + caniuse-lite: 1.0.30001620 + electron-to-chromium: 1.4.775 + node-releases: 2.0.14 + update-browserslist-db: 1.0.16(browserslist@4.23.0) - caniuse-lite@1.0.30001352: {} + buffer-from@1.1.2: {} - caniuse-lite@1.0.30001562: {} + builtin-modules@3.3.0: {} - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 + caniuse-lite@1.0.30001620: {} chrome-trace-event@1.0.3: {} - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 + clean-stack@2.2.0: {} + + clsx@2.1.1: {} color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-name@1.1.3: {} - color-name@1.1.4: {} color-string@1.9.1: @@ -1393,137 +1450,53 @@ snapshots: color-convert: 2.0.1 color-string: 1.9.1 - colord@2.9.3: {} - commander@2.20.3: {} - commander@7.2.0: {} - commondir@1.0.1: {} concat-map@0.0.1: {} - cosmiconfig@7.1.0: - dependencies: - '@types/parse-json': 4.0.2 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - - css-declaration-sorter@6.4.1(postcss@8.4.31): + cross-spawn@7.0.3: dependencies: - postcss: 8.4.31 + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 - css-select@4.3.0: - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 4.3.1 - domutils: 2.8.0 - nth-check: 2.1.1 - - css-tree@1.1.3: - dependencies: - mdn-data: 2.0.14 - source-map: 0.6.1 - - css-what@6.1.0: {} - - cssesc@3.0.0: {} - - cssnano-preset-default@5.2.14(postcss@8.4.31): - dependencies: - css-declaration-sorter: 6.4.1(postcss@8.4.31) - cssnano-utils: 3.1.0(postcss@8.4.31) - postcss: 8.4.31 - postcss-calc: 8.2.4(postcss@8.4.31) - postcss-colormin: 5.3.1(postcss@8.4.31) - postcss-convert-values: 5.1.3(postcss@8.4.31) - postcss-discard-comments: 5.1.2(postcss@8.4.31) - postcss-discard-duplicates: 5.1.0(postcss@8.4.31) - postcss-discard-empty: 5.1.1(postcss@8.4.31) - postcss-discard-overridden: 5.1.0(postcss@8.4.31) - postcss-merge-longhand: 5.1.7(postcss@8.4.31) - postcss-merge-rules: 5.1.4(postcss@8.4.31) - postcss-minify-font-values: 5.1.0(postcss@8.4.31) - postcss-minify-gradients: 5.1.1(postcss@8.4.31) - postcss-minify-params: 5.1.4(postcss@8.4.31) - postcss-minify-selectors: 5.2.1(postcss@8.4.31) - postcss-normalize-charset: 5.1.0(postcss@8.4.31) - postcss-normalize-display-values: 5.1.0(postcss@8.4.31) - postcss-normalize-positions: 5.1.1(postcss@8.4.31) - postcss-normalize-repeat-style: 5.1.1(postcss@8.4.31) - postcss-normalize-string: 5.1.0(postcss@8.4.31) - postcss-normalize-timing-functions: 5.1.0(postcss@8.4.31) - postcss-normalize-unicode: 5.1.1(postcss@8.4.31) - postcss-normalize-url: 5.1.0(postcss@8.4.31) - postcss-normalize-whitespace: 5.1.1(postcss@8.4.31) - postcss-ordered-values: 5.1.3(postcss@8.4.31) - postcss-reduce-initial: 5.1.2(postcss@8.4.31) - postcss-reduce-transforms: 5.1.0(postcss@8.4.31) - postcss-svgo: 5.1.0(postcss@8.4.31) - postcss-unique-selectors: 5.1.1(postcss@8.4.31) + csstype@3.1.3: {} - cssnano-utils@3.1.0(postcss@8.4.31): - dependencies: - postcss: 8.4.31 - - cssnano@5.1.15(postcss@8.4.31): - dependencies: - cssnano-preset-default: 5.2.14(postcss@8.4.31) - lilconfig: 2.1.0 - postcss: 8.4.31 - yaml: 1.10.2 + deepmerge@4.3.1: {} - csso@4.2.0: + del@5.1.0: dependencies: - css-tree: 1.1.3 - - csstype@3.1.0: {} + globby: 10.0.2 + graceful-fs: 4.2.11 + is-glob: 4.0.3 + is-path-cwd: 2.2.0 + is-path-inside: 3.0.3 + p-map: 3.0.0 + rimraf: 3.0.2 + slash: 3.0.0 - decky-frontend-lib@3.25.0: {} - - decode-uri-component@0.2.2: {} - - deepmerge@4.2.2: {} - - dom-serializer@1.4.1: + dir-glob@3.0.1: dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - entities: 2.2.0 + path-type: 4.0.0 - domelementtype@2.3.0: {} + eastasianwidth@0.2.0: {} - domhandler@4.3.1: - dependencies: - domelementtype: 2.3.0 + electron-to-chromium@1.4.775: {} - domutils@2.8.0: - dependencies: - dom-serializer: 1.4.1 - domelementtype: 2.3.0 - domhandler: 4.3.1 + emoji-regex@8.0.0: {} - electron-to-chromium@1.4.587: {} + emoji-regex@9.2.2: {} - enhanced-resolve@5.9.3: + enhanced-resolve@5.16.1: dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 tapable: 2.2.1 - entities@2.2.0: {} - - error-ex@1.3.2: - dependencies: - is-arrayish: 0.2.1 - - es-module-lexer@0.9.3: {} - - escalade@3.1.1: {} + es-module-lexer@1.5.3: {} - escape-string-regexp@1.0.5: {} + escalade@3.1.2: {} eslint-scope@5.1.1: dependencies: @@ -1540,35 +1513,60 @@ snapshots: estree-walker@0.6.1: {} - estree-walker@1.0.1: {} - estree-walker@2.0.2: {} - eventemitter3@4.0.7: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.5 events@3.3.0: {} fast-deep-equal@3.1.3: {} + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.7 + fast-json-stable-stringify@2.1.0: {} - filter-obj@1.1.0: {} + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 - fs-extra@10.1.0: + foreground-child@3.2.1: dependencies: - graceful-fs: 4.2.10 - jsonfile: 6.1.0 - universalify: 2.0.1 + cross-spawn: 7.0.3 + signal-exit: 4.1.0 fs.realpath@1.0.0: {} fsevents@2.3.3: optional: true - function-bind@1.1.1: {} + function-bind@1.1.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 glob-to-regexp@0.4.1: {} + glob@10.4.1: + dependencies: + foreground-child: 3.2.1 + jackspeak: 3.4.0 + minimatch: 9.0.4 + minipass: 7.1.2 + path-scurry: 1.11.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -1578,24 +1576,28 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - graceful-fs@4.2.10: {} + globby@10.0.2: + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + glob: 7.2.3 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 - has-flag@3.0.0: {} + graceful-fs@4.2.11: {} has-flag@4.0.0: {} - has@1.0.3: + hasown@2.0.2: dependencies: - function-bind: 1.1.1 + function-bind: 1.1.2 - icss-utils@5.1.0(postcss@8.4.31): - dependencies: - postcss: 8.4.31 + ignore@5.3.1: {} - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 + indent-string@4.0.0: {} inflight@1.0.6: dependencies: @@ -1606,379 +1608,238 @@ snapshots: interpret@1.4.0: {} - is-arrayish@0.2.1: {} - is-arrayish@0.3.2: {} - is-builtin-module@3.1.0: + is-builtin-module@3.2.1: dependencies: builtin-modules: 3.3.0 - is-core-module@2.9.0: - dependencies: - has: 1.0.3 - - is-module@1.0.0: {} - - is-reference@1.2.1: - dependencies: - '@types/estree': 0.0.51 - - jest-worker@27.5.1: - dependencies: - '@types/node': 17.0.42 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - js-tokens@4.0.0: {} - - json-parse-even-better-errors@2.3.1: {} - - json-schema-traverse@0.4.1: {} - - jsonfile@6.1.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.10 - - lilconfig@2.1.0: {} - - lines-and-columns@1.2.4: {} - - loader-runner@4.3.0: {} - - lodash.memoize@4.1.2: {} - - lodash.uniq@4.5.0: {} - - lodash@4.17.21: {} - - magic-string@0.25.9: + is-core-module@2.13.1: dependencies: - sourcemap-codec: 1.4.8 - - mdn-data@2.0.14: {} + hasown: 2.0.2 - merge-stream@2.0.0: {} - - mime-db@1.52.0: {} + is-extglob@2.1.1: {} - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 + is-fullwidth-code-point@3.0.0: {} - minimatch@3.1.2: + is-glob@4.0.3: dependencies: - brace-expansion: 1.1.11 + is-extglob: 2.1.1 - minimist@1.2.6: {} - - nanoid@3.3.7: {} + is-module@1.0.0: {} - neo-async@2.6.2: {} + is-number@7.0.0: {} - node-releases@2.0.13: {} + is-path-cwd@2.2.0: {} - normalize-url@6.1.0: {} + is-path-inside@3.0.3: {} - nth-check@2.1.1: + is-reference@1.2.1: dependencies: - boolbase: 1.0.0 + '@types/estree': 1.0.5 - once@1.4.0: + is-reference@3.0.2: dependencies: - wrappy: 1.0.2 + '@types/estree': 1.0.5 - p-finally@1.0.0: {} - - p-queue@6.6.2: - dependencies: - eventemitter3: 4.0.7 - p-timeout: 3.2.0 + is-what@5.0.2: {} - p-timeout@3.2.0: - dependencies: - p-finally: 1.0.0 + isexe@2.0.0: {} - parent-module@1.0.1: + jackspeak@3.4.0: dependencies: - callsites: 3.1.0 + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 - parse-json@5.2.0: + jest-worker@27.5.1: dependencies: - '@babel/code-frame': 7.22.13 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - path-is-absolute@1.0.1: {} - - path-parse@1.0.7: {} + '@types/node': 20.12.12 + merge-stream: 2.0.0 + supports-color: 8.1.1 - path-type@4.0.0: {} + js-tokens@4.0.0: {} - picocolors@1.0.0: {} + json-parse-even-better-errors@2.3.1: {} - picomatch@2.3.1: {} + json-schema-traverse@0.4.1: {} - postcss-calc@8.2.4(postcss@8.4.31): - dependencies: - postcss: 8.4.31 - postcss-selector-parser: 6.0.13 - postcss-value-parser: 4.2.0 + loader-runner@4.3.0: {} - postcss-colormin@5.3.1(postcss@8.4.31): - dependencies: - browserslist: 4.22.1 - caniuse-api: 3.0.0 - colord: 2.9.3 - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + lodash@4.17.21: {} - postcss-convert-values@5.1.3(postcss@8.4.31): + loose-envify@1.4.0: dependencies: - browserslist: 4.22.1 - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + js-tokens: 4.0.0 - postcss-discard-comments@5.1.2(postcss@8.4.31): - dependencies: - postcss: 8.4.31 + lru-cache@10.2.2: {} - postcss-discard-duplicates@5.1.0(postcss@8.4.31): + magic-string@0.30.10: dependencies: - postcss: 8.4.31 + '@jridgewell/sourcemap-codec': 1.4.15 - postcss-discard-empty@5.1.1(postcss@8.4.31): + merge-anything@6.0.2: dependencies: - postcss: 8.4.31 + is-what: 5.0.2 - postcss-discard-overridden@5.1.0(postcss@8.4.31): - dependencies: - postcss: 8.4.31 + merge-stream@2.0.0: {} - postcss-merge-longhand@5.1.7(postcss@8.4.31): - dependencies: - postcss: 8.4.31 - postcss-value-parser: 4.2.0 - stylehacks: 5.1.1(postcss@8.4.31) + merge2@1.4.1: {} - postcss-merge-rules@5.1.4(postcss@8.4.31): + micromatch@4.0.7: dependencies: - browserslist: 4.22.1 - caniuse-api: 3.0.0 - cssnano-utils: 3.1.0(postcss@8.4.31) - postcss: 8.4.31 - postcss-selector-parser: 6.0.13 + braces: 3.0.3 + picomatch: 2.3.1 - postcss-minify-font-values@5.1.0(postcss@8.4.31): - dependencies: - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + mime-db@1.52.0: {} - postcss-minify-gradients@5.1.1(postcss@8.4.31): + mime-types@2.1.35: dependencies: - colord: 2.9.3 - cssnano-utils: 3.1.0(postcss@8.4.31) - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + mime-db: 1.52.0 - postcss-minify-params@5.1.4(postcss@8.4.31): + minimatch@3.1.2: dependencies: - browserslist: 4.22.1 - cssnano-utils: 3.1.0(postcss@8.4.31) - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + brace-expansion: 1.1.11 - postcss-minify-selectors@5.2.1(postcss@8.4.31): + minimatch@9.0.4: dependencies: - postcss: 8.4.31 - postcss-selector-parser: 6.0.13 + brace-expansion: 2.0.1 - postcss-modules-extract-imports@3.0.0(postcss@8.4.31): - dependencies: - postcss: 8.4.31 + minimist@1.2.8: {} - postcss-modules-local-by-default@4.0.3(postcss@8.4.31): - dependencies: - icss-utils: 5.1.0(postcss@8.4.31) - postcss: 8.4.31 - postcss-selector-parser: 6.0.13 - postcss-value-parser: 4.2.0 + minipass@7.1.2: {} - postcss-modules-scope@3.0.0(postcss@8.4.31): - dependencies: - postcss: 8.4.31 - postcss-selector-parser: 6.0.13 + neo-async@2.6.2: {} - postcss-modules-values@4.0.0(postcss@8.4.31): - dependencies: - icss-utils: 5.1.0(postcss@8.4.31) - postcss: 8.4.31 + node-releases@2.0.14: {} - postcss-normalize-charset@5.1.0(postcss@8.4.31): + once@1.4.0: dependencies: - postcss: 8.4.31 + wrappy: 1.0.2 - postcss-normalize-display-values@5.1.0(postcss@8.4.31): + p-map@3.0.0: dependencies: - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + aggregate-error: 3.1.0 - postcss-normalize-positions@5.1.1(postcss@8.4.31): - dependencies: - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + path-is-absolute@1.0.1: {} - postcss-normalize-repeat-style@5.1.1(postcss@8.4.31): - dependencies: - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + path-key@3.1.1: {} - postcss-normalize-string@5.1.0(postcss@8.4.31): - dependencies: - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + path-parse@1.0.7: {} - postcss-normalize-timing-functions@5.1.0(postcss@8.4.31): + path-scurry@1.11.1: dependencies: - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + lru-cache: 10.2.2 + minipass: 7.1.2 - postcss-normalize-unicode@5.1.1(postcss@8.4.31): - dependencies: - browserslist: 4.22.1 - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + path-type@4.0.0: {} - postcss-normalize-url@5.1.0(postcss@8.4.31): - dependencies: - normalize-url: 6.1.0 - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + picocolors@1.0.1: {} - postcss-normalize-whitespace@5.1.1(postcss@8.4.31): - dependencies: - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + picomatch@2.3.1: {} - postcss-ordered-values@5.1.3(postcss@8.4.31): - dependencies: - cssnano-utils: 3.1.0(postcss@8.4.31) - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + punycode@2.3.1: {} - postcss-reduce-initial@5.1.2(postcss@8.4.31): - dependencies: - browserslist: 4.22.1 - caniuse-api: 3.0.0 - postcss: 8.4.31 + queue-microtask@1.2.3: {} - postcss-reduce-transforms@5.1.0(postcss@8.4.31): + randombytes@2.1.0: dependencies: - postcss: 8.4.31 - postcss-value-parser: 4.2.0 + safe-buffer: 5.2.1 - postcss-selector-parser@6.0.13: + react-icons@4.12.0(react@18.3.1): dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 + react: 18.3.1 - postcss-svgo@5.1.0(postcss@8.4.31): + react@18.3.1: dependencies: - postcss: 8.4.31 - postcss-value-parser: 4.2.0 - svgo: 2.8.0 + loose-envify: 1.4.0 - postcss-unique-selectors@5.1.1(postcss@8.4.31): + rechoir@0.6.2: dependencies: - postcss: 8.4.31 - postcss-selector-parser: 6.0.13 + resolve: 1.22.8 - postcss-value-parser@4.2.0: {} + regenerator-runtime@0.14.1: {} - postcss@8.4.31: + resolve@1.22.8: dependencies: - nanoid: 3.3.7 - picocolors: 1.0.0 - source-map-js: 1.0.2 - - punycode@2.1.1: {} + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 - query-string@7.1.3: - dependencies: - decode-uri-component: 0.2.2 - filter-obj: 1.1.0 - split-on-first: 1.1.0 - strict-uri-encode: 2.0.0 + reusify@1.0.4: {} - randombytes@2.1.0: + rimraf@3.0.2: dependencies: - safe-buffer: 5.2.1 - - react-icons@4.12.0: {} + glob: 7.2.3 - rechoir@0.6.2: + rollup-plugin-delete@2.0.0: dependencies: - resolve: 1.22.0 + del: 5.1.0 - resolve-from@4.0.0: {} - - resolve@1.22.0: + rollup-plugin-external-globals@0.11.0(rollup@4.24.4): dependencies: - is-core-module: 2.9.0 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 + '@rollup/pluginutils': 5.1.0(rollup@4.24.4) + estree-walker: 3.0.3 + is-reference: 3.0.2 + magic-string: 0.30.10 + rollup: 4.24.4 - rollup-plugin-import-assets@1.1.1(rollup@2.75.6): + rollup-plugin-import-assets@1.1.1(rollup@4.24.4): dependencies: - rollup: 2.75.6 + rollup: 4.24.4 rollup-pluginutils: 2.8.2 url-join: 4.0.1 - rollup-plugin-styles@4.0.0(rollup@2.75.6): - dependencies: - '@rollup/pluginutils': 4.2.1 - '@types/cssnano': 5.1.0(postcss@8.4.31) - cosmiconfig: 7.1.0 - cssnano: 5.1.15(postcss@8.4.31) - fs-extra: 10.1.0 - icss-utils: 5.1.0(postcss@8.4.31) - mime-types: 2.1.35 - p-queue: 6.6.2 - postcss: 8.4.31 - postcss-modules-extract-imports: 3.0.0(postcss@8.4.31) - postcss-modules-local-by-default: 4.0.3(postcss@8.4.31) - postcss-modules-scope: 3.0.0(postcss@8.4.31) - postcss-modules-values: 4.0.0(postcss@8.4.31) - postcss-value-parser: 4.2.0 - query-string: 7.1.3 - resolve: 1.22.0 - rollup: 2.75.6 - source-map-js: 1.0.2 - tslib: 2.4.0 - rollup-pluginutils@2.8.2: dependencies: estree-walker: 0.6.1 - rollup@2.75.6: + rollup@4.24.4: + dependencies: + '@types/estree': 1.0.6 optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.24.4 + '@rollup/rollup-android-arm64': 4.24.4 + '@rollup/rollup-darwin-arm64': 4.24.4 + '@rollup/rollup-darwin-x64': 4.24.4 + '@rollup/rollup-freebsd-arm64': 4.24.4 + '@rollup/rollup-freebsd-x64': 4.24.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.24.4 + '@rollup/rollup-linux-arm-musleabihf': 4.24.4 + '@rollup/rollup-linux-arm64-gnu': 4.24.4 + '@rollup/rollup-linux-arm64-musl': 4.24.4 + '@rollup/rollup-linux-powerpc64le-gnu': 4.24.4 + '@rollup/rollup-linux-riscv64-gnu': 4.24.4 + '@rollup/rollup-linux-s390x-gnu': 4.24.4 + '@rollup/rollup-linux-x64-gnu': 4.24.4 + '@rollup/rollup-linux-x64-musl': 4.24.4 + '@rollup/rollup-win32-arm64-msvc': 4.24.4 + '@rollup/rollup-win32-ia32-msvc': 4.24.4 + '@rollup/rollup-win32-x64-msvc': 4.24.4 fsevents: 2.3.3 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safe-buffer@5.2.1: {} - schema-utils@3.1.1: + schema-utils@3.3.0: dependencies: - '@types/json-schema': 7.0.11 + '@types/json-schema': 7.0.15 ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) - serialize-javascript@6.0.0: + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + shelljs@0.8.5: dependencies: glob: 7.2.3 @@ -1987,14 +1848,18 @@ snapshots: shx@0.3.4: dependencies: - minimist: 1.2.6 + minimist: 1.2.8 shelljs: 0.8.5 + signal-exit@4.1.0: {} + simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 - source-map-js@1.0.2: {} + slash@3.0.0: {} + + slash@4.0.0: {} source-map-support@0.5.21: dependencies: @@ -2003,23 +1868,25 @@ snapshots: source-map@0.6.1: {} - sourcemap-codec@1.4.8: {} - - split-on-first@1.1.0: {} - - stable@0.1.8: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 - strict-uri-encode@2.0.0: {} + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 - stylehacks@5.1.1(postcss@8.4.31): + strip-ansi@6.0.1: dependencies: - browserslist: 4.22.1 - postcss: 8.4.31 - postcss-selector-parser: 6.0.13 + ansi-regex: 5.0.1 - supports-color@5.5.0: + strip-ansi@7.1.0: dependencies: - has-flag: 3.0.0 + ansi-regex: 6.0.1 supports-color@8.1.1: dependencies: @@ -2027,92 +1894,115 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svgo@2.8.0: + tailwind-merge@2.3.0: dependencies: - '@trysound/sax': 0.2.0 - commander: 7.2.0 - css-select: 4.3.0 - css-tree: 1.1.3 - csso: 4.2.0 - picocolors: 1.0.0 - stable: 0.1.8 + '@babel/runtime': 7.24.5 tapable@2.2.1: {} - terser-webpack-plugin@5.3.3(webpack@5.73.0): + terser-webpack-plugin@5.3.10(webpack@5.91.0): dependencies: - '@jridgewell/trace-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 - schema-utils: 3.1.1 - serialize-javascript: 6.0.0 - terser: 5.14.1 - webpack: 5.73.0 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.31.0 + webpack: 5.91.0 - terser@5.14.1: + terser@5.31.0: dependencies: - '@jridgewell/source-map': 0.3.2 - acorn: 8.7.1 + '@jridgewell/source-map': 0.3.6 + acorn: 8.11.3 commander: 2.20.3 source-map-support: 0.5.21 - tslib@2.4.0: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tslib@2.6.2: {} - typescript@4.7.3: {} + tslib@2.8.1: {} - universalify@2.0.1: {} + typescript@5.6.3: {} - update-browserslist-db@1.0.13(browserslist@4.22.1): + undici-types@5.26.5: {} + + update-browserslist-db@1.0.16(browserslist@4.23.0): dependencies: - browserslist: 4.22.1 - escalade: 3.1.1 - picocolors: 1.0.0 + browserslist: 4.23.0 + escalade: 3.1.2 + picocolors: 1.0.1 uri-js@4.4.1: dependencies: - punycode: 2.1.1 + punycode: 2.3.1 url-join@4.0.1: {} - util-deprecate@1.0.2: {} + use-sync-external-store@1.2.0(react@18.3.1): + dependencies: + react: 18.3.1 - watchpack@2.4.0: + watchpack@2.4.1: dependencies: glob-to-regexp: 0.4.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 webpack-sources@3.2.3: {} - webpack@5.73.0: + webpack@5.91.0: dependencies: - '@types/eslint-scope': 3.7.3 - '@types/estree': 0.0.51 - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/wasm-edit': 1.11.1 - '@webassemblyjs/wasm-parser': 1.11.1 - acorn: 8.7.1 - acorn-import-assertions: 1.8.0(acorn@8.7.1) - browserslist: 4.22.1 + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.5 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/wasm-edit': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + acorn: 8.11.3 + acorn-import-assertions: 1.9.0(acorn@8.11.3) + browserslist: 4.23.0 chrome-trace-event: 1.0.3 - enhanced-resolve: 5.9.3 - es-module-lexer: 0.9.3 + enhanced-resolve: 5.16.1 + es-module-lexer: 1.5.3 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 loader-runner: 4.3.0 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 3.1.1 + schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.3(webpack@5.73.0) - watchpack: 2.4.0 + terser-webpack-plugin: 5.3.10(webpack@5.91.0) + watchpack: 2.4.1 webpack-sources: 3.2.3 transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} - yaml@1.10.2: {} + zustand@4.5.2(@types/react@16.14.0)(react@18.3.1): + dependencies: + use-sync-external-store: 1.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 16.14.0 + react: 18.3.1 diff --git a/rollup.config.js b/rollup.config.js index a0bcc37..5055b43 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,41 +1,18 @@ -import commonjs from "@rollup/plugin-commonjs"; -import json from "@rollup/plugin-json"; -import { nodeResolve } from "@rollup/plugin-node-resolve"; -import replace from "@rollup/plugin-replace"; -import typescript from "@rollup/plugin-typescript"; -import { defineConfig } from "rollup"; -import importAssets from "rollup-plugin-import-assets"; -import styles from "rollup-plugin-styles"; +import deckyPlugin from "@decky/rollup"; +import alias from "@rollup/plugin-alias"; -import { name } from "./plugin.json"; - -export default defineConfig({ - input: "./src/index.tsx", +export default deckyPlugin({ plugins: [ - commonjs(), - nodeResolve(), - typescript(), - json(), - styles(), - replace({ - preventAssignment: false, - "process.env.NODE_ENV": JSON.stringify("production"), - }), - importAssets({ - publicPath: `http://127.0.0.1:1337/plugins/${name}/`, + alias({ + entries: [ + { find: "@cssloader/backend", replacement: `${import.meta.dirname}//src/backend` }, + { find: "@/backend", replacement: `${import.meta.dirname}/src/backend-impl` }, + { find: "@/lib", replacement: `${import.meta.dirname}/src/lib` }, + { find: "@/styles", replacement: `${import.meta.dirname}/src/styles` }, + { find: "@/types", replacement: `${import.meta.dirname}/src/types` }, + { find: "@/modules", replacement: `${import.meta.dirname}/src/modules` }, + { find: "@/decky-patches", replacement: `${import.meta.dirname}/src/decky-patches` }, + ], }), ], - context: "window", - external: ["react", "react-dom", "decky-frontend-lib"], - output: { - file: "dist/index.js", - globals: { - react: "SP_REACT", - "react-dom": "SP_REACTDOM", - "decky-frontend-lib": "DFL", - }, - format: "iife", - exports: "default", - assetFileNames: "[name]-[hash][extname]", - }, }); diff --git a/src/api.ts b/src/api.ts deleted file mode 100644 index dd2a9d7..0000000 --- a/src/api.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { ServerAPI } from "decky-frontend-lib"; -import { CssLoaderState } from "./state"; -import { toast, storeWrite, downloadThemeFromUrl, reloadBackend } from "./python"; -import { ThemeQueryRequest } from "./apiTypes"; -import { generateParamStr } from "./logic"; - -var server: ServerAPI | undefined = undefined; -var globalState: CssLoaderState | undefined = undefined; - -export function setServer(s: ServerAPI): void { - server = s; -} -export function setStateClass(s: CssLoaderState): void { - globalState = s; -} - -export function logOut(): void { - const setGlobalState = globalState!.setGlobalState.bind(globalState); - setGlobalState("apiShortToken", ""); - setGlobalState("apiFullToken", ""); - setGlobalState("apiTokenExpireDate", undefined); - setGlobalState("apiMeData", undefined); - storeWrite("shortToken", ""); -} - -export async function logInWithShortToken( - shortTokenInterimValue?: string | undefined -): Promise { - const { apiUrl, apiShortToken } = globalState!.getPublicState(); - const shortTokenValue = shortTokenInterimValue ? shortTokenInterimValue : apiShortToken; - const setGlobalState = globalState!.setGlobalState.bind(globalState); - if (shortTokenValue.length === 12) { - return server! - .fetchNoCors(`${apiUrl}/auth/authenticate_token`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ token: shortTokenValue }), - }) - .then((deckyRes) => { - if (deckyRes.success) { - return deckyRes.result; - } - throw new Error(`Fetch not successful!`); - }) - .then((res) => { - // @ts-ignore - return JSON.parse(res?.body || ""); - }) - .then((json) => { - if (json) { - return json; - } - throw new Error(`No json returned!`); - }) - .then((data) => { - if (data && data?.token) { - storeWrite("shortToken", shortTokenValue); - setGlobalState("apiShortToken", shortTokenValue); - setGlobalState("apiFullToken", data.token); - setGlobalState("apiTokenExpireDate", new Date().valueOf() + 1000 * 60 * 10); - genericGET(`/auth/me`, true, data.token).then((meData) => { - if (meData?.username) { - setGlobalState("apiMeData", meData); - toast("Logged In!", `Logged in as ${meData.username}`); - } - }); - } else { - toast("Error Authenticating", JSON.stringify(data)); - } - }) - .catch((err) => { - console.error(`Error authenticating from short token.`, err); - }); - } else { - toast("Invalid Token", "Token must be 12 characters long."); - } -} - -// This returns the token that is intended to be used in whatever call -export function refreshToken(onError: () => void = () => {}): Promise { - const { apiFullToken, apiTokenExpireDate, apiUrl } = globalState!.getPublicState(); - const setGlobalState = globalState!.setGlobalState.bind(globalState); - if (!apiFullToken) { - return Promise.resolve(undefined); - } - if (apiTokenExpireDate === undefined) { - return Promise.resolve(apiFullToken); - } - // @ts-ignore - if (new Date().valueOf() < apiTokenExpireDate) { - return Promise.resolve(apiFullToken); - } - return server! - .fetchNoCors(`${apiUrl}/auth/refresh_token`, { - method: "POST", - headers: { - Authorization: `Bearer ${apiFullToken}`, - }, - }) - .then((deckyRes) => { - if (deckyRes.success) { - return deckyRes.result; - } - throw new Error(`Fetch not successful!`); - }) - .then((res) => { - if (res.status >= 200 && res.status <= 300 && res.body) { - // @ts-ignore - return JSON.parse(res.body || ""); - } - throw new Error(`Res not OK!, code ${res.status}`); - }) - .then((json) => { - if (json.token) { - return json.token; - } - throw new Error(`No token returned!`); - }) - .then((token) => { - setGlobalState("apiFullToken", token); - setGlobalState("apiTokenExpireDate", new Date().valueOf() + 1000 * 10 * 60); - return token; - }) - .catch((err) => { - console.error(`Error Refreshing Token!`, err); - onError(); - }); -} - -export async function genericGET( - fetchPath: string, - requiresAuth: boolean = false, - customAuthToken: string | undefined = undefined, - onError: () => void = () => {}, - failSilently: boolean = false -) { - const { apiUrl } = globalState!.getPublicState(); - function doTheFetching(authToken: string | undefined = undefined) { - return server! - .fetchNoCors(`${apiUrl}${fetchPath}`, { - method: "GET", - headers: authToken - ? { - Authorization: `Bearer ${authToken}`, - } - : {}, - }) - .then((deckyRes) => { - if (deckyRes.success) { - return deckyRes.result; - } - throw new Error(`Fetch not successful!`); - }) - .then((res) => { - if (res.status >= 200 && res.status <= 300 && res.body) { - // @ts-ignore - return JSON.parse(res.body || ""); - } - throw new Error(`Res not OK!, code ${res.status}`); - }) - .then((json) => { - if (json) { - return json; - } - throw new Error(`No json returned!`); - }) - .catch((err) => { - if (!failSilently) { - console.error(`Error fetching ${fetchPath}`, err); - } - onError(); - }); - } - if (requiresAuth) { - if (customAuthToken) { - return doTheFetching(customAuthToken); - } - return refreshToken(onError).then((token) => { - if (token) { - return doTheFetching(token); - } else { - toast("Error Refreshing Token!", ""); - return; - } - }); - } else { - return doTheFetching(); - } -} - -export function getThemes( - searchOpts: ThemeQueryRequest, - apiPath: string, - globalStateVarName: string, - setSnapIndex: (i: number) => void, - requiresAuth: boolean = false -) { - const setGlobalState = globalState!.setGlobalState.bind(globalState); - // TODO: Refactor, this works now, just jank - const prependString = - // If the user searches for desktop themes, show desktop themes, otherwise only show BPM themes - (searchOpts.filters.includes("Desktop") - ? "-Preset" - : // If the user searches for presets, show presets, otherwise exclude them - searchOpts.filters === "Preset" - ? "BPM-CSS" - : "BPM-CSS.-Preset") + - // If there are other filters after the prepend, add a ".", otherwise don't - (searchOpts.filters !== "All" ? "." : ""); - - const queryStr = generateParamStr( - searchOpts.filters !== "All" ? searchOpts : { ...searchOpts, filters: "" }, - prependString - ); - genericGET(`${apiPath}${queryStr}`, requiresAuth).then((data) => { - if (data.total > 0) { - setGlobalState(globalStateVarName, data); - } else { - setGlobalState(globalStateVarName, { total: 0, items: [] }); - } - setSnapIndex(-1); - }); -} - -export function toggleStar(themeId: string, isStarred: boolean, authToken: string) { - const { apiUrl } = globalState!.getPublicState(); - return server! - .fetchNoCors(`${apiUrl}/users/me/stars/${themeId}`, { - method: isStarred ? "DELETE" : "POST", - headers: { - Authorization: `Bearer ${authToken}`, - }, - }) - .then((deckyRes) => { - if (deckyRes.success) { - return deckyRes.result; - } - throw new Error(`Fetch not successful!`); - }) - .then((res) => { - if (res.status >= 200 && res.status <= 300) { - // @ts-ignore - return true; - } - throw new Error(`Res not OK!, code ${res.status}`); - }) - .catch((err) => { - console.error(`Error starring theme`, err); - }); -} - -export async function installTheme(themeId: string) { - const setGlobalState = globalState!.setGlobalState.bind(globalState); - setGlobalState("isInstalling", true); - await downloadThemeFromUrl(themeId); - await reloadBackend(); - setGlobalState("isInstalling", false); - return; -} diff --git a/src/apiTypes/index.ts b/src/apiTypes/index.ts deleted file mode 100644 index f83d273..0000000 --- a/src/apiTypes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./CSSThemeTypes"; -export * from "./AccountData"; -export * from "./BlobTypes"; diff --git a/src/backend-impl/decky-backend-repository-impl.ts b/src/backend-impl/decky-backend-repository-impl.ts new file mode 100644 index 0000000..b14341c --- /dev/null +++ b/src/backend-impl/decky-backend-repository-impl.ts @@ -0,0 +1,43 @@ +import { callable, toaster, fetchNoCors } from "@decky/api"; +import { CallError, FetchError, type IBackendRepository } from "@cssloader/backend"; + +class DeckyBackendRepository implements IBackendRepository { + async call(methodName: string, args: Args) { + const func = callable(methodName); + try { + return await func(...args); + } catch (error: unknown) { + throw new CallError( + "Error Calling Backend", + methodName, + error instanceof Error ? error.message : "No Error Message Provided" + ); + } + } + async fetch(url: string, request: RequestInit, mode: "json" | "text" = "json") { + try { + console.debug("CSSLOADER FETCH", url, request); + // TODO: Think this is a decky types issue + // @ts-ignore + const res = await fetchNoCors(url, request); + if (!res.ok) { + throw new Error(`Res Not Okay - Code ${res.status}`); + } + if (mode === "text") { + return res.text() as Return; + } + return res.json() as Return; + } catch (error: unknown) { + throw new FetchError( + "Error Fetching", + url, + error instanceof Error ? error.message : "No Error Message Provided" + ); + } + } + toast(title: string, body?: string) { + toaster.toast({ title: title, body: body ?? "", duration: 5000 }); + } +} + +export default DeckyBackendRepository; diff --git a/src/backend-impl/decky-backend-service.ts b/src/backend-impl/decky-backend-service.ts new file mode 100644 index 0000000..c228d89 --- /dev/null +++ b/src/backend-impl/decky-backend-service.ts @@ -0,0 +1,4 @@ +import { Backend } from "@cssloader/backend"; +import DeckyBackendRepository from "./decky-backend-repository-impl"; + +export const backend = Backend.getInstance(new DeckyBackendRepository()); diff --git a/src/backend-impl/decky-theme-store.ts b/src/backend-impl/decky-theme-store.ts new file mode 100644 index 0000000..7e45a58 --- /dev/null +++ b/src/backend-impl/decky-theme-store.ts @@ -0,0 +1,26 @@ +import { + CSSLoaderStateActions, + CSSLoaderStateValues, + ICSSLoaderState, + createCSSLoaderStore, +} from "@cssloader/backend"; +import { backend } from "./decky-backend-service"; +import { useStore } from "zustand"; + +export const cssLoaderStore = createCSSLoaderStore(backend); + +const useCSSLoaderStore = (fn: (state: ICSSLoaderState) => any) => useStore(cssLoaderStore, fn); + +export const useCSSLoaderValue = ( + key: T +): ICSSLoaderState[T] => useCSSLoaderStore((state) => state[key]); + +export const useCSSLoaderAction = ( + key: T +): ICSSLoaderState[T] => useCSSLoaderStore((state) => state[key]); + +export const getCSSLoaderState = () => cssLoaderStore.getState(); +export const setCSSLoaderState = ( + key: T, + value: ICSSLoaderState[T] +) => cssLoaderStore.setState({ [key]: value }); diff --git a/src/backend-impl/index.ts b/src/backend-impl/index.ts new file mode 100644 index 0000000..6a8f304 --- /dev/null +++ b/src/backend-impl/index.ts @@ -0,0 +1,2 @@ +export * from "./decky-theme-store"; +export * from "./decky-backend-service"; diff --git a/src/backend-impl/readme.md b/src/backend-impl/readme.md new file mode 100644 index 0000000..e4593b1 --- /dev/null +++ b/src/backend-impl/readme.md @@ -0,0 +1,6 @@ +# CSS Loader Decky Backend + +The backend folder is meant to be treated as an 'npm package' or external lib, +it contains agnostic code that can be used on both decky and desktop. + +`backend-impl` contains re-exports of the backend code that use deck specific repositories diff --git a/src/backend/backendHelpers/index.ts b/src/backend/backendHelpers/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/backend/backendHelpers/toggleTheme.tsx b/src/backend/backendHelpers/toggleTheme.tsx deleted file mode 100644 index 7aa12e0..0000000 --- a/src/backend/backendHelpers/toggleTheme.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Dispatch, SetStateAction } from "react"; -import { Flags, Theme } from "../../ThemeTypes"; -import * as python from "../../python"; -import { OptionalDepsModalRoot } from "../../components"; -import { showModal } from "decky-frontend-lib"; -import { enableNavPatch } from "../../deckyPatches/NavPatch"; -import { NavPatchInfoModalRoot } from "../../deckyPatches/NavPatchInfoModal"; - -// rerender and setCollapsed only apply to the QAM list version of the ThemeToggle, not the one in the fullscreen 'Your Themes' modal -export async function toggleTheme( - data: Theme, - enabled: boolean, - rerender: () => void = () => {}, - setCollapsed: Dispatch> = () => {} -) { - const { selectedPreset, navPatchInstance } = python.globalState!.getPublicState(); - - // Optional Deps Themes - if (enabled && data.flags.includes(Flags.optionalDeps)) { - showModal(); - rerender && rerender(); - } else { - // Actually enabling the theme - await python.setThemeState(data.name, enabled); - await python.getInstalledThemes(); - } - - // Re-collapse menu - setCollapsed && setCollapsed(true); - - // Dependency Toast - if (data.dependencies.length > 0) { - if (enabled) { - python.toast( - `${data.display_name} enabled other themes`, - // This lists out the themes by name, but often overflowed off screen - // @ts-ignore - // `${new Intl.ListFormat().format(data.dependencies)} ${ - // data.dependencies.length > 1 ? "are" : "is" - // } required for this theme` - // This just gives the number of themes - `${ - data.dependencies.length === 1 - ? `1 other theme is required by ${data.display_name}` - : `${data.dependencies.length} other themes are required by ${data.display_name}` - }` - ); - } - if (!enabled && !data.flags.includes(Flags.dontDisableDeps)) { - python.toast( - `${data.display_name} disabled other themes`, - `${ - data.dependencies.length === 1 - ? `1 theme was originally enabled by ${data.display_name}` - : `${data.dependencies.length} themes were originally enabled by ${data.display_name}` - }` - ); - } - } - - // Nav Patch - if (enabled && data.flags.includes(Flags.navPatch) && !navPatchInstance) { - showModal(); - } - - // Preset Updating - if (!selectedPreset) return; - // Fetch this here so that the data is up to date - const { localThemeList } = python.globalState!.getPublicState(); - - // This is copied from the desktop codebase - await python.generatePresetFromThemeNames( - selectedPreset.name, - localThemeList.filter((e) => e.enabled && !e.flags.includes(Flags.isPreset)).map((e) => e.name) - ); - // Getting the new data for the preset - await python.getInstalledThemes(); -} diff --git a/src/backend/errors/call-error.ts b/src/backend/errors/call-error.ts new file mode 100644 index 0000000..392e136 --- /dev/null +++ b/src/backend/errors/call-error.ts @@ -0,0 +1,20 @@ +export class CallError extends Error { + private title: string; + private route: string; + private body: string; + + constructor(title: string, route: string, body: string) { + super(body); + this.title = title; + this.route = route; + this.body = body; + } + + getError() { + return { + title: this.title, + route: this.route, + body: this.body, + }; + } +} diff --git a/src/backend/errors/fetch-error.ts b/src/backend/errors/fetch-error.ts new file mode 100644 index 0000000..420ee58 --- /dev/null +++ b/src/backend/errors/fetch-error.ts @@ -0,0 +1,20 @@ +export class FetchError extends Error { + private title: string; + private route: string; + private body: string; + + constructor(title: string, route: string, body: string) { + super(body); + this.title = title; + this.route = route; + this.body = body; + } + + getError() { + return { + title: this.title, + route: this.route, + body: this.body, + }; + } +} diff --git a/src/backend/errors/index.ts b/src/backend/errors/index.ts new file mode 100644 index 0000000..5751e30 --- /dev/null +++ b/src/backend/errors/index.ts @@ -0,0 +1,2 @@ +export * from "./call-error"; +export * from "./fetch-error"; diff --git a/src/backend/index.ts b/src/backend/index.ts new file mode 100644 index 0000000..4f46195 --- /dev/null +++ b/src/backend/index.ts @@ -0,0 +1,4 @@ +export * from "./errors"; +export * from "./state"; +export * from "./repositories"; +export * from "./services"; diff --git a/src/backend/pythonMethods/pluginSettingsMethods.ts b/src/backend/pythonMethods/pluginSettingsMethods.ts deleted file mode 100644 index 20f35bf..0000000 --- a/src/backend/pythonMethods/pluginSettingsMethods.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { storeRead, toast } from "../../python"; -import { server, globalState } from "../pythonRoot"; -import { booleanStoreRead, stringStoreRead } from "./storeUtils"; - -export function enableServer() { - return server!.callPluginMethod("enable_server", {}); -} - -export async function getServerState() { - const deckyRes = await server!.callPluginMethod<{}, boolean>("get_server_state", {}); - if (!deckyRes.success) { - toast("Error fetching server state", deckyRes.result); - return false; - } - return deckyRes.result; -} - -export async function getWatchState() { - const deckyRes = await server!.callPluginMethod<{}, boolean>("get_watch_state", {}); - if (!deckyRes.success) { - toast("Error fetching watch state", deckyRes.result); - return false; - } - return deckyRes.result; -} - -export async function getBetaTranslationsState() { - return stringStoreRead("beta_translations"); -} - -export function toggleWatchState(bool: boolean, onlyThisSession: boolean = false) { - return server!.callPluginMethod<{ enable: boolean; only_this_session: boolean }, void>( - "toggle_watch_state", - { - enable: bool, - only_this_session: onlyThisSession, - } - ); -} - -// Todo: when i rewrite store interop, move this -export function setHiddenMotd(id: string) { - return server!.callPluginMethod<{ key: string; val: string }>("store_write", { - key: "hiddenMotd", - val: id, - }); -} -export function getHiddenMotd() { - return server!.callPluginMethod<{ key: string }, string>("store_read", { - key: "hiddenMotd", - }); -} - -export function fetchClassMappings() { - return server!.callPluginMethod<{}>("fetch_class_mappings", {}); -} diff --git a/src/backend/pythonMethods/storeUtils.ts b/src/backend/pythonMethods/storeUtils.ts deleted file mode 100644 index 585e75a..0000000 --- a/src/backend/pythonMethods/storeUtils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { server, toast } from "../../python"; - -export async function booleanStoreRead(key: string) { - const deckyRes = await server!.callPluginMethod<{ key: string }, string>("store_read", { - key, - }); - if (!deckyRes.success) { - toast(`Error fetching ${key}`, deckyRes.result); - return false; - } - return deckyRes.result === "1" || deckyRes.result === "true"; -} - -export async function booleanStoreWrite(key: string, value: boolean) { - const deckyRes = await server!.callPluginMethod<{ key: string; val: string }>("store_write", { - key, - val: value ? "1" : "0", - }); - if (!deckyRes.success) { - toast(`Error setting ${key}`, deckyRes.result); - } -} - -export async function stringStoreRead(key: string) { - const deckyRes = await server!.callPluginMethod<{ key: string }, string>("store_read", { - key, - }); - if (!deckyRes.success) { - toast(`Error fetching ${key}`, deckyRes.result); - return ""; - } - return deckyRes.result; -} -export async function stringStoreWrite(key: string, value: string) { - const deckyRes = await server!.callPluginMethod<{ key: string; val: string }>("store_write", { - key, - val: value, - }); - if (!deckyRes.success) { - toast(`Error setting ${key}`, deckyRes.result); - } -} diff --git a/src/backend/pythonRoot.ts b/src/backend/pythonRoot.ts deleted file mode 100644 index d2edb2c..0000000 --- a/src/backend/pythonRoot.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { server } from "../python"; -import { globalState } from "../python"; - -export { server, globalState }; diff --git a/src/backend/repositories/backend-repository.ts b/src/backend/repositories/backend-repository.ts new file mode 100644 index 0000000..e1433a2 --- /dev/null +++ b/src/backend/repositories/backend-repository.ts @@ -0,0 +1,5 @@ +export interface IBackendRepository { + call: (methodName: string, args: Args) => Promise; + toast: (title: string, body?: string) => void; + fetch: (url: string, request: RequestInit, mode?: "json" | "text") => Promise; +} diff --git a/src/backend/repositories/index.ts b/src/backend/repositories/index.ts new file mode 100644 index 0000000..dcaa08f --- /dev/null +++ b/src/backend/repositories/index.ts @@ -0,0 +1 @@ +export * from "./backend-repository"; diff --git a/src/backend/services/backend-service.ts b/src/backend/services/backend-service.ts new file mode 100644 index 0000000..304a1cf --- /dev/null +++ b/src/backend/services/backend-service.ts @@ -0,0 +1,114 @@ +import { Theme, ThemeError } from "../../types"; +import { IBackendRepository } from "../repositories"; + +export class Backend { + private static instance: Backend; + private static repository: IBackendRepository; + + constructor(repository: IBackendRepository) { + if (!Backend.repository) { + Backend.repository = repository; + } + } + + static getInstance(repository: IBackendRepository) { + if (!Backend.instance) { + Backend.instance = new Backend(repository); + } + return Backend.instance; + } + + async reset(): Promise { + await Backend.repository.call<[], void>("reset", []); + } + async dummyFunction(): Promise { + // While most of the try catching should happen in the stores, this makes sense here + try { + const value = await Backend.repository.call<[], boolean>("dummy_function", []); + return value; + } catch (error) { + return false; + } + } + async storeRead(key: string) { + return Backend.repository.call<[string], string>("store_read", [key]); + } + async storeWrite(key: string, value: string) { + await Backend.repository.call<[string, string], void>("store_write", [key, value]); + } + async getServerState() { + return await Backend.repository.call<[], boolean>("get_server_state", []); + } + async enableServer() { + return await Backend.repository.call<[], void>("enable_server", []); + } + async getWatchState() { + return await Backend.repository.call<[], boolean>("get_watch_state", []); + } + async toggleWatchState(bool: boolean, onlyThisSession: boolean) { + return await Backend.repository.call<[boolean, boolean], void>("toggle_watch_state", [ + bool, + onlyThisSession, + ]); + } + async getThemes() { + return await Backend.repository.call<[], Theme[]>("get_themes", []); + } + async getThemeErrors() { + return await Backend.repository.call<[], { fails: ThemeError[] }>("get_last_load_errors", []); + } + async setThemeState( + themeName: string, + state: boolean, + enableDeps: boolean = true, + enableDepValues: boolean = true + ) { + return await Backend.repository.call<[string, boolean, boolean, boolean], void>( + "set_theme_state", + [themeName, state, enableDeps, enableDepValues] + ); + } + async setPatchOfTheme(themeName: string, patchName: string, value: string) { + return await Backend.repository.call<[string, string, string], void>("set_patch_of_theme", [ + themeName, + patchName, + value, + ]); + } + async setComponentOfThemePatch( + themeName: string, + patchName: string, + componentName: string, + value: string + ) { + return await Backend.repository.call<[string, string, string, string], void>( + "set_component_of_theme_patch", + [themeName, patchName, componentName, value] + ); + } + async generatePresetThemeFromThemeNames(presetName: string, dependencies: string[]) { + return await Backend.repository.call<[string, string[]], void>( + "generate_preset_theme_from_theme_names", + [presetName, dependencies] + ); + } + async fetchThemePath() { + return await Backend.repository.call<[], string>("fetch_theme_path", []); + } + async downloadThemeFromUrl(themeId: string, apiUrl: string) { + return Backend.repository.call<[string, string]>("download_theme_from_url", [themeId, apiUrl]); + } + async getBackendVersion() { + return await Backend.repository.call<[], number>("get_backend_version", []); + } + async deleteTheme(themeName: string) { + return await Backend.repository.call<[string], void>("delete_theme", [themeName]); + } + async fetch(url: string, request: RequestInit = {}, mode: "json" | "text" = "json") { + return Backend.repository.fetch(url, request, mode); + } + + toast(title: string, body?: string) { + Backend.repository.toast(title, body); + } +} diff --git a/src/backend/services/index.ts b/src/backend/services/index.ts new file mode 100644 index 0000000..c451092 --- /dev/null +++ b/src/backend/services/index.ts @@ -0,0 +1 @@ +export * from "./backend-service"; diff --git a/src/backend/state/index.ts b/src/backend/state/index.ts new file mode 100644 index 0000000..a00293e --- /dev/null +++ b/src/backend/state/index.ts @@ -0,0 +1 @@ +export * from "./theme-store"; diff --git a/src/backend/state/theme-store.ts b/src/backend/state/theme-store.ts new file mode 100644 index 0000000..fb09732 --- /dev/null +++ b/src/backend/state/theme-store.ts @@ -0,0 +1,597 @@ +import { + Flags, + FullAccountData, + MinimalCSSThemeInfo, + Motd, + Theme, + ThemeError, + UpdateStatus, +} from "../../types"; +import { createStore } from "zustand"; +import type { Backend } from "../services"; +import { FetchError } from "../errors"; + +const apiUrl = "https://api.deckthemes.com"; + +export interface CSSLoaderStateValues { + apiUrl: string; + // Account Data + apiShortToken: string; + apiFullToken: string; + apiMeData: FullAccountData | undefined; + apiTokenExpireDate: number | undefined; + + // Theme Metadata + updateStatuses: UpdateStatus[]; + nextUpdateCheckTime: number; // Unix time stamp; + updateCheckTimeout: NodeJS.Timeout | undefined; + unpinnedThemes: string[]; + isWorking: boolean; + selectedPreset: Theme | undefined; + themeRootPath: string; + themeErrors: ThemeError[]; + themes: Theme[]; + + // Plugin Settings + dummyFunctionResult: boolean; + backendVersion: number; + motd: Motd | undefined; + hiddenMotdId: string; + serverState: boolean; + watchState: boolean; + translationsBranch: "-1" | "0" | "1"; + patrons: string[]; +} + +export interface CSSLoaderStateActions { + initializeStore: () => Promise; + deactivate: () => void; + toast: (message: string) => void; + reloadPlugin: () => Promise; + reloadThemes: () => Promise; + refreshToken: () => Promise; + apiFetch: ( + url: string, + request?: RequestInit, + requiresAuth?: boolean | string + ) => Promise; + logInWithShortToken: (newToken?: string) => Promise; + logOut: () => void; + getThemes: () => Promise; + changePreset: (presetName: string) => Promise; + testBackend: () => Promise; + bulkThemeUpdateCheck: () => Promise; + scheduleBulkThemeUpdateCheck: () => void; + getMotd: () => Promise; + hideMotd: () => Promise; + regenerateCurrentPreset: () => Promise; + setPatchValue: (themeName: string, patchName: string, value: string) => Promise; + setComponentValue: ( + themeName: string, + patchName: string, + componentName: string, + value: string + ) => Promise; + installTheme: (themeId: string) => Promise; + toggleTheme: ( + theme: Theme, + value: boolean, + enableDeps?: boolean, + enableDepValues?: boolean + ) => Promise; + pinTheme: (themeId: string) => Promise; + unpinTheme: (themeId: string) => Promise; + deleteTheme: (themeId: string, refreshAfter?: boolean) => Promise; + setTranslationBranch: (branch: "-1" | "0" | "1") => Promise; + setServerState: (state: boolean) => Promise; + setWatchState: (state: boolean) => Promise; +} + +export interface ICSSLoaderState extends CSSLoaderStateValues, CSSLoaderStateActions {} + +export const createCSSLoaderStore = (backend: Backend) => + createStore((set, get) => { + async function apiFetch( + fetchPath: string, + request?: RequestInit, + // Can be a boolean (to automatically fetch token), or a string (to use a custom token) + requiresAuth?: boolean | string + ) { + try { + const { refreshToken } = get(); + let authToken = undefined; + if (requiresAuth) { + authToken = typeof requiresAuth === "string" ? requiresAuth : await refreshToken(); + } + return await backend.fetch(`${apiUrl}${fetchPath}`, { + method: "GET", + ...request, + headers: { + ...(request?.headers || {}), + Authorization: `Bearer ${authToken}`, + }, + }); + } catch (error) { + if (error instanceof FetchError) { + throw error; + } + throw new FetchError("Fetch Failed", fetchPath, "Unknown Error"); + } + } + + async function getPatrons() { + try { + const data = await backend.fetch(`${apiUrl}/patrons`, {}, "text"); + if (data) { + return data.split("\n"); + } + } catch (error) { + console.error(error); + } + return []; + } + + return { + apiUrl: apiUrl, + // Account Data + apiShortToken: "", + apiFullToken: "", + apiMeData: undefined, + apiTokenExpireDate: undefined, + + // Theme Metadata + updateStatuses: [], + nextUpdateCheckTime: 0, + updateCheckTimeout: undefined, + isWorking: false, + unpinnedThemes: [], + selectedPreset: undefined, + themeRootPath: "", + themeErrors: [], + themes: [], + + // Plugin Settings + dummyFunctionResult: false, + backendVersion: 9, + motd: undefined, + hiddenMotdId: "", + serverState: false, + watchState: false, + translationsBranch: "-1", + patrons: [], + + initializeStore: async () => { + try { + const dummyFunctionResult = await backend.dummyFunction(); + set({ dummyFunctionResult }); + // If the backend doesn't work, no point in running the rest + if (!dummyFunctionResult) return; + + const backendVersion = await backend.getBackendVersion(); + set({ backendVersion }); + + const themes = (await backend.getThemes()) ?? []; + set({ + themes, + selectedPreset: themes.find((e) => e.flags.includes(Flags.isPreset) && e.enabled), + }); + + const themePath = await backend.fetchThemePath(); + set({ themeRootPath: themePath }); + + const unpinnedThemesStr = await backend.storeRead("unpinnedThemes"); + const unpinnedThemes: string[] = unpinnedThemesStr ? JSON.parse(unpinnedThemesStr) : []; + const allThemeIds = themes.map((e) => e.id); + // If a theme is in the unpinned store but no longer exists, remove it from the unpinned store + let unpinnedClone = [...unpinnedThemes]; + unpinnedThemes.forEach((e) => { + if (!allThemeIds.includes(e)) { + unpinnedClone = unpinnedClone.filter((id) => id !== e); + } + }); + set({ unpinnedThemes: unpinnedClone }); + backend.storeWrite("unpinnedThemes", JSON.stringify(unpinnedClone)); + + const shortToken = await backend.storeRead("shortToken"); + set({ apiShortToken: shortToken ?? "" }); + const hiddenMotd = await backend.storeRead("hiddenMotd"); + set({ hiddenMotdId: hiddenMotd ?? "" }); + + if (shortToken) { + await get().logInWithShortToken(); + } + + const serverState = await backend.getServerState(); + const watchState = await backend.getWatchState(); + set({ serverState, watchState }); + const translationsBranch = await backend.storeRead("beta_translations"); + set({ + translationsBranch: ["-1", "0", "1"].includes(translationsBranch) + ? (translationsBranch as "-1" | "0" | "1") + : "-1", + }); + + const { bulkThemeUpdateCheck, scheduleBulkThemeUpdateCheck } = get(); + await bulkThemeUpdateCheck(); + scheduleBulkThemeUpdateCheck(); + + const patrons = await getPatrons(); + set({ patrons }); + } catch (error) { + console.log("Error During Initialzation", error); + } + }, + deactivate: () => { + const { updateCheckTimeout } = get(); + if (updateCheckTimeout) clearTimeout(updateCheckTimeout); + }, + toast: (message: string) => { + backend.toast("CSS Loader", message); + }, + reloadPlugin: async () => { + set({ isWorking: true }); + try { + const { reloadThemes, initializeStore, bulkThemeUpdateCheck, dummyFunctionResult } = + get(); + + // If the dummy func result is false, the plugin never initialized properly anyway, so we should just re-initialize the whole thing. + if (dummyFunctionResult === false) { + await initializeStore(); + } else { + // Otherwise, we can just reload the necessary stuff + const dummyFunctionResult = await backend.dummyFunction(); + set({ dummyFunctionResult }); + await reloadThemes(); + await bulkThemeUpdateCheck(); + } + } catch (error) {} + set({ isWorking: false }); + }, + reloadThemes: async () => { + try { + await backend.reset(); + await get().getThemes(); + } catch (error) { + console.error("Error Reloading Themes", error); + } + }, + logInWithShortToken: async (newToken?: string) => { + try { + const token = newToken ?? get().apiShortToken; + if (!token) { + throw new Error("No Token Provided"); + } + // This can't use apiFetch because it doesn't use header based auth + const json = await backend.fetch<{ token: string }>(`${apiUrl}/auth/authenticate_token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ token }), + }); + if (!json.token) { + throw new FetchError( + "Token Authentication Failed", + `${apiUrl}/auth/authenticate_token`, + "No Token in Response" + ); + } + backend.storeWrite("shortToken", token); + set({ + apiShortToken: token, + apiFullToken: json.token, + apiTokenExpireDate: new Date().valueOf() + 1000 * 10 * 60, + }); + const meJson = await apiFetch("/auth/me", undefined, true); + if (meJson) { + set({ apiMeData: meJson }); + } + } catch (error) { + backend.toast("CSSLoader", "Failed to log in"); + } + }, + logOut: () => { + set({ + apiShortToken: "", + apiFullToken: "", + apiMeData: undefined, + apiTokenExpireDate: undefined, + }); + backend.storeWrite("shortToken", ""); + }, + refreshToken: async (): Promise => { + const { apiFullToken, apiTokenExpireDate } = get(); + if (!apiFullToken) { + return undefined; + } + if (apiTokenExpireDate === undefined || new Date().valueOf() < apiTokenExpireDate) { + return apiFullToken; + } + try { + const json = await backend.fetch<{ token: string }>(`${apiUrl}/auth/refresh_token`, { + method: "POST", + headers: { + Authorization: `Bearer ${apiFullToken}`, + }, + }); + + if (!json.token) { + throw new FetchError( + "Token Refresh Failed", + `${apiUrl}/auth/refresh_token`, + "No Token in Response" + ); + } + set({ + apiFullToken: json.token, + apiTokenExpireDate: new Date().valueOf() + 1000 * 10 * 60, + }); + return json.token; + } catch (error) { + throw error; + } + }, + apiFetch: apiFetch, + getThemes: async () => { + try { + const { fails: themeErrors } = await backend.getThemeErrors(); + set({ themeErrors }); + const themes = await backend.getThemes(); + set({ + themes, + selectedPreset: themes.find((e) => e.flags.includes(Flags.isPreset) && e.enabled), + }); + } catch (error) { + console.error("Error Fetching Themes", error); + } + }, + changePreset: async (presetName: string) => { + try { + const { selectedPreset, themes } = get(); + + if (selectedPreset) { + // If you already have a preset enabled, disabling the preset disables all of it's dependencies with it. + await backend.setThemeState(selectedPreset.name, false); + } else { + // If you don't have a preset, you need to disable all currently enabled themes and THEN enable the preset + await Promise.all( + themes.filter((e) => e.enabled).map((e) => backend.setThemeState(e.name, false)) + ); + } + // Actually enabling the preset itself + if (presetName !== "None") { + await backend.setThemeState(presetName, true); + } + await get().getThemes(); + } catch (error) {} + }, + testBackend: async () => { + try { + const dummyFunctionResult = await backend.dummyFunction(); + set({ dummyFunctionResult }); + } catch (error) { + set({ dummyFunctionResult: false }); + } + }, + bulkThemeUpdateCheck: async () => { + const { themes } = get(); + + async function fetchThemeIDS(idsToQuery: string[]): Promise { + const queryStr = "?ids=" + idsToQuery.join("."); + try { + const value = await apiFetch(`/themes/ids${queryStr}`); + if (value) return value; + } catch {} + return []; + } + + let idsToQuery: string[] = themes.map((e) => e.id); + if (idsToQuery.length === 0) set({ updateStatuses: [] }); + + const themeArr = await fetchThemeIDS(idsToQuery); + + if (themeArr.length === 0) set({ updateStatuses: [] }); + + const updateStatusArr: UpdateStatus[] = themes.map((localEntry) => { + const remoteEntry = themeArr.find( + (remote) => remote.id === localEntry.id || remote.name === localEntry.id + ); + if (!remoteEntry) { + return [localEntry.id, "local", false]; + } + if (remoteEntry.version === localEntry.version) { + return [localEntry.id, "installed", remoteEntry]; + } + return [localEntry.id, "outdated", remoteEntry]; + }); + set({ updateStatuses: updateStatusArr }); + }, + scheduleBulkThemeUpdateCheck: () => { + function recursiveCheck() { + const timeout = setTimeout(async () => { + // Putting this in the function as im not sure the value would update otherwise + const { nextUpdateCheckTime } = get(); + if (!(new Date().valueOf() > nextUpdateCheckTime)) { + recursiveCheck(); + return; + } + // After testing, it appears that, if there is no wifi, bulkThemeUpdateCheck returns an empty array, this is okay, the try catch is just for extra safety + try { + const { bulkThemeUpdateCheck } = get(); + await bulkThemeUpdateCheck(); + + set({ nextUpdateCheckTime: new Date().valueOf() + 24 * 60 * 60 * 1000 }); + } catch (err) { + console.log("Error Checking For Theme Updates", err); + } + recursiveCheck(); + }, 5 * 60 * 1000); + set({ updateCheckTimeout: timeout }); + } + set({ nextUpdateCheckTime: new Date().valueOf() + 24 * 60 * 60 * 1000 }); + recursiveCheck(); + }, + getMotd: async () => { + try { + const value = await apiFetch("/motd"); + if (value) { + set({ motd: value }); + } + } catch (error) {} + }, + hideMotd: async () => { + try { + const { motd } = get(); + if (!motd) return; + await backend.storeWrite("hiddenMotd", motd.id); + set({ hiddenMotdId: motd.id }); + } catch (error) {} + }, + regenerateCurrentPreset: async () => { + try { + const { selectedPreset, themes } = get(); + if (!selectedPreset) return; + await backend.generatePresetThemeFromThemeNames( + selectedPreset.name, + // This will handle if you just toggles/un-toggled a theme, as well as if you changed a patch/component + themes.filter((e) => e.enabled && !e.flags.includes(Flags.isPreset)).map((e) => e.name) + ); + } catch (error) {} + }, + setPatchValue: async (themeName: string, patchName: string, value: string) => { + try { + await backend.setPatchOfTheme(themeName, patchName, value); + const { selectedPreset, regenerateCurrentPreset } = get(); + if (selectedPreset && selectedPreset.dependencies.includes(themeName)) { + await regenerateCurrentPreset(); + } + } catch (error) {} + }, + setComponentValue: async ( + themeName: string, + patchName: string, + componentName: string, + value: string + ) => { + try { + await backend.setComponentOfThemePatch(themeName, patchName, componentName, value); + const { selectedPreset, regenerateCurrentPreset, getThemes } = get(); + if (selectedPreset && selectedPreset.dependencies.includes(themeName)) { + await regenerateCurrentPreset(); + } + // TODO: POTENTIALLY NOT NEEDED + await getThemes(); + } catch (error) {} + }, + installTheme: async (themeId: string) => { + set({ isWorking: true }); + try { + await backend.downloadThemeFromUrl(themeId, apiUrl); + const { updateStatuses, reloadThemes } = get(); + await reloadThemes(); + const updateStatusesClone = updateStatuses.filter((e) => e[0] !== themeId); + updateStatusesClone.push([themeId, "installed", false]); + set({ updateStatuses: updateStatusesClone }); + } catch (error) {} + set({ isWorking: false }); + }, + toggleTheme: async ( + theme: Theme, + value: boolean, + enableDeps?: boolean, + enableDepValues?: boolean + ) => { + try { + await backend.setThemeState(theme.name, value, enableDeps, enableDepValues); + await get().getThemes(); + + if (!enableDeps && theme.dependencies.length > 0) { + if (value) { + backend.toast( + `${theme.display_name} enabled other themes`, + `${theme.dependencies.length} other theme${ + theme.dependencies.length === 1 ? " is" : "s are" + } required by ${theme.display_name}` + ); + } else if (!theme.flags.includes(Flags.dontDisableDeps)) { + backend.toast( + `${theme.display_name} disabled other themes`, + `${theme.dependencies.length} other theme${ + theme.dependencies.length === 1 ? " was" : "s were" + } originally enabled by ${theme.display_name}` + ); + } + } + const { selectedPreset } = get(); + if (selectedPreset) { + await get().regenerateCurrentPreset(); + await get().getThemes(); + } + } catch (error) { + console.error(error); + } + }, + pinTheme: async (themeId: string) => { + try { + const { unpinnedThemes } = get(); + const unpinnedClone = unpinnedThemes.filter((e) => e !== themeId); + set({ unpinnedThemes: unpinnedClone }); + backend.storeWrite("unpinnedThemes", JSON.stringify(unpinnedClone)); + } catch (error) {} + }, + unpinTheme: async (themeId: string) => { + try { + const { unpinnedThemes } = get(); + const unpinnedClone = [...unpinnedThemes, themeId]; + set({ unpinnedThemes: unpinnedClone }); + backend.storeWrite("unpinnedThemes", JSON.stringify(unpinnedClone)); + } catch (error) {} + }, + deleteTheme: async (themeId: string, refreshAfter: boolean = true) => { + set({ isWorking: true }); + try { + const { themes, unpinnedThemes } = get(); + // The python defs say theme name, just gonna assume it's this and not ID + const themeName = themes.find((e) => e.id === themeId)?.name; + if (!themeName) return; + await backend.deleteTheme(themeName); + + // This doesn't actually 'pin' the theme, it just removes it from the unpinned list so that if it's ever reinstalled it isn't hidden + if (unpinnedThemes.includes(themeId)) { + get().pinTheme(themeId); + } + + refreshAfter && (await get().getThemes()); + } catch (error) {} + set({ isWorking: false }); + }, + setTranslationBranch: async (branch: "-1" | "0" | "1") => { + try { + await backend.storeWrite("beta_translations", branch); + const newValue = await backend.storeRead("beta_translations"); + set({ + translationsBranch: ["-1", "0", "1"].includes(newValue) + ? (newValue as "-1" | "0" | "1") + : "-1", + }); + } catch (error) {} + }, + setServerState: async (state: boolean) => { + try { + if (state) { + await backend.enableServer(); + } + await backend.storeWrite("server", state ? "1" : "0"); + const newValue = await backend.getServerState(); + set({ serverState: newValue }); + } catch (error) {} + }, + setWatchState: async (state: boolean) => { + try { + await backend.toggleWatchState(state, false); + const newValue = await backend.getWatchState(); + set({ watchState: newValue }); + } catch (error) {} + }, + }; + }); diff --git a/src/components/DepsOptionSelector.tsx b/src/components/DepsOptionSelector.tsx deleted file mode 100644 index 5e05bb8..0000000 --- a/src/components/DepsOptionSelector.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { DialogButton, Focusable } from "decky-frontend-lib"; -import * as python from "../python"; - -export function DepsOptionSelector({ - themeName, - closeModal = undefined, -}: { - themeName: string; - closeModal?: any; -}) { - function enableTheme(enableDeps: boolean = true, enableDepValues: boolean = true) { - python.resolve(python.setThemeState(themeName, true, enableDeps, enableDepValues), () => { - python.getInstalledThemes(); - closeModal && closeModal(); - }); - } - return ( - - enableTheme(true, true)} style={{ margin: "0 10px" }}> - Enable with configuration {"(Recommended)"} - - enableTheme(true, false)} style={{ margin: "0 10px" }}> - Enable without configuration - - enableTheme(false, false)} style={{ margin: "0 10px" }}> - Enable only this theme - - - ); -} diff --git a/src/components/Modals/AuthorViewModal.tsx b/src/components/Modals/AuthorViewModal.tsx deleted file mode 100644 index 7f2bf68..0000000 --- a/src/components/Modals/AuthorViewModal.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import * as python from "../../python"; -import { CssLoaderContextProvider, useCssLoaderState } from "../../state"; -import { Focusable, ModalRoot } from "decky-frontend-lib"; -import { genericGET } from "../../api"; -import { PartialCSSThemeInfo, ThemeQueryResponse, UserInfo } from "../../apiTypes"; -import { ImSpinner5 } from "react-icons/im"; -import { VariableSizeCard } from "../ThemeManager"; -import { ThemeBrowserCardStyles } from "../Styles"; -import { SupporterIcon } from "../SupporterIcon"; - -export function AuthorViewModalRoot({ - closeModal, - authorData, -}: { - closeModal?: any; - authorData: UserInfo; -}) { - return ( - <> - - {/* @ts-ignore */} - - - - - - ); -} - -function AuthorViewModal({ - authorData, - closeModal, -}: { - authorData: UserInfo; - closeModal: () => {}; -}) { - const { setGlobalState } = useCssLoaderState(); - - const [loaded, setLoaded] = useState(false); - const [themes, setThemes] = useState([]); - - const firstThemeRef = useRef(); - - async function fetchThemeData() { - const data: ThemeQueryResponse = await genericGET( - `/users/${authorData.id}/themes?page=1&perPage=50&filters=CSS&order=Most Downloaded` - ); - if (data?.total && data.total > 0) { - setThemes(data.items); - setLoaded(true); - } - } - useEffect(() => { - fetchThemeData(); - }, []); - - useEffect(() => { - if (firstThemeRef?.current) { - setTimeout(() => { - firstThemeRef?.current?.focus(); - }, 10); - } - }, [loaded]); - - return ( - - {loaded ? ( - <> - - -
- - {authorData.username} -
- -
-
- - {themes.map((e, i) => { - return ( - { - setGlobalState("currentExpandedTheme", e); - closeModal(); - }} - refPassthrough={i === 0 ? firstThemeRef : null} - cols={4} - data={e} - /> - ); - })} - - - ) : ( - <> - -
- - Loading -
- - )} -
- ); -} diff --git a/src/components/Modals/CreatePresetModal.tsx b/src/components/Modals/CreatePresetModal.tsx deleted file mode 100644 index 3731880..0000000 --- a/src/components/Modals/CreatePresetModal.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { ConfirmModal, TextField } from "decky-frontend-lib"; -import { useState } from "react"; -import * as python from "../../python"; -import { CssLoaderContextProvider, useCssLoaderState } from "../../state"; - -export function CreatePresetModalRoot({ closeModal }: { closeModal: any }) { - return ( - <> - {/* @ts-ignore */} - - - - - ); -} - -function CreatePresetModal({ closeModal }: { closeModal: () => void }) { - const { localThemeList, selectedPreset } = useCssLoaderState(); - const [presetName, setPresetName] = useState(""); - const enabledNumber = localThemeList.filter((e) => e.enabled).length; - - return ( - { - if (presetName.length === 0) { - python.toast("No Name!", "Please add a name to your profile."); - return; - } - // TODO: Potentially dont need 2 reloads here, not entirely sure - await python.generatePreset(presetName); - await python.reloadBackend(); - if (selectedPreset) { - await python.setThemeState(selectedPreset?.name, false); - } - await python.setThemeState(presetName + ".profile", true); - await python.getInstalledThemes(); - closeModal(); - }} - > -
- { - setPresetName(e.target.value); - }} - /> - - ); -} diff --git a/src/components/Modals/DeleteConfirmationModal.tsx b/src/components/Modals/DeleteConfirmationModal.tsx deleted file mode 100644 index cd2e48e..0000000 --- a/src/components/Modals/DeleteConfirmationModal.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { CssLoaderContextProvider } from "../../state"; -import * as python from "../../python"; -import { ConfirmModal, ModalRoot } from "decky-frontend-lib"; - -export function DeleteConfirmationModalRoot({ - themesToBeDeleted, - closeModal, - leaveDeleteMode, -}: { - themesToBeDeleted: string[]; - closeModal?: any; - leaveDeleteMode?: () => void; -}) { - async function deleteThemes() { - for (let i = 0; i < themesToBeDeleted.length; i++) { - await python.deleteTheme(themesToBeDeleted[i]); - } - await python.getInstalledThemes(); - leaveDeleteMode && leaveDeleteMode(); - closeModal(); - } - - return ( - - {/* @ts-ignore */} - - - - - ); -} - -function DeleteConfirmationModal({ themesToBeDeleted }: { themesToBeDeleted: string[] }) { - return ( -
- Are you sure you want to delete{" "} - {themesToBeDeleted.length === 1 ? `this theme` : `these ${themesToBeDeleted.length} themes`}? -
- ); -} diff --git a/src/components/Modals/ThemeSettingsModal/ThemeSettingsModal.tsx b/src/components/Modals/ThemeSettingsModal/ThemeSettingsModal.tsx deleted file mode 100644 index 7fe1ef7..0000000 --- a/src/components/Modals/ThemeSettingsModal/ThemeSettingsModal.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { useState, useEffect, useMemo } from "react"; - -import { DialogButton, Focusable, ModalRoot, Toggle } from "decky-frontend-lib"; -import { CssLoaderContextProvider, useCssLoaderState } from "../../../state"; -import { Theme } from "../../../ThemeTypes"; -import { globalState } from "../../../python"; -import { ThemeSettingsModalButtons } from "./ThemeSettingsModalButtons"; -import { toggleTheme } from "../../../backend/backendHelpers/toggleTheme"; -import { ThemePatch } from "../../ThemePatch"; -export function ThemeSettingsModalRoot({ - closeModal, - selectedTheme, -}: { - closeModal?: any; - selectedTheme: string; -}) { - return ( - - {/* @ts-ignore */} - - - - - ); -} - -function ThemeSettingsModal({ - closeModal, - selectedTheme, -}: { - closeModal: any; - selectedTheme: string; -}) { - const { localThemeList, updateStatuses } = useCssLoaderState(); - const [themeData, setThemeData] = useState( - localThemeList.find((e) => e.id === selectedTheme) - ); - - useEffect(() => { - setThemeData(localThemeList.find((e) => e.id === selectedTheme)); - return () => { - setThemeData(undefined); - }; - }, [selectedTheme, localThemeList]); - - return ( - <> - - - {themeData ? ( - <> - -
- {themeData.display_name} - - {themeData.version} | {themeData.author} - -
- { - toggleTheme(themeData, checked); - }} - /> -
- {themeData.enabled && themeData.patches.length > 0 && ( - <> - - {themeData.patches.map((x, i, arr) => ( - - ))} - - - )} - - ) : ( - No Theme Data - )} - - { - closeModal(); - }} - > - Close - - {themeData && } - -
- - ); -} diff --git a/src/components/Modals/ThemeSettingsModal/ThemeSettingsModalButtons.tsx b/src/components/Modals/ThemeSettingsModal/ThemeSettingsModalButtons.tsx deleted file mode 100644 index c123297..0000000 --- a/src/components/Modals/ThemeSettingsModal/ThemeSettingsModalButtons.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { DialogButton, Focusable, showModal } from "decky-frontend-lib"; -import { LocalThemeStatus, Theme } from "../../../ThemeTypes"; -import { FaDownload, FaEye, FaEyeSlash, FaRegStar, FaStar, FaTrashAlt } from "react-icons/fa"; -import { DeleteConfirmationModalRoot } from "../DeleteConfirmationModal"; -import { useCssLoaderState } from "../../../state"; -import * as python from "../../../python"; -import { - genericGET, - logInWithShortToken, - refreshToken, - toggleStar as apiToggleStar, - installTheme, -} from "../../../api"; -import { useState, useEffect } from "react"; - -export function ThemeSettingsModalButtons({ - themeData, - closeModal, -}: { - themeData: Theme; - closeModal: () => void; -}) { - const { unpinnedThemes, apiShortToken, apiFullToken, updateStatuses, setGlobalState } = - useCssLoaderState(); - const isPinned = !unpinnedThemes.includes(themeData.id); - const [starFetchLoaded, setStarFetchLoaded] = useState(false); - const [isStarred, setStarred] = useState(false); - const [blurButtons, setBlurButtons] = useState(false); - - const [updateStatus, setUpdateStatus] = useState("installed"); - useEffect(() => { - if (!themeData) return; - const themeArrPlace = updateStatuses.find((f) => f[0] === themeData.id); - if (themeArrPlace) { - setUpdateStatus(themeArrPlace[1]); - } - }, [themeData]); - - async function toggleStar() { - if (apiFullToken) { - setBlurButtons(true); - const newToken = await refreshToken(); - if (themeData && newToken) { - apiToggleStar(themeData.id, isStarred, newToken).then((bool) => { - if (bool) { - setStarred((cur) => !cur); - setBlurButtons(false); - } - }); - } - } else { - python.toast("Not Logged In!", "You can only star themes if logged in."); - } - } - - async function getStarredStatus() { - if (themeData && apiShortToken) { - if (!apiFullToken) { - await logInWithShortToken(); - } - const data = (await genericGET(`/users/me/stars/${themeData.id}`, true, undefined)) as { - starred: boolean; - }; - if (data) { - setStarFetchLoaded(true); - setStarred(data.starred); - } - } - } - useEffect(() => { - getStarredStatus(); - }, []); - - return ( - <> - - {updateStatus === "outdated" && ( - { - await installTheme(themeData.id); - // This just updates the updateStatuses arr to know that this theme now is up to date, no need to re-fetch the API to know that - setGlobalState( - "updateStatuses", - updateStatuses.map((e) => - e[0] === themeData.id ? [themeData.id, "installed", false] : e - ) - ); - }} - > - - Update - - )} - { - if (isPinned) { - python.unpinTheme(themeData.id); - } else { - python.pinTheme(themeData.id); - } - }} - > - {isPinned ? ( - - ) : ( - - )} - - {starFetchLoaded && ( - - {isStarred ? : } - - )} - { - showModal( - - ); - }} - > - - - - - ); -} diff --git a/src/components/Modals/index.ts b/src/components/Modals/index.ts deleted file mode 100644 index e6ef5d1..0000000 --- a/src/components/Modals/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./CreatePresetModal"; -export * from "./ThemeSettingsModal"; diff --git a/src/components/OptionalDepsModal.tsx b/src/components/OptionalDepsModal.tsx deleted file mode 100644 index 3518822..0000000 --- a/src/components/OptionalDepsModal.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { ModalRoot } from "decky-frontend-lib"; -import { DepsOptionSelector } from "./DepsOptionSelector"; -import { Theme } from "../ThemeTypes"; -export function OptionalDepsModalRoot({ - themeData, - closeModal, -}: { - themeData: Theme; - closeModal?: any; -}) { - return ( - - - - ); -} - -export function OptionalDepsModal({ - themeData, - closeModal, -}: { - themeData: Theme; - closeModal: any; -}) { - return ( - <> -

- Enable dependencies for {themeData.name}? -

- - {themeData.name} enables optional themes to enhance this theme. Disabling these may break - the theme, or make the theme look completely different. Specific optional themes can be - configured and or enabled/disabled anytime via the Quick Access Menu. - - - Enable without configuration will enable optional themes but not overwrite their - configuration, and Enable only this theme will not enable any optional themes. - - - - - ); -} diff --git a/src/components/PatchComponent.tsx b/src/components/PatchComponent.tsx deleted file mode 100644 index 566b1c3..0000000 --- a/src/components/PatchComponent.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { VFC } from "react"; - -import * as python from "../python"; - -import Color from "color"; -import { showModal, ButtonItem, PanelSectionRow } from "decky-frontend-lib"; - -import { ColorPickerModal } from "decky-frontend-lib"; -import { ThemePatchComponent } from "../ThemeTypes"; -import { FaFolder } from "react-icons/fa"; -import { useCssLoaderState } from "../state"; - -export const PatchComponent: VFC<{ - data: ThemePatchComponent; - selectedLabel: string; - themeName: string; - patchName: string; - bottomSeparatorValue: "standard" | "none"; -}> = ({ data, selectedLabel, themeName, patchName, bottomSeparatorValue }) => { - const { selectedPreset } = useCssLoaderState(); - if (selectedLabel === data.on) { - // The only value that changes from component to component is the value, so this can just be re-used - async function setComponentAndReload(value: string) { - await python.setComponentOfThemePatch(themeName, patchName, data.name, value); - if (selectedPreset && selectedPreset.dependencies.includes(themeName)) { - python.generatePresetFromThemeNames(selectedPreset.name, selectedPreset.dependencies); - } - python.getInstalledThemes(); - } - switch (data.type) { - case "image-picker": - // This makes things compatible with people using HoloISO or who don't have the user /deck/ - function getRootPath() { - python.resolve(python.fetchThemePath(), (path: string) => pickImage(path)); - } - // These have to - async function pickImage(rootPath: string) { - const res = await python.openFilePicker(rootPath); - if (!res.path.includes(rootPath)) { - python.toast("Invalid File", "Images must be within themes folder"); - return; - } - if (!/\.(jpg|jpeg|png|webp|avif|gif|svg)$/.test(res.path)) { - python.toast("Invalid File", "Must be an image file"); - return; - } - const relativePath = res.path.split(`${rootPath}/`)[1]; - setComponentAndReload(relativePath); - } - return ( - - getRootPath()} - layout="below" - > -
- Open {data.name} -
- -
-
-
-
- ); - case "color-picker": - const colorObj = Color(data.value).hsl(); - const curColorHSLArray = colorObj.array(); - - return ( - <> - - - showModal( - // @ts-ignore -- showModal passes the closeModal function to this, but for some reason it's giving me a typescript error because I didn't explicitly pass it myself - { - setComponentAndReload(HSLString); - }} - defaultH={curColorHSLArray[0]} - defaultS={curColorHSLArray[1]} - defaultL={curColorHSLArray[2]} - defaultA={curColorHSLArray[3] ?? 1} - title={data.name} - /> - ) - } - layout={"below"} - > -
- Open {data.name} -
-
-
-
- - - - ); - } - } - return null; -}; diff --git a/src/components/QAMTab/PresetSelectionDropdown.tsx b/src/components/QAMTab/PresetSelectionDropdown.tsx deleted file mode 100644 index d1528fd..0000000 --- a/src/components/QAMTab/PresetSelectionDropdown.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { DropdownItem, PanelSectionRow, showModal } from "decky-frontend-lib"; -import { useCssLoaderState } from "../../state"; -import { Flags } from "../../ThemeTypes"; -import { useMemo } from "react"; -import { changePreset, getInstalledThemes } from "../../python"; -import { CreatePresetModalRoot } from "../Modals/CreatePresetModal"; -import { FiPlusCircle } from "react-icons/fi"; -import { useRerender } from "../../hooks"; - -export function PresetSelectionDropdown() { - const { localThemeList, selectedPreset } = useCssLoaderState(); - const presets = useMemo( - () => localThemeList.filter((e) => e.flags.includes(Flags.isPreset)), - [localThemeList] - ); - const [render, rerender] = useRerender(); - return ( - <> - {render && ( - - e.enabled && e.flags.includes(Flags.isPreset)).length > 1 - ? "Invalid State" - : selectedPreset?.name || "None" - } - rgOptions={[ - ...(localThemeList.filter((e) => e.enabled && e.flags.includes(Flags.isPreset)) - .length > 1 - ? [{ data: "Invalid State", label: "Invalid State" }] - : []), - { data: "None", label: "None" }, - ...presets.map((e) => ({ label: e.display_name, data: e.name })), - // This is a jank way of only adding it if creatingNewProfile = false - { - data: "New Profile", - label: ( -
- - New Profile -
- ), - }, - ]} - onChange={async ({ data }) => { - if (data === "New Profile") { - showModal( - // @ts-ignore - - ); - rerender(); - return; - } - await changePreset(data, localThemeList); - getInstalledThemes(); - }} - /> -
- )} - - ); -} diff --git a/src/components/QAMTab/QAMThemeToggleList.tsx b/src/components/QAMTab/QAMThemeToggleList.tsx deleted file mode 100644 index 7f2aa49..0000000 --- a/src/components/QAMTab/QAMThemeToggleList.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Focusable } from "decky-frontend-lib"; -import { useCssLoaderState } from "../../state"; -import { ThemeToggle } from "../ThemeToggle"; -import { Flags } from "../../ThemeTypes"; -import { ThemeErrorCard } from "../ThemeErrorCard"; -import { BsArrowDown } from "react-icons/bs"; -import { FaEyeSlash } from "react-icons/fa"; - -export function QAMThemeToggleList() { - const { localThemeList, unpinnedThemes } = useCssLoaderState(); - - if (localThemeList.length === 0) { - return ( - <> - You have no themes installed. Get started by selecting the download icon above! - - ); - } - - return ( - <> - {/* This styles the collapse buttons, putting it here just means it only needs to be rendered once instead of like 20 times */} - - - <> - {localThemeList - .filter((e) => !unpinnedThemes.includes(e.id) && !e.flags.includes(Flags.isPreset)) - .map((x) => ( - - ))} - - - {unpinnedThemes.length > 0 && ( -
- -
- {unpinnedThemes.length} theme{unpinnedThemes.length > 1 ? "s are" : "is"} hidden. -
-
- )} - - ); -} diff --git a/src/components/QAMTab/index.ts b/src/components/QAMTab/index.ts deleted file mode 100644 index c63a957..0000000 --- a/src/components/QAMTab/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./QAMThemeToggleList"; -export * from "./PresetSelectionDropdown"; -export * from "./MOTDDisplay"; diff --git a/src/components/Styles/ExpandedViewStyles.tsx b/src/components/Styles/ExpandedViewStyles.tsx deleted file mode 100644 index 52779b8..0000000 --- a/src/components/Styles/ExpandedViewStyles.tsx +++ /dev/null @@ -1,214 +0,0 @@ -export function ExpandedViewStyles({ - gapBetweenCarouselAndImage, - imageAreaPadding, - imageAreaWidth, - selectedImageHeight, - selectedImageWidth, - imageCarouselEntryHeight, - imageCarouselEntryWidth, -}: { - gapBetweenCarouselAndImage: number; - imageAreaPadding: number; - imageAreaWidth: number; - selectedImageHeight: number; - selectedImageWidth: number; - imageCarouselEntryHeight: number; - imageCarouselEntryWidth: number; -}) { - return ( - - ); -} diff --git a/src/components/Styles/ThemeBrowserCardStyles.tsx b/src/components/Styles/ThemeBrowserCardStyles.tsx deleted file mode 100644 index 6617e14..0000000 --- a/src/components/Styles/ThemeBrowserCardStyles.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useCssLoaderState } from "../../state"; - -export function ThemeBrowserCardStyles({ customCardSize }: { customCardSize?: number }) { - const { browserCardSize } = customCardSize - ? { browserCardSize: customCardSize } - : useCssLoaderState(); - - return ( - - ); -} diff --git a/src/components/Styles/index.ts b/src/components/Styles/index.ts deleted file mode 100644 index 3d66ae3..0000000 --- a/src/components/Styles/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./ExpandedViewStyles"; -export * from "./ThemeBrowserCardStyles"; diff --git a/src/components/ThemeErrorCard.tsx b/src/components/ThemeErrorCard.tsx deleted file mode 100644 index 48e5fad..0000000 --- a/src/components/ThemeErrorCard.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Focusable, PanelSectionRow } from "decky-frontend-lib"; -import { ThemeError } from "../ThemeTypes"; - -export function ThemeErrorCard({ errorData }: { errorData: ThemeError }) { - return ( - {}} - style={{ - width: "100%", - margin: 0, - padding: 0, - }} - > -
- - {errorData[0]} - - {errorData[1]} -
-
- ); -} diff --git a/src/components/ThemeManager/BrowserItemCard.tsx b/src/components/ThemeManager/BrowserItemCard.tsx deleted file mode 100644 index 06385eb..0000000 --- a/src/components/ThemeManager/BrowserItemCard.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { FC } from "react"; -import { useCssLoaderState } from "../../state"; -import { Theme } from "../../ThemeTypes"; -import { Focusable, Navigation } from "decky-frontend-lib"; -import { AiOutlineDownload } from "react-icons/ai"; -import { PartialCSSThemeInfo, ThemeQueryRequest } from "../../apiTypes"; -import { FaBullseye, FaDownload, FaStar } from "react-icons/fa"; -import { shortenNumber } from "../../logic/numbers"; - -const cardWidth = { - 5: 152, - 4: 195, - 3: 260, -}; - -export const VariableSizeCard: FC<{ - data: PartialCSSThemeInfo; - cols: number; - searchOpts?: ThemeQueryRequest; - prevSearchOptsVarName?: string; - refPassthrough?: any; - onClick?: () => void; -}> = ({ - data: e, - cols: size, - refPassthrough = undefined, - searchOpts, - prevSearchOptsVarName, - onClick, -}) => { - const { localThemeList, apiUrl, setGlobalState } = useCssLoaderState(); - function checkIfThemeInstalled(themeObj: PartialCSSThemeInfo) { - const filteredArr: Theme[] = localThemeList.filter( - (e: Theme) => e.name === themeObj.name && e.author === themeObj.specifiedAuthor - ); - if (filteredArr.length > 0) { - if (filteredArr[0].version === themeObj.version) { - return "installed"; - } else { - return "outdated"; - } - } else { - return "uninstalled"; - } - } - function imageURLCreator(): string { - if (e?.images[0]?.id && e.images[0].id !== "MISSING") { - return `url(${apiUrl}/blobs/${e?.images[0].id})`; - } else { - return `url(https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Steam_Deck_logo_%28blue_background%29.svg/2048px-Steam_Deck_logo_%28blue_background%29.svg.png)`; - } - } - - const installStatus = checkIfThemeInstalled(e); - - return ( - <> -
- {installStatus === "outdated" && ( -
- -
- )} - { - if (onClick) { - onClick(); - return; - } - if (searchOpts && prevSearchOptsVarName) { - setGlobalState(prevSearchOptsVarName, searchOpts); - } - setGlobalState("currentExpandedTheme", e); - Navigation.Navigate("/cssloader/expanded-view"); - }} - > -
- -
-
-
- - {shortenNumber(e.download.downloadCount) ?? e.download.downloadCount} -
-
- - {shortenNumber(e.starCount) ?? e.starCount} -
-
- - {e.target} -
-
-
-
- {e.displayName} - - {e.version} - Last Updated {new Date(e.updated).toLocaleDateString()} - - By {e.specifiedAuthor} -
- -
- - ); -}; diff --git a/src/components/ThemeManager/BrowserSearchFields.tsx b/src/components/ThemeManager/BrowserSearchFields.tsx deleted file mode 100644 index ede5102..0000000 --- a/src/components/ThemeManager/BrowserSearchFields.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import { - DialogButton, - Dropdown, - DropdownOption, - Focusable, - gamepadDialogClasses, - gamepadSliderClasses, - PanelSectionRow, - SliderField, - TextField, -} from "decky-frontend-lib"; -import { useEffect, useMemo, memo } from "react"; -import { TiRefreshOutline } from "react-icons/ti"; -import { FaRotate } from "react-icons/fa6"; -import { ThemeQueryRequest } from "../../apiTypes"; -import { genericGET } from "../../api"; -import { useCssLoaderState } from "../../state"; -import { FilterDropdownCustomLabel } from "./FilterDropdownCustomLabel"; - -export function BrowserSearchFields({ - searchOpts, - searchOptsVarName, - prevSearchOptsVarName, - unformattedFilters, - unformattedFiltersVarName, - onReload, - requiresAuth = false, - getTargetsPath, -}: { - searchOpts: ThemeQueryRequest; - searchOptsVarName: string; - prevSearchOptsVarName: string; - unformattedFilters: { filters: string[]; order: string[] }; - unformattedFiltersVarName: string; - getTargetsPath: string; - requiresAuth?: boolean; - onReload: () => void; -}) { - const { browserCardSize, setGlobalState } = useCssLoaderState(); - - async function getThemeTargets() { - genericGET(`${getTargetsPath}`, requiresAuth).then((data) => { - if (data?.filters) { - setGlobalState(unformattedFiltersVarName, { - filters: data.filters, - order: data.order, - }); - } - }); - } - - const formattedFilters = useMemo<{ filters: DropdownOption[]; order: DropdownOption[] }>( - () => ({ - filters: [ - { - data: "All", - label: ( - prev + Number(cur), - 0 - ) || "" - } - /> - ), - }, - ...Object.entries(unformattedFilters.filters) - .filter(([_, itemCount]) => Number(itemCount) > 0) - .map(([filterValue, itemCount]) => ({ - data: filterValue, - label: , - })), - ], - order: unformattedFilters.order.map((e) => ({ data: e, label: e })), - }), - [unformattedFilters] - ); - useEffect(() => { - if (unformattedFilters.filters.length < 2) { - getThemeTargets(); - } - }, []); - - const repoOptions: never[] = []; - return ( - <> - - -
- Sort - { - setGlobalState(prevSearchOptsVarName, searchOpts); - setGlobalState(searchOptsVarName, { ...searchOpts, order: e.data }); - }} - /> -
-
- Filter - { - setGlobalState(prevSearchOptsVarName, searchOpts); - setGlobalState(searchOptsVarName, { ...searchOpts, filters: e.data }); - }} - /> - -
-
-
-
- -
- { - setGlobalState(prevSearchOptsVarName, searchOpts); - setGlobalState(searchOptsVarName, { ...searchOpts, search: e.target.value }); - }} - /> -
- - - Refresh - -
- { - setGlobalState("browserCardSize", num); - }} - /> -
- -
-
- - ); -} - -export const MemoizedSearchFields = memo(BrowserSearchFields); diff --git a/src/components/ThemeManager/FilterDropdownCustomLabel.tsx b/src/components/ThemeManager/FilterDropdownCustomLabel.tsx deleted file mode 100644 index 4ff5d25..0000000 --- a/src/components/ThemeManager/FilterDropdownCustomLabel.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export function FilterDropdownCustomLabel({ - filterValue, - itemCount, -}: { - filterValue: string; - itemCount: number | string; -}) { - return ( -
- {filterValue} - {itemCount} -
- ); -} diff --git a/src/components/ThemeManager/LoadMoreButton.tsx b/src/components/ThemeManager/LoadMoreButton.tsx deleted file mode 100644 index cab5ba6..0000000 --- a/src/components/ThemeManager/LoadMoreButton.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { DialogButton } from "decky-frontend-lib"; -import { useState, Dispatch, SetStateAction, useEffect } from "react"; -import { ThemeQueryRequest, ThemeQueryResponse } from "../../apiTypes"; -import { generateParamStr } from "../../logic"; -import { genericGET } from "../../api"; -import { useCssLoaderState } from "../../state"; - -export function LoadMoreButton({ - fetchPath = "/themes", - origSearchOpts, - themeArr, - themeArrVarName, - paramStrFilterPrepend = "", - setSnapIndex = undefined, -}: { - fetchPath: string; - origSearchOpts: ThemeQueryRequest; - themeArrVarName: string; - themeArr: ThemeQueryResponse; - paramStrFilterPrepend: string; - setSnapIndex?: Dispatch>; -}) { - const { setGlobalState } = useCssLoaderState(); - const [loadMoreCurPage, setLoadMorePage] = useState(1); - const [loading, setLoading] = useState(false); - - function loadMore() { - setLoading(true); - // This just changes "All" to "", as that is what the backend looks for - let searchOptClone = { ...origSearchOpts }; - searchOptClone.page = loadMoreCurPage + 1; - const searchOpts = generateParamStr( - searchOptClone.filters !== "All" ? searchOptClone : { ...searchOptClone, filters: "" }, - paramStrFilterPrepend - ); - genericGET(`${fetchPath}${searchOpts}`).then((data) => { - if (data) { - setGlobalState(themeArrVarName, { - total: themeArr.total, - items: [...themeArr.items, ...data.items], - }); - if (setSnapIndex) { - setSnapIndex(origSearchOpts.perPage * loadMoreCurPage - 1); - } - setLoadMorePage((curPage) => curPage + 1); - } - setLoading(false); - }); - } - - useEffect(() => { - setLoadMorePage(1); - }, [origSearchOpts]); - return ( - <> - {themeArr.items.length < themeArr.total ? ( - <> - - Load More - - - ) : null} - - ); -} diff --git a/src/components/ThemeManager/index.ts b/src/components/ThemeManager/index.ts deleted file mode 100644 index 7820595..0000000 --- a/src/components/ThemeManager/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./LoadMoreButton"; -export * from "./FilterDropdownCustomLabel"; -export * from "./BrowserItemCard"; -export * from "./BrowserSearchFields"; diff --git a/src/components/ThemePatch.tsx b/src/components/ThemePatch.tsx deleted file mode 100644 index 4437e53..0000000 --- a/src/components/ThemePatch.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { DropdownItem, PanelSectionRow, SliderField, ToggleField } from "decky-frontend-lib"; -import * as python from "../python"; -import { useState, VFC } from "react"; -import { Patch } from "../ThemeTypes"; -import { PatchComponent } from "./PatchComponent"; -import { useCssLoaderState } from "../state"; - -export const ThemePatch: VFC<{ - data: Patch; - index: number; - fullArr: Patch[]; - themeName: string; - modal?: boolean; -}> = ({ data, index, fullArr, themeName, modal = false }) => { - const { selectedPreset } = useCssLoaderState(); - const [selectedIndex, setIndex] = useState(data.options.indexOf(data.value)); - - const [selectedLabel, setLabel] = useState(data.value); - - const bottomSeparatorValue = fullArr.length - 1 === index ? "standard" : "none"; - - async function setPatchValue(value: string) { - await python.setPatchOfTheme(themeName, data.name, value); - // This was before all currently toggled themes were part of a dependency, this (and probably lots of the other preset code) can be changed to assume that by default - if (selectedPreset && selectedPreset.dependencies.includes(themeName)) { - return python.generatePresetFromThemeNames(selectedPreset.name, selectedPreset.dependencies); - } - return; - } - - function ComponentWrapper() { - return ( - <> - {data.components.length > 0 ? ( - <> - {data.components.map((e) => ( - - ))} - - ) : null} - - ); - } - - switch (data.type) { - case "slider": - return ( - <> - - } - min={0} - max={data.options.length - 1} - value={selectedIndex} - onChange={(value) => { - setPatchValue(data.options[value]); - setIndex(value); - setLabel(data.options[value]); - data.value = data.options[value]; - }} - notchCount={data.options.length} - notchLabels={data.options.map((e, i) => ({ - notchIndex: i, - label: e, - value: i, - }))} - /> - - - - ); - case "checkbox": - return ( - <> - - } - checked={data.value === "Yes"} - onChange={(bool) => { - const newValue = bool ? "Yes" : "No"; - setPatchValue(newValue); - setLabel(newValue); - setIndex(data.options.findIndex((e) => e === newValue)); - data.value = newValue; - }} - /> - - - - ); - case "dropdown": - return ( - <> - - } - menuLabel={`${data.name}`} - rgOptions={data.options.map((x, i) => { - return { data: i, label: x }; - })} - selectedOption={selectedIndex} - onChange={(index) => { - setIndex(index.data); - data.value = index.label as string; - setLabel(data.value); - setPatchValue(data.value); - }} - /> - - - - ); - case "none": - return ( - <> - - {modal ? ( - {data.name} - ) : ( - - )} - - - - ); - default: - return null; - } -}; - -const PatchLabel = ({ name }: { name: string }) => { - return ( -
- {name} -
- ); -}; diff --git a/src/components/ThemeSettings/DeleteMenu.tsx b/src/components/ThemeSettings/DeleteMenu.tsx deleted file mode 100644 index ba5c747..0000000 --- a/src/components/ThemeSettings/DeleteMenu.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { - DialogButton, - DialogCheckbox, - Focusable, - PanelSection, - showModal, -} from "decky-frontend-lib"; -import { Theme } from "../../ThemeTypes"; -import { useState } from "react"; -import { DeleteConfirmationModalRoot } from "../Modals/DeleteConfirmationModal"; - -export function DeleteMenu({ - themeList, - leaveDeleteMode, -}: { - themeList: Theme[]; - leaveDeleteMode: () => void; -}) { - let [choppingBlock, setChoppingBlock] = useState([]); // name arr - return ( - - - {themeList.map((e) => ( -
- { - if (checked) { - setChoppingBlock([...choppingBlock, e.name]); - } else { - setChoppingBlock(choppingBlock.filter((f) => f !== e.name)); - } - }} - checked={choppingBlock.includes(e.name)} - label={e.name} - /> -
- ))} - { - showModal( - - ); - }} - > - Delete - -
-
- ); -} diff --git a/src/components/ThemeSettings/FullscreenProfileEntry.tsx b/src/components/ThemeSettings/FullscreenProfileEntry.tsx deleted file mode 100644 index 2192945..0000000 --- a/src/components/ThemeSettings/FullscreenProfileEntry.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { DialogButton, Focusable, PanelSectionRow } from "decky-frontend-lib"; -import { Flags, LocalThemeStatus, Theme } from "../../ThemeTypes"; -import { useCssLoaderState } from "../../state"; -import { AiOutlineDownload } from "react-icons/ai"; -import { FaTrash } from "react-icons/fa"; -import { installTheme } from "../../api"; - -export function FullscreenProfileEntry({ - data: e, - handleUninstall, - isInstalling, - handleUpdate, -}: { - data: Theme; - handleUninstall: (e: Theme) => void; - handleUpdate: (e: Theme) => void; - isInstalling: boolean; -}) { - const { updateStatuses } = useCssLoaderState(); - let [updateStatus]: [LocalThemeStatus] = ["installed"]; - const themeArrPlace = updateStatuses.find((f) => f[0] === e.id); - if (themeArrPlace) { - updateStatus = themeArrPlace[1]; - } - return ( - -
- {e.display_name} - - {/* Update Button */} - {updateStatus === "outdated" && ( - handleUpdate(e)} - disabled={isInstalling} - > - - - )} - {/* This shows when a theme is local, but not a preset */} - {updateStatus === "local" && !e.flags.includes(Flags.isPreset) && ( - - Local Theme - - )} - handleUninstall(e)} - disabled={isInstalling} - > - - - -
-
- ); -} diff --git a/src/components/ThemeSettings/FullscreenSingleThemeEntry.tsx b/src/components/ThemeSettings/FullscreenSingleThemeEntry.tsx deleted file mode 100644 index 7b3ec10..0000000 --- a/src/components/ThemeSettings/FullscreenSingleThemeEntry.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { DialogButton, Focusable, ToggleField, showModal } from "decky-frontend-lib"; -import { LocalThemeStatus, Theme } from "../../ThemeTypes"; -import { useCssLoaderState } from "../../state"; -import * as python from "../../python"; -import { ImCog } from "react-icons/im"; -import { toggleTheme } from "../../backend/backendHelpers/toggleTheme"; -import { ThemeSettingsModalRoot } from "../Modals/ThemeSettingsModal"; -import { FaEye, FaEyeSlash, FaTrash } from "react-icons/fa"; -import { BsGearFill } from "react-icons/bs"; - -export function FullscreenSingleThemeEntry({ - data: e, - showModalButtonPrompt = false, - handleUninstall, - handleUpdate, - isInstalling, -}: { - data: Theme; - showModalButtonPrompt?: boolean; - handleUninstall: (e: Theme) => void; - handleUpdate: (e: Theme) => void; - isInstalling: boolean; -}) { - const { unpinnedThemes, updateStatuses } = useCssLoaderState(); - const isPinned = !unpinnedThemes.includes(e.id); - - let [updateStatus]: [LocalThemeStatus] = ["installed"]; - const themeArrPlace = updateStatuses.find((f) => f[0] === e.id); - if (themeArrPlace) { - updateStatus = themeArrPlace[1]; - } - - // I extracted these here as doing conditional props inline sucks - const modalButtonProps = showModalButtonPrompt - ? { - onOptionsActionDescription: "Expand Settings", - onOptionsButton: () => { - showModal(); - }, - } - : {}; - - const updateButtonProps = - updateStatus === "outdated" - ? { - onSecondaryButton: () => { - handleUpdate(e); - }, - onSecondaryActionDescription: "Update Theme", - } - : {}; - - return ( - <> -
- {updateStatus === "outdated" && ( -
- )} - - {e.display_name}} - checked={e.enabled} - onChange={(switchValue: boolean) => { - toggleTheme(e, switchValue); - }} - /> - - { - if (isPinned) { - python.unpinTheme(e.id); - } else { - python.pinTheme(e.id); - } - }} - > - {isPinned ? ( - - ) : ( - - )} - - { - showModal(); - }} - > - - -
- - ); -} diff --git a/src/components/ThemeSettings/UpdateAllThemesButton.tsx b/src/components/ThemeSettings/UpdateAllThemesButton.tsx deleted file mode 100644 index 414589e..0000000 --- a/src/components/ThemeSettings/UpdateAllThemesButton.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { DialogButton } from "decky-frontend-lib"; -import { useCssLoaderState } from "../../state"; -import { Theme } from "../../ThemeTypes"; -import { FaDownload } from "react-icons/fa"; - -export function UpdateAllThemesButton({ - handleUpdate, -}: { - handleUpdate: (entry: Theme) => Promise; -}) { - const { updateStatuses, localThemeList } = useCssLoaderState(); - - async function updateAll() { - const themesToBeUpdated = updateStatuses.filter((e) => e[1] === "outdated"); - for (let i = 0; i < themesToBeUpdated.length; i++) { - const entry = localThemeList.find((f) => f.id === themesToBeUpdated[i][0]); - if (!entry) break; - await handleUpdate(entry); - } - } - return ( - <> - {updateStatuses.filter((e) => e[1] === "outdated").length > 0 && ( - - - Update All Themes - - )} - - ); -} diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx deleted file mode 100644 index 711a8cb..0000000 --- a/src/components/ThemeToggle.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { ButtonItem, Focusable, PanelSectionRow, ToggleField, showModal } from "decky-frontend-lib"; -import { VFC, useState, useMemo } from "react"; -import { Flags, LocalThemeStatus, Theme, UpdateStatus } from "../ThemeTypes"; - -import { ThemePatch } from "./ThemePatch"; -import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; -import { useCssLoaderState } from "../state"; -import { useRerender } from "../hooks"; -// This has to be a direct import to avoid the circular dependency -import { ThemeSettingsModalRoot } from "./Modals/ThemeSettingsModal"; -import { installTheme } from "../api"; -import { toggleTheme } from "../backend/backendHelpers/toggleTheme"; - -export const ThemeToggle: VFC<{ - data: Theme; - collapsible?: boolean; - showModalButtonPrompt?: boolean; - isFullscreen?: boolean; -}> = ({ data, collapsible = false, showModalButtonPrompt = false, isFullscreen = false }) => { - const { updateStatuses, setGlobalState, isInstalling } = useCssLoaderState(); - const [collapsed, setCollapsed] = useState(true); - - const [render, rerender] = useRerender(); - - const isPreset = useMemo(() => { - if (data.flags.includes(Flags.isPreset)) { - return true; - } - return false; - // This might not actually memoize it as data.flags is an array, so idk if it deep checks the values here - }, [data.flags]); - - let [updateStatus]: [LocalThemeStatus] = ["installed"]; - const themeArrPlace = updateStatuses.find((f) => f[0] === data.id); - if (themeArrPlace) { - updateStatus = themeArrPlace[1]; - } - - // I extracted these here as doing conditional props inline sucks - const modalButtonProps = showModalButtonPrompt - ? { - onOptionsActionDescription: "Expand Settings", - onOptionsButton: () => { - showModal( - // @ts-ignore - - ); - }, - } - : {}; - - const updateButtonProps = - updateStatus === "outdated" - ? { - onSecondaryButton: async () => { - await installTheme(data.id); - // This just updates the updateStatuses arr to know that this theme now is up to date, no need to re-fetch the API to know that - setGlobalState( - "updateStatuses", - updateStatuses.map((e) => (e[0] === data.id ? [data.id, "installed", false] : e)) - ); - }, - onSecondaryActionDescription: "Update Theme", - } - : {}; - - return ( - <> - {render && ( - <> - - - {updateStatus === "outdated" && ( -
- )} - 0 ? "none" : "standard"} - checked={data.enabled} - label={data.display_name} - description={ - isPreset - ? `Preset` - : `${updateStatus === "outdated" ? "Update Available" : data.version} | ${ - data.author - }` - } - onChange={async (switchValue: boolean) => { - toggleTheme(data, switchValue, rerender, setCollapsed); - }} - /> -
-
- {data.enabled && data.patches.length > 0 && ( - <> - {collapsible && ( -
- - setCollapsed(!collapsed)} - > - {collapsed ? ( - - ) : ( - - )} - - -
- )} - {!collapsible || !collapsed - ? data.patches.map((x, i, arr) => ( - - )) - : null} - - )} - - )} - - ); -}; diff --git a/src/components/TitleView.tsx b/src/components/TitleView.tsx deleted file mode 100644 index c6d5212..0000000 --- a/src/components/TitleView.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { DialogButton, Navigation, staticClasses, Focusable } from "decky-frontend-lib"; -import { BsGearFill } from "react-icons/bs"; -import { FaDownload } from "react-icons/fa"; -import { useCssLoaderState } from "../state"; - -export function TitleView({ onDocsClick }: { onDocsClick?: () => {} }) { - const { localThemeList } = useCssLoaderState(); - - const onSettingsClick = () => { - Navigation.CloseSideMenus(); - Navigation.Navigate("/cssloader/settings"); - }; - - const onStoreClick = () => { - Navigation.CloseSideMenus(); - Navigation.Navigate("/cssloader/theme-manager"); - }; - - return ( - - -
CSS Loader
- - - - - - -
- ); -} diff --git a/src/components/index.ts b/src/components/index.ts deleted file mode 100644 index 1c3c511..0000000 --- a/src/components/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from "./ThemeToggle"; -export * from "./ThemePatch"; -export * from "./PatchComponent"; -export * from "./TitleView"; -export * from "./ThemeManager"; -export * from "./OptionalDepsModal"; -export * from "./QAMTab"; -export * from "./Modals"; diff --git a/src/decky-patches/decky-patch-store.ts b/src/decky-patches/decky-patch-store.ts new file mode 100644 index 0000000..fee5b98 --- /dev/null +++ b/src/decky-patches/decky-patch-store.ts @@ -0,0 +1,82 @@ +import { Patch } from "@decky/ui"; +import type { Backend } from "@cssloader/backend"; +import { createStore, useStore } from "zustand"; +import { disableNavPatch, enableNavPatch } from "./nav-patch"; +import { disableUnminifyMode, enableUnminifyMode } from "./unminify-mode"; +import { backend } from "@/backend"; + +interface DeckyPatchStoreValues { + unminifyModeOn: boolean; + navPatchInstance: Patch | undefined; +} +interface DeckyPatchStoreActions { + initializeStore: () => Promise; + deactivate: () => Promise; + setNavPatchState: (value: boolean, shouldToast?: boolean) => void; + setUnminifyModeState: (value: boolean, shouldToast?: boolean) => void; +} + +export interface IDeckyPatchState extends DeckyPatchStoreActions, DeckyPatchStoreValues {} + +const createDeckyPatchStore = (backend: Backend) => + createStore((set, get) => ({ + unminifyModeOn: false, + navPatchInstance: undefined, + initializeStore: async () => { + try { + const shouldEnable = await backend.storeRead("enableNavPatch"); + return; + if (shouldEnable) { + const patch = enableNavPatch(); + set({ navPatchInstance: patch }); + } + } catch (error) {} + }, + deactivate: async () => { + const { navPatchInstance, unminifyModeOn } = get(); + if (navPatchInstance) { + disableNavPatch(navPatchInstance); + set({ navPatchInstance: undefined }); + } + if (unminifyModeOn) { + disableUnminifyMode(); + } + }, + setNavPatchState: (enabled: boolean, shouldToast: boolean = false) => { + const { navPatchInstance } = get(); + if (enabled) { + // Don't Patch Twice + if (!navPatchInstance) { + const patch = enableNavPatch(); + set({ navPatchInstance: patch }); + } + } else { + disableNavPatch(navPatchInstance); + set({ navPatchInstance: undefined }); + } + shouldToast && backend.toast("Nav Patch", enabled ? "Enabled" : "Disabled"); + backend.storeWrite("enableNavPatch", enabled + ""); + }, + setUnminifyModeState: (enabled: boolean, shouldToast: boolean = false) => { + if (enabled) { + enableUnminifyMode(); + } else { + disableUnminifyMode(); + } + shouldToast && backend.toast("Unminify Mode", enabled ? "Enabled" : "Disabled"); + }, + })); + +const deckyPatchState = createDeckyPatchStore(backend); + +export const getDeckyPatchState = () => deckyPatchState.getState(); + +const useDeckyPatchState = (fn: (state: IDeckyPatchState) => any) => useStore(deckyPatchState, fn); + +export const useDeckyPatchStateValue = ( + key: T +): IDeckyPatchState[T] => useDeckyPatchState((state) => state[key]); + +export const useDeckyPatchStateAction = ( + key: T +): IDeckyPatchState[T] => useDeckyPatchState((state) => state[key]); diff --git a/src/decky-patches/index.ts b/src/decky-patches/index.ts new file mode 100644 index 0000000..cd716ab --- /dev/null +++ b/src/decky-patches/index.ts @@ -0,0 +1 @@ +export * from "./decky-patch-store"; diff --git a/src/decky-patches/nav-patch/index.ts b/src/decky-patches/nav-patch/index.ts new file mode 100644 index 0000000..ed95419 --- /dev/null +++ b/src/decky-patches/nav-patch/index.ts @@ -0,0 +1 @@ +export * from "./nav-patch"; diff --git a/src/decky-patches/nav-patch/nav-patch.ts b/src/decky-patches/nav-patch/nav-patch.ts new file mode 100644 index 0000000..3efb62a --- /dev/null +++ b/src/decky-patches/nav-patch/nav-patch.ts @@ -0,0 +1,30 @@ +import { Patch, findModuleExport, replacePatch } from "@decky/ui"; + +export const NavController = findModuleExport( + (e) => e?.prototype?.FindNextFocusableChildInDirection +); +export function enableNavPatch(): Patch { + const patch = replacePatch( + NavController.prototype, + "FindNextFocusableChildInDirection", + function (args) { + const e = args[0]; + const t = args[1]; + const r = args[2]; + let n = t == 1 ? 1 : -1; + // @ts-ignore + for (let t = e + n; t >= 0 && t < this.m_rgChildren.length; t += n) { + // @ts-ignore + const e = this.m_rgChildren[t].FindFocusableNode(r); + if (e && window.getComputedStyle(e.m_element).display !== "none") return e; + } + return null; + } + ); + return patch; +} + +export function disableNavPatch(navPatchInstance: Patch | undefined) { + if (!navPatchInstance) return; + navPatchInstance.unpatch(); +} diff --git a/src/deckyPatches/ClassHashMap.tsx b/src/decky-patches/unminify-mode/class-hash-map.ts similarity index 90% rename from src/deckyPatches/ClassHashMap.tsx rename to src/decky-patches/unminify-mode/class-hash-map.ts index 156e395..86c96ff 100644 --- a/src/deckyPatches/ClassHashMap.tsx +++ b/src/decky-patches/unminify-mode/class-hash-map.ts @@ -1,4 +1,4 @@ -import { classMap } from "decky-frontend-lib"; +import { classMap } from "@decky/ui"; export var classHashMap = new Map(); @@ -12,6 +12,7 @@ export function initializeClassHashMap() { // Filter out things that start with a number (eg: Breakpoints like 800px) // I have confirmed the new classes don't start with numbers if (isNaN(Number(value.charAt(0)))) { + // @ts-expect-error filteredModule[propertyName] = value; } }); @@ -24,9 +25,12 @@ export function initializeClassHashMap() { const mappings = allClasses.reduce((acc, cur) => { Object.entries(cur).forEach(([property, value]) => { + // @ts-expect-error if (acc[property]) { + // @ts-expect-error acc[property].push(value); } else { + // @ts-expect-error acc[property] = [value]; } }); diff --git a/src/decky-patches/unminify-mode/index.ts b/src/decky-patches/unminify-mode/index.ts new file mode 100644 index 0000000..1e9c4c3 --- /dev/null +++ b/src/decky-patches/unminify-mode/index.ts @@ -0,0 +1 @@ +export * from "./unminify-mode"; diff --git a/src/deckyPatches/SteamTabElementsFinder.tsx b/src/decky-patches/unminify-mode/steam-tab-elements-finder.ts similarity index 80% rename from src/deckyPatches/SteamTabElementsFinder.tsx rename to src/decky-patches/unminify-mode/steam-tab-elements-finder.ts index 3bdeda0..70d1a10 100644 --- a/src/deckyPatches/SteamTabElementsFinder.tsx +++ b/src/decky-patches/unminify-mode/steam-tab-elements-finder.ts @@ -1,4 +1,4 @@ -import { getGamepadNavigationTrees } from "decky-frontend-lib"; +import { getGamepadNavigationTrees } from "@decky/ui"; export function getElementFromNavID(navID: string) { const all = getGamepadNavigationTrees(); @@ -17,5 +17,5 @@ export function getMainMenu() { return getElementFromNavID("MainNavMenuContainer"); } export function getRootElements() { - return [getSP(), getQAM(), getMainMenu()].filter((e) => e); + return [getSP(), getQAM(), getMainMenu()].filter(Boolean); } diff --git a/src/deckyPatches/UnminifyMode.tsx b/src/decky-patches/unminify-mode/unminify-mode.ts similarity index 93% rename from src/deckyPatches/UnminifyMode.tsx rename to src/decky-patches/unminify-mode/unminify-mode.ts index d104ead..dde18a3 100644 --- a/src/deckyPatches/UnminifyMode.tsx +++ b/src/decky-patches/unminify-mode/unminify-mode.ts @@ -1,5 +1,5 @@ -import { classHashMap, initializeClassHashMap } from "./ClassHashMap"; -import { getRootElements } from "./SteamTabElementsFinder"; +import { classHashMap, initializeClassHashMap } from "./class-hash-map"; +import { getRootElements } from "./steam-tab-elements-finder"; export function unminifyElement(element: Element) { if (element.classList.length === 0) return; diff --git a/src/deckyPatches/NavControllerFinder.tsx b/src/deckyPatches/NavControllerFinder.tsx deleted file mode 100644 index 7b95239..0000000 --- a/src/deckyPatches/NavControllerFinder.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Module, findModuleChild } from "decky-frontend-lib"; - -export const NavController = findModuleChild((m: Module) => { - if (typeof m !== "object") return undefined; - - // Pre Chromium-109 - if (m?.CFocusNavNode) { - return m.CFocusNavNode; - } - - for (let prop in m) { - if (m[prop]?.prototype?.FindNextFocusableChildInDirection) { - return m[prop]; - } - } - - return undefined; -}); diff --git a/src/deckyPatches/NavPatch.tsx b/src/deckyPatches/NavPatch.tsx deleted file mode 100644 index 9c48b08..0000000 --- a/src/deckyPatches/NavPatch.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { replacePatch } from "decky-frontend-lib"; -import { NavController } from "./NavControllerFinder"; -import { globalState, toast, storeWrite } from "../python"; - -export function enableNavPatch(shouldToast: boolean = false) { - const setGlobalState = globalState!.setGlobalState.bind(globalState); - const { navPatchInstance } = globalState!.getPublicState(); - // Don't patch twice - if (navPatchInstance) return; - const patch = replacePatch( - NavController.prototype, - "FindNextFocusableChildInDirection", - function (args) { - const e = args[0]; - const t = args[1]; - const r = args[2]; - let n = t == 1 ? 1 : -1; - // @ts-ignore - for (let t = e + n; t >= 0 && t < this.m_rgChildren.length; t += n) { - // @ts-ignore - const e = this.m_rgChildren[t].FindFocusableNode(r); - if (e && window.getComputedStyle(e.m_element).display !== "none") return e; - } - return null; - } - ); - setGlobalState("navPatchInstance", patch); - shouldToast && toast("CSS Loader", "Nav Patch Enabled"); - return; -} - -export function disableNavPatch(shouldToast: boolean = false) { - const setGlobalState = globalState!.setGlobalState.bind(globalState); - const { navPatchInstance } = globalState!.getPublicState(); - // Don't unpatch something that doesn't exist - // Probably the closest thing JS can get to null dereference - if (!navPatchInstance) return; - navPatchInstance.unpatch(); - setGlobalState("navPatchInstance", undefined); - shouldToast && toast("CSS Loader", "Nav Patch Disabled"); - return; -} - -export function setNavPatch(value: boolean, shouldToast: boolean = false) { - value ? enableNavPatch(shouldToast) : disableNavPatch(shouldToast); - storeWrite("enableNavPatch", value + ""); -} diff --git a/src/deckyPatches/NavPatchInfoModal.tsx b/src/deckyPatches/NavPatchInfoModal.tsx deleted file mode 100644 index 13c1d53..0000000 --- a/src/deckyPatches/NavPatchInfoModal.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { DialogButton, Focusable, ConfirmModal } from "decky-frontend-lib"; -import { Theme } from "../ThemeTypes"; -import { setNavPatch } from "./NavPatch"; -export function NavPatchInfoModalRoot({ - themeData, - closeModal, -}: { - themeData: Theme; - closeModal?: any; -}) { - function onButtonClick() { - setNavPatch(true, true); - closeModal(); - } - return ( - - - {themeData.name} hides elements that can be selected using a controller. For this to work - correctly, CSS Loader needs to patch controller navigation. Not enabling this feature will - cause visually hidden elements to be able to be selected using a controller. - - - ); -} diff --git a/src/deckyPatches/dump-mappings.ts b/src/deckyPatches/dump-mappings.ts deleted file mode 100644 index 8a76b63..0000000 --- a/src/deckyPatches/dump-mappings.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { saveMappings } from "../python"; - -export async function dumpMappings() { - try { - if (!window.DFL) return; - const map = window.DFL.classModuleMap as Map; - if (!map) return; - const jsonStr = JSON.stringify(Object.fromEntries(map)); - - await saveMappings(jsonStr); - } catch (error) { - console.error("ERROR SAVING MAPPINGS", error); - } -} diff --git a/src/hooks/index.ts b/src/hooks/index.ts deleted file mode 100644 index d8e5bf3..0000000 --- a/src/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./useRerender"; diff --git a/src/index.tsx b/src/index.tsx index 4cd407c..b2f96bd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,209 +1,53 @@ -import { - ButtonItem, - definePlugin, - PanelSection, - PanelSectionRow, - ServerAPI, -} from "decky-frontend-lib"; -import { useEffect, useState } from "react"; -import * as python from "./python"; -import * as api from "./api"; +import { StyleProvider, TitleView } from "@/lib"; import { RiPaintFill } from "react-icons/ri"; - -import { ThemeManagerRouter } from "./pages/theme-manager"; -import { CssLoaderContextProvider, CssLoaderState, useCssLoaderState } from "./state"; -import { MOTDDisplay, PresetSelectionDropdown, QAMThemeToggleList, TitleView } from "./components"; -import { ExpandedViewPage } from "./pages/theme-manager/ExpandedView"; -import { Flags, Theme } from "./ThemeTypes"; -import { dummyFunction, getInstalledThemes, reloadBackend } from "./python"; -import { bulkThemeUpdateCheck } from "./logic/bulkThemeUpdateCheck"; -import { disableNavPatch, enableNavPatch } from "./deckyPatches/NavPatch"; -import { SettingsPageRouter } from "./pages/settings/SettingsPageRouter"; -import { disableUnminifyMode } from "./deckyPatches/UnminifyMode"; - -function Content() { - const { localThemeList, setGlobalState } = useCssLoaderState(); - - const [dummyFuncResult, setDummyResult] = useState(false); - - function dummyFuncTest() { - dummyFunction().then((res) => { - if (res.success) { - setDummyResult(res.result); - return; - } - setDummyResult(false); - }); - } - - function reload() { - reloadBackend(); - dummyFuncTest(); - bulkThemeUpdateCheck().then((data) => setGlobalState("updateStatuses", data)); - } - - useEffect(() => { - setGlobalState( - "selectedPreset", - localThemeList.find((e) => e.flags.includes(Flags.isPreset) && e.enabled) - ); - }, [localThemeList]); - - useEffect(() => { - dummyFuncTest(); - getInstalledThemes(); - }, []); - - return ( - <> - - - {dummyFuncResult ? ( - <> - - {localThemeList.length > 0 && } - - - ) : ( - - - CssLoader failed to initialize, try reloading, and if that doesn't work, try - restarting your deck. - - - )} - - - reload()}> - Refresh - - - - - ); -} - -export default definePlugin((serverApi: ServerAPI) => { - const state: CssLoaderState = new CssLoaderState(); - python.setServer(serverApi); - python.setStateClass(state); - api.setServer(serverApi); - api.setStateClass(state); - - python.resolve(python.getThemes(), async (allThemes: Theme[]) => { - // Set selectedPreset - state.setGlobalState( - "selectedPreset", - allThemes.find((e) => e.flags.includes(Flags.isPreset) && e.enabled) - ); - - // Check for updates, and schedule a check 24 hours from now - bulkThemeUpdateCheck(allThemes).then((data) => { - state.setGlobalState("updateStatuses", data); - }); - python.scheduleCheckForUpdates(); - - // If a user has magically deleted a theme in the unpinnedList and the store wasn't updated, this fixes that - python.resolve(python.storeRead("unpinnedThemes"), (unpinnedJsonStr: string) => { - const unpinnedThemes: string[] = unpinnedJsonStr ? JSON.parse(unpinnedJsonStr) : []; - const allIds = allThemes.map((e) => e.id); - - // If a theme is in the unpinned store but no longer exists, remove it from the unpinned store - let unpinnedClone = [...unpinnedThemes]; - unpinnedThemes.forEach((e) => { - if (!allIds.includes(e)) { - unpinnedClone = unpinnedClone.filter((id) => id !== e); - } - }); - - state.setGlobalState("unpinnedThemes", unpinnedClone); - if (JSON.stringify(unpinnedClone) !== unpinnedJsonStr) { - python.storeWrite("unpinnedThemes", JSON.stringify(unpinnedClone)); - } - }); - }); - - // Api Token - python.resolve(python.storeRead("shortToken"), (token: string) => { - if (token) { - state.setGlobalState("apiShortToken", token); - } - }); - - // Hidden MOTD - python.resolve(python.storeRead("hiddenMotd"), (id: string) => { - if (id) { - state.setGlobalState("hiddenMotd", id); - } - }); - - // Nav Patch - python.resolve(python.storeRead("enableNavPatch"), (value: string) => { - if (value === "true") { - enableNavPatch(); - } - }); - - serverApi.routerHook.addRoute("/cssloader/theme-manager", () => ( - - - +import { QamTabPage } from "@/modules/qam-tab-page"; +import { definePlugin, routerHook } from "@decky/api"; +import { getCSSLoaderState } from "@/backend"; +import { getDeckyPatchState } from "./decky-patches"; +import { ThemeStoreRouter } from "./modules/theme-store"; +import { ExpandedViewPage } from "./modules/expanded-view"; +import { SettingsPageRouter } from "./modules/settings"; + +export default definePlugin(() => { + getCSSLoaderState().initializeStore(); + getDeckyPatchState().initializeStore(); + + routerHook.addRoute("/cssloader/theme-store", () => ( + + + )); - serverApi.routerHook.addRoute("/cssloader/settings", () => ( - - - + routerHook.addRoute("/cssloader/expanded-view", () => ( + + + )); - serverApi.routerHook.addRoute("/cssloader/expanded-view", () => ( - - - + routerHook.addRoute("/cssloader/settings", () => ( + + + )); return { + name: "SDH-CSSLoader", titleView: ( - + - + ), title:
CSSLoader
, - alwaysRender: true, + icon: , content: ( - - - + + + ), - icon: , + alwaysRender: true, onDismount: () => { - const { updateCheckTimeout } = state.getPublicState(); - if (updateCheckTimeout) clearTimeout(updateCheckTimeout); - disableUnminifyMode(); - disableNavPatch(); + getCSSLoaderState().deactivate(); + getDeckyPatchState().deactivate(); }, }; }); diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts new file mode 100644 index 0000000..aa048af --- /dev/null +++ b/src/lib/components/index.ts @@ -0,0 +1,7 @@ +export * from "./title-view"; +export * from "./motd-display"; +export * from "./preset-selection-dropdown"; +export * from "./theme-patch"; +export * from "./modals"; +export * from "./theme-card"; +export * from "./theme-error-card"; diff --git a/src/lib/components/modals/author-view-modal/AuthorViewModal.tsx b/src/lib/components/modals/author-view-modal/AuthorViewModal.tsx new file mode 100644 index 0000000..b68cca9 --- /dev/null +++ b/src/lib/components/modals/author-view-modal/AuthorViewModal.tsx @@ -0,0 +1,83 @@ +import { Modal } from "../../../primitives"; +import { PartialCSSThemeInfo, ThemeQueryResponse, UserInfo } from "@/types"; +import { useEffect, useRef, useState } from "react"; +import { useCSSLoaderAction } from "@/backend"; +import { SupporterIcon } from "./SupporterIcon"; +import { ImSpinner5 } from "react-icons/im"; +import { Focusable } from "@decky/ui"; +import { ThemeCard, ThemeCardCSSVariableProvider } from "../../theme-card"; + +export function AuthorViewModal({ + closeModal, + authorData, + onThemeClick, +}: { + closeModal?: () => void; + authorData: UserInfo; + onThemeClick?: (themeId: string) => void; +}) { + const apiFetch = useCSSLoaderAction("apiFetch"); + + const [loaded, setLoaded] = useState(false); + const [themes, setThemes] = useState([]); + + const firstThemeRef = useRef(null); + + async function fetchThemeData() { + const data: ThemeQueryResponse = await apiFetch( + `/users/${authorData.id}/themes?page=1&perPage=50&filters=CSS&order=Most Downloaded` + ); + if (data?.total && data.total > 0) { + setThemes(data.items); + setLoaded(true); + } + } + useEffect(() => { + fetchThemeData(); + }, []); + + useEffect(() => { + if (firstThemeRef?.current) { + setTimeout(() => { + firstThemeRef?.current?.focus(); + }, 10); + } + }, [loaded]); + + return ( + + + {loaded ? ( + <> +
+ + {authorData.username} +
+ +
+
+ + {themes.map((theme, i) => { + return ( + { + closeModal?.(); + onThemeClick?.(theme.id); + }} + /> + ); + })} + + + ) : ( +
+ + {/* Re-using expanded view's loading class */} + Loading +
+ )} +
+ ); +} diff --git a/src/components/SupporterIcon.tsx b/src/lib/components/modals/author-view-modal/SupporterIcon.tsx similarity index 66% rename from src/components/SupporterIcon.tsx rename to src/lib/components/modals/author-view-modal/SupporterIcon.tsx index 0efd4b0..1ce6478 100644 --- a/src/components/SupporterIcon.tsx +++ b/src/lib/components/modals/author-view-modal/SupporterIcon.tsx @@ -1,19 +1,20 @@ +import { UserInfo } from "@/types"; import { RiMedalFill } from "react-icons/ri"; -import { UserInfo } from "../apiTypes/CSSThemeTypes"; -export function SupporterIcon({ author }: { author: UserInfo }) { +// Not even gonna try and refactor this +export function SupporterIcon({ authorData }: { authorData: UserInfo }) { const randId = Math.trunc(Math.random() * 69420); return ( <> - {author?.premiumTier && author?.premiumTier !== "None" && ( + {authorData?.premiumTier && authorData?.premiumTier !== "None" && (
- {`Tier ${author?.premiumTier?.slice(-1)} Patreon Supporter`} + {`Tier ${authorData?.premiumTier?.slice(-1)} Patreon Supporter`}
)} diff --git a/src/lib/components/modals/author-view-modal/index.ts b/src/lib/components/modals/author-view-modal/index.ts new file mode 100644 index 0000000..b0b4256 --- /dev/null +++ b/src/lib/components/modals/author-view-modal/index.ts @@ -0,0 +1 @@ +export * from "./AuthorViewModal"; diff --git a/src/lib/components/modals/delete-confirmation-modal/DeleteConfirmationModal.tsx b/src/lib/components/modals/delete-confirmation-modal/DeleteConfirmationModal.tsx new file mode 100644 index 0000000..060d623 --- /dev/null +++ b/src/lib/components/modals/delete-confirmation-modal/DeleteConfirmationModal.tsx @@ -0,0 +1,33 @@ +import { useCSSLoaderAction } from "@/backend"; +import { ConfirmModal } from "../../../primitives"; + +export function DeleteConfirmationModal({ + closeModal, + themeIdsToBeDeleted, + onDeleteFinish, +}: { + closeModal?: () => void; + themeIdsToBeDeleted: string[]; + onDeleteFinish?: () => void; +}) { + const deleteTheme = useCSSLoaderAction("deleteTheme"); + async function deleteThemes() { + for (let i = 0; i < themeIdsToBeDeleted.length; i++) { + await deleteTheme(themeIdsToBeDeleted[i], i === themeIdsToBeDeleted.length - 1); + } + onDeleteFinish?.(); + closeModal?.(); + } + + return ( + +
+ Are you sure you want to delete{" "} + {themeIdsToBeDeleted.length === 1 + ? `this theme` + : `these ${themeIdsToBeDeleted.length} themes`} + ? +
+
+ ); +} diff --git a/src/lib/components/modals/delete-confirmation-modal/index.ts b/src/lib/components/modals/delete-confirmation-modal/index.ts new file mode 100644 index 0000000..a06f077 --- /dev/null +++ b/src/lib/components/modals/delete-confirmation-modal/index.ts @@ -0,0 +1 @@ +export * from "./DeleteConfirmationModal"; diff --git a/src/lib/components/modals/index.ts b/src/lib/components/modals/index.ts new file mode 100644 index 0000000..7a8075e --- /dev/null +++ b/src/lib/components/modals/index.ts @@ -0,0 +1,5 @@ +export * from "./nav-patch-info-modal"; +export * from "./optional-deps-modal"; +export * from "./author-view-modal"; +export * from "./theme-settings-modal"; +export * from "./delete-confirmation-modal"; diff --git a/src/lib/components/modals/nav-patch-info-modal/NavPatchInfoModal.tsx b/src/lib/components/modals/nav-patch-info-modal/NavPatchInfoModal.tsx new file mode 100644 index 0000000..fd80ab4 --- /dev/null +++ b/src/lib/components/modals/nav-patch-info-modal/NavPatchInfoModal.tsx @@ -0,0 +1,24 @@ +import { useDeckyPatchStateAction } from "@/decky-patches"; +import { ConfirmModal } from "../../../primitives"; + +export function NavPatchInfoModal({ closeModal }: { closeModal?: () => void }) { + const setNavPatchState = useDeckyPatchStateAction("setNavPatchState"); + return ( + { + setNavPatchState(true, true); + closeModal?.(); + }} + > +

+ This theme hides elements that can be selected using a controller. For this to work + correctly, CSS Loader needs to patch controller navigation. Not enabling this feature will + cause visually hidden elements to be able to be selected using a controller. +

+
+ ); +} diff --git a/src/lib/components/modals/nav-patch-info-modal/index.ts b/src/lib/components/modals/nav-patch-info-modal/index.ts new file mode 100644 index 0000000..3073a34 --- /dev/null +++ b/src/lib/components/modals/nav-patch-info-modal/index.ts @@ -0,0 +1 @@ +export * from "./NavPatchInfoModal"; diff --git a/src/lib/components/modals/optional-deps-modal/OptionalDepsModal.tsx b/src/lib/components/modals/optional-deps-modal/OptionalDepsModal.tsx new file mode 100644 index 0000000..93d0f8e --- /dev/null +++ b/src/lib/components/modals/optional-deps-modal/OptionalDepsModal.tsx @@ -0,0 +1,45 @@ +import { Theme } from "@/types"; +import { Modal } from "../../../primitives"; +import { DialogButton, Focusable } from "@decky/ui"; + +export function OptionalDepsModal({ + closeModal, + theme, + onSelect, +}: { + closeModal?: () => void; + theme: Theme; + onSelect: (enableDeps: boolean, enableDepValues: boolean) => void; +}) { + function handleChoice(enableDeps: boolean, enableDepValues: boolean) { + onSelect(enableDeps, enableDepValues); + closeModal?.(); + } + + return ( + +
+

+ {theme.display_name} enables optional themes to enhance this theme. Disabling these may + break the theme, or make the theme look completely different. Specific optional themes can + be configured and or enabled/disabled anytime via the Quick Access Menu. +

+

+ Enable without configuration will enable optional themes but not overwrite their + configuration, and Enable only this theme will not enable any optional themes. +

+
+ + handleChoice(true, true)}> + Enable with configuration {"(Recommended)"} + + handleChoice(true, false)}> + Enable without configuration + + handleChoice(false, false)}> + Enable only this theme + + +
+ ); +} diff --git a/src/lib/components/modals/optional-deps-modal/index.ts b/src/lib/components/modals/optional-deps-modal/index.ts new file mode 100644 index 0000000..dbf87ce --- /dev/null +++ b/src/lib/components/modals/optional-deps-modal/index.ts @@ -0,0 +1 @@ +export * from "./OptionalDepsModal"; diff --git a/src/lib/components/modals/theme-settings-modal/ThemeSettingsModal.tsx b/src/lib/components/modals/theme-settings-modal/ThemeSettingsModal.tsx new file mode 100644 index 0000000..e25b26d --- /dev/null +++ b/src/lib/components/modals/theme-settings-modal/ThemeSettingsModal.tsx @@ -0,0 +1,62 @@ +import { useCSSLoaderValue } from "@/backend"; +import { Modal } from "../../../primitives"; +import { DialogButton, Focusable, Toggle } from "@decky/ui"; +import { toggleThemeWithModals } from "../../../utils"; +import { ThemePatch } from "../../theme-patch"; +import { ThemeSettingsModalActionButtons } from "./ThemeSettingsModalActionButtons"; + +export function ThemeSettingsModal({ + closeModal, + themeId, +}: { + closeModal?: () => void; + themeId: string; +}) { + const themes = useCSSLoaderValue("themes"); + const theme = themes.find((theme) => theme.id === themeId); + + return ( + + {!!theme ? ( + + +
+ {theme.display_name} + + {theme.version} | {theme.author} + +
+ { + toggleThemeWithModals(theme, checked); + }} + /> +
+ {theme.enabled && theme.patches.length > 0 ? ( + + {theme.patches.map((patch, index) => ( + + ))} + + ) : null} + + Close + + +
+ ) : ( + + Theme Not Found + Go Back + + )} +
+ ); +} diff --git a/src/lib/components/modals/theme-settings-modal/ThemeSettingsModalActionButtons.tsx b/src/lib/components/modals/theme-settings-modal/ThemeSettingsModalActionButtons.tsx new file mode 100644 index 0000000..3e04190 --- /dev/null +++ b/src/lib/components/modals/theme-settings-modal/ThemeSettingsModalActionButtons.tsx @@ -0,0 +1,75 @@ +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; +import { LocalThemeStatus, Theme } from "@/types"; +import { DialogButton, Focusable, showModal } from "@decky/ui"; +import { FaDownload, FaEye, FaEyeSlash, FaTrash } from "react-icons/fa6"; +import { DeleteConfirmationModal } from "../delete-confirmation-modal"; + +// TODO: Re-add star status to this modal +export function ThemeSettingsModalActionButtons({ + theme, + closeModal, +}: { + theme: Theme; + closeModal?: () => void; +}) { + const isWorking = useCSSLoaderValue("isWorking"); + + // Update Check + const updateStatuses = useCSSLoaderValue("updateStatuses"); + const installTheme = useCSSLoaderAction("installTheme"); + + let updateStatus: LocalThemeStatus = "installed"; + const themeArrPlace = updateStatuses.find((f) => f[0] === theme.id); + if (themeArrPlace) updateStatus = themeArrPlace[1]; + const isOutdated = updateStatus === "outdated"; + + function handleUpdate() { + void installTheme(theme.id); + } + + // Pinning + const unpinnedThemes = useCSSLoaderValue("unpinnedThemes"); + const pinTheme = useCSSLoaderAction("pinTheme"); + const unpinTheme = useCSSLoaderAction("unpinTheme"); + const isPinned = !unpinnedThemes.includes(theme.id); + function handlePinClick() { + if (isPinned) { + void unpinTheme(theme.id); + } else { + void pinTheme(theme.id); + } + } + + return ( + + {isOutdated && ( + + + Update + + )} + + {isPinned ? ( + + ) : ( + + )} + + { + showModal( + + ); + }} + > + + + + ); +} diff --git a/src/components/Modals/ThemeSettingsModal/index.ts b/src/lib/components/modals/theme-settings-modal/index.ts similarity index 100% rename from src/components/Modals/ThemeSettingsModal/index.ts rename to src/lib/components/modals/theme-settings-modal/index.ts diff --git a/src/components/QAMTab/MOTDDisplay.tsx b/src/lib/components/motd-display/MOTDDisplay.tsx similarity index 58% rename from src/components/QAMTab/MOTDDisplay.tsx rename to src/lib/components/motd-display/MOTDDisplay.tsx index c586651..57dff91 100644 --- a/src/components/QAMTab/MOTDDisplay.tsx +++ b/src/lib/components/motd-display/MOTDDisplay.tsx @@ -1,49 +1,37 @@ -import { DialogButton, Focusable, PanelSection } from "decky-frontend-lib"; -import { useEffect, useState, useMemo } from "react"; -import { Motd } from "../../apiTypes/Motd"; -import { genericGET } from "../../api"; +import { DialogButton, Focusable, PanelSection } from "@decky/ui"; +import { useEffect, useMemo } from "react"; +import { Motd } from "@/types"; import { FaTimes } from "react-icons/fa"; -import { useCssLoaderState } from "../../state"; -import { getHiddenMotd, setHiddenMotd } from "../../backend/pythonMethods/pluginSettingsMethods"; +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; + +const SEVERITIES = { + High: { + color: "#bb1414", + text: "#fff", + }, + Medium: { + color: "#bbbb14", + text: "#fff", + }, + Low: { + color: "#1488bb", + text: "#fff", + }, +}; export function MOTDDisplay() { - const [motd, setMotd] = useState(); - const { hiddenMotd, setGlobalState } = useCssLoaderState(); + const getMotd = useCSSLoaderAction("getMotd"); + const hideMotd = useCSSLoaderAction("hideMotd"); + const motd = useCSSLoaderValue("motd"); + const hiddenMotdId = useCSSLoaderValue("hiddenMotdId"); + useEffect(() => { - async function getMotd() { - const res = await genericGET("/motd", false, undefined, () => {}, true); - setMotd(res); - } - getMotd(); + void getMotd(); }, []); - async function dismiss() { - if (motd) { - await setHiddenMotd(motd.id); - const res = await getHiddenMotd(); - if (res.success) { - setGlobalState("hiddenMotd", res.result); - } - } - } - - const SEVERITIES = { - High: { - color: "#bb1414", - text: "#fff", - }, - Medium: { - color: "#bbbb14", - text: "#fff", - }, - Low: { - color: "#1488bb", - text: "#fff", - }, - }; const hidden = useMemo(() => { - return hiddenMotd === motd?.id; - }, [hiddenMotd, motd]); + return hiddenMotdId === motd?.id; + }, [hiddenMotdId, motd]); const severity = SEVERITIES[motd?.severity || "Low"]; @@ -79,7 +67,7 @@ export function MOTDDisplay() { top: ".75em", right: ".75em", }} - onClick={dismiss} + onClick={hideMotd} > e.flags.includes(Flags.isPreset)); + const hasInvalidPresetState = presets.filter((e) => e.enabled).length > 1; + + const [render, rerender] = useForcedRerender(); + + return ( + <> + {render && ( + + ({ label: e.display_name, data: e.name })), + { + data: "New Profile", + label: ( +
+ + New Profile +
+ ), + }, + ]} + onChange={async ({ data }) => { + if (data === "New Profile") { + // showModal(); + rerender(); + return; + } + await changePreset(data); + }} + /> +
+ )} + + ); +} diff --git a/src/lib/components/preset-selection-dropdown/index.ts b/src/lib/components/preset-selection-dropdown/index.ts new file mode 100644 index 0000000..8313d47 --- /dev/null +++ b/src/lib/components/preset-selection-dropdown/index.ts @@ -0,0 +1 @@ +export * from "./PresetSelectionDropdown"; diff --git a/src/lib/components/theme-card/ThemeCard.tsx b/src/lib/components/theme-card/ThemeCard.tsx new file mode 100644 index 0000000..584b047 --- /dev/null +++ b/src/lib/components/theme-card/ThemeCard.tsx @@ -0,0 +1,68 @@ +import { PartialCSSThemeInfo } from "@/types"; +import { forwardRef } from "react"; +import { useCSSLoaderValue } from "@/backend"; +import { AiOutlineDownload } from "react-icons/ai"; +import { Focusable } from "@decky/ui"; +import { FaBullseye, FaDownload, FaStar } from "react-icons/fa6"; +import { useThemeInstallState } from "../../hooks"; +// Hard-coded path to prevent require cycle +import { shortenNumber } from "../../utils/shorten-number"; + +interface ThemeCardProps { + theme: PartialCSSThemeInfo; + onClick?: () => void; +} + +export const ThemeCard = forwardRef(({ theme, onClick }, ref) => { + const apiUrl = useCSSLoaderValue("apiUrl"); + const installStatus = useThemeInstallState(theme); + + const imageUrl = + theme?.images[0]?.id && theme.images[0].id !== "MISSING" + ? `${apiUrl}/blobs/${theme.images[0].id}` + : `https://share.deckthemes.com/cssplaceholder.png`; + + return ( +
+ {installStatus === "outdated" && ( +
+ +
+ )} + +
+ +
+
+
+ + + {shortenNumber(theme.download.downloadCount) ?? theme.download.downloadCount} + +
+
+ + {shortenNumber(theme.starCount) ?? theme.starCount} +
+
+ + {theme.target} +
+
+
+
+ {theme.displayName} + + {theme.version} - Last Updated {new Date(theme.updated).toLocaleDateString()} + + By {theme.specifiedAuthor} +
+ +
+ ); +}); diff --git a/src/lib/components/theme-card/ThemeCardCSSVariableProvider.tsx b/src/lib/components/theme-card/ThemeCardCSSVariableProvider.tsx new file mode 100644 index 0000000..6a32cc5 --- /dev/null +++ b/src/lib/components/theme-card/ThemeCardCSSVariableProvider.tsx @@ -0,0 +1,7 @@ +import { themeCardStylesGenerator } from "@/styles"; + +export type ColumnNumbers = 3 | 4 | 5; + +export function ThemeCardCSSVariableProvider({ cardSize }: { cardSize: ColumnNumbers }) { + return ; +} diff --git a/src/lib/components/theme-card/index.ts b/src/lib/components/theme-card/index.ts new file mode 100644 index 0000000..983ed59 --- /dev/null +++ b/src/lib/components/theme-card/index.ts @@ -0,0 +1,2 @@ +export * from "./ThemeCard"; +export * from "./ThemeCardCSSVariableProvider"; diff --git a/src/lib/components/theme-error-card/ThemeErrorCard.tsx b/src/lib/components/theme-error-card/ThemeErrorCard.tsx new file mode 100644 index 0000000..a8849b8 --- /dev/null +++ b/src/lib/components/theme-error-card/ThemeErrorCard.tsx @@ -0,0 +1,24 @@ +import { ThemeError } from "@/types"; +import { Focusable } from "@decky/ui"; + +export function ThemeErrorCard({ error }: { error: ThemeError }) { + return ( + {}} + className="w-full m-0 p-0" + > +
+ + {error[0]} + + {error[1]} +
+
+ ); +} diff --git a/src/lib/components/theme-error-card/index.ts b/src/lib/components/theme-error-card/index.ts new file mode 100644 index 0000000..815602e --- /dev/null +++ b/src/lib/components/theme-error-card/index.ts @@ -0,0 +1 @@ +export * from "./ThemeErrorCard"; diff --git a/src/lib/components/theme-patch/ThemePatch.tsx b/src/lib/components/theme-patch/ThemePatch.tsx new file mode 100644 index 0000000..154d120 --- /dev/null +++ b/src/lib/components/theme-patch/ThemePatch.tsx @@ -0,0 +1,103 @@ +import { Patch } from "@/types"; +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; +import { useState } from "react"; +import { DropdownItem, PanelSectionRow, SliderField, ToggleField } from "@decky/ui"; +import { ThemePatchComponent } from "./ThemePatchComponent"; + +export function ThemePatch({ + patch, + shouldHaveBottomSeparator, + themeName, + inModal, +}: { + patch: Patch; + shouldHaveBottomSeparator: boolean; + themeName: string; + inModal?: boolean; +}) { + const bottomSeparatorValue = shouldHaveBottomSeparator ? "standard" : "none"; + + const setPatchValue = useCSSLoaderAction("setPatchValue"); + + const [selectedValueIndex, setSelectedValueIndex] = useState(patch.options.indexOf(patch.value)); + + function onValueChange(value: string) { + const index = patch.options.indexOf(value); + setSelectedValueIndex(index); + // I vaguely remember this fixing some optimistic state update issue + patch.value = value; + void setPatchValue(themeName, patch.name, value); + } + + return ( + <> + + {patch.type === "slider" && ( + } + min={0} + max={patch.options.length - 1} + value={selectedValueIndex} + notchCount={patch.options.length} + notchLabels={patch.options.map((option, index) => ({ + notchIndex: index, + label: option, + value: index, + }))} + onChange={(index) => { + onValueChange(patch.options[index]); + }} + /> + )} + {patch.type === "checkbox" && ( + } + checked={patch.value === "Yes"} + onChange={(value) => { + // TODO: TEST THIS + const newValue = value ? "Yes" : "No"; + onValueChange(newValue); + }} + /> + )} + {patch.type === "dropdown" && ( + } + menuLabel={patch.name} + rgOptions={patch.options.map((option, index) => ({ label: option, data: index }))} + selectedOption={selectedValueIndex} + onChange={(value) => { + onValueChange(value.label as string); + }} + /> + )} + {patch.type === "none" && ( + <> + {inModal ? ( + {patch.name} + ) : ( + + )} + + )} + + {patch.components.map((component) => ( + + ))} + + ); +} + +// TODO: IS THIS NEEDED? +function PatchLabel({ name }: { name: string }) { + return
{name}
; +} diff --git a/src/lib/components/theme-patch/ThemePatchComponent.tsx b/src/lib/components/theme-patch/ThemePatchComponent.tsx new file mode 100644 index 0000000..2509a53 --- /dev/null +++ b/src/lib/components/theme-patch/ThemePatchComponent.tsx @@ -0,0 +1,93 @@ +import { ThemePatchComponent } from "@/types"; +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; +import { ButtonItem, ColorPickerModal, PanelSectionRow, showModal } from "@decky/ui"; +import { FaFolder } from "react-icons/fa"; +import { FileSelectionType, openFilePicker } from "@decky/api"; +import Color from "color"; + +export function ThemePatchComponent({ + component, + currentPatchValue, + patchName, + themeName, + shouldHaveBottomSeparator, +}: { + component: ThemePatchComponent; + currentPatchValue?: string; + patchName: string; + themeName: string; + shouldHaveBottomSeparator: boolean; +}) { + const bottomSeparatorValue = shouldHaveBottomSeparator ? "standard" : "none"; + + const setComponentValue = useCSSLoaderAction("setComponentValue"); + const themeRootPath = useCSSLoaderValue("themeRootPath"); + const toast = useCSSLoaderAction("toast"); + if (currentPatchValue !== component.on) return null; + + function onValueChange(value: string) { + void setComponentValue(themeName, patchName, component.name, value); + } + + async function handleImagePicker() { + // TODO: GATE BY FILE EXTENSION + try { + const filePickerRes = await openFilePicker(FileSelectionType.FILE, themeRootPath); + if (!filePickerRes.path.includes(themeRootPath)) { + throw new Error("Images must be within themes folder"); + } + if (!/\.(jpg|jpeg|png|webp|avif|gif|svg)$/.test(filePickerRes.path)) { + throw new Error("Invalid File Type"); + } + const relativePath = filePickerRes.path.split(`${themeRootPath}/`)[1]; + onValueChange(relativePath); + } catch (error) { + if (error instanceof Error) { + toast(error.message); + } + } + } + + function handleColorPicker() { + const curColorHSLArray = Color(component.value).hsl().array(); + showModal( + // @ts-expect-error + { + onValueChange(hslString); + }} + /> + ); + } + + return ( + + +
+ Open {component.name} +
+ {component.type === "color-picker" ? ( +
+ ) : ( + + )} +
+
+ + + ); +} diff --git a/src/lib/components/theme-patch/index.ts b/src/lib/components/theme-patch/index.ts new file mode 100644 index 0000000..a0c404e --- /dev/null +++ b/src/lib/components/theme-patch/index.ts @@ -0,0 +1 @@ +export * from "./ThemePatch"; diff --git a/src/lib/components/title-view/TitleView.tsx b/src/lib/components/title-view/TitleView.tsx new file mode 100644 index 0000000..5a25b9b --- /dev/null +++ b/src/lib/components/title-view/TitleView.tsx @@ -0,0 +1,39 @@ +import { DialogButton, Navigation, Focusable, quickAccessMenuClasses } from "@decky/ui"; +import { BsGearFill } from "react-icons/bs"; +import { FaDownload } from "react-icons/fa"; +import { useCSSLoaderValue } from "@/backend"; +import { cn } from "../../utils"; + +export function TitleView() { + const themes = useCSSLoaderValue("themes"); + + const onSettingsClick = () => { + Navigation.CloseSideMenus(); + Navigation.Navigate("/cssloader/settings"); + }; + + const onStoreClick = () => { + Navigation.CloseSideMenus(); + Navigation.Navigate("/cssloader/theme-store"); + }; + + return ( + +
CSS Loader
+ + + + + + +
+ ); +} diff --git a/src/lib/components/title-view/index.ts b/src/lib/components/title-view/index.ts new file mode 100644 index 0000000..0ad5366 --- /dev/null +++ b/src/lib/components/title-view/index.ts @@ -0,0 +1 @@ +export * from "./TitleView"; diff --git a/src/lib/hooks/index.ts b/src/lib/hooks/index.ts new file mode 100644 index 0000000..b5dcc91 --- /dev/null +++ b/src/lib/hooks/index.ts @@ -0,0 +1,2 @@ +export * from "./useForcedRerender"; +export * from "./useThemeInstallState"; diff --git a/src/hooks/useRerender.ts b/src/lib/hooks/useForcedRerender.ts similarity index 88% rename from src/hooks/useRerender.ts rename to src/lib/hooks/useForcedRerender.ts index de6cd4b..64b7d78 100644 --- a/src/hooks/useRerender.ts +++ b/src/lib/hooks/useForcedRerender.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; // This should only be used to fix weird bugs with how valve's toggles/dropdowns/etc don't update state // If used, state why in a comment next to the invocation -export function useRerender(): [boolean, () => void] { +export function useForcedRerender(): [boolean, () => void] { const [render, setRender] = useState(true); useEffect(() => { if (render === false) { diff --git a/src/lib/hooks/useThemeInstallState.ts b/src/lib/hooks/useThemeInstallState.ts new file mode 100644 index 0000000..de40e31 --- /dev/null +++ b/src/lib/hooks/useThemeInstallState.ts @@ -0,0 +1,12 @@ +import { LocalThemeStatus, PartialCSSThemeInfo, Theme } from "@/types"; +import { useCSSLoaderValue } from "@/backend"; + +export function useThemeInstallState(theme: Theme | PartialCSSThemeInfo): LocalThemeStatus { + const updateStatuses = useCSSLoaderValue("updateStatuses"); + + const status = updateStatuses.find((status) => status[0] === theme.id); + if (status) { + return status[1]; + } + return "notinstalled"; +} diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..dbef729 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1,4 @@ +export * from "./components"; +export * from "./hooks"; +export * from "./utils"; +export * from "./providers"; diff --git a/src/lib/primitives/ConfirmModal.tsx b/src/lib/primitives/ConfirmModal.tsx new file mode 100644 index 0000000..69a0c78 --- /dev/null +++ b/src/lib/primitives/ConfirmModal.tsx @@ -0,0 +1,31 @@ +import { ConfirmModal as CM } from "@decky/ui"; +import { StyleProvider } from "../providers"; + +export function ConfirmModal({ + closeModal, + children, + onConfirm, + title, + confirmText, + cancelText, +}: { + closeModal?: () => void; + children: React.ReactNode; + onConfirm: () => void; + title?: string; + confirmText?: string; + cancelText?: string; +}) { + return ( + + {children} + + ); +} diff --git a/src/lib/primitives/Modal.tsx b/src/lib/primitives/Modal.tsx new file mode 100644 index 0000000..bc5b3eb --- /dev/null +++ b/src/lib/primitives/Modal.tsx @@ -0,0 +1,23 @@ +import { ModalRoot } from "@decky/ui"; +import { StyleProvider } from "../providers"; + +export function Modal({ + closeModal, + children, + title, +}: { + closeModal?: () => void; + title?: string; + children: React.ReactNode; +}) { + return ( + + +
+ {title &&

{title}

} + {children} +
+
+
+ ); +} diff --git a/src/lib/primitives/index.ts b/src/lib/primitives/index.ts new file mode 100644 index 0000000..bbb92a4 --- /dev/null +++ b/src/lib/primitives/index.ts @@ -0,0 +1,2 @@ +export * from "./Modal"; +export * from "./ConfirmModal"; diff --git a/src/lib/providers/index.ts b/src/lib/providers/index.ts new file mode 100644 index 0000000..8a0bc2d --- /dev/null +++ b/src/lib/providers/index.ts @@ -0,0 +1 @@ +export * from "./style-provider"; diff --git a/src/lib/providers/style-provider/StyleProvider.tsx b/src/lib/providers/style-provider/StyleProvider.tsx new file mode 100644 index 0000000..0be318c --- /dev/null +++ b/src/lib/providers/style-provider/StyleProvider.tsx @@ -0,0 +1,10 @@ +import { styles } from "@/styles"; + +export function StyleProvider({ children }: { children: React.ReactNode }) { + return ( + <> + + {children} + + ); +} diff --git a/src/lib/providers/style-provider/index.ts b/src/lib/providers/style-provider/index.ts new file mode 100644 index 0000000..5f834cd --- /dev/null +++ b/src/lib/providers/style-provider/index.ts @@ -0,0 +1 @@ +export * from "./StyleProvider"; diff --git a/src/lib/utils/classname-merger.ts b/src/lib/utils/classname-merger.ts new file mode 100644 index 0000000..ab65735 --- /dev/null +++ b/src/lib/utils/classname-merger.ts @@ -0,0 +1,6 @@ +import clsx from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...args: any[]) { + return twMerge(clsx(args)); +} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts new file mode 100644 index 0000000..daefe64 --- /dev/null +++ b/src/lib/utils/index.ts @@ -0,0 +1,3 @@ +export * from "./classname-merger"; +export * from "./toggleThemeWithModals"; +export * from "./shorten-number"; diff --git a/src/logic/numbers.ts b/src/lib/utils/shorten-number.ts similarity index 98% rename from src/logic/numbers.ts rename to src/lib/utils/shorten-number.ts index d2e8fce..f3c170d 100644 --- a/src/logic/numbers.ts +++ b/src/lib/utils/shorten-number.ts @@ -37,6 +37,7 @@ export function shortenNumber(num: number) { shortNumber = String(Math.round(10 * (num / Math.pow(10, exponent))) / 10); for (var suffix in suffixes) { + // @ts-ignore if (exponent < suffixes[suffix]) { shortNumber += suffix; break; diff --git a/src/lib/utils/toggleThemeWithModals.tsx b/src/lib/utils/toggleThemeWithModals.tsx new file mode 100644 index 0000000..7b73cf7 --- /dev/null +++ b/src/lib/utils/toggleThemeWithModals.tsx @@ -0,0 +1,29 @@ +import { Flags, Theme } from "@/types"; +import { showModal } from "@decky/ui"; +import { getCSSLoaderState } from "@/backend"; +import { getDeckyPatchState } from "../../decky-patches"; +// Hardcoded to prevent dep cycle +import { OptionalDepsModal } from "../components/modals/optional-deps-modal"; +import { NavPatchInfoModal } from "../components/modals/nav-patch-info-modal"; + +export async function toggleThemeWithModals(theme: Theme, value: boolean, rerender?: () => void) { + const { toggleTheme } = getCSSLoaderState(); + const { navPatchInstance } = getDeckyPatchState(); + if (value && theme.flags.includes(Flags.optionalDeps)) { + showModal( + { + toggleTheme(theme, value, enableDeps, enableDepValues); + }} + /> + ); + rerender?.(); + } else { + await toggleTheme(theme, value); + } + + if (value && theme.flags.includes(Flags.navPatch) && !navPatchInstance) { + showModal(); + } +} diff --git a/src/logic/bulkThemeUpdateCheck.ts b/src/logic/bulkThemeUpdateCheck.ts deleted file mode 100644 index 7c4ec45..0000000 --- a/src/logic/bulkThemeUpdateCheck.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Theme, UpdateStatus } from "../ThemeTypes"; -import { genericGET } from "../api"; -import { MinimalCSSThemeInfo } from "../apiTypes"; -import { globalState } from "../python"; -const apiUrl = "https://api.deckthemes.com"; - -async function fetchThemeIDS(idsToQuery: string[]): Promise { - const queryStr = "?ids=" + idsToQuery.join("."); - return genericGET(`/themes/ids${queryStr}`) - .then((data) => { - if (data) return data; - return []; - }) - .catch((err) => { - console.error("Error Fetching Theme Updates!", err); - return []; - }); -} - -export async function bulkThemeUpdateCheck(customThemeArr?: Theme[]) { - const localThemeList = customThemeArr || globalState!.getPublicState().localThemeList; - let idsToQuery: string[] = localThemeList.map((e) => e.id); - - if (idsToQuery.length === 0) return []; - - const themeArr = await fetchThemeIDS(idsToQuery); - - if (themeArr.length === 0) return []; - - const updateStatusArr: UpdateStatus[] = localThemeList.map((localEntry) => { - const remoteEntry = themeArr.find( - (remote) => remote.id === localEntry.id || remote.name === localEntry.id - ); - if (!remoteEntry) return [localEntry.id, "local", false]; - if (remoteEntry.version === localEntry.version) - return [localEntry.id, "installed", remoteEntry]; - return [localEntry.id, "outdated", remoteEntry]; - }); - - return updateStatusArr; -} diff --git a/src/logic/calcButtonColor.ts b/src/logic/calcButtonColor.ts deleted file mode 100644 index 5b12215..0000000 --- a/src/logic/calcButtonColor.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function calcButtonColor(installStatus: string) { - let filterCSS = ""; - switch (installStatus) { - case "outdated": - filterCSS = "invert(6%) sepia(90%) saturate(200%) hue-rotate(160deg) contrast(122%)"; - break; - default: - filterCSS = ""; - break; - } - return filterCSS; -} diff --git a/src/logic/generateParamStr.ts b/src/logic/generateParamStr.ts deleted file mode 100644 index cd0efd3..0000000 --- a/src/logic/generateParamStr.ts +++ /dev/null @@ -1,16 +0,0 @@ -export function generateParamStr(origSearchOpts: any, filterPrepend: string = "") { - // This can be done with 'new URLSearchParams(obj)' but I want more control - const searchOpts = Object.assign({}, origSearchOpts); - if (filterPrepend) { - searchOpts.filters = filterPrepend + searchOpts.filters; - } - let paramString = "?"; - Object.keys(searchOpts).forEach((key, i) => { - // @ts-ignore typescript doesn't know how object.keys works 🙄 - if (searchOpts[key]) { - // @ts-ignore - paramString += `${i !== 0 ? "&" : ""}${key}=${searchOpts[key]}`; - } - }); - return paramString; -} diff --git a/src/logic/index.ts b/src/logic/index.ts deleted file mode 100644 index 837d353..0000000 --- a/src/logic/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./calcButtonColor"; -export * from "./generateParamStr"; diff --git a/src/modules/expanded-view/components/ExpandedViewAuthorViewModal.tsx b/src/modules/expanded-view/components/ExpandedViewAuthorViewModal.tsx new file mode 100644 index 0000000..a6cf5f7 --- /dev/null +++ b/src/modules/expanded-view/components/ExpandedViewAuthorViewModal.tsx @@ -0,0 +1,3 @@ +import { UserInfo } from "@/types"; + +export function ExpandedViewAuthorViewModal({ authorData }: { authorData: UserInfo }) {} diff --git a/src/modules/expanded-view/components/ExpandedViewButtonsSection.tsx b/src/modules/expanded-view/components/ExpandedViewButtonsSection.tsx new file mode 100644 index 0000000..dccddeb --- /dev/null +++ b/src/modules/expanded-view/components/ExpandedViewButtonsSection.tsx @@ -0,0 +1,106 @@ +import { shortenNumber, useThemeInstallState, ThemeSettingsModal } from "@/lib"; +import { useExpandedViewAction, useExpandedViewValue } from "../context"; +import { FaRegStar, FaStar } from "react-icons/fa"; +import { DialogButton, Focusable, showModal } from "@decky/ui"; +import { useEffect, useRef, useState } from "react"; +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; +import { ImCog } from "react-icons/im"; + +export function ExpandedViewButtonsSection() { + const data = useExpandedViewValue("data"); + const isStarred = useExpandedViewValue("isStarred"); + const toggleStar = useExpandedViewAction("toggleStar"); + + const apiFullToken = useCSSLoaderValue("apiFullToken"); + // Because this is an action handled by the expanded view store and not the backend theme store, we can't just use the backend's isWorking + const [starButtonBlurred, setStarButtonBlurred] = useState(false); + + const isWorking = useCSSLoaderValue("isWorking"); + + const installTheme = useCSSLoaderAction("installTheme"); + + const installStatus = useThemeInstallState(data); + + async function handleStar() { + setStarButtonBlurred(true); + await toggleStar(); + setStarButtonBlurred(false); + } + + const downloadButtonRef = useRef(null); + const [hasBeenFocused, setHasFocused] = useState(false); + useEffect(() => { + if (downloadButtonRef?.current && !hasBeenFocused) { + downloadButtonRef.current.focus(); + setHasFocused(true); + } + }, [downloadButtonRef, hasBeenFocused]); + + return ( + + {/* Star */} +
+
+
+ {isStarred ? : } + {/* Need to make the text size smaller or else it wraps */} + = 100 ? "0.75rem" : "1rem" }}> + {shortenNumber(data.starCount) ?? data.starCount} Star + {data.starCount === 1 ? "" : "s"} + +
+ void handleStar()} + disabled={starButtonBlurred || !apiFullToken} + > +
+ + {!apiFullToken ? "Log In to Star" : isStarred ? "Unstar Theme" : "Star Theme"} + +
+
+
+
+ + {/* Download / Configure */} +
+
+ Install {data.displayName} + + {shortenNumber(data.download.downloadCount) ?? data.download.downloadCount} Download + {data.download.downloadCount === 1 ? "" : "s"} + + + { + installTheme(data.id); + }} + > + + {/* Technically 'local' should mean a remote copy doesn't exist, but there might be weird network race conditions on the install status check */} + {(installStatus === "installed" || installStatus === "local") && "Reinstall"} + {installStatus === "outdated" && "Update"} + {installStatus === "notinstalled" && "Install"} + + + {(installStatus === "installed" || installStatus === "local") && ( + { + showModal(); + }} + className="cl_expandedview_configure_button" + > + + + )} + +
+
+
+ ); +} diff --git a/src/modules/expanded-view/components/ExpandedViewCssVariables.tsx b/src/modules/expanded-view/components/ExpandedViewCssVariables.tsx new file mode 100644 index 0000000..b7e0ac2 --- /dev/null +++ b/src/modules/expanded-view/components/ExpandedViewCssVariables.tsx @@ -0,0 +1,21 @@ +import { useExpandedViewValue } from "../context"; + +export function ExpandedViewCssVariables() { + const imageDimensionKeys = useExpandedViewValue("imageAreaStyleKeys"); + + return ( + + ); +} diff --git a/src/modules/expanded-view/components/ExpandedViewImageContainer.tsx b/src/modules/expanded-view/components/ExpandedViewImageContainer.tsx new file mode 100644 index 0000000..590a276 --- /dev/null +++ b/src/modules/expanded-view/components/ExpandedViewImageContainer.tsx @@ -0,0 +1,71 @@ +import { Focusable, ScrollPanelGroup } from "@decky/ui"; +import { useExpandedViewAction, useExpandedViewValue } from "../context"; + +export function ExpandedViewImageContainer() { + const data = useExpandedViewValue("data"); + const { + imageCarouselEntryWidth, + imageCarouselEntryHeight, + selectedImageHeight, + selectedImageWidth, + } = useExpandedViewValue("imageAreaStyleKeys"); + const focusedImageId = useExpandedViewValue("focusedImageId"); + + const setFocusedImage = useExpandedViewAction("setFocusedImage"); + + return ( + + {/* Image Carousel Container */} + {data.images.length > 1 && ( + + {data.images.map((image) => ( + { + setFocusedImage(image.id); + }} + className="cl_expandedview_imagecarouselentry" + focusWithinClassName="gpfocuswithin" + onActivate={() => {}} + > + + + ))} + + )} + + {/* Selected Image Display */} + {}} + > + 0 + ? `https://api.deckthemes.com/blobs/${focusedImageId}` + : `https://share.deckthemes.com/cssplaceholder.png` + } + /> + {data.images.length > 1 && ( +
+ + {data.images.findIndex((blob) => blob.id === focusedImageId) + 1}/{data.images.length} + +
+ )} +
+
+ ); +} diff --git a/src/modules/expanded-view/components/ExpandedViewLoadingPage.tsx b/src/modules/expanded-view/components/ExpandedViewLoadingPage.tsx new file mode 100644 index 0000000..0442405 --- /dev/null +++ b/src/modules/expanded-view/components/ExpandedViewLoadingPage.tsx @@ -0,0 +1,12 @@ +import { ImSpinner5 } from "react-icons/im"; + +export function ExpandedViewLoadingPage() { + return ( + <> +
+ + Loading +
+ + ); +} diff --git a/src/modules/expanded-view/components/ExpandedViewScrollingSection.tsx b/src/modules/expanded-view/components/ExpandedViewScrollingSection.tsx new file mode 100644 index 0000000..446ce01 --- /dev/null +++ b/src/modules/expanded-view/components/ExpandedViewScrollingSection.tsx @@ -0,0 +1,89 @@ +import { DialogButton, Focusable, Navigation, ScrollPanelGroup, showModal } from "@decky/ui"; +import { useExpandedViewAction, useExpandedViewValue } from "../context"; +import { ExpandedViewImageContainer } from "./ExpandedViewImageContainer"; +import { useThemeBrowserSharedAction } from "@/modules/theme-store/context"; +import { AuthorViewModal } from "@/lib"; + +export function ExpandedViewScrollingSection() { + const data = useExpandedViewValue("data"); + const close = useExpandedViewAction("close"); + const openTheme = useExpandedViewAction("openTheme"); + + const setTargetOverride = useThemeBrowserSharedAction("setTargetOverride"); + + return ( + { + if (!evt?.detail?.button) return; + if (evt.detail.button === 2) { + close(); + } + }} + > + + + + {/* Title / Version */} +
+ {data.displayName} + {data.version} +
+ {/* Author / Modified Date */} + + { + showModal( + openTheme(themeId)} + /> + ); + }} + > + By {data.specifiedAuthor} + + Last Updated {new Date(data.updated).toLocaleDateString()} + + {/* Description */} + {}} + > + Description + 400 ? "text-sm" : ""}> + {data.description || ( + No description provided. + )} + + + {/* Targets */} + + Targets + + {data.targets.map((target) => ( + { + setTargetOverride(target); + Navigation.NavigateBack(); + }} + className="cl_expandedview_targetbutton" + > + {target} + + ))} + + +
+
+
+ ); +} diff --git a/src/modules/expanded-view/components/index.ts b/src/modules/expanded-view/components/index.ts new file mode 100644 index 0000000..640ffca --- /dev/null +++ b/src/modules/expanded-view/components/index.ts @@ -0,0 +1,4 @@ +export * from "./ExpandedViewLoadingPage"; +export * from "./ExpandedViewScrollingSection"; +export * from "./ExpandedViewCssVariables"; +export * from "./ExpandedViewButtonsSection"; diff --git a/src/modules/expanded-view/context/ExpandedViewStore.tsx b/src/modules/expanded-view/context/ExpandedViewStore.tsx new file mode 100644 index 0000000..d26dc75 --- /dev/null +++ b/src/modules/expanded-view/context/ExpandedViewStore.tsx @@ -0,0 +1,155 @@ +import { cssLoaderStore, getCSSLoaderState } from "@/backend"; +import { FullCSSThemeInfo } from "@/types"; +import { Navigation } from "@decky/ui"; +import { createStore, useStore } from "zustand"; + +interface IExpandedViewStoreValues { + loaded: boolean; + error: string | null; + openedId: string | null; + isStarred: boolean; + data: FullCSSThemeInfo; + focusedImageId: string; + imageAreaStyleKeys: { + imageAreaWidth: number; + imageAreaPadding: number; + gapBetweenCarouselAndImage: number; + selectedImageWidth: number; + selectedImageHeight: number; + imageCarouselEntryWidth: number; + imageCarouselEntryHeight: number; + }; +} + +interface IExpandedViewStoreActions { + openTheme: (themeId: string) => Promise; + toggleStar: () => Promise; + setFocusedImage: (imageId: string) => void; + close: () => void; +} + +export interface IExpandedViewStore extends IExpandedViewStoreValues, IExpandedViewStoreActions {} + +const expandedViewStore = createStore((set, get) => { + function setImageSizes() { + const imageAreaWidth = 556; + const imageAreaPadding = 16; + const gapBetweenCarouselAndImage = 8; + const selectedImageWidth = + get().data?.images?.length > 1 ? 434.8 : imageAreaWidth - imageAreaPadding * 2; + const selectedImageHeight = (selectedImageWidth / 16) * 10; + const imageCarouselEntryWidth = + imageAreaWidth - imageAreaPadding * 2 - selectedImageWidth - gapBetweenCarouselAndImage; + set({ + imageAreaStyleKeys: { + imageAreaWidth, + imageAreaPadding, + gapBetweenCarouselAndImage, + selectedImageWidth, + selectedImageHeight, + imageCarouselEntryWidth, + imageCarouselEntryHeight: (imageCarouselEntryWidth / 16) * 10, + }, + }); + } + return { + loaded: false, + openedId: null, + data: {} as FullCSSThemeInfo, + isStarred: false, + error: null, + focusedImageId: "", + imageAreaStyleKeys: { + imageAreaWidth: 0, + imageAreaPadding: 0, + gapBetweenCarouselAndImage: 0, + selectedImageWidth: 0, + selectedImageHeight: 0, + imageCarouselEntryWidth: 0, + imageCarouselEntryHeight: 0, + }, + openTheme: async (themeId) => { + set({ loaded: false, error: null, openedId: themeId }); + Navigation.Navigate("/cssloader/expanded-view"); + const { apiFetch, apiFullToken } = getCSSLoaderState(); + try { + const response = await apiFetch(`/themes/${themeId}`); + if (!response) { + throw new Error("No response returned"); + } + set({ data: response, loaded: true, focusedImageId: response.images[0]?.id || "" }); + setImageSizes(); + + if (!apiFullToken) return; + const starResponse = await apiFetch<{ starred: boolean }>( + `/users/me/stars/${themeId}`, + {}, + true + ); + if (!starResponse) { + // Silently error + set({ isStarred: false }); + } + set({ isStarred: starResponse.starred }); + // If you star and then quickly refresh, the API hasn't updated the cached starcount + if (response.starCount === 0) { + set({ data: { ...response, starCount: 1 } }); + } + } catch (error) { + set({ error: "Error fetching theme!", loaded: true }); + setImageSizes(); + } + }, + toggleStar: async () => { + try { + const { data, isStarred } = get(); + const { apiFetch, apiFullToken } = getCSSLoaderState(); + if (!apiFullToken && !data.id) return; + await apiFetch(`/users/me/stars/${data.id}`, { + method: isStarred ? "DELETE" : "POST", + }); + const newIsStarred = !isStarred; + set({ + isStarred: newIsStarred, + data: { + ...data, + starCount: newIsStarred + ? data.starCount + 1 + : // If it was at 0 stars before (api hadn't updated, prevent it from going to -1) + data.starCount === 0 + ? 0 + : data.starCount - 1, + }, + }); + } catch (error) { + // TODO: (potentially) handle error + } + }, + close: () => { + set({ + loaded: false, + isStarred: false, + openedId: null, + data: {} as FullCSSThemeInfo, + error: null, + focusedImageId: "", + }); + setImageSizes(); + Navigation.NavigateBack(); + }, + setFocusedImage: (imageId) => { + set({ focusedImageId: imageId }); + }, + }; +}); + +const useExpandedViewState = (fn: (state: IExpandedViewStore) => any) => + useStore(expandedViewStore, fn); + +export const useExpandedViewValue = ( + key: T +): IExpandedViewStore[T] => useExpandedViewState((state) => state[key]); + +export const useExpandedViewAction = ( + key: T +): IExpandedViewStore[T] => useExpandedViewState((state) => state[key]); diff --git a/src/modules/expanded-view/context/index.ts b/src/modules/expanded-view/context/index.ts new file mode 100644 index 0000000..66cfa22 --- /dev/null +++ b/src/modules/expanded-view/context/index.ts @@ -0,0 +1 @@ +export * from "./ExpandedViewStore"; diff --git a/src/modules/expanded-view/index.ts b/src/modules/expanded-view/index.ts new file mode 100644 index 0000000..7ec876d --- /dev/null +++ b/src/modules/expanded-view/index.ts @@ -0,0 +1,2 @@ +export * from "./pages"; +export * from "./context"; diff --git a/src/modules/expanded-view/pages/ExpandedViewPage.tsx b/src/modules/expanded-view/pages/ExpandedViewPage.tsx new file mode 100644 index 0000000..68c1c2e --- /dev/null +++ b/src/modules/expanded-view/pages/ExpandedViewPage.tsx @@ -0,0 +1,27 @@ +import { Focusable } from "@decky/ui"; +import { + ExpandedViewLoadingPage, + ExpandedViewCssVariables, + ExpandedViewScrollingSection, + ExpandedViewButtonsSection, +} from "../components"; +import { useExpandedViewValue } from "../context"; + +export function ExpandedViewPage() { + const loaded = useExpandedViewValue("loaded"); + const error = useExpandedViewValue("error"); + + if (!loaded) return ; + + if (error) return {error}; + + return ( +
+ + + + + +
+ ); +} diff --git a/src/modules/expanded-view/pages/index.ts b/src/modules/expanded-view/pages/index.ts new file mode 100644 index 0000000..7d6a1e1 --- /dev/null +++ b/src/modules/expanded-view/pages/index.ts @@ -0,0 +1 @@ +export * from "./ExpandedViewPage"; diff --git a/src/modules/qam-tab-page/components/QamDummyFunctionBoundary.tsx b/src/modules/qam-tab-page/components/QamDummyFunctionBoundary.tsx new file mode 100644 index 0000000..9aff28a --- /dev/null +++ b/src/modules/qam-tab-page/components/QamDummyFunctionBoundary.tsx @@ -0,0 +1,21 @@ +import { useCSSLoaderValue } from "@/backend"; +import { PanelSection, PanelSectionRow } from "@decky/ui"; + +export function QamDummyFunctionBoundary({ children }: { children: React.ReactNode }) { + const dummyFunctionResult = useCSSLoaderValue("dummyFunctionResult"); + + if (!dummyFunctionResult) { + return ( + <> + + + CSS Loader failed to initialize, try reloading, and if that doesn't work, try restarting + your deck. + + + + ); + } + + return <>{children}; +} diff --git a/src/modules/qam-tab-page/components/QamHiddenThemesDisplay.tsx b/src/modules/qam-tab-page/components/QamHiddenThemesDisplay.tsx new file mode 100644 index 0000000..85dcb7e --- /dev/null +++ b/src/modules/qam-tab-page/components/QamHiddenThemesDisplay.tsx @@ -0,0 +1,19 @@ +import { useCSSLoaderValue } from "@/backend"; +import { FaEyeSlash } from "react-icons/fa"; + +export function QamHiddenThemesDisplay() { + const unpinnedThemes = useCSSLoaderValue("unpinnedThemes"); + + if (unpinnedThemes.length === 0) { + return null; + } + + return ( +
+ + + {unpinnedThemes.length} theme{unpinnedThemes.length > 1 ? "s are" : "is"} hidden. + +
+ ); +} diff --git a/src/modules/qam-tab-page/components/QamRefreshButton.tsx b/src/modules/qam-tab-page/components/QamRefreshButton.tsx new file mode 100644 index 0000000..9e9850e --- /dev/null +++ b/src/modules/qam-tab-page/components/QamRefreshButton.tsx @@ -0,0 +1,32 @@ +import { ButtonItem, PanelSectionRow } from "@decky/ui"; +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; +import { useEffect, useRef } from "react"; + +export function QamRefreshButton() { + const reloadPlugin = useCSSLoaderAction("reloadPlugin"); + const isWorking = useCSSLoaderValue("isWorking"); + + const refreshButtonRef = useRef(null); + + async function handleRefresh() { + await reloadPlugin(); + // This just ensures focus isn't lost + refreshButtonRef.current?.focus(); + } + + return ( + + { + void handleRefresh(); + }} + layout="below" + > + Refresh + + + ); +} diff --git a/src/modules/qam-tab-page/components/QamThemeList.tsx b/src/modules/qam-tab-page/components/QamThemeList.tsx new file mode 100644 index 0000000..8cfa0c7 --- /dev/null +++ b/src/modules/qam-tab-page/components/QamThemeList.tsx @@ -0,0 +1,25 @@ +import { Focusable } from "@decky/ui"; +import { useCSSLoaderValue } from "../../../backend-impl/decky-theme-store"; +import { Flags } from "@/types"; +import { QamThemeToggle } from "./QamThemeToggle"; + +export function QamThemeList() { + const themes = useCSSLoaderValue("themes"); + const unpinnedThemes = useCSSLoaderValue("unpinnedThemes"); + + if (themes.length === 0) { + return You have no themes, visit the theme store to download some!; + } + + return ( + + {themes + .filter( + (theme) => !unpinnedThemes.includes(theme.id) && !theme.flags.includes(Flags.isPreset) + ) + .map((theme) => ( + + ))} + + ); +} diff --git a/src/modules/qam-tab-page/components/QamThemeToggle.tsx b/src/modules/qam-tab-page/components/QamThemeToggle.tsx new file mode 100644 index 0000000..f7f00aa --- /dev/null +++ b/src/modules/qam-tab-page/components/QamThemeToggle.tsx @@ -0,0 +1,91 @@ +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; +import { ThemePatch, ThemeSettingsModal, toggleThemeWithModals, useForcedRerender } from "@/lib"; +import { useEffect, useState } from "react"; +import { LocalThemeStatus, Theme } from "@/types"; +import { ButtonItem, Focusable, PanelSectionRow, ToggleField, showModal } from "@decky/ui"; +import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; + +export function QamThemeToggle({ theme }: { theme: Theme }) { + const updateStatuses = useCSSLoaderValue("updateStatuses"); + const isWorking = useCSSLoaderValue("isWorking"); + const installTheme = useCSSLoaderAction("installTheme"); + + const [collapsed, setCollapsed] = useState(true); + const [render, rerender] = useForcedRerender(); + + let updateStatus: LocalThemeStatus = "installed"; + const themeArrPlace = updateStatuses.find((f) => f[0] === theme.id); + if (themeArrPlace) updateStatus = themeArrPlace[1]; + const isOutdated = updateStatus === "outdated"; + + // Re-collapse the theme when the theme is updated + useEffect(() => { + setCollapsed(true); + }, [theme.enabled]); + + if (!render) return null; + return ( + <> + + { + showModal(); + }} + onSecondaryActionDescription={isOutdated ? "Update Theme" : undefined} + onSecondaryButton={ + isOutdated + ? async () => { + await installTheme(theme.id); + } + : undefined + } + > + {isOutdated &&
} + 0 ? "none" : "standard"} + checked={theme.enabled} + label={theme.display_name} + description={`${updateStatus === "outdated" ? "Update Available" : theme.version} | ${ + theme.author + }`} + onChange={(switchValue: boolean) => { + toggleThemeWithModals(theme, switchValue, rerender); + }} + /> + + + {theme.enabled && theme.patches.length > 0 && ( + <> +
+ + setCollapsed(!collapsed)} + > + {collapsed ? ( + + ) : ( + + )} + + +
+ {!collapsed && + theme.patches.map((patch, index) => ( + + ))} + + )} + + ); +} diff --git a/src/modules/qam-tab-page/components/index.ts b/src/modules/qam-tab-page/components/index.ts new file mode 100644 index 0000000..9118669 --- /dev/null +++ b/src/modules/qam-tab-page/components/index.ts @@ -0,0 +1,5 @@ +export * from "./QamDummyFunctionBoundary"; +export * from "./QamRefreshButton"; +export * from "./QamThemeList"; +export * from "./QamHiddenThemesDisplay"; +export * from "./QamThemeToggle"; diff --git a/src/modules/qam-tab-page/index.ts b/src/modules/qam-tab-page/index.ts new file mode 100644 index 0000000..c4e34b2 --- /dev/null +++ b/src/modules/qam-tab-page/index.ts @@ -0,0 +1 @@ +export * from "./pages"; diff --git a/src/modules/qam-tab-page/pages/QamTabPage.tsx b/src/modules/qam-tab-page/pages/QamTabPage.tsx new file mode 100644 index 0000000..4ff9838 --- /dev/null +++ b/src/modules/qam-tab-page/pages/QamTabPage.tsx @@ -0,0 +1,29 @@ +import { PanelSection } from "@decky/ui"; +import { MOTDDisplay, PresetSelectionDropdown } from "@/lib"; +import { + QamDummyFunctionBoundary, + QamHiddenThemesDisplay, + QamRefreshButton, + QamThemeList, +} from "../components"; +import { useCSSLoaderValue } from "@/backend"; + +export function QamTabPage() { + const themes = useCSSLoaderValue("themes"); + + return ( + <> + + + + {themes.length > 0 && } + + + + + + + + + ); +} diff --git a/src/modules/qam-tab-page/pages/index.ts b/src/modules/qam-tab-page/pages/index.ts new file mode 100644 index 0000000..a7e9004 --- /dev/null +++ b/src/modules/qam-tab-page/pages/index.ts @@ -0,0 +1 @@ +export * from "./QamTabPage"; diff --git a/src/modules/settings/SettingsPageRouter.tsx b/src/modules/settings/SettingsPageRouter.tsx new file mode 100644 index 0000000..38357e7 --- /dev/null +++ b/src/modules/settings/SettingsPageRouter.tsx @@ -0,0 +1,46 @@ +import { SidebarNavigation } from "@decky/ui"; +import { FaFolder, FaGear, FaGithub, FaHeart, FaPaintRoller } from "react-icons/fa6"; +import { CreditsPage } from "./credits"; +import { DonatePage } from "./donate/DonatePage"; +import { PluginSettingsPage } from "./plugin"; +import { ProfileSettings } from "./profile"; +import { ThemeSettings } from "./theme"; + +export function SettingsPageRouter() { + return ( + , + route: "/cssloader/settings/themes", + content: , + }, + { + title: "Profiles", + icon: , + route: "/cssloader/settings/profiles", + content: , + }, + { + title: "Settings", + icon: , + route: "/cssloader/settings/plugin", + content: , + }, + { + title: "Donate", + icon: , + route: "/cssloader/settings/donate", + content: , + }, + { + title: "Credits", + icon: , + route: "/cssloader/settings/credits", + content: , + }, + ]} + /> + ); +} diff --git a/src/pages/settings/Credits.tsx b/src/modules/settings/credits/CreditsPage.tsx similarity index 54% rename from src/pages/settings/Credits.tsx rename to src/modules/settings/credits/CreditsPage.tsx index fadf189..4d95181 100644 --- a/src/pages/settings/Credits.tsx +++ b/src/modules/settings/credits/CreditsPage.tsx @@ -1,10 +1,10 @@ -export function Credits() { +export function CreditsPage() { return (
-
+
-

Developers

-
    + Developers +
    • SuchMeme - github.com/suchmememanyskill
    • @@ -17,19 +17,18 @@ export function Credits() {
-

Support

+ Support See the DeckThemes Discord server for support.
- discord.gg/HsU72Kfnpf + deckthemes.com/discord
-

- Create and Submit Your Own Theme -

+ Create and Submit Your Own Theme - Instructions for theme creation/submission are available DeckThemes' docs website. + Instructions for theme creation/submission are available DeckThemes' documentation + website.
docs.deckthemes.com
diff --git a/src/modules/settings/credits/index.ts b/src/modules/settings/credits/index.ts new file mode 100644 index 0000000..3f85546 --- /dev/null +++ b/src/modules/settings/credits/index.ts @@ -0,0 +1 @@ +export * from "./CreditsPage"; diff --git a/src/modules/settings/donate/DonatePage.tsx b/src/modules/settings/donate/DonatePage.tsx new file mode 100644 index 0000000..016ea86 --- /dev/null +++ b/src/modules/settings/donate/DonatePage.tsx @@ -0,0 +1,64 @@ +import { useCSSLoaderValue } from "@/backend"; +import { Focusable, Navigation, PanelSection } from "@decky/ui"; +import { SiKofi, SiPatreon } from "react-icons/si"; + +export function DonatePage() { + const patrons = useCSSLoaderValue("patrons"); + return ( + +

+ Donations help to cover the costs of hosting the store, as well as funding development for + CSS Loader and its related projects. +

+ + Navigation.NavigateToExternalWeb("https://patreon.com/deckthemes")} + className="flex flex-col gap-2" + focusWithinClassName="gpfocuswithin" + > +
+
+ + Patreon +
+ Recurring Donation + patreon.com/deckthemes +
+
    +
  • + {/* Potentially could expand this to add it to deckthemes and audioloader */} + Your name in CSS Loader +
  • +
  • Patreon badge on deckthemes.com
  • +
  • + {/* Could also impl. this on deck store to make it more meaningful */} + Colored name + VIP channel in the DeckThemes Discord +
  • +
+
+ Navigation.NavigateToExternalWeb("https://ko-fi.com/suchmememanyskill")} + className="flex flex-col" + focusWithinClassName="gpfocuswithin" + > +
+ + Kofi +
+ One-time Donation + ko-fi.com/suchmememanyskill +
+
+ {patrons.length > 0 && ( +
+ Patreon Supporters + {patrons.map((patron) => ( + {}} focusWithinClassName="gpfocuswithin"> +

{patron}

+
+ ))} +
+ )} +
+ ); +} diff --git a/src/modules/settings/index.ts b/src/modules/settings/index.ts new file mode 100644 index 0000000..8c8f223 --- /dev/null +++ b/src/modules/settings/index.ts @@ -0,0 +1 @@ +export * from "./SettingsPageRouter"; diff --git a/src/modules/settings/plugin/PluginSettingsPage.tsx b/src/modules/settings/plugin/PluginSettingsPage.tsx new file mode 100644 index 0000000..da2067f --- /dev/null +++ b/src/modules/settings/plugin/PluginSettingsPage.tsx @@ -0,0 +1,69 @@ +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; +import { useDeckyPatchStateAction, useDeckyPatchStateValue } from "@/decky-patches"; +import { DropdownItem, Focusable, ToggleField } from "@decky/ui"; + +export function PluginSettingsPage() { + const serverState = useCSSLoaderValue("serverState"); + const watchState = useCSSLoaderValue("watchState"); + const translationsBranch = useCSSLoaderValue("translationsBranch"); + + const setServerState = useCSSLoaderAction("setServerState"); + const setWatchState = useCSSLoaderAction("setWatchState"); + const setTranslationBranch = useCSSLoaderAction("setTranslationBranch"); + + const unminifyModeOn = useDeckyPatchStateValue("unminifyModeOn"); + const navPatchInstance = useDeckyPatchStateValue("navPatchInstance"); + const setNavPatchState = useDeckyPatchStateAction("setNavPatchState"); + const setUnminifyModeState = useDeckyPatchStateAction("setUnminifyModeState"); + return ( + + + setTranslationBranch(data.data)} + /> + + + { + setServerState(value); + }} + /> + + + setNavPatchState(value, true)} + /> + + + + + + setUnminifyModeState(value, true)} + /> + + + ); +} diff --git a/src/modules/settings/plugin/index.ts b/src/modules/settings/plugin/index.ts new file mode 100644 index 0000000..3c43dec --- /dev/null +++ b/src/modules/settings/plugin/index.ts @@ -0,0 +1 @@ +export * from "./PluginSettingsPage"; diff --git a/src/modules/settings/profile/ProfileSettings.tsx b/src/modules/settings/profile/ProfileSettings.tsx new file mode 100644 index 0000000..03c5057 --- /dev/null +++ b/src/modules/settings/profile/ProfileSettings.tsx @@ -0,0 +1,70 @@ +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; +import { PresetSelectionDropdown } from "@/lib"; +import { Flags, LocalThemeStatus, Theme } from "@/types"; +import { DialogButton, Focusable, PanelSectionRow } from "@decky/ui"; +import { AiOutlineDownload } from "react-icons/ai"; +import { FaTrash } from "react-icons/fa"; + +export function ProfileSettings() { + const themes = useCSSLoaderValue("themes"); + const profiles = themes.filter((e) => e.flags.includes(Flags.isPreset)); + + return ( + + + + {profiles.map((profile) => ( + + ))} + + + ); +} + +function ProfileEntry({ data }: { data: Theme }) { + const isWorking = useCSSLoaderValue("isWorking"); + const updateStatuses = useCSSLoaderValue("updateStatuses"); + + let updateStatus: LocalThemeStatus = "installed"; + const themeArrPlace = updateStatuses.find((f) => f[0] === data.id); + if (themeArrPlace) updateStatus = themeArrPlace[1]; + const isOutdated = updateStatus === "outdated"; + + const installTheme = useCSSLoaderAction("installTheme"); + const deleteTheme = useCSSLoaderAction("deleteTheme"); + + return ( + +
+ {data.name} +
+ {isOutdated && ( + installTheme(data.id)} + disabled={isWorking} + > + + + )} + deleteTheme(data.id)} + disabled={isWorking} + > + + +
+
+
+ ); +} diff --git a/src/modules/settings/profile/index.ts b/src/modules/settings/profile/index.ts new file mode 100644 index 0000000..d57089f --- /dev/null +++ b/src/modules/settings/profile/index.ts @@ -0,0 +1 @@ +export * from "./ProfileSettings"; diff --git a/src/modules/settings/theme/ThemeDeleteMenu.tsx b/src/modules/settings/theme/ThemeDeleteMenu.tsx new file mode 100644 index 0000000..e7440ed --- /dev/null +++ b/src/modules/settings/theme/ThemeDeleteMenu.tsx @@ -0,0 +1,41 @@ +import { DeleteConfirmationModal } from "@/lib"; +import { Theme } from "@/types"; +import { DialogButton, DialogCheckbox, Focusable, showModal } from "@decky/ui"; +import { useState } from "react"; + +export function ThemeDeleteMenu({ + sortedThemeList, + onLeave, +}: { + sortedThemeList: Theme[]; + onLeave: () => void; +}) { + const [choppingBlock, setChoppingBlock] = useState([]); + + return ( + + {sortedThemeList.map((theme) => ( + { + if (checked) { + setChoppingBlock([...choppingBlock, theme.id]); + } else { + setChoppingBlock(choppingBlock.filter((f) => f !== theme.id)); + } + }} + checked={choppingBlock.includes(theme.id)} + label={theme.display_name} + /> + ))} + { + showModal( + + ); + }} + > + Delete + + + ); +} diff --git a/src/modules/settings/theme/ThemeSettings.tsx b/src/modules/settings/theme/ThemeSettings.tsx new file mode 100644 index 0000000..da2f303 --- /dev/null +++ b/src/modules/settings/theme/ThemeSettings.tsx @@ -0,0 +1,63 @@ +import { useCSSLoaderValue } from "@/backend"; +import { ThemeErrorCard } from "@/lib"; +import { Flags } from "@/types"; +import { DialogButton, Focusable } from "@decky/ui"; +import { useMemo, useState } from "react"; +import { UpdateAllThemesButton } from "./UpdateAllThemesButton"; +import { ThemeSettingsEntry } from "./ThemeSettingsEntry"; +import { ThemeDeleteMenu } from "./ThemeDeleteMenu"; + +export function ThemeSettings() { + const [deleteMode, setDeleteMode] = useState(false); + const themes = useCSSLoaderValue("themes"); + const unpinnedThemes = useCSSLoaderValue("unpinnedThemes"); + const themeErrors = useCSSLoaderValue("themeErrors"); + + // This sorts the themes as pinned first, then unpinned, but it freezes it so that if you pin a theme the list doesn't jump around + const sortedList = useMemo(() => { + return themes + .filter((e) => !e.flags.includes(Flags.isPreset)) + .sort((a, b) => { + const aPinned = !unpinnedThemes.includes(a.id); + const bPinned = !unpinnedThemes.includes(b.id); + if (aPinned === bPinned) { + return a.name.localeCompare(b.name); + } + return Number(bPinned) - Number(aPinned); + }); + }, [themes.length]); + + return ( + + {deleteMode ? "Delete" : "Installed"} Themes + + setDeleteMode(!deleteMode)} + > + {deleteMode ? "Go Back" : "Delete Themes"} + + + + {deleteMode ? ( + setDeleteMode(false)} /> + ) : ( + + {sortedList.map((theme) => ( + + ))} + + )} + {themeErrors.length > 0 && ( + + Theme Errors + + {themeErrors.map((e) => ( + + ))} + + + )} + + ); +} diff --git a/src/modules/settings/theme/ThemeSettingsEntry.tsx b/src/modules/settings/theme/ThemeSettingsEntry.tsx new file mode 100644 index 0000000..79f96da --- /dev/null +++ b/src/modules/settings/theme/ThemeSettingsEntry.tsx @@ -0,0 +1,80 @@ +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; +import { ThemeSettingsModal, toggleThemeWithModals, useThemeInstallState } from "@/lib"; +import { Theme } from "@/types"; +import { DialogButton, Focusable, showModal, ToggleField } from "@decky/ui"; +import { BsGearFill } from "react-icons/bs"; +import { FaEye, FaEyeSlash } from "react-icons/fa6"; + +export function ThemeSettingsEntry({ theme }: { theme: Theme }) { + const unpinnedThemes = useCSSLoaderValue("unpinnedThemes"); + const isWorking = useCSSLoaderValue("isWorking"); + const isPinned = !unpinnedThemes.includes(theme.id); + const updateStatus = useThemeInstallState(theme); + const isOutdated = updateStatus === "outdated"; + + const installTheme = useCSSLoaderAction("installTheme"); + const pinTheme = useCSSLoaderAction("pinTheme"); + const unpinTheme = useCSSLoaderAction("unpinTheme"); + + return ( +
+ {updateStatus === "outdated" && ( +
+ )} + { + showModal(); + }} + onSecondaryActionDescription={isOutdated ? "Update Theme" : undefined} + onSecondaryButton={isOutdated ? () => installTheme(theme.id) : undefined} + > + {theme.display_name}} + checked={theme.enabled} + onChange={(bool) => toggleThemeWithModals(theme, bool)} + /> + { + if (isPinned) { + unpinTheme(theme.id); + } else { + pinTheme(theme.id); + } + }} + > + {isPinned ? ( + + ) : ( + + )} + + { + showModal(); + }} + > + + + +
+ ); +} diff --git a/src/modules/settings/theme/UpdateAllThemesButton.tsx b/src/modules/settings/theme/UpdateAllThemesButton.tsx new file mode 100644 index 0000000..3aa6da6 --- /dev/null +++ b/src/modules/settings/theme/UpdateAllThemesButton.tsx @@ -0,0 +1,27 @@ +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; +import { DialogButton } from "@decky/ui"; +import { FaDownload } from "react-icons/fa6"; + +export function UpdateAllThemesButton() { + const updateStatuses = useCSSLoaderValue("updateStatuses"); + const themes = useCSSLoaderValue("themes"); + const installTheme = useCSSLoaderAction("installTheme"); + + async function updateAll() { + const outdatedThemes = updateStatuses.filter((f) => f[1] === "outdated").map((f) => f[0]); + for (const themeId of outdatedThemes) { + await installTheme(themeId); + } + } + + return ( + <> + {updateStatuses.filter((e) => e[1] === "outdated").length > 0 && ( + + + Update All Themes + + )} + + ); +} diff --git a/src/modules/settings/theme/index.ts b/src/modules/settings/theme/index.ts new file mode 100644 index 0000000..f5cc7f0 --- /dev/null +++ b/src/modules/settings/theme/index.ts @@ -0,0 +1 @@ +export * from "./ThemeSettings"; diff --git a/src/modules/theme-store/components/BrowserSearchFields.tsx b/src/modules/theme-store/components/BrowserSearchFields.tsx new file mode 100644 index 0000000..0294a66 --- /dev/null +++ b/src/modules/theme-store/components/BrowserSearchFields.tsx @@ -0,0 +1,103 @@ +import { useEffect, useMemo } from "react"; +import { + useThemeBrowserSharedAction, + useThemeBrowserSharedValue, + useThemeBrowserStoreAction, + useThemeBrowserStoreValue, +} from "../context"; +import { + DialogButton, + Dropdown, + DropdownOption, + Focusable, + PanelSectionRow, + SliderField, + TextField, +} from "@decky/ui"; +import { FilterOptionLabel } from "./FilterOptionLabel"; +import { FaRotate } from "react-icons/fa6"; + +export function BrowserSearchFields() { + const { filters, order } = useThemeBrowserStoreValue("filterOptions"); + const searchOpts = useThemeBrowserStoreValue("searchOpts"); + const setSearchOpts = useThemeBrowserStoreAction("setSearchOpts"); + const refreshThemes = useThemeBrowserStoreAction("refreshThemes"); + + const browserCardSize = useThemeBrowserSharedValue("browserCardSize"); + const setBrowserCardSize = useThemeBrowserSharedAction("setBrowserCardSize"); + + const formattedFilters: DropdownOption[] = useMemo(() => { + const totalNumOptions = Object.values(filters).reduce((acc, cur) => acc + Number(cur), 0); + return [ + { data: "All", label: }, + ...Object.entries(filters) + .filter(([_, itemCount]) => Number(itemCount) > 0) + .map(([name, itemCount]) => ({ + data: name, + label: , + })), + ]; + }, [filters]); + + return ( + <> + + +
+ Sort + ({ data: e, label: e }))} + strDefaultLabel="Last Updated" + selectedOption={searchOpts.order} + onChange={(value) => { + const newSearchOpts = { ...searchOpts, order: value.data, page: 1 }; + void setSearchOpts(newSearchOpts); + }} + /> +
+
+ Filter + { + const newSearchOpts = { ...searchOpts, filters: value.data, page: 1 }; + void setSearchOpts(newSearchOpts); + }} + /> +
+
+
+ + +
+ { + const newSearchOpts = { ...searchOpts, search: event.target.value, page: 1 }; + void setSearchOpts(newSearchOpts); + }} + /> +
+ + + Refresh + +
+ +
+
+
+ + ); +} diff --git a/src/modules/theme-store/components/FilterOptionLabel.tsx b/src/modules/theme-store/components/FilterOptionLabel.tsx new file mode 100644 index 0000000..3fde8a8 --- /dev/null +++ b/src/modules/theme-store/components/FilterOptionLabel.tsx @@ -0,0 +1,14 @@ +export function FilterOptionLabel({ + text, + itemCount, +}: { + text: string; + itemCount: number | string; +}) { + return ( +
+ {text} + {itemCount} +
+ ); +} diff --git a/src/modules/theme-store/components/LoadMoreButton.tsx b/src/modules/theme-store/components/LoadMoreButton.tsx new file mode 100644 index 0000000..c7c63a0 --- /dev/null +++ b/src/modules/theme-store/components/LoadMoreButton.tsx @@ -0,0 +1,26 @@ +import { DialogButton } from "@decky/ui"; +import { useThemeBrowserStoreAction, useThemeBrowserStoreValue } from "../context"; + +export function LoadMoreButton() { + const searchOpts = useThemeBrowserStoreValue("searchOpts"); + const themeTotal = useThemeBrowserStoreValue("themeTotal"); + const themes = useThemeBrowserStoreValue("themes"); + const loading = useThemeBrowserStoreValue("loading"); + const setSearchOpts = useThemeBrowserStoreAction("setSearchOpts"); + + function handleClick() { + void setSearchOpts({ ...searchOpts, page: searchOpts.page + 1 }); + } + + return ( +
+ {themes.length < themeTotal ? ( +
+ + Load More + +
+ ) : null} +
+ ); +} diff --git a/src/modules/theme-store/components/ThemeBrowserPage.tsx b/src/modules/theme-store/components/ThemeBrowserPage.tsx new file mode 100644 index 0000000..c187f53 --- /dev/null +++ b/src/modules/theme-store/components/ThemeBrowserPage.tsx @@ -0,0 +1,65 @@ +import { Focusable } from "@decky/ui"; +import { useThemeBrowserStoreAction, useThemeBrowserStoreValue } from "../context"; +import { BrowserSearchFields } from "./BrowserSearchFields"; +import { useCSSLoaderValue } from "@/backend"; +import { useEffect, useRef } from "react"; +import { ImSpinner5 } from "react-icons/im"; +import { LoadMoreButton } from "./LoadMoreButton"; +import { useExpandedViewAction } from "@/modules/expanded-view"; +import { ThemeCard } from "@/lib"; + +export function ThemeBrowserPage() { + const initializeStore = useThemeBrowserStoreAction("initializeStore"); + const themes = useThemeBrowserStoreValue("themes"); + const loading = useThemeBrowserStoreValue("loading"); + const indexToSnapToOnLoad = useThemeBrowserStoreValue("indexToSnapToOnLoad"); + const backendVersion = useCSSLoaderValue("backendVersion"); + + const openTheme = useExpandedViewAction("openTheme"); + + const endOfPageRef = useRef(null); + const firstCardRef = useRef(null); + + useEffect(() => { + void initializeStore(); + }, []); + + useEffect(() => { + if (endOfPageRef?.current) { + endOfPageRef?.current?.focus(); + } + }, [indexToSnapToOnLoad]); + + return ( + <> + + + {loading ? ( +
+ + {/* Re-using expanded view's loading class */} + Loading +
+ ) : ( + themes + .filter((theme) => theme.manifestVersion <= backendVersion) + .map((theme, index) => ( + openTheme(theme.id)} + ref={ + index === indexToSnapToOnLoad + ? endOfPageRef + : index === 0 + ? firstCardRef + : undefined + } + key={theme.id} + theme={theme} + /> + )) + )} +
+ + + ); +} diff --git a/src/modules/theme-store/components/index.ts b/src/modules/theme-store/components/index.ts new file mode 100644 index 0000000..1ae67d4 --- /dev/null +++ b/src/modules/theme-store/components/index.ts @@ -0,0 +1 @@ +export * from "./ThemeBrowserPage"; diff --git a/src/modules/theme-store/context/ThemeBrowserSharedStore.tsx b/src/modules/theme-store/context/ThemeBrowserSharedStore.tsx new file mode 100644 index 0000000..de146b0 --- /dev/null +++ b/src/modules/theme-store/context/ThemeBrowserSharedStore.tsx @@ -0,0 +1,44 @@ +// This is for things that are shared across the entire Theme Browser page and all tabs. + +import { createStore, useStore } from "zustand"; +import { ColumnNumbers } from "../../../lib/components/theme-card"; + +interface ThemeBrowserSharedStoreValues { + browserCardSize: ColumnNumbers; + currentTab: string; + targetOverride: string | null; +} + +interface ThemeBrowserSharedStoreActions { + setBrowserCardSize: (value: ColumnNumbers) => void; + setCurrentTab: (value: string) => void; + setTargetOverride: (value: string | null) => void; +} + +interface IThemeBrowserSharedStore + extends ThemeBrowserSharedStoreValues, + ThemeBrowserSharedStoreActions {} + +export const themeBrowserSharedStore = createStore((set) => { + return { + browserCardSize: 3, + currentTab: "bpm-themes", + targetOverride: "", + setBrowserCardSize: (value: ColumnNumbers) => set({ browserCardSize: value }), + setCurrentTab: (value: string) => set({ currentTab: value }), + setTargetOverride: (value: string | null) => set({ targetOverride: value }), + }; +}); + +export const getThemeBrowserSharedState = () => themeBrowserSharedStore.getState(); + +const useThemeBrowserSharedState = (fn: (state: IThemeBrowserSharedStore) => any) => + useStore(themeBrowserSharedStore, fn); + +export const useThemeBrowserSharedValue = ( + key: T +): IThemeBrowserSharedStore[T] => useThemeBrowserSharedState((state) => state[key]); + +export const useThemeBrowserSharedAction = ( + key: T +): IThemeBrowserSharedStore[T] => useThemeBrowserSharedState((state) => state[key]); diff --git a/src/modules/theme-store/context/ThemeBrowserStore.tsx b/src/modules/theme-store/context/ThemeBrowserStore.tsx new file mode 100644 index 0000000..c242109 --- /dev/null +++ b/src/modules/theme-store/context/ThemeBrowserStore.tsx @@ -0,0 +1,191 @@ +import { createContext, useContext, useRef } from "react"; +import { + FilterQueryResponse, + PartialCSSThemeInfo, + ThemeQueryRequest, + ThemeQueryResponse, +} from "@/types"; +import { StoreApi, createStore, useStore } from "zustand"; +import { getCSSLoaderState } from "@/backend"; +import { isEqual } from "lodash"; +import { getThemeBrowserSharedState, themeBrowserSharedStore } from "./ThemeBrowserSharedStore"; + +interface ThemeBrowserStoreValues { + loading: boolean; + themes: PartialCSSThemeInfo[]; + themeTotal: number; + searchOpts: ThemeQueryRequest; + prevSearchOpts: ThemeQueryRequest; + filterOptions: FilterQueryResponse; + indexToSnapToOnLoad: number; +} + +interface ThemeBrowserStoreActions { + initializeStore: () => Promise; + getFilters: () => Promise; + setSearchOpts: (searchOpts: ThemeQueryRequest, forceRefresh?: boolean) => Promise; + refreshThemes: () => Promise; + getThemes: () => Promise; +} + +interface IThemeBrowserStore extends ThemeBrowserStoreValues, ThemeBrowserStoreActions {} + +const ThemeBrowserStoreContext = createContext | null>(null); + +function generateParamStr(searchOpts: ThemeQueryRequest, themeType: "ALL" | "DESKTOP" | "BPM") { + const searchOptsClone = structuredClone(searchOpts); + + let prependString = + themeType === "ALL" ? "CSS" : themeType === "DESKTOP" ? "DESKTOP-CSS" : "BPM-CSS"; + // "All" is a fake term made up by the frontend to have a unique key for it, the server just expects empty + searchOptsClone.filters === "All" ? (searchOptsClone.filters = "") : (prependString += "."); + searchOptsClone.filters = prependString + searchOptsClone.filters; + + // @ts-expect-error + const paramStr = new URLSearchParams(searchOptsClone).toString(); + return paramStr; +} + +export function ThemeBrowserStoreProvider({ + children, + filterPath, + themePath, + themeType, + requiresAuth = false, +}: { + children: React.ReactNode; + filterPath: string; + themePath: string; + themeType: "ALL" | "DESKTOP" | "BPM"; + requiresAuth?: boolean; +}) { + const storeRef = useRef | null>(null); + + if (!storeRef.current) { + storeRef.current = createStore((set, get) => ({ + loading: true, + themes: [], + themeTotal: 0, + searchOpts: { + page: 1, + perPage: 50, + filters: "All", + order: "Last Updated", + search: "", + }, + prevSearchOpts: { + page: 1, + perPage: 50, + filters: "All", + order: "Last Updated", + search: "", + }, + filterOptions: { + filters: [], + order: ["Last Updated"], + }, + indexToSnapToOnLoad: -1, + initializeStore: async () => { + try { + await get().getFilters(); + + // When you navigate to the expanded view and back, it re-loads the page, which re-runs this, so we can just check if there is a target override + const { targetOverride } = getThemeBrowserSharedState(); + if (targetOverride) { + get().setSearchOpts( + { + ...get().searchOpts, + filters: targetOverride, + page: 1, + }, + true + ); + themeBrowserSharedStore.setState({ targetOverride: null }); + } else { + await get().getThemes(); + } + } catch (error) {} + }, + getFilters: async () => { + const { apiFetch } = getCSSLoaderState(); + const typeMapping = { + ALL: "CSS", + DESKTOP: "DESKTOP-CSS", + BPM: "BPM-CSS", + }; + + try { + const response = await apiFetch( + `${filterPath}?type=${typeMapping[themeType]}`, + {}, + requiresAuth + ); + if (response.filters) { + set({ filterOptions: response }); + } + } catch (error) {} + }, + setSearchOpts: async (searchOpts, forceRefresh?: boolean) => { + const { searchOpts: prevSearchOpts, themes, getThemes } = get(); + set({ searchOpts, prevSearchOpts }); + + if (!isEqual(prevSearchOpts, searchOpts) || forceRefresh || themes.length === 0) { + await getThemes(); + } + }, + refreshThemes: async () => { + // setSearchOpts calls get + const { searchOpts, setSearchOpts } = get(); + await setSearchOpts({ ...searchOpts, page: 1 }, true); + }, + getThemes: async () => { + set({ loading: true }); + try { + const { searchOpts } = get(); + + const { apiFetch } = getCSSLoaderState(); + const response = await apiFetch( + `${themePath}?${generateParamStr(searchOpts, themeType)}`, + {}, + requiresAuth + ); + if (response.items) { + set({ themeTotal: response.total }); + if (searchOpts.page === 1) { + set({ themes: response.items, indexToSnapToOnLoad: -1 }); + } else { + set({ + themes: [...get().themes, ...response.items], + // This ensures that you snap back to the last theme you were viewing + // For example, if you were at the end of page 1 (theme 50) and you load page 2, you should snap back to theme 50 + indexToSnapToOnLoad: searchOpts.perPage * (searchOpts.page - 1) - 1, + }); + } + } + } catch (error) {} + set({ loading: false }); + }, + })); + } + + return ( + + {children} + + ); +} + +export const useThemeBrowserStore = (selector: (state: IThemeBrowserStore) => T) => { + const store = useContext(ThemeBrowserStoreContext); + if (!store) { + throw new Error("Missing StoreProvider"); + } + return useStore(store, selector); +}; +export const useThemeBrowserStoreValue = ( + key: T +): IThemeBrowserStore[T] => useThemeBrowserStore((state) => state[key]); + +export const useThemeBrowserStoreAction = ( + key: T +): IThemeBrowserStore[T] => useThemeBrowserStore((state) => state[key]); diff --git a/src/modules/theme-store/context/index.ts b/src/modules/theme-store/context/index.ts new file mode 100644 index 0000000..22277fe --- /dev/null +++ b/src/modules/theme-store/context/index.ts @@ -0,0 +1,2 @@ +export * from "./ThemeBrowserStore"; +export * from "./ThemeBrowserSharedStore"; diff --git a/src/modules/theme-store/index.ts b/src/modules/theme-store/index.ts new file mode 100644 index 0000000..c4e34b2 --- /dev/null +++ b/src/modules/theme-store/index.ts @@ -0,0 +1 @@ +export * from "./pages"; diff --git a/src/modules/theme-store/pages/AccountPage.tsx b/src/modules/theme-store/pages/AccountPage.tsx new file mode 100644 index 0000000..1101548 --- /dev/null +++ b/src/modules/theme-store/pages/AccountPage.tsx @@ -0,0 +1,69 @@ +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; +import { DialogButton, Focusable, TextField } from "@decky/ui"; +import { useState } from "react"; +import { FaArrowRightToBracket } from "react-icons/fa6"; + +export function AccountPage() { + const apiFullToken = useCSSLoaderValue("apiFullToken"); + + return ( +
+

{apiFullToken ? "Your Account" : "Log In"}

+ {apiFullToken ? : } +

+ Logging in gives you access to star themes, saving them to their own page where you can + quickly find them. +
+ To get started, create an account on deckthemes.com and generate an account key from your + profile page. +
+

+
+ ); +} +function LoggedInSection() { + const apiMeData = useCSSLoaderValue("apiMeData"); + const logOut = useCSSLoaderAction("logOut"); + return ( + + + {apiMeData ? `Logged in as ${apiMeData.username}` : "Loading..."} + + + Unlink My Deck + + + ); +} + +function LoggedOutSection() { + const apiFullToken = useCSSLoaderValue("apiFullToken"); + const logInWithShortToken = useCSSLoaderAction("logInWithShortToken"); + const apiShortToken = useCSSLoaderValue("apiShortToken"); + + const [shortTokenInterimValue, setShortTokenIntValue] = useState(apiShortToken); + + return ( + +
+ setShortTokenIntValue(e.target.value)} + /> +
+ { + logInWithShortToken(shortTokenInterimValue); + }} + > + + Log In + +
+ ); +} diff --git a/src/modules/theme-store/pages/ThemeStoreRouter.tsx b/src/modules/theme-store/pages/ThemeStoreRouter.tsx new file mode 100644 index 0000000..0cb2505 --- /dev/null +++ b/src/modules/theme-store/pages/ThemeStoreRouter.tsx @@ -0,0 +1,101 @@ +import { Tabs } from "@decky/ui"; +import { ThemeBrowserPage } from "../components"; +import { + ThemeBrowserStoreProvider, + useThemeBrowserSharedAction, + useThemeBrowserSharedValue, +} from "../context"; +import { AccountPage } from "./AccountPage"; +import { useCSSLoaderValue } from "@/backend"; +import { Permissions } from "@/types"; +import { ThemeCardCSSVariableProvider } from "@/lib"; + +// TODO: Make the tab definition a constant so that it isn't re-generated every page load + +export function ThemeStoreRouter() { + const currentTab = useThemeBrowserSharedValue("currentTab"); + const setCurrentTab = useThemeBrowserSharedAction("setCurrentTab"); + + const apiMeData = useCSSLoaderValue("apiMeData"); + + const tabs = [ + { + id: "bpm-themes", + title: "Deck UI Themes", + content: ( + + + + ), + }, + { + id: "desktop-themes", + title: "Desktop Themes", + content: ( + + + + ), + }, + { + id: "account", + title: "Account", + content: , + }, + ]; + + apiMeData?.permissions?.includes(Permissions.viewSubs) && + tabs.splice(2, 0, { + id: "submissions", + title: "Submissions", + content: ( + + + + ), + }); + + apiMeData?.username && + tabs.splice(2, 0, { + id: "starred-themes", + title: "Starred Themes", + content: ( + + + + ), + }); + + return ( +
+ + setCurrentTab(tab)} tabs={tabs}> +
+ ); +} + +function BrowserCardSizeVariableProvider() { + const browserCardSize = useThemeBrowserSharedValue("browserCardSize"); + + return ; +} diff --git a/src/modules/theme-store/pages/index.ts b/src/modules/theme-store/pages/index.ts new file mode 100644 index 0000000..d0b93b7 --- /dev/null +++ b/src/modules/theme-store/pages/index.ts @@ -0,0 +1 @@ +export * from "./ThemeStoreRouter"; diff --git a/src/pages/settings/DonatePage.tsx b/src/pages/settings/DonatePage.tsx deleted file mode 100644 index 6d60556..0000000 --- a/src/pages/settings/DonatePage.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { - DialogButton, - Focusable, - Navigation, - Panel, - PanelSection, - ScrollPanelGroup, -} from "decky-frontend-lib"; -import { useEffect, useMemo, useState } from "react"; -import { SiKofi, SiPatreon } from "react-icons/si"; -import { server } from "../../python"; - -export function DonatePage() { - const [loaded, setLoaded] = useState(false); - const [supporters, setSupporters] = useState(""); - - const formattedSupporters = useMemo(() => { - const numOfNamesPerPage = 10; - const supportersArr = supporters.split("\n"); - const newArr = []; - for (let i = 0; i < supportersArr.length; i += numOfNamesPerPage) { - newArr.push(supportersArr.slice(i, i + numOfNamesPerPage).join("\n")); - } - return newArr; - }, [supporters]); - - function fetchSupData() { - server! - .fetchNoCors("https://api.deckthemes.com/patrons", { method: "GET" }) - .then((deckyRes) => { - if (deckyRes.success) { - return deckyRes.result; - } - throw new Error("unsuccessful"); - }) - .then((res) => { - if (res.status === 200) { - return res.body; - } - throw new Error("Res not OK"); - }) - .then((text) => { - if (text) { - setLoaded(true); - setSupporters(text); - } - }) - .catch((err) => { - console.error("CSS Loader - Error Fetching Supporter Data", err); - }); - } - useEffect(() => { - fetchSupData(); - }, []); - return ( -
- -

- Donations help to cover the costs of hosting the store, as well as funding development for - CSS Loader and its related projects. -

- - Navigation.NavigateToExternalWeb("https://patreon.com/deckthemes")} - focusWithinClassName="gpfocuswithin" - className="patreon-or-kofi-container patreon" - > -
- - Patreon -
- Recurring Donation - patreon.com/deckthemes - Perks: -
    -
  • - {/* Potentially could expand this to add it to deckthemes and audioloader */} - Your name in CSS Loader -
  • -
  • Patreon badge on deckthemes.com
  • -
  • - {/* Could also impl. this on deck store to make it more meaningful */} - Colored name + VIP channel in the DeckThemes Discord -
  • -
-
- Navigation.NavigateToExternalWeb("https://ko-fi.com/suchmememanyskill")} - focusWithinClassName="gpfocuswithin" - className="patreon-or-kofi-container" - > -
- - Ko-Fi -
- One-time Donation - ko-fi.com/suchmememanyskill -
-
- {loaded ? ( -
- - {formattedSupporters.map((e) => { - return ( - {}} focusWithinClassName="gpfocuswithin"> -

{e}

-
- ); - })} -
-
- ) : null} -
- ); -} diff --git a/src/pages/settings/PluginSettings.tsx b/src/pages/settings/PluginSettings.tsx deleted file mode 100644 index 907405a..0000000 --- a/src/pages/settings/PluginSettings.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { ButtonItem, DropdownItem, Focusable, ToggleField } from "decky-frontend-lib"; -import { useMemo, useState, useEffect } from "react"; -import { useCssLoaderState } from "../../state"; -import { toast } from "../../python"; -import { setNavPatch } from "../../deckyPatches/NavPatch"; -import { - getWatchState, - getServerState, - enableServer, - toggleWatchState, - getBetaTranslationsState, - fetchClassMappings, -} from "../../backend/pythonMethods/pluginSettingsMethods"; -import { booleanStoreWrite, stringStoreWrite } from "../../backend/pythonMethods/storeUtils"; -import { disableUnminifyMode, enableUnminifyMode } from "../../deckyPatches/UnminifyMode"; -import { dumpMappings } from "../../deckyPatches/dump-mappings"; - -export function PluginSettings() { - const { navPatchInstance, unminifyModeOn, setGlobalState } = useCssLoaderState(); - const [serverOn, setServerOn] = useState(false); - const [watchOn, setWatchOn] = useState(false); - const [betaTranslationsOn, setBetaTranslationsOn] = useState("-1"); - - const navPatchEnabled = useMemo(() => !!navPatchInstance, [navPatchInstance]); - - async function fetchServerState() { - const value = await getServerState(); - setServerOn(value); - } - async function fetchWatchState() { - const value = await getWatchState(); - setWatchOn(value); - } - async function fetchBetaTranslationsState() { - const value = await getBetaTranslationsState(); - if (!["0", "1", "-1"].includes(value)) { - setBetaTranslationsOn("-1"); - return; - } - setBetaTranslationsOn(value); - } - - useEffect(() => { - void fetchServerState(); - void fetchWatchState(); - void fetchBetaTranslationsState(); - }, []); - - function setUnminify(enabled: boolean) { - setGlobalState("unminifyModeOn", enabled); - if (enabled) { - enableUnminifyMode(); - return; - } - disableUnminifyMode(); - } - - async function setWatch(enabled: boolean) { - await toggleWatchState(enabled, false); - await fetchWatchState(); - } - - async function setServer(enabled: boolean) { - if (enabled) await enableServer(); - await booleanStoreWrite("server", enabled); - await fetchServerState(); - } - - async function setBetaTranslations(value: string) { - await stringStoreWrite("beta_translations", value); - await fetchClassMappings(); - await fetchBetaTranslationsState(); - } - - return ( -
- - setBetaTranslations(data.data)} - /> - - - { - setServer(value); - }} - /> - - - setNavPatch(value, true)} - /> - - - - - - - - - dumpMappings()} - > - Dump - - -
- ); -} diff --git a/src/pages/settings/PresetSettings.tsx b/src/pages/settings/PresetSettings.tsx deleted file mode 100644 index b1ddc20..0000000 --- a/src/pages/settings/PresetSettings.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Focusable, PanelSection } from "decky-frontend-lib"; -import { useCssLoaderState } from "../../state"; -import { Flags, Theme } from "../../ThemeTypes"; -import { useState } from "react"; -import { PresetSelectionDropdown } from "../../components"; -import { FullscreenProfileEntry } from "../../components/ThemeSettings/FullscreenProfileEntry"; -import { installTheme } from "../../api"; -import * as python from "../../python"; - -export function PresetSettings() { - const { localThemeList, setGlobalState, updateStatuses } = useCssLoaderState(); - - const [isInstalling, setInstalling] = useState(false); - - async function handleUpdate(e: Theme) { - setInstalling(true); - await installTheme(e.id); - // This just updates the updateStatuses arr to know that this theme now is up to date, no need to re-fetch the API to know that - setGlobalState( - "updateStatuses", - updateStatuses.map((f) => (f[0] === e.id ? [e.id, "installed", false] : e)) - ); - setInstalling(false); - } - - async function handleUninstall(listEntry: Theme) { - setInstalling(true); - await python.deleteTheme(listEntry.name); - await python.reloadBackend(); - setInstalling(false); - } - - return ( -
- - - - {localThemeList - .filter((e) => e.flags.includes(Flags.isPreset)) - .map((e) => ( - - ))} - - -
- ); -} diff --git a/src/pages/settings/SettingsPageRouter.tsx b/src/pages/settings/SettingsPageRouter.tsx deleted file mode 100644 index b2f8952..0000000 --- a/src/pages/settings/SettingsPageRouter.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { SidebarNavigation } from "decky-frontend-lib"; -import { BsFolderFill, BsGearFill } from "react-icons/bs"; -import { RiPaintFill, RiSettings2Fill } from "react-icons/ri"; -import { ThemeSettings } from "./ThemeSettings"; -import { PresetSettings } from "./PresetSettings"; -import { PluginSettings } from "./PluginSettings"; -import { Credits } from "./Credits"; -import { AiFillGithub, AiFillHeart } from "react-icons/ai"; -import { DonatePage } from "./DonatePage"; -import { FaFolder, FaGithub, FaHeart } from "react-icons/fa"; - -export function SettingsPageRouter() { - return ( - <> - - , - route: "/cssloader/settings/themes", - content: , - }, - { - title: "Profiles", - icon: , - route: "/cssloader/settings/profiles", - - content: , - }, - { - title: "Settings", - icon: , - route: "/cssloader/settings/plugin", - - content: , - }, - { - title: "Donate", - icon: , - route: "/cssloader/settings/donate", - - content: , - }, - { - title: "Credits", - icon: , - route: "/cssloader/settings/credits", - - content: , - }, - ]} - > - - ); -} diff --git a/src/pages/settings/ThemeSettings.tsx b/src/pages/settings/ThemeSettings.tsx deleted file mode 100644 index 09c8f50..0000000 --- a/src/pages/settings/ThemeSettings.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { DialogButton, DialogCheckbox, Focusable, PanelSection } from "decky-frontend-lib"; -import { useCssLoaderState } from "../../state"; -import { useMemo, useState } from "react"; -import { Flags, Theme } from "../../ThemeTypes"; -import { FullscreenSingleThemeEntry } from "../../components/ThemeSettings/FullscreenSingleThemeEntry"; -import { ThemeErrorCard } from "../../components/ThemeErrorCard"; -import { installTheme } from "../../api"; -import * as python from "../../python"; -import { DeleteMenu } from "../../components/ThemeSettings/DeleteMenu"; -import { UpdateAllThemesButton } from "../../components/ThemeSettings/UpdateAllThemesButton"; - -export function ThemeSettings() { - const { localThemeList, unpinnedThemes, themeErrors, setGlobalState, updateStatuses } = - useCssLoaderState(); - - const [isInstalling, setInstalling] = useState(false); - const [mode, setMode] = useState<"view" | "delete">("view"); - - const sortedList = useMemo(() => { - return localThemeList - .filter((e) => !e.flags.includes(Flags.isPreset)) - .sort((a, b) => { - const aPinned = !unpinnedThemes.includes(a.id); - const bPinned = !unpinnedThemes.includes(b.id); - // This sorts the pinned themes alphabetically, then the non-pinned alphabetically - if (aPinned === bPinned) { - return a.name.localeCompare(b.name); - } - return Number(bPinned) - Number(aPinned); - }); - }, [localThemeList.length]); - - async function handleUpdate(e: Theme) { - setInstalling(true); - const unpinned = unpinnedThemes.includes(e.id); - await installTheme(e.id); - // This just updates the updateStatuses arr to know that this theme now is up to date, no need to re-fetch the API to know that - setGlobalState( - "updateStatuses", - updateStatuses.map((f) => (f[0] === e.id ? [e.id, "installed", false] : e)) - ); - // Remove duplicate theme from unpinned list. - if (unpinned) python.pinTheme(e.id); - setInstalling(false); - } - - async function handleUninstall(listEntry: Theme) { - setInstalling(true); - await python.deleteTheme(listEntry.name); - if (unpinnedThemes.includes(listEntry.id)) { - // This isn't really pinning it, it's just removing its name from the unpinned list. - python.pinTheme(listEntry.id); - } - await python.reloadBackend(); - setInstalling(false); - } - - return ( -
- - - - - (mode === "delete" ? setMode("view") : setMode("delete"))} - > - {mode === "delete" ? "Go Back" : "Delete Themes"} - - - - {mode === "view" && ( - <> - - {sortedList.map((e) => ( - - ))} - - - )} - {mode === "delete" && ( - setMode("view")} themeList={sortedList} /> - )} - - - {themeErrors.length > 0 && ( - - - {themeErrors.map((e) => { - return ; - })} - - - )} -
- ); -} diff --git a/src/pages/theme-manager/ExpandedView.tsx b/src/pages/theme-manager/ExpandedView.tsx deleted file mode 100644 index 653b9fb..0000000 --- a/src/pages/theme-manager/ExpandedView.tsx +++ /dev/null @@ -1,423 +0,0 @@ -import { - DialogButton, - Focusable, - Navigation, - showModal, - ScrollPanelGroup, -} from "decky-frontend-lib"; -import { useEffect, useRef, useState, VFC } from "react"; -import { ImCog, ImSpinner5 } from "react-icons/im"; -import { BsStar, BsStarFill } from "react-icons/bs"; - -import * as python from "../../python"; -import { genericGET, refreshToken, toggleStar as apiToggleStar, installTheme } from "../../api"; - -import { useCssLoaderState } from "../../state"; -import { Theme } from "../../ThemeTypes"; -import { FullCSSThemeInfo, PartialCSSThemeInfo } from "../../apiTypes"; -import { ThemeSettingsModalRoot } from "../../components/Modals/ThemeSettingsModal"; -import { AuthorViewModalRoot } from "../../components/Modals/AuthorViewModal"; -import { ExpandedViewStyles } from "../../components/Styles"; -import { shortenNumber } from "../../logic/numbers"; -import { FaRegStar, FaStar } from "react-icons/fa"; - -export const ExpandedViewPage: VFC = () => { - const { - localThemeList: installedThemes, - currentExpandedTheme, - isInstalling, - apiFullToken, - themeSearchOpts, - setGlobalState, - } = useCssLoaderState(); - - const [fullThemeData, setFullData] = useState(); - const [loaded, setLoaded] = useState(false); - const [isStarred, setStarred] = useState(false); - const [blurStarButton, setBlurStar] = useState(false); - - async function getStarredStatus() { - if (fullThemeData) { - genericGET(`/users/me/stars/${fullThemeData.id}`, true).then((data) => { - if (data.starred) { - setStarred(data.starred); - } - if (data.starred && fullThemeData?.starCount === 0) { - setFullData({ - ...fullThemeData, - starCount: 1, - }); - } - }); - } - } - - async function toggleStar() { - if (apiFullToken) { - setBlurStar(true); - const newToken = await refreshToken(); - if (fullThemeData && newToken) { - apiToggleStar(fullThemeData.id, isStarred, newToken).then((bool) => { - if (bool) { - setFullData({ - ...fullThemeData, - starCount: isStarred - ? fullThemeData.starCount === 0 - ? // This stops it from going below 0 - fullThemeData.starCount - : fullThemeData.starCount - 1 - : fullThemeData.starCount + 1, - }); - setStarred((cur) => !cur); - setBlurStar(false); - } - }); - } - } else { - python.toast("Not Logged In!", "You can only star themes if logged in."); - } - } - - function checkIfThemeInstalled(themeObj: PartialCSSThemeInfo) { - const filteredArr: Theme[] = installedThemes.filter( - (e: Theme) => e.name === themeObj.name && e.author === themeObj.specifiedAuthor - ); - if (filteredArr.length > 0) { - if (filteredArr[0].version === themeObj.version) { - return "installed"; - } else { - return "outdated"; - } - } else { - return "uninstalled"; - } - } - // These are just switch statements I use to determine text/css for the buttons - // I put them up here just because I find it clearer to read when they aren't inline - function calcButtonText(installStatus: string) { - let buttonText = ""; - switch (installStatus) { - case "installed": - buttonText = "Reinstall"; - break; - case "outdated": - buttonText = "Update"; - break; - default: - buttonText = "Install"; - break; - } - return buttonText; - } - - // For some reason, setting the ref as the useEffect dependency didn't work... - const downloadButtonRef = useRef(null); - const [hasBeenFocused, setHasFocused] = useState(false); - useEffect(() => { - if (downloadButtonRef?.current && !hasBeenFocused) { - downloadButtonRef.current.focus(); - setHasFocused(true); - } - }); - - useEffect(() => { - if (currentExpandedTheme?.id) { - setLoaded(false); - setFocusedImage(0); - genericGET(`/themes/${currentExpandedTheme.id}`).then((data) => { - setFullData(data); - setLoaded(true); - }); - } - }, [currentExpandedTheme]); - - useEffect(() => { - if (apiFullToken && fullThemeData) { - getStarredStatus(); - } - }, [apiFullToken, fullThemeData]); - - const [focusedImage, setFocusedImage] = useState(0); - - if (!loaded) { - return ( - <> - -
- - Loading -
- - ); - } - - // if theres no theme in the detailed view - if (fullThemeData) { - const imageAreaWidth = 556; - const imageAreaPadding = 16; - const gapBetweenCarouselAndImage = 8; - const selectedImageWidth = - fullThemeData.images.length > 1 ? 434.8 : imageAreaWidth - imageAreaPadding * 2; - const selectedImageHeight = (selectedImageWidth / 16) * 10; - const imageCarouselEntryWidth = - imageAreaWidth - imageAreaPadding * 2 - selectedImageWidth - gapBetweenCarouselAndImage; - const imageCarouselEntryHeight = (imageCarouselEntryWidth / 16) * 10; - - // This returns 'installed', 'outdated', or 'uninstalled' - const installStatus = checkIfThemeInstalled(fullThemeData); - return ( - <> - - - { - if (!evt?.detail?.button) return; - if (evt.detail.button === 2) { - Navigation.NavigateBack(); - } - }} - > - {/* Img + Info */} - - {/* Images */} - - {/* Vertical Image Carousel */} - {fullThemeData.images.length > 1 && ( - - {fullThemeData.images.map((e, id) => { - return ( - { - setFocusedImage(id); - }} - className="image-carousel-entry" - focusWithinClassName="gpfocuswithin" - onActivate={() => {}} - > - - - ); - })} - - )} - - {/* Selected Image Display */} - {}} - > - 0 - ? `https://api.deckthemes.com/blobs/${fullThemeData.images?.[focusedImage]?.id}` - : `https://share.deckthemes.com/cssplaceholder.png` - } - /> - {fullThemeData.images.length > 1 && ( -
- - {focusedImage + 1}/{fullThemeData.images.length} - -
- )} -
-
- - - {/* Info */} -
-
- {fullThemeData.displayName} - {fullThemeData.version} -
-
- { - showModal(); - }} - > - By {fullThemeData.specifiedAuthor} - - Last Updated {new Date(fullThemeData.updated).toLocaleDateString()} -
-
- {/* Description */} - {}} - > - Description - 400 ? "text-sm" : ""}> - {fullThemeData?.description || ( - - No description provided. - - )} - - - {/* Targets */} - - Targets - - {fullThemeData.targets.map((e) => ( - { - setGlobalState("themeSearchOpts", { ...themeSearchOpts, filters: e }); - setGlobalState("currentTab", "ThemeBrowser"); - setGlobalState("forceScrollBackUp", true); - Navigation.NavigateBack(); - }} - className="target-text" - > - {e} - - ))} - - -
-
-
- {/* Buttons */} - -
-
- {isStarred ? : } - {/* Need to make the text size smaller or else it wraps */} - = 100 ? "0.75em" : "1em" }}> - {shortenNumber(fullThemeData.starCount) ?? fullThemeData.starCount} Star - {fullThemeData.starCount === 1 ? "" : "s"} - -
- -
- - {!apiFullToken ? "Log In to Star" : isStarred ? "Unstar Theme" : "Star Theme"} - -
-
-
-
- Install {fullThemeData.displayName} - - {shortenNumber(fullThemeData.download.downloadCount) ?? - fullThemeData.download.downloadCount}{" "} - Download - {fullThemeData.download.downloadCount === 1 ? "" : "s"} - - - { - installTheme(fullThemeData.id); - }} - > - - {calcButtonText(installStatus)} - - - {installStatus === "installed" && ( - { - showModal( - e.id === fullThemeData.id)?.id || - // using name here because in submissions id is different - installedThemes.find((e) => e.name === fullThemeData.name)!.id - } - /> - ); - }} - className="configure-button" - > - - - )} - -
-
-
- - ); - } - return ( - <> -
- Error fetching selected theme, please go back and retry. -
- - ); -}; diff --git a/src/pages/theme-manager/LogInPage.tsx b/src/pages/theme-manager/LogInPage.tsx deleted file mode 100644 index 7c2a6a9..0000000 --- a/src/pages/theme-manager/LogInPage.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { DialogButton, Focusable, TextField, ToggleField } from "decky-frontend-lib"; -import { SiWebauthn } from "react-icons/si"; -import { useEffect, useMemo, useState, VFC } from "react"; -import { logInWithShortToken, logOut } from "../../api"; -import { useCssLoaderState } from "../../state"; -import { enableServer, getServerState, storeWrite } from "../../python"; -import { disableNavPatch, enableNavPatch } from "../../deckyPatches/NavPatch"; -import { FaArrowRightToBracket } from "react-icons/fa6"; - -export const LogInPage: VFC = () => { - const { apiShortToken, apiFullToken, apiMeData } = useCssLoaderState(); - const [shortTokenInterimValue, setShortTokenIntValue] = useState(apiShortToken); - - return ( - // The outermost div is to push the content down into the visible area -
-
- {apiFullToken ? ( -

Your Account

- ) : ( -

- Log In -

- )} - {apiFullToken ? ( - <> - -
- {apiMeData ? ( - <> - Logged In As {apiMeData.username} - - ) : ( - Loading... - )} -
- - Unlink My Deck - -
- - ) : ( - <> - -
- setShortTokenIntValue(e.target.value)} - /> -
- { - logInWithShortToken(shortTokenInterimValue); - }} - style={{ - maxWidth: "30%", - height: "50%", - marginLeft: "auto", - display: "flex", - alignItems: "center", - justifyContent: "center", - gap: "0.5em", - }} - > - - Log In - -
- - )} -

- Logging in gives you access to star themes, saving them to their own page where you can - quickly find them. -
- Create an account on deckthemes.com and generate an account key on your profile page. -
-

-
-
- ); -}; diff --git a/src/pages/theme-manager/StarredThemesPage.tsx b/src/pages/theme-manager/StarredThemesPage.tsx deleted file mode 100644 index 1f96d8f..0000000 --- a/src/pages/theme-manager/StarredThemesPage.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Focusable } from "decky-frontend-lib"; -import { useCssLoaderState } from "../../state"; -import * as python from "../../python"; -import { BrowserSearchFields, LoadMoreButton, VariableSizeCard } from "../../components"; -import { useEffect, useRef, useState } from "react"; -import { isEqual } from "lodash"; -import { getThemes } from "../../api"; - -export function StarredThemesPage() { - const { - apiFullToken, - apiMeData, - starredSearchOpts: searchOpts, - starredServerFilters: serverFilters, - starredThemeList: themeArr, - browserCardSize, - prevStarSearchOpts: prevSearchOpts, - backendVersion, - } = useCssLoaderState(); - - function reloadThemes() { - getThemes(searchOpts, "/users/me/stars", "starredThemeList", setSnapIndex, true); - python.reloadBackend(); - } - - useEffect(() => { - if (!isEqual(prevSearchOpts, searchOpts) || themeArr.total === 0) { - getThemes(searchOpts, "/users/me/stars", "starredThemeList", setSnapIndex, true); - } - }, [searchOpts, prevSearchOpts, apiMeData]); - - const endOfPageRef = useRef(); - const [indexToSnapTo, setSnapIndex] = useState(-1); - useEffect(() => { - if (endOfPageRef?.current) { - endOfPageRef?.current?.focus(); - } - }, [indexToSnapTo]); - - if (!apiFullToken) { - return ( - <> -
- You Are Not Logged In! - Link your deck to your deckthemes.com account to sync Starred Themes -
- - ); - } - return ( - <> - - - {themeArr.items - .filter((e) => e.manifestVersion <= backendVersion) - .map((e, i) => ( - - ))} - -
-
- -
-
- - ); -} diff --git a/src/pages/theme-manager/SubmissionBrowserPage.tsx b/src/pages/theme-manager/SubmissionBrowserPage.tsx deleted file mode 100644 index 1b89cb1..0000000 --- a/src/pages/theme-manager/SubmissionBrowserPage.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Focusable } from "decky-frontend-lib"; -import { useCssLoaderState } from "../../state"; -import * as python from "../../python"; -import { BrowserSearchFields, LoadMoreButton, VariableSizeCard } from "../../components"; -import { useEffect, useRef, useState } from "react"; -import { isEqual } from "lodash"; -import { getThemes } from "../../api"; - -export function SubmissionsPage() { - const { - apiFullToken, - submissionSearchOpts: searchOpts, - submissionServerFilters: serverFilters, - submissionThemeList: themeArr, - browserCardSize, - prevSubSearchOpts: prevSearchOpts, - apiMeData, - backendVersion, - } = useCssLoaderState(); - - function reloadThemes() { - getThemes(searchOpts, "/themes/awaiting_approval", "submissionThemeList", setSnapIndex, true); - python.reloadBackend(); - } - - useEffect(() => { - if (!isEqual(prevSearchOpts, searchOpts) || themeArr.total === 0) { - getThemes(searchOpts, "/themes/awaiting_approval", "submissionThemeList", setSnapIndex, true); - } - }, [searchOpts, prevSearchOpts, apiMeData]); - - const endOfPageRef = useRef(); - const [indexToSnapTo, setSnapIndex] = useState(-1); - useEffect(() => { - if (endOfPageRef?.current) { - endOfPageRef?.current?.focus(); - } - }, [indexToSnapTo]); - - if (!apiFullToken) { - return ( - <> -
- You Are Not Logged In! - Link your deck to your deckthemes.com account to sync Starred Themes -
- - ); - } - return ( - <> - - - {themeArr.items - .filter((e) => e.manifestVersion <= backendVersion) - .map((e, i) => ( - - ))} - -
-
- -
-
- - ); -} diff --git a/src/pages/theme-manager/ThemeBrowserPage.tsx b/src/pages/theme-manager/ThemeBrowserPage.tsx deleted file mode 100644 index 1cfe69f..0000000 --- a/src/pages/theme-manager/ThemeBrowserPage.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { Focusable } from "decky-frontend-lib"; -import { useLayoutEffect, useState, FC, useEffect, useRef } from "react"; -import * as python from "../../python"; -import { getThemes } from "../../api"; -import { logInWithShortToken } from "../../api"; -import { isEqual } from "lodash"; - -// Interfaces for the JSON objects the lists work with -import { useCssLoaderState } from "../../state"; -import { BrowserSearchFields, VariableSizeCard, LoadMoreButton } from "../../components"; - -export const ThemeBrowserPage: FC = () => { - const { - browseThemeList: themeArr, - themeSearchOpts: searchOpts, - apiShortToken, - apiFullToken, - serverFilters, - browserCardSize = 3, - prevSearchOpts, - backendVersion, - forceScrollBackUp, - setGlobalState, - } = useCssLoaderState(); - - function reloadThemes() { - getThemes(searchOpts, "/themes", "browseThemeList", setSnapIndex); - python.reloadBackend(); - } - - useEffect(() => { - if (!isEqual(prevSearchOpts, searchOpts) || themeArr.total === 0) { - getThemes(searchOpts, "/themes", "browseThemeList", setSnapIndex); - } - }, [searchOpts, prevSearchOpts]); - - // Runs upon opening the page every time - useLayoutEffect(() => { - python.getBackendVersion(); - if (apiShortToken && !apiFullToken) { - logInWithShortToken(); - } - // Installed themes aren't used on this page, but they are used on other pages, so fetching them here means that as you navigate to the others they will be already loaded - python.getInstalledThemes(); - }, []); - - const endOfPageRef = useRef(); - const firstCardRef = useRef(); - useLayoutEffect(() => { - if (forceScrollBackUp) { - // Valve would RE FOCUS THE ONE YOU LAST CLICKED ON after this ran, so i had to add a delay - setTimeout(() => { - firstCardRef?.current && firstCardRef.current?.focus(); - setGlobalState("forceScrollBackUp", false); - }, 100); - } - }, []); - - const [indexToSnapTo, setSnapIndex] = useState(-1); - useEffect(() => { - if (endOfPageRef?.current) { - endOfPageRef?.current?.focus(); - } - }, [indexToSnapTo]); - - return ( - <> - { - reloadThemes(); - }} - /> - {/* I wrap everything in a Focusable, because that ensures that the dpad/stick navigation works correctly */} - - {themeArr.items - .filter((e) => e.manifestVersion <= backendVersion) - .map((e, i) => ( - - ))} - -
-
- -
-
- - ); -}; diff --git a/src/pages/theme-manager/ThemeManagerRouter.tsx b/src/pages/theme-manager/ThemeManagerRouter.tsx deleted file mode 100644 index add161d..0000000 --- a/src/pages/theme-manager/ThemeManagerRouter.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Tabs } from "decky-frontend-lib"; -import { Permissions } from "../../apiTypes"; -import { useCssLoaderState } from "../../state"; -import { LogInPage } from "./LogInPage"; -import { StarredThemesPage } from "./StarredThemesPage"; -import { SubmissionsPage } from "./SubmissionBrowserPage"; -import { ThemeBrowserPage } from "./ThemeBrowserPage"; -import { ThemeBrowserCardStyles } from "../../components/Styles"; -export function ThemeManagerRouter() { - const { apiMeData, currentTab, setGlobalState, browserCardSize } = useCssLoaderState(); - return ( -
- - { - setGlobalState("currentTab", tabID); - }} - tabs={[ - { - title: "All Themes", - content: , - id: "ThemeBrowser", - }, - ...(!!apiMeData - ? [ - { - title: "Starred Themes", - content: , - id: "StarredThemes", - }, - ...(apiMeData.permissions.includes(Permissions.viewSubs) - ? [ - { - title: "Submissions", - content: , - id: "SubmissionsPage", - }, - ] - : []), - ] - : []), - { - title: "DeckThemes Account", - content: , - id: "LogInPage", - }, - ]} - /> -
- ); -} diff --git a/src/pages/theme-manager/index.ts b/src/pages/theme-manager/index.ts deleted file mode 100644 index f16d7eb..0000000 --- a/src/pages/theme-manager/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./ThemeBrowserPage"; -export * from "./ExpandedView"; -export * from "./LogInPage"; -export * from "./StarredThemesPage"; -export * from "./SubmissionBrowserPage"; -export * from "./ThemeManagerRouter"; diff --git a/src/python.ts b/src/python.ts deleted file mode 100644 index 3637cfe..0000000 --- a/src/python.ts +++ /dev/null @@ -1,278 +0,0 @@ -// Code from https://github.com/NGnius/PowerTools/blob/dev/src/python.ts -import { ServerAPI } from "decky-frontend-lib"; -import { CssLoaderState } from "./state"; -import { Theme, ThemeError } from "./ThemeTypes"; -import { bulkThemeUpdateCheck } from "./logic/bulkThemeUpdateCheck"; - -export var server: ServerAPI | undefined = undefined; -export var globalState: CssLoaderState | undefined = undefined; - -export function setServer(s: ServerAPI) { - server = s; -} -export function setStateClass(s: CssLoaderState): void { - globalState = s; -} - -export async function openFilePicker(path: string) { - return await server!.openFilePicker(path, true); -} - -export function fetchThemePath() { - return server!.callPluginMethod("fetch_theme_path", {}); -} - -export function resolve(promise: Promise, setter: any) { - (async function () { - let data = await promise; - if (data.success) { - console.debug("Got resolved", data, "promise", promise); - setter(data.result); - } else { - console.warn("Resolve failed:", data, "promise", promise); - } - })(); -} - -export function execute(promise: Promise) { - (async function () { - let data = await promise; - if (data.success) { - console.debug("Got executed", data, "promise", promise); - } else { - console.warn("Execute failed:", data, "promise", promise); - } - })(); -} - -export async function scheduleCheckForUpdates() { - const setGlobalState = globalState!.setGlobalState.bind(globalState); - function recursiveCheck() { - const timeout = setTimeout(async () => { - // Putting this in the function as im not sure the value would update otherwise - const { nextUpdateCheckTime } = globalState!.getPublicState(); - if (!(new Date().valueOf() > nextUpdateCheckTime)) { - recursiveCheck(); - return; - } - // After testing, it appears that, if there is no wifi, bulkThemeUpdateCheck returns an empty array, this is okay, the try catch is just for extra safety - try { - const data = await bulkThemeUpdateCheck(); - if (data) { - // 24hrs from now - setGlobalState("updateStatuses", data); - } - setGlobalState("nextUpdateCheckTime", new Date().valueOf() + 24 * 60 * 60 * 1000); - } catch (err) { - console.log("Error Checking For Theme Updates", err); - } - recursiveCheck(); - }, 5 * 60 * 1000); - setGlobalState("updateCheckTimeout", timeout); - } - // Initially setting it - // 24hrs from now - setGlobalState("nextUpdateCheckTime", new Date().valueOf() + 24 * 60 * 60 * 1000); - recursiveCheck(); -} - -export async function changePreset(themeName: string, themeList: Theme[]) { - return new Promise(async (resolve) => { - const { selectedPreset } = globalState!.getPublicState(); - - if (selectedPreset) { - // If you already have a preset enabled, since all currently enabled themes are part of that preset, you only need to disable it, not every theme - await setThemeState(selectedPreset!.name, false); - } else { - // On the contrary, if you have no preset, you still do have to disable the current themes and then enable the preset - await Promise.all( - themeList.filter((e) => e.enabled).map((e) => setThemeState(e.name, false)) - ); - } - - if (themeName !== "None") { - await setThemeState(themeName, true); - } - resolve(true); - }); -} - -export async function getInstalledThemes(): Promise { - const setGlobalState = globalState!.setGlobalState.bind(globalState); - const errorRes = await server!.callPluginMethod<{}, { fails: ThemeError[] }>( - "get_last_load_errors", - {} - ); - if (errorRes.success) { - setGlobalState("themeErrors", errorRes.result.fails); - } - const themeRes = await server!.callPluginMethod<{}, Theme[]>("get_themes", {}); - if (themeRes.success) { - setGlobalState("localThemeList", themeRes.result); - return themeRes.result; - } -} - -export async function reloadBackend(): Promise { - return server!.callPluginMethod<{}, { fails: ThemeError[] }>("reset", {}).then((res) => { - return getInstalledThemes(); - }); -} - -export function getThemes() { - return server!.callPluginMethod<{}, Theme[]>("get_themes", {}); -} - -export function setThemeState( - name: string, - state: boolean, - set_deps?: boolean, - set_deps_value?: boolean -): Promise { - return server!.callPluginMethod("set_theme_state", { - name: name, - state: state, - set_deps: set_deps ?? true, - set_deps_value: set_deps_value ?? true, - }); -} - -export function setPatchOfTheme(themeName: string, patchName: string, value: string): Promise { - return server!.callPluginMethod("set_patch_of_theme", { - themeName: themeName, - patchName: patchName, - value: value, - }); -} - -export function setComponentOfThemePatch( - themeName: string, - patchName: string, - componentName: string, - value: string -): Promise { - return server!.callPluginMethod("set_component_of_theme_patch", { - themeName: themeName, - patchName: patchName, - componentName: componentName, - value: value, - }); -} - -export function toast(title: string, message: string) { - // This is a weirdo self-invoking function, but it works. - return (() => { - try { - return server?.toaster.toast({ - title: title, - body: message, - duration: 8000, - }); - } catch (e) { - console.log("CSSLoader Toaster Error", e); - } - })(); -} - -export function downloadThemeFromUrl(themeId: string): Promise { - const { apiUrl } = globalState!.getPublicState(); - return server!.callPluginMethod("download_theme_from_url", { id: themeId, url: apiUrl }); -} - -export function deleteTheme(themeName: string): Promise { - return server!.callPluginMethod("delete_theme", { themeName: themeName }); -} - -export function storeRead(key: string) { - return server!.callPluginMethod("store_read", { key: key }); -} - -export function storeWrite(key: string, value: string) { - return server!.callPluginMethod("store_write", { key: key, val: value }); -} - -export function enableServer() { - return server!.callPluginMethod("enable_server", {}); -} -export function getServerState() { - return server!.callPluginMethod<{}, boolean>("get_server_state", {}); -} - -export function getBackendVersion(): Promise { - const setGlobalState = globalState!.setGlobalState.bind(globalState); - return server!.callPluginMethod<{}, Theme[]>("get_backend_version", {}).then((data) => { - if (data.success) { - setGlobalState("backendVersion", data.result); - } - return; - }); -} - -export function dummyFunction() { - return server!.callPluginMethod<{}, boolean>("dummy_function", {}); -} - -export function genericGET(fetchUrl: string, authToken?: string | undefined) { - return server! - .fetchNoCors(`${fetchUrl}`, { - method: "GET", - headers: authToken - ? { - Authorization: `Bearer ${authToken}`, - } - : {}, - }) - .then((deckyRes) => { - if (deckyRes.success) { - return deckyRes.result; - } - throw new Error(`Fetch not successful!`); - }) - .then((res) => { - if (res.status >= 200 && res.status <= 300 && res.body) { - // @ts-ignore - return JSON.parse(res.body || ""); - } - throw new Error(`Res not OK!, code ${res.status}`); - }) - .then((json) => { - if (json) { - return json; - } - throw new Error(`No json returned!`); - }) - .catch((err) => { - console.error(`Error fetching ${fetchUrl}`, err); - }); -} - -export function unpinTheme(id: string) { - const { unpinnedThemes } = globalState!.getPublicState(); - const setGlobalState = globalState!.setGlobalState.bind(globalState); - const newArr = [...unpinnedThemes, id]; - setGlobalState("unpinnedThemes", newArr); - return storeWrite("unpinnedThemes", JSON.stringify(newArr)); -} - -export function pinTheme(id: string) { - const { unpinnedThemes } = globalState!.getPublicState(); - const setGlobalState = globalState!.setGlobalState.bind(globalState); - const newArr = unpinnedThemes.filter((e) => e !== id); - setGlobalState("unpinnedThemes", newArr); - return storeWrite("unpinnedThemes", JSON.stringify(newArr)); -} - -export function generatePreset(name: string) { - return server!.callPluginMethod("generate_preset_theme", { name: name }); -} - -export function generatePresetFromThemeNames(name: string, themeNames: string[]) { - return server!.callPluginMethod("generate_preset_theme_from_theme_names", { - name: name, - themeNames: themeNames, - }); -} - -export function saveMappings(val: string) { - return server!.callPluginMethod("save_mappings", { val: val }); -} diff --git a/src/state/CssLoaderState.tsx b/src/state/CssLoaderState.tsx deleted file mode 100644 index 057d510..0000000 --- a/src/state/CssLoaderState.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import { Patch, SingleDropdownOption } from "decky-frontend-lib"; -import { createContext, FC, useContext, useEffect, useState } from "react"; -import { - AccountData, - FilterQueryResponse, - PartialCSSThemeInfo, - ThemeQueryRequest, - ThemeQueryResponse, -} from "../apiTypes"; -import { Theme, ThemeError, UpdateStatus } from "../ThemeTypes"; - -interface PublicCssLoaderState { - // Browse Page - serverFilters: FilterQueryResponse; - prevSearchOpts: ThemeQueryRequest; - browseThemeList: ThemeQueryResponse; - themeSearchOpts: ThemeQueryRequest; - - // Starred Themes Page - starredSearchOpts: ThemeQueryRequest; - starredServerFilters: FilterQueryResponse; - starredThemeList: ThemeQueryResponse; - prevStarSearchOpts: ThemeQueryRequest; - - // Submission Page - prevSubSearchOpts: ThemeQueryRequest; - submissionSearchOpts: ThemeQueryRequest; - submissionServerFilters: FilterQueryResponse; - submissionThemeList: ThemeQueryResponse; - - currentTab: string; - forceScrollBackUp: boolean; - - // Api - selectedRepo: SingleDropdownOption; - apiUrl: string; - apiShortToken: string; - apiFullToken: string; - apiTokenExpireDate: Date | number | undefined; - apiMeData: AccountData | undefined; - // This is a unix timestamp - nextUpdateCheckTime: number; - updateCheckTimeout: NodeJS.Timeout | undefined; - - unminifyModeOn: boolean; - navPatchInstance: Patch | undefined; - updateStatuses: UpdateStatus[]; - selectedPreset: Theme | undefined; - localThemeList: Theme[]; - themeErrors: ThemeError[]; - currentSettingsPageTheme: string | undefined; - unpinnedThemes: string[]; - isInstalling: boolean; - currentExpandedTheme: PartialCSSThemeInfo | undefined; - browserCardSize: number; - backendVersion: number; - hiddenMotd: string; -} - -interface PublicCssLoaderContext extends PublicCssLoaderState { - setGlobalState(key: string, data: any): void; - getGlobalState(key: string): any; -} - -// This class creates the getter and setter functions for all of the global state data. -export class CssLoaderState { - private currentTab: string = "ThemeBrowser"; - private forceScrollBackUp: boolean = false; - private nextUpdateCheckTime: number = 0; - private updateCheckTimeout: NodeJS.Timeout | undefined = undefined; - private navPatchInstance: Patch | undefined = undefined; - - private updateStatuses: UpdateStatus[] = []; - private selectedPreset: Theme | undefined = undefined; - private apiUrl: string = "https://api.deckthemes.com"; - private apiShortToken: string = ""; - private apiFullToken: string = ""; - private apiTokenExpireDate: Date | number | undefined = undefined; - private apiMeData: AccountData | undefined = undefined; - private localThemeList: Theme[] = []; - private themeErrors: ThemeError[] = []; - private selectedRepo: SingleDropdownOption = { - data: 1, - label: "All", - }; - private isInstalling: boolean = false; - private currentExpandedTheme: PartialCSSThemeInfo | undefined = undefined; - private browserCardSize: number = 3; - - private browseThemeList: ThemeQueryResponse = { total: 0, items: [] }; - private prevSearchOpts: ThemeQueryRequest = { - page: 1, - perPage: 50, - filters: "All", - order: "Last Updated", - search: "", - }; - private serverFilters: FilterQueryResponse = { - filters: ["All"], - order: ["Last Updated"], - }; - private themeSearchOpts: ThemeQueryRequest = { - page: 1, - perPage: 50, - filters: "All", - order: "Last Updated", - search: "", - }; - - // Stars - private prevStarSearchOpts: ThemeQueryRequest = { - page: 1, - perPage: 50, - filters: "All", - order: "Last Updated", - search: "", - }; - private currentSettingsPageTheme: string | undefined = undefined; - private unpinnedThemes: string[] = []; - - private starredSearchOpts: ThemeQueryRequest = { - page: 1, - perPage: 50, - filters: "All", - order: "Last Updated", - search: "", - }; - private starredServerFilters: FilterQueryResponse = { - filters: ["All"], - order: ["Last Updated"], - }; - private starredThemeList: ThemeQueryResponse = { total: 0, items: [] }; - - // Submissions - private prevSubSearchOpts: ThemeQueryRequest = { - page: 1, - perPage: 50, - filters: "All", - order: "Last Updated", - search: "", - }; - private submissionSearchOpts: ThemeQueryRequest = { - page: 1, - perPage: 50, - filters: "All", - order: "Last Updated", - search: "", - }; - private submissionServerFilters: FilterQueryResponse = { - filters: ["All"], - order: ["Last Updated"], - }; - private submissionThemeList: ThemeQueryResponse = { total: 0, items: [] }; - private backendVersion: number = 6; - private hiddenMotd: string = ""; - private unminifyModeOn: boolean = false; - - // You can listen to this eventBus' 'stateUpdate' event and use that to trigger a useState or other function that causes a re-render - public eventBus = new EventTarget(); - - getPublicState() { - return { - currentTab: this.currentTab, - forceScrollBackUp: this.forceScrollBackUp, - nextUpdateCheckTime: this.nextUpdateCheckTime, - updateCheckTimeout: this.updateCheckTimeout, - apiUrl: this.apiUrl, - apiShortToken: this.apiShortToken, - apiFullToken: this.apiFullToken, - apiTokenExpireDate: this.apiTokenExpireDate, - apiMeData: this.apiMeData, - updateStatuses: this.updateStatuses, - selectedPreset: this.selectedPreset, - localThemeList: this.localThemeList, - themeErrors: this.themeErrors, - currentSettingsPageTheme: this.currentSettingsPageTheme, - unpinnedThemes: this.unpinnedThemes, - isInstalling: this.isInstalling, - hiddenMotd: this.hiddenMotd, - unminifyModeOn: this.unminifyModeOn, - - navPatchInstance: this.navPatchInstance, - selectedRepo: this.selectedRepo, - currentExpandedTheme: this.currentExpandedTheme, - browserCardSize: this.browserCardSize, - - // Browse Page - themeSearchOpts: this.themeSearchOpts, - serverFilters: this.serverFilters, - browseThemeList: this.browseThemeList, - prevSearchOpts: this.prevSearchOpts, - - // Starred - prevStarSearchOpts: this.prevStarSearchOpts, - starredSearchOpts: this.starredSearchOpts, - starredServerFilters: this.starredServerFilters, - starredThemeList: this.starredThemeList, - - // Submissions - prevSubSearchOpts: this.prevSubSearchOpts, - submissionSearchOpts: this.submissionSearchOpts, - submissionServerFilters: this.submissionServerFilters, - submissionThemeList: this.submissionThemeList, - backendVersion: this.backendVersion, - }; - } - - getGlobalState(key: string) { - return this[key]; - } - - setGlobalState(key: string, data: any) { - this[key] = data; - this.forceUpdate(); - } - - private forceUpdate() { - this.eventBus.dispatchEvent(new Event("stateUpdate")); - } -} - -const CssLoaderContext = createContext(null as any); -export const useCssLoaderState = () => useContext(CssLoaderContext); - -interface ProviderProps { - cssLoaderStateClass: CssLoaderState; -} - -// This is a React Component that you can wrap multiple separate things in, as long as they both have used the same instance of the CssLoaderState class, they will have synced state -export const CssLoaderContextProvider: FC = ({ children, cssLoaderStateClass }) => { - const [publicState, setPublicState] = useState({ - ...cssLoaderStateClass.getPublicState(), - }); - - useEffect(() => { - function onUpdate() { - setPublicState({ ...cssLoaderStateClass.getPublicState() }); - } - - cssLoaderStateClass.eventBus.addEventListener("stateUpdate", onUpdate); - - return () => cssLoaderStateClass.eventBus.removeEventListener("stateUpdate", onUpdate); - }, []); - - const getGlobalState = (key: string) => cssLoaderStateClass.getGlobalState(key); - const setGlobalState = (key: string, data: any) => cssLoaderStateClass.setGlobalState(key, data); - - return ( - - {children} - - ); -}; diff --git a/src/state/index.ts b/src/state/index.ts deleted file mode 100644 index 5bffe45..0000000 --- a/src/state/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./CssLoaderState"; diff --git a/src/styles/index.ts b/src/styles/index.ts new file mode 100644 index 0000000..6e06c6a --- /dev/null +++ b/src/styles/index.ts @@ -0,0 +1,2 @@ +export * from "./styles-as-string"; +export * from "./theme-card-styles-generator"; diff --git a/src/styles/styles-as-string.ts b/src/styles/styles-as-string.ts new file mode 100644 index 0000000..7e3aa77 --- /dev/null +++ b/src/styles/styles-as-string.ts @@ -0,0 +1,661 @@ +import { gamepadDialogClasses } from "@decky/ui"; + +export const styles = ` +/* THIS FILE IS NOT USED IN BUILD */ +/* ANY MODIFICATIONS HERE MUST BE COPY PASTED INTO styles-as-string.ts */ +/* THAT IS NEEDED FOR STATIC CLASS INJECTION */ +/* LINT ERRORS ARE TO BE EXPECTED, BECAUSE THIS USES TEMPLATE LITERALS THAT WILL BE FILLED IN BY styles-as-string.ts */ + +/* +MARK: TAILWIND +*/ + +.flex { + display: flex !important; +} + +.flex-col { + flex-direction: column !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-1 { + flex: 1 1 0% !important; +} + +.grid { + display: grid !important; +} + +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; +} + +.grid-cols-\\[1fr\\,2fr\\] { + grid-template-columns: 1fr 2fr !important; +} + +.gap-1 { + gap: 0.25rem !important; +} + +.gap-2 { + gap: 0.5rem !important; +} + +.gap-3 { + gap: 0.75rem !important; +} + +.gap-4 { + gap: 1rem !important; +} + +.gap-8 { + gap: 2rem !important; +} + +.items-center { + align-items: center !important; +} + +.items-stretch { + align-items: stretch !important; +} + +.justify-center { + justify-content: center !important; +} + +.justify-around { + justify-content: space-around !important; +} + +.justify-between { + justify-content: space-between !important; +} + +.p-0 { + padding: 0 !important; +} + +.pl-4 { + padding-left: 1rem !important; +} + +.m-0 { + margin: 0 !important; +} + +.ml-auto { + margin-left: auto !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.mb-4 { + margin-bottom: 1rem !important; +} + +.mt-4 { + margin-top: 1rem !important; +} + +.w-full { + width: 100% !important; +} + +.max-w-1\/2 { + max-width: 50% !important; +} + +.relative { + position: relative !important; +} + +.font-bold { + font-weight: bold !important; +} + +.text-xs { + font-size: 0.75rem !important; +} + +.text-lg { + font-size: 1.125rem !important; +} + +.text-xl { + font-size: 1.25rem !important; +} + +.text-2xl { + font-size: 1.5rem !important; +} + +.absolute-center { + position: absolute !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; +} + +.cl_title { + font-size: 2rem !important; + font-weight: bold !important; +} + +/* +MARK: Fullscreen Routes +*/ + +.cl_fullscreenroute_container { + margin-top: 40px !important; + height: calc(100% - 40px) !important; + background: #0e141b !important; +} + +/* +MARK: TitleView +*/ + + +.cl-title-view-button { + height: 28px !important; + width: 40px !important; + min-width: 0 !important; + padding: 10px 12px !important; +} + +.cl-title-view-button-icon { + margin-top: -4px !important; + display: block !important; +} + +@keyframes onboardingButton { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + } +} + +.cl-animate-onboarding { + animation: onboardingButton 1s infinite ease-in-out !important; +} + +/* +MARK: QAM Tab +*/ + + +.cl-qam-collapse-button-container > div > div > div > div > button { + height: 10px !important; +} + +.cl-qam-themetoggle-notifbubble { + position: absolute !important; + top: 0 !important; + right: -1rem !important; + background: linear-gradient(45deg, transparent 49%, #fca904 50%) !important; + /* The focus ring has a z-index of 10000, so this is just to be cheeky */ + z-index: 10001 !important; + width: 20px !important; + height: 20px !important; +} + +.cl-qam-collapse-button-down-arrow { + transform: translateY(-13px) !important; + font-size: 1.5rem !important; +} + +.cl-qam-collapse-button-up-arrow { + transform: translateY(-12px) !important; + font-size: 1.5rem !important; +} + +.cl-qam-component-icon-container { + margin-left: auto !important; + width: 24px !important; + height: 24px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +.cl-qam-component-color-preview { + width: 20px !important; + height: 20px !important; +} + +.cl-qam-hidden-themes-display { + display: flex !important; + align-items: center !important; + gap: 0.25rem !important; + font-size: 0.75rem !important; + padding: 8px 0 !important; +} + +/* Optional Deps Modal */ + +.cl-optional-deps-modal-title { + margin-block-end: 10px !important; + margin-block-start: 0px !important; + overflow-x: hidden !important; + font-size: 1.5rem !important; + white-space: nowrap !important; +} + +/* +MARK: Store +*/ + + +.cl-store-filter-field-container { + display: flex !important; + flex-direction: column !important; + min-width: 49% !important; +} + + +.cl-store-dropdown-hide-spacer > button > div > div { + width: 100% !important; + display: flex !important; + align-items: start !important; +} + +.cl-store-dropdown-hide-spacer > button > div > .${gamepadDialogClasses.Spacer} { + width: 0 !important; +} + +.cl-store-searchbar { + min-width: 55% !important; +} + +.cl-store-refresh-button { + display: flex !important; + align-items: center !important; + justify-content: center !important; + gap: 0.5rem !important; + max-width: 20% !important; + height: 48% !important; +} + +.cl-store-loadmore-container { + display: flex !important; + justify-content: center !important; + align-items: center !important; + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.cl-store-scale-slider { + min-width: 20% !important; +} + +.cl-store-scale-slider > div > div > .${gamepadDialogClasses.FieldChildrenInner} { + min-width: 100% !important; +} + +.cl-store-theme-grid-container { + display: flex !important; + flex-wrap: wrap !important; + justify-content: center !important; + gap: 5px !important; +} + +/* Store Theme Cards */ +/* The variables should be injected wherever needed */ +/* This module actually is based on font-size, so EM makes sense over REM */ + +/* TODO: For some reason I made half of these classes with dashes and the other half with underscores, standardize it!!! */ +.cl_storeitem_notifbubble { + position: absolute !important; + background: linear-gradient(135deg, #fca904 50%, transparent 51%) !important; + z-index: 10001 !important; + left: 0 !important; + top: 0 !important; + color: black; + font-size: var(--cl-storeitem-fontsize) !important; + width: var(--cl-storeitem-bubblesize) !important; + height: var(--cl-storeitem-bubblesize) !important; +} +.cl_storeitem_bubbleicon { + padding: 0.25em !important; +} +.cl_storeitem_container { + display: flex !important; + flex-direction: column !important; + background-color: #ACB2C924 !important; + overflow: hidden !important; + width: var(--cl-storeitem-width) !important; +} +.gpfocuswithin.cl_storeitem_container { + background-color: #ACB2C947 !important; +} +.cl_storeitem_imagecontainer { + overflow: hidden !important; + position: relative !important; + width: var(--cl-storeitem-width) !important; + height: var(--cl-storeitem-imgheight) !important; +} +.cl_storeitem_supinfocontainer { + display: flex !important; + gap: 0.5em !important; + width: 100% !important; + align-items: center !important; + justify-content: center !important; + position: absolute !important; + bottom: 0 !important; + transform: translateY(100%) !important; + opacity: 0 !important; + transition-property: transform,opacity !important; + transition-timing-function: cubic-bezier(0.17, 0.45, 0.14, 0.83) !important; + transition-duration: 0.15s !important; + font-size: var(--cl-storeitem-fontsize) !important; +} +.gpfocuswithin > div > .cl_storeitem_supinfocontainer { + transform: translateY(0) !important; + opacity: 1 !important; + transition-delay: 0.1s !important; +} +.cl_storeitem_maininfocontainer { + display: flex !important; + flex-direction: column !important; + padding: 0.5em !important; + font-size: var(--cl-storeitem-fontsize) !important; +} +.cl_storeitem_image { + width: 100%; + height: 100%; + object-fit: cover !important; + transition-property: filter,transform !important; + transition-duration: 0.32s !important; + transition-timing-function: cubic-bezier(0.17, 0.45, 0.14, 0.83) !important; +} +.cl_storeitem_imagedarkener { + position: absolute !important; + top: 0 !important; + left: 0 !important; + opacity: 0 !important; + transition-property: opacity !important; + transition-duration: 0.65s !important; + transition-timing-function: cubic-bezier(0.17, 0.45, 0.14, 0.83) !important; + background: linear-gradient(0deg, rgba(0,0,0,.5) 0%, rgba(0,0,0,0) 30%) !important; + mix-blend-mode: multiply !important; + width: var(--cl-storeitem-width) !important; + height: var(--cl-storeitem-imgheight) !important; +} +.gpfocuswithin > div > .cl_storeitem_imagedarkener { + opacity: 1 !important; +} +.cl_storeitem_title { + font-weight: bold !important; + text-overflow: ellipsis !important; + overflow: hidden !important; + white-space: nowrap !important; +} +.cl_storeitem_iconinfoitem { + display: flex !important; + gap: 0.25em !important; + align-items: center !important; +} +.cl_storeitem_subtitle { + font-size: 0.75em !important; +} + +/* +MARK: Expanded View +*/ + + +@keyframes cl_spin { + to { + transform: rotate(360deg); + } +} +.cl_spinny { + animation: cl_spin 1s linear infinite !important; +} + +.cl_expandedview_loadingtext { + font-size: 2.5rem !important; + font-weight: bold !important; +} + +.cl_expandedview_container { + background: rgb(27, 40, 56) !important; + padding: 0 1rem !important; + gap: 1rem !important; + display: flex !important; + justify-content: space-between !important; +} + +.cl_expandedview_scrollpanel { + display: flex !important; + margin-bottom: 40px !important; + height: calc(100vh - 80px) !important +} + +.cl_expandedview_themedatacontainer { + display: flex !important; + flex-direction: column !important; + height: max-content !important; + min-height: 100% !important; + background: rgba(14, 20, 27, 0.8) !important; + width: var(--cl-ev-image-area-width) !important; +} + +.cl_expandedview_imageareacontainer { + display: flex !important; + gap: var(--cl-ev-gap-between-carousel-and-image) !important; + padding: var(--cl-ev-image-area-padding) !important; +} + +.cl_expandedview_imagecarouselcontainer { + display: flex !important; + justify-content: space-around !important; + flex-direction: column !important; + width: var(--cl-ev-image-carousel-entry-width) !important; + height: var(--cl-ev-selected-image-height) !important; +} + +.cl_expandedview_imagecarouselentry { + width: var(--cl-ev-image-carousel-entry-width) !important; + height: var(--cl-ev-image-carousel-entry-height) !important; + position: relative !important; +} + +.cl_expandedview_selectedimage { + position: relative !important; + width: var(--cl-ev-selected-image-width) !important; + height: var(--cl-ev-selected-image-height) !important; +} + +.cl_expandedview_imagenumbercontainer { + width: 3em !important; + height: 2em !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + background: #000a !important; + position: absolute !important; + bottom: 1em !important; + right: 1em !important; +} + +.cl_expandedview_infocontainer { + padding-left: 1rem !important; + padding-right: 1rem !important; + padding-bottom: 1rem !important; + display: flex !important; + flex-direction: column !important; + gap: 0.25rem !important; +} + +.cl_expandedview_title { + white-space: nowrap !important; + text-overflow: ellipsis !important; + overflow: hidden !important; + font-size: 1.5rem !important; + font-weight: bold !important; +} + +.cl_expandedview_version { + font-size: 1.25rem !important; + font-weight: bold !important; +} + +.cl_expandedview_graytext { + color: rgb(124, 142, 163) !important; +} + +.cl_expandedview_bluetext { + color: rgb(26, 159, 255) !important; +} + +.cl_expandedview_targetbuttonscontainer { + display: flex !important; + gap: 0.25rem !important; +} + +.cl_expandedview_targetbutton { + background: rgba(59, 90, 114, 0.5) !important; + color: rgb(26, 159, 255) !important; + padding: 8px 12px !important; + width: fit-content !important; +} + +.cl_expandedview_targetbutton.gpfocuswithin { + background: white !important; + color: black !important; +} + +.cl_expandedview_buttonscontainer { + position: sticky !important; + padding-top: 1rem !important; + flex: 1 !important; + display: flex !important; + flex-direction: column !important; + gap: 0.25em !important; +} + +.cl_expandedview_singlebuttoncontainer { + background: #2a4153 !important; + padding: 1rem !important; +} + +.cl_expandedview_starbutton { + min-width: 30% !important; + padding: 8px 12px !important; + width: fit-content !important; +} + +.cl_expandedview_installtext { + width: 200px !important; + white-space: nowrap !important; + text-overflow: ellipsis !important; + overflow: hidden !important; +} + +.cl_expandedview_bluebutton { + background: #1a9fff !important; +} +.cl_expandedview_bluebutton.gpfocuswithin { + background: white !important; +} + +.cl_expandedview_configure_button { + width: 1rem !important; + min-width: 1rem !important; + position: relative; +} + +/* +MARK: Account Page +*/ + +.cl_accountpage_actionbutton { + max-width: 30% !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + gap: 0.5rem !important; +} + +/* +MARK: Author View Modal +*/ + +.cl_authorview_avatar { + margin-right: 0.25em !important; +} + +.cl_authorview_username { + font-size: 2em; + font-weight: bold; +} + +.cl_authorview_authorcontainer { + display: flex !important; + margin-bottom: 1em !important; + align-items: center !important; +} +.cl_authorview_supportericoncontainer { + margin-left: auto !important; + transform: translateY(2px) !important; +} + +/* +MARK: Theme Settings Modal +*/ + +.cl_squaredialogbutton { + width: fit-content !important; + min-width: fit-content !important; + height: fit-content !important; + padding: 10px 12px !important; +} + +/* +MARK: Settings Page +*/ + +.cl_themesettings_togglecontainer { + flex-grow: 1 !important; + position: relative !important; + display: flex !important; + gap: 0.25rem !important; +} +/* The actual element of the ToggleContainer with the BG */ +.cl_themesettings_togglecontainer > div { + background: rgba(255,255,255,.15); + border-radius: 2px; + padding-left: 5px; + padding-right: 5px; + margin-left: 0; + margin-right: 0; + height: 1.25em !important; + flex: 1 1 0% !important; +} + +.cl_themesettings_themelabel { + white-space: nowrap; + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; +} + +.cl_squaredialogbutton_icontranslate { + transform: translateY(-2px) !important; +} +`; diff --git a/src/styles/styles.css b/src/styles/styles.css new file mode 100644 index 0000000..12b1171 --- /dev/null +++ b/src/styles/styles.css @@ -0,0 +1,657 @@ +/* THIS FILE IS NOT USED IN BUILD */ +/* ANY MODIFICATIONS HERE MUST BE COPY PASTED INTO styles-as-string.ts */ +/* THAT IS NEEDED FOR STATIC CLASS INJECTION */ +/* LINT ERRORS ARE TO BE EXPECTED, BECAUSE THIS USES TEMPLATE LITERALS THAT WILL BE FILLED IN BY styles-as-string.ts */ + +/* +MARK: TAILWIND +*/ + +.flex { + display: flex !important; +} + +.flex-col { + flex-direction: column !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-1 { + flex: 1 1 0% !important; +} + +.grid { + display: grid !important; +} + +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; +} + +.grid-cols-\\[1fr\\,2fr\\] { + grid-template-columns: 1fr 2fr !important; +} + +.gap-1 { + gap: 0.25rem !important; +} + +.gap-2 { + gap: 0.5rem !important; +} + +.gap-3 { + gap: 0.75rem !important; +} + +.gap-4 { + gap: 1rem !important; +} + +.gap-8 { + gap: 2rem !important; +} + +.items-center { + align-items: center !important; +} + +.items-stretch { + align-items: stretch !important; +} + +.justify-center { + justify-content: center !important; +} + +.justify-around { + justify-content: space-around !important; +} + +.justify-between { + justify-content: space-between !important; +} + +.p-0 { + padding: 0 !important; +} + +.pl-4 { + padding-left: 1rem !important; +} + +.m-0 { + margin: 0 !important; +} + +.ml-auto { + margin-left: auto !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.mb-4 { + margin-bottom: 1rem !important; +} + +.mt-4 { + margin-top: 1rem !important; +} + +.w-full { + width: 100% !important; +} + +.max-w-1\/2 { + max-width: 50% !important; +} + +.relative { + position: relative !important; +} + +.font-bold { + font-weight: bold !important; +} + +.text-xs { + font-size: 0.75rem !important; +} + +.text-lg { + font-size: 1.125rem !important; +} + +.text-xl { + font-size: 1.25rem !important; +} + +.text-2xl { + font-size: 1.5rem !important; +} + +.absolute-center { + position: absolute !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; +} + +.cl_title { + font-size: 2rem !important; + font-weight: bold !important; +} + +/* +MARK: Fullscreen Routes +*/ + +.cl_fullscreenroute_container { + margin-top: 40px !important; + height: calc(100% - 40px) !important; + background: #0e141b !important; +} + +/* +MARK: TitleView +*/ + + +.cl-title-view-button { + height: 28px !important; + width: 40px !important; + min-width: 0 !important; + padding: 10px 12px !important; +} + +.cl-title-view-button-icon { + margin-top: -4px !important; + display: block !important; +} + +@keyframes onboardingButton { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + } +} + +.cl-animate-onboarding { + animation: onboardingButton 1s infinite ease-in-out !important; +} + +/* +MARK: QAM Tab +*/ + + +.cl-qam-collapse-button-container > div > div > div > div > button { + height: 10px !important; +} + +.cl-qam-themetoggle-notifbubble { + position: absolute !important; + top: 0 !important; + right: -1rem !important; + background: linear-gradient(45deg, transparent 49%, #fca904 50%) !important; + /* The focus ring has a z-index of 10000, so this is just to be cheeky */ + z-index: 10001 !important; + width: 20px !important; + height: 20px !important; +} + +.cl-qam-collapse-button-down-arrow { + transform: translateY(-13px) !important; + font-size: 1.5rem !important; +} + +.cl-qam-collapse-button-up-arrow { + transform: translateY(-12px) !important; + font-size: 1.5rem !important; +} + +.cl-qam-component-icon-container { + margin-left: auto !important; + width: 24px !important; + height: 24px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +.cl-qam-component-color-preview { + width: 20px !important; + height: 20px !important; +} + +.cl-qam-hidden-themes-display { + display: flex !important; + align-items: center !important; + gap: 0.25rem !important; + font-size: 0.75rem !important; + padding: 8px 0 !important; +} + +/* Optional Deps Modal */ + +.cl-optional-deps-modal-title { + margin-block-end: 10px !important; + margin-block-start: 0px !important; + overflow-x: hidden !important; + font-size: 1.5rem !important; + white-space: nowrap !important; +} + +/* +MARK: Store +*/ + + +.cl-store-filter-field-container { + display: flex !important; + flex-direction: column !important; + min-width: 49% !important; +} + + +.cl-store-dropdown-hide-spacer > button > div > div { + width: 100% !important; + display: flex !important; + align-items: start !important; +} + +.cl-store-dropdown-hide-spacer > button > div > .${gamepadDialogClasses.Spacer} { + width: 0 !important; +} + +.cl-store-searchbar { + min-width: 55% !important; +} + +.cl-store-refresh-button { + display: flex !important; + align-items: center !important; + justify-content: center !important; + gap: 0.5rem !important; + max-width: 20% !important; + height: 48% !important; +} + +.cl-store-loadmore-container { + display: flex !important; + justify-content: center !important; + align-items: center !important; + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.cl-store-scale-slider { + min-width: 20% !important; +} + +.cl-store-scale-slider > div > div > .${gamepadDialogClasses.FieldChildrenInner} { + min-width: 100% !important; +} + +.cl-store-theme-grid-container { + display: flex !important; + flex-wrap: wrap !important; + justify-content: center !important; + gap: 5px !important; +} + +/* Store Theme Cards */ +/* The variables should be injected wherever needed */ +/* This module actually is based on font-size, so EM makes sense over REM */ + +/* TODO: For some reason I made half of these classes with dashes and the other half with underscores, standardize it!!! */ +.cl_storeitem_notifbubble { + position: absolute !important; + background: linear-gradient(135deg, #fca904 50%, transparent 51%) !important; + z-index: 10001 !important; + left: 0 !important; + top: 0 !important; + color: black; + font-size: var(--cl-storeitem-fontsize) !important; + width: var(--cl-storeitem-bubblesize) !important; + height: var(--cl-storeitem-bubblesize) !important; +} +.cl_storeitem_bubbleicon { + padding: 0.25em !important; +} +.cl_storeitem_container { + display: flex !important; + flex-direction: column !important; + background-color: #ACB2C924 !important; + overflow: hidden !important; + width: var(--cl-storeitem-width) !important; +} +.gpfocuswithin.cl_storeitem_container { + background-color: #ACB2C947 !important; +} +.cl_storeitem_imagecontainer { + overflow: hidden !important; + position: relative !important; + width: var(--cl-storeitem-width) !important; + height: var(--cl-storeitem-imgheight) !important; +} +.cl_storeitem_supinfocontainer { + display: flex !important; + gap: 0.5em !important; + width: 100% !important; + align-items: center !important; + justify-content: center !important; + position: absolute !important; + bottom: 0 !important; + transform: translateY(100%) !important; + opacity: 0 !important; + transition-property: transform,opacity !important; + transition-timing-function: cubic-bezier(0.17, 0.45, 0.14, 0.83) !important; + transition-duration: 0.15s !important; + font-size: var(--cl-storeitem-fontsize) !important; +} +.gpfocuswithin > div > .cl_storeitem_supinfocontainer { + transform: translateY(0) !important; + opacity: 1 !important; + transition-delay: 0.1s !important; +} +.cl_storeitem_maininfocontainer { + display: flex !important; + flex-direction: column !important; + padding: 0.5em !important; + font-size: var(--cl-storeitem-fontsize) !important; +} +.cl_storeitem_image { + width: 100%; + height: 100%; + object-fit: cover !important; + transition-property: filter,transform !important; + transition-duration: 0.32s !important; + transition-timing-function: cubic-bezier(0.17, 0.45, 0.14, 0.83) !important; +} +.cl_storeitem_imagedarkener { + position: absolute !important; + top: 0 !important; + left: 0 !important; + opacity: 0 !important; + transition-property: opacity !important; + transition-duration: 0.65s !important; + transition-timing-function: cubic-bezier(0.17, 0.45, 0.14, 0.83) !important; + background: linear-gradient(0deg, rgba(0,0,0,.5) 0%, rgba(0,0,0,0) 30%) !important; + mix-blend-mode: multiply !important; + width: var(--cl-storeitem-width) !important; + height: var(--cl-storeitem-imgheight) !important; +} +.gpfocuswithin > div > .cl_storeitem_imagedarkener { + opacity: 1 !important; +} +.cl_storeitem_title { + font-weight: bold !important; + text-overflow: ellipsis !important; + overflow: hidden !important; + white-space: nowrap !important; +} +.cl_storeitem_iconinfoitem { + display: flex !important; + gap: 0.25em !important; + align-items: center !important; +} +.cl_storeitem_subtitle { + font-size: 0.75em !important; +} + +/* +MARK: Expanded View +*/ + + +@keyframes cl_spin { + to { + transform: rotate(360deg); + } +} +.cl_spinny { + animation: cl_spin 1s linear infinite !important; +} + +.cl_expandedview_loadingtext { + font-size: 2.5rem !important; + font-weight: bold !important; +} + +.cl_expandedview_container { + background: rgb(27, 40, 56) !important; + padding: 0 1rem !important; + gap: 1rem !important; + display: flex !important; + justify-content: space-between !important; +} + +.cl_expandedview_scrollpanel { + display: flex !important; + margin-bottom: 40px !important; + height: calc(100vh - 80px) !important +} + +.cl_expandedview_themedatacontainer { + display: flex !important; + flex-direction: column !important; + height: max-content !important; + min-height: 100% !important; + background: rgba(14, 20, 27, 0.8) !important; + width: var(--cl-ev-image-area-width) !important; +} + +.cl_expandedview_imageareacontainer { + display: flex !important; + gap: var(--cl-ev-gap-between-carousel-and-image) !important; + padding: var(--cl-ev-image-area-padding) !important; +} + +.cl_expandedview_imagecarouselcontainer { + display: flex !important; + justify-content: space-around !important; + flex-direction: column !important; + width: var(--cl-ev-image-carousel-entry-width) !important; + height: var(--cl-ev-selected-image-height) !important; +} + +.cl_expandedview_imagecarouselentry { + width: var(--cl-ev-image-carousel-entry-width) !important; + height: var(--cl-ev-image-carousel-entry-height) !important; + position: relative !important; +} + +.cl_expandedview_selectedimage { + position: relative !important; + width: var(--cl-ev-selected-image-width) !important; + height: var(--cl-ev-selected-image-height) !important; +} + +.cl_expandedview_imagenumbercontainer { + width: 3em !important; + height: 2em !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + background: #000a !important; + position: absolute !important; + bottom: 1em !important; + right: 1em !important; +} + +.cl_expandedview_infocontainer { + padding-left: 1rem !important; + padding-right: 1rem !important; + padding-bottom: 1rem !important; + display: flex !important; + flex-direction: column !important; + gap: 0.25rem !important; +} + +.cl_expandedview_title { + white-space: nowrap !important; + text-overflow: ellipsis !important; + overflow: hidden !important; + font-size: 1.5rem !important; + font-weight: bold !important; +} + +.cl_expandedview_version { + font-size: 1.25rem !important; + font-weight: bold !important; +} + +.cl_expandedview_graytext { + color: rgb(124, 142, 163) !important; +} + +.cl_expandedview_bluetext { + color: rgb(26, 159, 255) !important; +} + +.cl_expandedview_targetbuttonscontainer { + display: flex !important; + gap: 0.25rem !important; +} + +.cl_expandedview_targetbutton { + background: rgba(59, 90, 114, 0.5) !important; + color: rgb(26, 159, 255) !important; + padding: 8px 12px !important; + width: fit-content !important; +} + +.cl_expandedview_targetbutton.gpfocuswithin { + background: white !important; + color: black !important; +} + +.cl_expandedview_buttonscontainer { + position: sticky !important; + padding-top: 1rem !important; + flex: 1 !important; + display: flex !important; + flex-direction: column !important; + gap: 0.25em !important; +} + +.cl_expandedview_singlebuttoncontainer { + background: #2a4153 !important; + padding: 1rem !important; +} + +.cl_expandedview_starbutton { + min-width: 30% !important; + padding: 8px 12px !important; + width: fit-content !important; +} + +.cl_expandedview_installtext { + width: 200px !important; + white-space: nowrap !important; + text-overflow: ellipsis !important; + overflow: hidden !important; +} + +.cl_expandedview_bluebutton { + background: #1a9fff !important; +} +.cl_expandedview_bluebutton.gpfocuswithin { + background: white !important; +} + +.cl_expandedview_configure_button { + width: 1rem !important; + min-width: 1rem !important; + position: relative; +} + +/* +MARK: Account Page +*/ + +.cl_accountpage_actionbutton { + max-width: 30% !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + gap: 0.5rem !important; +} + +/* +MARK: Author View Modal +*/ + +.cl_authorview_avatar { + margin-right: 0.25em !important; +} + +.cl_authorview_username { + font-size: 2em; + font-weight: bold; +} + +.cl_authorview_authorcontainer { + display: flex !important; + margin-bottom: 1em !important; + align-items: center !important; +} +.cl_authorview_supportericoncontainer { + margin-left: auto !important; + transform: translateY(2px) !important; +} + +/* +MARK: Theme Settings Modal +*/ + +.cl_squaredialogbutton { + width: fit-content !important; + min-width: fit-content !important; + height: fit-content !important; + padding: 10px 12px !important; +} + +/* +MARK: Settings Page +*/ + +.cl_themesettings_togglecontainer { + flex-grow: 1 !important; + position: relative !important; + display: flex !important; + gap: 0.25rem !important; +} +/* The actual element of the ToggleContainer with the BG */ +.cl_themesettings_togglecontainer > div { + background: rgba(255,255,255,.15); + border-radius: 2px; + padding-left: 5px; + padding-right: 5px; + margin-left: 0; + margin-right: 0; + height: 1.25em !important; + flex: 1 1 0% !important; +} + +.cl_themesettings_themelabel { + white-space: nowrap; + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; +} + +.cl_squaredialogbutton_icontranslate { + transform: translateY(-2px) !important; +} \ No newline at end of file diff --git a/src/styles/theme-card-styles-generator.ts b/src/styles/theme-card-styles-generator.ts new file mode 100644 index 0000000..f20de58 --- /dev/null +++ b/src/styles/theme-card-styles-generator.ts @@ -0,0 +1,18 @@ +import { ColumnNumbers } from "@/lib"; + +export function themeCardStylesGenerator(size: ColumnNumbers) { + return ` + :root { + --cl-storeitem-width: ${size === 3 ? "260px" : size === 4 ? "195px" : "152px"}; + --cl-storeitem-imgheight: ${ + size === 3 + ? (260 / 16) * 10 + "px" + : size === 4 + ? (195 / 16) * 10 + "px" + : (152 / 16) * 10 + "px" + }; + --cl-storeitem-fontsize: ${size === 3 ? "1em" : size === 4 ? "0.75em" : "0.5em"}; + --cl-storeitem-bubblesize: ${size === 3 ? "40px" : size === 4 ? "30px" : "20px"}; + } + `; +} diff --git a/src/styles/themeSettingsModalStyles.css b/src/styles/themeSettingsModalStyles.css deleted file mode 100644 index 2cffd90..0000000 --- a/src/styles/themeSettingsModalStyles.css +++ /dev/null @@ -1,70 +0,0 @@ -.CSSLoader_ThemeSettingsModal_ToggleParent { - width: 90%; -} - -.CSSLoader_ThemeSettingsModal_Title { - font-weight: bold; - font-size: 2em; -} - -.CSSLoader_ThemeSettingsModal_Subtitle { - font-size: 0.75em; -} - -.CSSLoader_ThemeSettingsModal_Container { - display: flex; - flex-direction: column; - align-items: center; - gap: 1em; - width: 100%; -} - -.CSSLoader_ThemeSettingsModal_ButtonsContainer { - display: flex; - gap: 0.25em; -} - -.CSSLoader_ThemeSettingsModalHeader_DialogButton { - width: fit-content !important; - min-width: fit-content !important; - height: fit-content !important; - padding: 10px 12px !important; -} - -.CSSLoader_ThemeSettingsModal_IconTranslate { - transform: translate(0px, 2px); -} - -.CSSLoader_ThemeSettingsModal_Footer { - display: flex; - width: 100%; - justify-content: space-between; -} - -.CSSLoader_ThemeSettingsModal_TitleContainer { - display: flex; - max-width: 80%; - flex-direction: column; -} - -.CSSLoader_ThemeSettingsModal_Header { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; -} - -.CSSLoader_ThemeSettingsModal_PatchContainer { - display: flex; - width: 100%; - flex-direction: column; -} - -.CSSLoader_ThemeSettingsModal_UpdateButton { - display: flex !important; - gap: 0.25em; -} - -.CSSLoader_ThemeSettingsModal_UpdateText { - font-size: 0.75em; -} diff --git a/src/apiTypes/AccountData.ts b/src/types/AccountData.ts similarity index 74% rename from src/apiTypes/AccountData.ts rename to src/types/AccountData.ts index b7f2a3e..57f11cc 100644 --- a/src/apiTypes/AccountData.ts +++ b/src/types/AccountData.ts @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from "react"; -import { PartialCSSThemeInfo, UserInfo } from "./CSSThemeTypes"; +import { PartialCSSThemeInfo, UserInfo } from "./ThemeQueryTypes"; export enum Permissions { "editAny" = "EditAnyPost", @@ -12,6 +12,15 @@ export interface AccountData extends UserInfo { permissions: Permissions[]; } +export type PremiumTiers = "None" | "Tier1" | "Tier2" | "Tier3"; + +export interface FullAccountData extends AccountData { + hasDeckKey: boolean; + premiumTier: PremiumTiers; + email: string; + lastLoginDate: string; +} + export interface AuthContextContents { accountInfo: AccountData | undefined; setAccountInfo: diff --git a/src/apiTypes/BlobTypes.ts b/src/types/BlobTypes.ts similarity index 100% rename from src/apiTypes/BlobTypes.ts rename to src/types/BlobTypes.ts diff --git a/src/apiTypes/Motd.ts b/src/types/Motd.ts similarity index 100% rename from src/apiTypes/Motd.ts rename to src/types/Motd.ts diff --git a/src/types/SubmissionTypes.ts b/src/types/SubmissionTypes.ts new file mode 100644 index 0000000..d69f945 --- /dev/null +++ b/src/types/SubmissionTypes.ts @@ -0,0 +1,16 @@ +export interface TaskQueryResponse { + id: string; + name: string; + status: string; + completed: Date; + started: Date; + success: boolean; +} + +export interface ZipSubmitRequest { + blob: string; + description: string; + privateSubmission: boolean; + imageBlobs: string[]; + target?: string; +} diff --git a/src/apiTypes/CSSThemeTypes.ts b/src/types/ThemeQueryTypes.ts similarity index 100% rename from src/apiTypes/CSSThemeTypes.ts rename to src/types/ThemeQueryTypes.ts diff --git a/src/ThemeTypes.ts b/src/types/ThemeTypes.ts similarity index 93% rename from src/ThemeTypes.ts rename to src/types/ThemeTypes.ts index cbab186..cb92410 100644 --- a/src/ThemeTypes.ts +++ b/src/types/ThemeTypes.ts @@ -1,4 +1,4 @@ -import { MinimalCSSThemeInfo } from "./apiTypes"; +import { MinimalCSSThemeInfo } from "./ThemeQueryTypes"; export interface Theme { id: string; @@ -37,7 +37,7 @@ export enum Flags { "navPatch" = "REQUIRE_NAV_PATCH", } -export type LocalThemeStatus = "installed" | "outdated" | "local"; +export type LocalThemeStatus = "installed" | "outdated" | "local" | "notinstalled"; export type UpdateStatus = [string, LocalThemeStatus, false | MinimalCSSThemeInfo]; type ThemeErrorTitle = string; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..e3b8aaa --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,6 @@ +export * from "./ThemeQueryTypes"; +export * from "./AccountData"; +export * from "./BlobTypes"; +export * from "./Motd"; +export * from "./SubmissionTypes"; +export * from "./ThemeTypes"; diff --git a/tsconfig.json b/tsconfig.json index ea2f05c..97d3ad2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,9 +15,16 @@ "noImplicitThis": true, "noImplicitAny": true, "strict": true, - "suppressImplicitAnyIndexErrors": true, "allowSyntheticDefaultImports": true, - "skipLibCheck": true + "paths": { + "@cssloader/backend": ["./src/backend"], + "@/backend": ["./src/backend-impl"], + "@/lib": ["./src/lib"], + "@/styles": ["./src/styles"], + "@/types": ["./src/types"], + "@/modules/*": ["./src/modules/*"], + "@/decky-patches": ["./src/decky-patches"] + } }, "include": ["src"], "exclude": ["node_modules"]