diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 3a25324..ffdb6e8 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -31,12 +31,16 @@ jobs: echo "tag=main" >> $GITHUB_OUTPUT echo "version=1.1.0" >> $GITHUB_OUTPUT echo "channel=stable" >> $GITHUB_OUTPUT + elif [[ "${{ github.ref_name }}" == "feature/nightly" ]] || [[ "${{ github.ref_name }}" == "nightly" ]]; then + echo "tag=nightly" >> $GITHUB_OUTPUT + echo "version=1.1.0-nightly" >> $GITHUB_OUTPUT + echo "channel=nightly" >> $GITHUB_OUTPUT else # Ersetze Slashes durch Bindestriche für gültige Docker Tags CLEAN_TAG=$(echo "${{ github.ref_name }}" | sed 's/\//-/g') echo "tag=$CLEAN_TAG" >> $GITHUB_OUTPUT - echo "version=1.1.0-nightly" >> $GITHUB_OUTPUT - echo "channel=nightly" >> $GITHUB_OUTPUT + echo "version=1.1.0-dev" >> $GITHUB_OUTPUT + echo "channel=dev" >> $GITHUB_OUTPUT fi # Nur auf main: auch :latest tag pushen diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b034943..e43cb38 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,6 +5,9 @@ variables: IMAGE_NAME: $DOCKERHUB_USERNAME/discordsoundbot-vib DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" + # Force clone via IP instead of hostname to bypass Unraid Docker DNS issues + CI_SERVER_URL: "http://10.10.10.10:9080" + GITLAB_FEATURES: "" docker-build: stage: build @@ -31,9 +34,9 @@ docker-build: export VERSION="1.1.0-dev" export CHANNEL="dev" fi - + - echo "Building for channel $CHANNEL with version $VERSION and tag $TAG" - + # Build - docker pull $IMAGE_NAME:$TAG || true - > diff --git a/Dockerfile b/Dockerfile index 91f8191..b99af26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,8 +21,8 @@ COPY server/package*.json ./ RUN npm install --no-audit --no-fund COPY server/ . RUN npm run build -# Nur Prod-Dependencies für Runtime behalten -RUN npm prune --omit=dev +# Nur Prod-Dependencies für Runtime behalten. rm -rf and cleanly install to prevent npm prune bugs +RUN rm -rf node_modules && npm install --omit=dev --no-audit --no-fund # --- Runtime image --- FROM node:20-slim AS runtime diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..62466b2 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,2384 @@ +{ + "name": "discord-soundboard-server", + "version": "1.1.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "discord-soundboard-server", + "version": "1.1.1", + "dependencies": { + "@discordjs/opus": "^0.9.0", + "@discordjs/voice": "^0.18.0", + "cors": "^2.8.5", + "discord.js": "^14.16.3", + "express": "^4.19.2", + "libsodium-wrappers": "^0.8.2", + "multer": "^2.0.0", + "sodium-native": "^4.0.8", + "tweetnacl": "^1.0.3", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^20.12.12", + "ts-node": "^10.9.2", + "typescript": "^5.5.4" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz", + "integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.33", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.38.40", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.40.tgz", + "integrity": "sha512-P/His8cotqZgQqrt+hzrocp9L8RhQQz1GkrCnC9TMJ8Uw2q0tg8YyqJyGULxhXn/8kxHETN4IppmOv+P2m82lQ==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/formatters/node_modules/discord-api-types": { + "version": "0.38.40", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.40.tgz", + "integrity": "sha512-P/His8cotqZgQqrt+hzrocp9L8RhQQz1GkrCnC9TMJ8Uw2q0tg8YyqJyGULxhXn/8kxHETN4IppmOv+P2m82lQ==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/node-pre-gyp": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@discordjs/node-pre-gyp/-/node-pre-gyp-0.4.5.tgz", + "integrity": "sha512-YJOVVZ545x24mHzANfYoy0BJX5PDyeZlpiJjDkUBM/V/Ao7TFX9lcUvCN4nr0tbr5ubeaXxtEBILUrHtTphVeQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@discordjs/opus": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@discordjs/opus/-/opus-0.9.0.tgz", + "integrity": "sha512-NEE76A96FtQ5YuoAVlOlB3ryMPrkXbUCTQICHGKb8ShtjXyubGicjRMouHtP1RpuDdm16cDa+oI3aAMo1zQRUQ==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@discordjs/node-pre-gyp": "^0.4.5", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/discord-api-types": { + "version": "0.38.40", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.40.tgz", + "integrity": "sha512-P/His8cotqZgQqrt+hzrocp9L8RhQQz1GkrCnC9TMJ8Uw2q0tg8YyqJyGULxhXn/8kxHETN4IppmOv+P2m82lQ==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util/node_modules/discord-api-types": { + "version": "0.38.40", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.40.tgz", + "integrity": "sha512-P/His8cotqZgQqrt+hzrocp9L8RhQQz1GkrCnC9TMJ8Uw2q0tg8YyqJyGULxhXn/8kxHETN4IppmOv+P2m82lQ==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/voice": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.18.0.tgz", + "integrity": "sha512-BvX6+VJE5/vhD9azV9vrZEt9hL1G+GlOdsQaVl5iv9n87fkXjf3cSwllhR3GdaUC8m6dqT8umXIWtn3yCu4afg==", + "license": "Apache-2.0", + "dependencies": { + "@types/ws": "^8.5.12", + "discord-api-types": "^0.37.103", + "prism-media": "^1.3.5", + "tslib": "^2.6.3", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/discord-api-types": { + "version": "0.38.40", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.40.tgz", + "integrity": "sha512-P/His8cotqZgQqrt+hzrocp9L8RhQQz1GkrCnC9TMJ8Uw2q0tg8YyqJyGULxhXn/8kxHETN4IppmOv+P2m82lQ==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.34.tgz", + "integrity": "sha512-by3/Z0Qp+L9cAySEsSNNwZ6WWw8ywgGLPQGgbQDhNRSitqYgkgp4pErd23ZSCavbtUA2CN4jQtoB3T8nk4j3Rg==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-addon-resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz", + "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==", + "license": "Apache-2.0", + "dependencies": { + "bare-module-resolve": "^1.10.0", + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-module-resolve": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz", + "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==", + "license": "Apache-2.0", + "dependencies": { + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-semver": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz", + "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==", + "license": "Apache-2.0" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.120", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.120.tgz", + "integrity": "sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==", + "license": "MIT" + }, + "node_modules/discord.js": { + "version": "14.25.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz", + "integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.2.0", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.33", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/discord.js/node_modules/discord-api-types": { + "version": "0.38.40", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.40.tgz", + "integrity": "sha512-P/His8cotqZgQqrt+hzrocp9L8RhQQz1GkrCnC9TMJ8Uw2q0tg8YyqJyGULxhXn/8kxHETN4IppmOv+P2m82lQ==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "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.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/libsodium": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.8.2.tgz", + "integrity": "sha512-TsnGYMoZtpweT+kR+lOv5TVsnJ/9U0FZOsLFzFOMWmxqOAYXjX3fsrPAW+i1LthgDKXJnI9A8dWEanT1tnJKIw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.8.2.tgz", + "integrity": "sha512-VFLmfxkxo+U9q60tjcnSomQBRx2UzlRjKWJqvB4K1pUqsMQg4cu3QXA2nrcsj9A1qRsnJBbi2Ozx1hsiDoCkhw==", + "license": "ISC", + "dependencies": { + "libsodium": "^0.8.0" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/magic-bytes.js": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", + "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==", + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/prism-media": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz", + "integrity": "sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==", + "license": "Apache-2.0", + "peerDependencies": { + "@discordjs/opus": ">=0.8.0 <1.0.0", + "ffmpeg-static": "^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0", + "node-opus": "^0.3.3", + "opusscript": "^0.0.8" + }, + "peerDependenciesMeta": { + "@discordjs/opus": { + "optional": true + }, + "ffmpeg-static": { + "optional": true + }, + "node-opus": { + "optional": true + }, + "opusscript": { + "optional": true + } + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-addon": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", + "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==", + "license": "Apache-2.0", + "dependencies": { + "bare-addon-resolve": "^1.3.0" + }, + "engines": { + "bare": ">=1.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sodium-native": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", + "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==", + "license": "MIT", + "dependencies": { + "require-addon": "^1.1.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/server/package.json b/server/package.json index 9066851..e02373e 100644 --- a/server/package.json +++ b/server/package.json @@ -10,24 +10,23 @@ "start": "node dist/index.js" }, "dependencies": { - "@discordjs/voice": "^0.18.0", "@discordjs/opus": "^0.9.0", + "@discordjs/voice": "^0.18.0", + "cors": "^2.8.5", "discord.js": "^14.16.3", "express": "^4.19.2", - "libsodium-wrappers": "^0.7.13", - "tweetnacl": "^1.0.3", - "sodium-native": "^4.0.8", - "cors": "^2.8.5", + "libsodium-wrappers": "^0.8.2", "multer": "^2.0.0", + "sodium-native": "^4.0.8", + "tweetnacl": "^1.0.3", "ws": "^8.18.0" }, "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/multer": "^1.4.12", "@types/node": "^20.12.12", "ts-node": "^10.9.2", "typescript": "^5.5.4" } } - - diff --git a/server/src/index.ts b/server/src/index.ts index e962928..2016f8a 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import express, { Request, Response } from 'express'; -// import multer from 'multer'; +import multer from 'multer'; import cors from 'cors'; import crypto from 'node:crypto'; import { Client, GatewayIntentBits, Partials, ChannelType, Events, type Message, VoiceState } from 'discord.js'; @@ -24,6 +24,7 @@ import sodium from 'libsodium-wrappers'; import nacl from 'tweetnacl'; // Streaming externer Plattformen entfernt – nur MP3-URLs werden noch unterstützt import child_process from 'node:child_process'; +import { PassThrough } from 'node:stream'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -116,16 +117,34 @@ function writePersistedState(state: PersistedState): void { } const persistedState: PersistedState = readPersistedState(); + +// Debounced write: sammelt Schreibaufrufe und schreibt max. alle 2 Sekunden +let _writeTimer: ReturnType | null = null; +function writePersistedStateDebounced(): void { + if (_writeTimer) return; + _writeTimer = setTimeout(() => { + _writeTimer = null; + writePersistedState(persistedState); + }, 2000); +} + const getPersistedVolume = (guildId: string): number => { const v = persistedState.volumes[guildId]; return typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 1; }; +/** Sicherstellen, dass ein relativer Pfad nicht aus SOUNDS_DIR ausbricht */ +function safeSoundsPath(rel: string): string | null { + const resolved = path.resolve(SOUNDS_DIR, rel); + if (!resolved.startsWith(path.resolve(SOUNDS_DIR) + path.sep) && resolved !== path.resolve(SOUNDS_DIR)) return null; + return resolved; +} + function incrementPlaysFor(relativePath: string) { try { const key = relativePath.replace(/\\/g, '/'); persistedState.plays[key] = (persistedState.plays[key] ?? 0) + 1; persistedState.totalPlays = (persistedState.totalPlays ?? 0) + 1; - writePersistedState(persistedState); + writePersistedStateDebounced(); // Debounced: Play-Counter sind nicht kritisch } catch {} } @@ -135,6 +154,112 @@ const NORMALIZE_I = String(process.env.NORMALIZE_I ?? '-16'); const NORMALIZE_LRA = String(process.env.NORMALIZE_LRA ?? '11'); const NORMALIZE_TP = String(process.env.NORMALIZE_TP ?? '-1.5'); +// Loudnorm-Cache: normalisierte PCM-Dateien werden gecacht, damit ffmpeg nur 1× pro Sound läuft +const NORM_CACHE_DIR = path.join(SOUNDS_DIR, '.norm-cache'); +fs.mkdirSync(NORM_CACHE_DIR, { recursive: true }); + +/** Erzeugt einen Cache-Key aus dem relativen Pfad (Slash-normalisiert, Sonderzeichen escaped) */ +function normCacheKey(filePath: string): string { + const rel = path.relative(SOUNDS_DIR, filePath).replace(/\\/g, '/'); + return rel.replace(/[/\\:*?"<>|]/g, '_') + '.pcm'; +} + +/** Liefert den Pfad zur gecachten normalisierten PCM-Datei, oder null wenn kein gültiger Cache existiert */ +function getNormCachePath(filePath: string): string | null { + const cacheFile = path.join(NORM_CACHE_DIR, normCacheKey(filePath)); + if (!fs.existsSync(cacheFile)) return null; + // Invalidieren wenn Quelldatei neuer als Cache + try { + const srcMtime = fs.statSync(filePath).mtimeMs; + const cacheMtime = fs.statSync(cacheFile).mtimeMs; + if (srcMtime > cacheMtime) { try { fs.unlinkSync(cacheFile); } catch {} return null; } + } catch { return null; } + return cacheFile; +} + +/** Normalisiert eine Audiodatei via ffmpeg loudnorm und speichert das Ergebnis im Cache */ +function normalizeToCache(filePath: string): Promise { + const cacheFile = path.join(NORM_CACHE_DIR, normCacheKey(filePath)); + return new Promise((resolve, reject) => { + const ffArgs = ['-hide_banner', '-loglevel', 'error', '-y', '-i', filePath, + '-af', `loudnorm=I=${NORMALIZE_I}:LRA=${NORMALIZE_LRA}:TP=${NORMALIZE_TP}`, + '-f', 's16le', '-ar', '48000', '-ac', '2', cacheFile]; + const ff = child_process.spawn('ffmpeg', ffArgs); + ff.on('error', reject); + ff.on('close', (code) => { + if (code === 0) resolve(cacheFile); + else reject(new Error(`ffmpeg exited with code ${code}`)); + }); + }); +} + +// Wie viele ffmpeg-Prozesse parallel beim Cache-Sync laufen dürfen. +// Standard: 2 (konservativ – lässt genug Headroom für Discord-Wiedergabe und Node.js). +// Empfehlung: i5/i7 (8-16 Kerne) → 2-3, Ryzen 9 / Xeon → 4-8 +// Über NORM_CONCURRENCY=4 env var erhöhbar. +const NORM_CONCURRENCY = Math.max(1, Number(process.env.NORM_CONCURRENCY ?? 2)); + +/** + * Vollständige Cache-Synchronisation: + * 1. Alle Sound-Dateien normalisieren, die noch nicht im Cache sind (parallel) + * 2. Verwaiste Cache-Dateien löschen (Sound wurde gelöscht/umbenannt) + * Läuft im Hintergrund, blockiert nicht den Server. + */ +async function syncNormCache(): Promise { + if (!NORMALIZE_ENABLE) return; + const t0 = Date.now(); + const allSounds = listAllSounds(); + + // Set aller erwarteten Cache-Keys + const expectedKeys = new Set(); + const toProcess: string[] = []; + + for (const s of allSounds) { + const fp = path.join(SOUNDS_DIR, s.relativePath); + const key = normCacheKey(fp); + expectedKeys.add(key); + if (!fs.existsSync(fp)) continue; + if (getNormCachePath(fp)) continue; // bereits gecacht & gültig + toProcess.push(fp); + } + + let created = 0; + let failed = 0; + const skipped = allSounds.length - toProcess.length; + + // Parallele Worker-Pool: NORM_CONCURRENCY ffmpeg-Prozesse gleichzeitig + const queue = [...toProcess]; + async function worker(): Promise { + while (queue.length > 0) { + const fp = queue.shift()!; + try { + await normalizeToCache(fp); + created++; + } catch (e) { + failed++; + console.warn(`Norm-cache failed: ${path.relative(SOUNDS_DIR, fp)}`, e); + } + } + } + const workers = Array.from({ length: Math.min(NORM_CONCURRENCY, toProcess.length || 1) }, worker); + await Promise.all(workers); + + // Verwaiste Cache-Dateien aufräumen + let cleaned = 0; + try { + for (const f of fs.readdirSync(NORM_CACHE_DIR)) { + if (!expectedKeys.has(f)) { + try { fs.unlinkSync(path.join(NORM_CACHE_DIR, f)); cleaned++; } catch {} + } + } + } catch {} + + const dt = ((Date.now() - t0) / 1000).toFixed(1); + console.log( + `Norm-Cache sync (${dt}s, ${NORM_CONCURRENCY} parallel): ${created} neu, ${skipped} vorhanden, ${failed} fehlgeschlagen, ${cleaned} verwaist entfernt (${allSounds.length} Sounds gesamt)` + ); +} + // --- Voice Abhängigkeiten prüfen --- await sodium.ready; // init nacl to ensure it loads @@ -164,6 +289,8 @@ const guildAudioState = new Map(); // Partymode: serverseitige Steuerung (global pro Guild) const partyTimers = new Map(); const partyActive = new Set(); +// Now-Playing: aktuell gespielter Sound pro Guild +const nowPlaying = new Map(); // SSE-Klienten für Broadcasts (z.B. Partymode Status) const sseClients = new Set(); function sseBroadcast(payload: any) { @@ -252,11 +379,36 @@ async function playFilePath(guildId: string, channelId: string, filePath: string : (state.currentVolume ?? 1); let resource: AudioResource; if (NORMALIZE_ENABLE) { - const ffArgs = ['-hide_banner', '-loglevel', 'error', '-i', filePath, - '-af', `loudnorm=I=${NORMALIZE_I}:LRA=${NORMALIZE_LRA}:TP=${NORMALIZE_TP}`, - '-f', 's16le', '-ar', '48000', '-ac', '2', 'pipe:1']; - const ff = child_process.spawn('ffmpeg', ffArgs); - resource = createAudioResource(ff.stdout as any, { inlineVolume: true, inputType: StreamType.Raw }); + const cachedPath = getNormCachePath(filePath); + if (cachedPath) { + // Cache-Hit: gecachte PCM-Datei als Stream lesen (kein ffmpeg, instant) + const pcmStream = fs.createReadStream(cachedPath); + resource = createAudioResource(pcmStream, { inlineVolume: true, inputType: StreamType.Raw }); + } else { + // Cache-Miss: ffmpeg streamen (sofortige Wiedergabe) UND gleichzeitig in Cache schreiben + const cacheFile = path.join(NORM_CACHE_DIR, normCacheKey(filePath)); + const ffArgs = ['-hide_banner', '-loglevel', 'error', '-i', filePath, + '-af', `loudnorm=I=${NORMALIZE_I}:LRA=${NORMALIZE_LRA}:TP=${NORMALIZE_TP}`, + '-f', 's16le', '-ar', '48000', '-ac', '2', 'pipe:1']; + const ff = child_process.spawn('ffmpeg', ffArgs); + // Tee: Daten gleichzeitig an Player und Cache-Datei + const playerStream = new PassThrough(); + const cacheWrite = fs.createWriteStream(cacheFile); + ff.stdout.on('data', (chunk: Buffer) => { + playerStream.write(chunk); + cacheWrite.write(chunk); + }); + ff.stdout.on('end', () => { + playerStream.end(); + cacheWrite.end(); + console.log(`${new Date().toISOString()} | Loudnorm cached: ${path.basename(filePath)}`); + }); + ff.on('error', () => { try { fs.unlinkSync(cacheFile); } catch {} }); + ff.on('close', (code) => { + if (code !== 0) { try { fs.unlinkSync(cacheFile); } catch {} } + }); + resource = createAudioResource(playerStream, { inlineVolume: true, inputType: StreamType.Raw }); + } } else { resource = createAudioResource(filePath, { inlineVolume: true }); } @@ -265,6 +417,10 @@ async function playFilePath(guildId: string, channelId: string, filePath: string state.player.play(resource); state.currentResource = resource; state.currentVolume = useVolume; + // Now-Playing broadcast + const soundLabel = relativeKey ? path.parse(relativeKey).name : path.parse(filePath).name; + nowPlaying.set(guildId, soundLabel); + sseBroadcast({ type: 'nowplaying', guildId, name: soundLabel }); if (relativeKey) incrementPlaysFor(relativeKey); } @@ -533,6 +689,10 @@ client.on(Events.MessageCreate, async (message: Message) => { if (!res.ok) throw new Error(`Download fehlgeschlagen: ${attachment.url}`); const arrayBuffer = await res.arrayBuffer(); fs.writeFileSync(targetPath, Buffer.from(arrayBuffer)); + // Sofort normalisieren für instant Play + if (NORMALIZE_ENABLE) { + normalizeToCache(targetPath).catch((e) => console.warn('Norm after upload failed:', e)); + } await message.author.send?.(`Sound gespeichert: ${path.basename(targetPath)}`); } } catch (err) { @@ -551,6 +711,83 @@ app.get('/api/health', (_req: Request, res: Response) => { res.json({ ok: true, totalPlays: persistedState.totalPlays ?? 0, categories: (persistedState.categories ?? []).length }); }); +type ListedSound = { + fileName: string; + name: string; + folder: string; + relativePath: string; +}; + +function listAllSounds(): ListedSound[] { + const rootEntries = fs.readdirSync(SOUNDS_DIR, { withFileTypes: true }); + const rootFiles: ListedSound[] = rootEntries + .filter((d) => { + if (!d.isFile()) return false; + const n = d.name.toLowerCase(); + return n.endsWith('.mp3') || n.endsWith('.wav'); + }) + .map((d) => ({ + fileName: d.name, + name: path.parse(d.name).name, + folder: '', + relativePath: d.name, + })); + + const folderItems: ListedSound[] = []; + const subFolders = rootEntries.filter((d) => d.isDirectory() && d.name !== '.norm-cache'); + for (const dirent of subFolders) { + const folderName = dirent.name; + const folderPath = path.join(SOUNDS_DIR, folderName); + const entries = fs.readdirSync(folderPath, { withFileTypes: true }); + for (const e of entries) { + if (!e.isFile()) continue; + const n = e.name.toLowerCase(); + if (!(n.endsWith('.mp3') || n.endsWith('.wav'))) continue; + folderItems.push({ + fileName: e.name, + name: path.parse(e.name).name, + folder: folderName, + relativePath: path.join(folderName, e.name), + }); + } + } + + return [...rootFiles, ...folderItems].sort((a, b) => a.name.localeCompare(b.name)); +} + +app.get('/api/analytics', (_req: Request, res: Response) => { + try { + const allItems = listAllSounds(); + const byKey = new Map(); + for (const it of allItems) { + byKey.set(it.relativePath, it); + if (!byKey.has(it.fileName)) byKey.set(it.fileName, it); + } + + const mostPlayed = Object.entries(persistedState.plays ?? {}) + .map(([rel, count]) => { + const item = byKey.get(rel); + if (!item) return null; + return { + name: item.name, + relativePath: item.relativePath, + count: Number(count) || 0, + }; + }) + .filter((x): x is { name: string; relativePath: string; count: number } => !!x) + .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)) + .slice(0, 10); + + res.json({ + totalSounds: allItems.length, + totalPlays: persistedState.totalPlays ?? 0, + mostPlayed, + }); + } catch (e: any) { + res.status(500).json({ error: e?.message ?? 'Analytics konnten nicht geladen werden' }); + } +}); + // --- Admin Auth --- type AdminPayload = { iat: number; exp: number }; function b64url(input: Buffer | string): string { @@ -616,40 +853,17 @@ app.get('/api/sounds', (req: Request, res: Response) => { const fuzzyParam = String((req.query as any).fuzzy ?? '0'); const useFuzzy = fuzzyParam === '1' || fuzzyParam === 'true'; - const rootEntries = fs.readdirSync(SOUNDS_DIR, { withFileTypes: true }); - const rootFiles = rootEntries - .filter((d) => { - if (!d.isFile()) return false; - const n = d.name.toLowerCase(); - return n.endsWith('.mp3') || n.endsWith('.wav'); - }) - .map((d) => ({ fileName: d.name, name: path.parse(d.name).name, folder: '', relativePath: d.name })); + const allItems = listAllSounds(); - const folders: Array<{ key: string; name: string; count: number }> = []; - - const subFolders = rootEntries.filter((d) => d.isDirectory()); - const folderItems: Array<{ fileName: string; name: string; folder: string; relativePath: string }> = []; - for (const dirent of subFolders) { - const folderName = dirent.name; - const folderPath = path.join(SOUNDS_DIR, folderName); - const entries = fs.readdirSync(folderPath, { withFileTypes: true }); - const audios = entries.filter((e) => { - if (!e.isFile()) return false; - const n = e.name.toLowerCase(); - return n.endsWith('.mp3') || n.endsWith('.wav'); - }); - for (const f of audios) { - folderItems.push({ - fileName: f.name, - name: path.parse(f.name).name, - folder: folderName, - relativePath: path.join(folderName, f.name) - }); - } - folders.push({ key: folderName, name: folderName, count: audios.length }); + // Ordner-Statistik aus allItems ableiten + const folderCounts = new Map(); + for (const it of allItems) { + if (it.folder) folderCounts.set(it.folder, (folderCounts.get(it.folder) ?? 0) + 1); + } + const folders: Array<{ key: string; name: string; count: number }> = []; + for (const [key, count] of folderCounts) { + folders.push({ key, name: key, count }); } - - const allItems = [...rootFiles, ...folderItems].sort((a, b) => a.name.localeCompare(b.name)); // Zeitstempel für Neu-Logik type ItemWithTime = { fileName: string; name: string; folder: string; relativePath: string; mtimeMs: number }; @@ -773,10 +987,13 @@ app.post('/api/admin/sounds/delete', requireAdmin, (req: Request, res: Response) if (!Array.isArray(paths) || paths.length === 0) return res.status(400).json({ error: 'paths[] erforderlich' }); const results: Array<{ path: string; ok: boolean; error?: string }> = []; for (const rel of paths) { - const full = path.join(SOUNDS_DIR, rel); + const full = safeSoundsPath(rel); + if (!full) { results.push({ path: rel, ok: false, error: 'Ungültiger Pfad' }); continue; } try { if (fs.existsSync(full) && fs.statSync(full).isFile()) { fs.unlinkSync(full); + // Loudnorm-Cache aufräumen + try { const cf = path.join(NORM_CACHE_DIR, normCacheKey(full)); if (fs.existsSync(cf)) fs.unlinkSync(cf); } catch {} results.push({ path: rel, ok: true }); } else { results.push({ path: rel, ok: false, error: 'nicht gefunden' }); @@ -792,23 +1009,69 @@ app.post('/api/admin/sounds/delete', requireAdmin, (req: Request, res: Response) app.post('/api/admin/sounds/rename', requireAdmin, (req: Request, res: Response) => { const { from, to } = req.body as { from?: string; to?: string }; if (!from || !to) return res.status(400).json({ error: 'from und to erforderlich' }); - const src = path.join(SOUNDS_DIR, from); - // Ziel nur Name ändern, Endung mp3 sicherstellen + const src = safeSoundsPath(from); + if (!src) return res.status(400).json({ error: 'Ungültiger Quellpfad' }); const parsed = path.parse(from); // UTF-8 Zeichen erlauben: Leerzeichen, Umlaute, etc. - nur problematische Zeichen filtern const sanitizedName = to.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_'); const dstRel = path.join(parsed.dir || '', `${sanitizedName}.mp3`); - const dst = path.join(SOUNDS_DIR, dstRel); + const dst = safeSoundsPath(dstRel); + if (!dst) return res.status(400).json({ error: 'Ungültiger Zielpfad' }); try { if (!fs.existsSync(src)) return res.status(404).json({ error: 'Quelle nicht gefunden' }); if (fs.existsSync(dst)) return res.status(409).json({ error: 'Ziel existiert bereits' }); fs.renameSync(src, dst); + // Loudnorm-Cache für alte Datei aufräumen (neue wird beim nächsten Play erzeugt) + try { const cf = path.join(NORM_CACHE_DIR, normCacheKey(src)); if (fs.existsSync(cf)) fs.unlinkSync(cf); } catch {} res.json({ ok: true, from, to: dstRel }); } catch (e: any) { res.status(500).json({ error: e?.message ?? 'Rename fehlgeschlagen' }); } }); +// --- Datei-Upload (Drag & Drop) --- +type MulterFile = { fieldname: string; originalname: string; encoding: string; mimetype: string; destination: string; filename: string; path: string; size: number; }; + +const uploadStorage = multer.diskStorage({ + destination: (_req: any, _file: any, cb: (e: null, dest: string) => void) => cb(null, SOUNDS_DIR), + filename: (_req: any, file: { originalname: string }, cb: (e: null, name: string) => void) => { + const safe = file.originalname.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_'); + const { name, ext } = path.parse(safe); + let finalName = safe; + let i = 2; + while (fs.existsSync(path.join(SOUNDS_DIR, finalName))) { + finalName = `${name}-${i}${ext}`; + i++; + } + cb(null, finalName); + }, +}); +const uploadMulter = multer({ + storage: uploadStorage, + fileFilter: (_req: any, file: { originalname: string }, cb: (e: null, ok: boolean) => void) => { + const ext = path.extname(file.originalname).toLowerCase(); + cb(null, ext === '.mp3' || ext === '.wav'); + }, + limits: { fileSize: 50 * 1024 * 1024, files: 20 }, +}); + +app.post('/api/upload', requireAdmin, (req: Request, res: Response) => { + uploadMulter.array('files', 20)(req, res, async (err: any) => { + if (err) return res.status(400).json({ error: err?.message ?? 'Upload fehlgeschlagen' }); + const files = (req as any).files as MulterFile[] | undefined; + if (!files?.length) return res.status(400).json({ error: 'Keine gültigen Dateien (nur MP3/WAV)' }); + const saved = files.map(f => ({ name: f.filename, size: f.size })); + // Normalisierung im Hintergrund starten + if (NORMALIZE_ENABLE) { + for (const f of files) { + normalizeToCache(f.path).catch(e => console.warn(`Norm after upload failed (${f.filename}):`, e)); + } + } + console.log(`${new Date().toISOString()} | Upload: ${files.map(f => f.filename).join(', ')}`); + res.json({ ok: true, files: saved }); + }); +}); + // --- Kategorien API --- app.get('/api/categories', (_req: Request, res: Response) => { res.json({ categories: persistedState.categories ?? [] }); @@ -968,7 +1231,6 @@ app.post('/api/play', async (req: Request, res: Response) => { let filePath: string; if (relativePath) filePath = path.join(SOUNDS_DIR, relativePath); else if (folder) { - // Bevorzugt .mp3, fallback .wav const mp3 = path.join(SOUNDS_DIR, folder, `${soundName}.mp3`); const wav = path.join(SOUNDS_DIR, folder, `${soundName}.wav`); filePath = fs.existsSync(mp3) ? mp3 : wav; @@ -979,109 +1241,9 @@ app.post('/api/play', async (req: Request, res: Response) => { } if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Sound nicht gefunden' }); - const guild = client.guilds.cache.get(guildId); - if (!guild) return res.status(404).json({ error: 'Guild nicht gefunden' }); - const channel = guild.channels.cache.get(channelId); - if (!channel || (channel.type !== ChannelType.GuildVoice && channel.type !== ChannelType.GuildStageVoice)) { - return res.status(400).json({ error: 'Ungültiger Voice-Channel' }); - } - - let state = guildAudioState.get(guildId); - if (!state) { - const connection = joinVoiceChannel({ - channelId, - guildId, - adapterCreator: guild.voiceAdapterCreator as any, - selfMute: false, - selfDeaf: false - }); - const player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); - connection.subscribe(player); - state = { connection, player, guildId, channelId, currentVolume: getPersistedVolume(guildId) }; - guildAudioState.set(guildId, state); - - // Connection State Logs - connection.on('stateChange', (oldState, newState) => { - console.log(`${new Date().toISOString()} | VoiceConnection: ${oldState.status} -> ${newState.status}`); - }); - player.on('stateChange', (oldState, newState) => { - console.log(`${new Date().toISOString()} | AudioPlayer: ${oldState.status} -> ${newState.status}`); - }); - player.on('error', (err) => { - console.error(`${new Date().toISOString()} | AudioPlayer error:`, err); - }); - - state.connection = await ensureConnectionReady(connection, channelId, guildId, guild); - attachVoiceLifecycle(state, guild); - - // Stage-Channel Entstummung anfordern/setzen - try { - const me = guild.members.me; - if (me && (channel.type === ChannelType.GuildStageVoice)) { - if ((me.voice as any)?.suppress) { - await me.voice.setSuppressed(false).catch(() => me.voice.setRequestToSpeak(true)); - console.log(`${new Date().toISOString()} | StageVoice: suppression versucht zu deaktivieren`); - } - } - } catch (e) { - console.warn(`${new Date().toISOString()} | StageVoice unsuppress/requestToSpeak fehlgeschlagen`, e); - } - - state.player.on(AudioPlayerStatus.Idle, () => { - // optional: Verbindung bestehen lassen oder nach Timeout trennen - }); - } else { - // Wechsel in anderen Channel, wenn nötig - const current = getVoiceConnection(guildId); - if (current && (current.joinConfig.channelId !== channelId)) { - current.destroy(); - const connection = joinVoiceChannel({ - channelId, - guildId, - adapterCreator: guild.voiceAdapterCreator as any, - selfMute: false, - selfDeaf: false - }); - const player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); - connection.subscribe(player); - state = { connection, player, guildId, channelId, currentVolume: getPersistedVolume(guildId) }; - guildAudioState.set(guildId, state); - - state.connection = await ensureConnectionReady(connection, channelId, guildId, guild); - attachVoiceLifecycle(state, guild); - - connection.on('stateChange', (o, n) => { - console.log(`${new Date().toISOString()} | VoiceConnection: ${o.status} -> ${n.status}`); - }); - player.on('stateChange', (o, n) => { - console.log(`${new Date().toISOString()} | AudioPlayer: ${o.status} -> ${n.status}`); - }); - player.on('error', (err) => { - console.error(`${new Date().toISOString()} | AudioPlayer error:`, err); - }); - } - } - - console.log(`${new Date().toISOString()} | createAudioResource: ${filePath}`); - // Volume bestimmen: bevorzugt Request-Volume, sonst bisheriger State-Wert, sonst 1 - const volumeToUse = typeof volume === 'number' && Number.isFinite(volume) - ? Math.max(0, Math.min(1, volume)) - : (state.currentVolume ?? 1); - const resource = createAudioResource(filePath, { inlineVolume: true }); - if (resource.volume) { - resource.volume.setVolume(volumeToUse); - console.log(`${new Date().toISOString()} | setVolume(${volumeToUse}) for ${soundName}`); - } - state.player.stop(); - state.player.play(resource); - state.currentResource = resource; - state.currentVolume = volumeToUse; - // Persistieren - persistedState.volumes[guildId] = volumeToUse; - writePersistedState(persistedState); - console.log(`${new Date().toISOString()} | player.play() called for ${soundName}`); - // Plays zählen (relativer Key verfügbar?) - if (relativePath) incrementPlaysFor(relativePath); + // Delegiere an playFilePath (inkl. Loudnorm, Connection-Management, Now-Playing Broadcast) + const relKey = relativePath || (folder ? `${folder}/${soundName}` : soundName); + await playFilePath(guildId, channelId, filePath, volume, relKey!); return res.json({ ok: true }); } catch (err: any) { console.error('Play-Fehler:', err); @@ -1139,6 +1301,9 @@ app.post('/api/stop', (req: Request, res: Response) => { const state = guildAudioState.get(guildId); if (!state) return res.status(404).json({ error: 'Kein aktiver Player' }); state.player.stop(true); + // Now-Playing löschen + nowPlaying.delete(guildId); + sseBroadcast({ type: 'nowplaying', guildId, name: '' }); // Partymode für diese Guild ebenfalls stoppen try { const t = partyTimers.get(guildId); @@ -1240,7 +1405,7 @@ app.get('/api/events', (req: Request, res: Response) => { // Snapshot senden try { - res.write(`data: ${JSON.stringify({ type: 'snapshot', party: Array.from(partyActive), selected: persistedState.selectedChannels ?? {}, volumes: persistedState.volumes ?? {} })}\n\n`); + res.write(`data: ${JSON.stringify({ type: 'snapshot', party: Array.from(partyActive), selected: persistedState.selectedChannels ?? {}, volumes: persistedState.volumes ?? {}, nowplaying: Object.fromEntries(nowPlaying) })}\n\n`); } catch {} // Ping, damit Proxies die Verbindung offen halten @@ -1254,6 +1419,45 @@ app.get('/api/events', (req: Request, res: Response) => { }); }); +// --- Medien-URL abspielen --- +// Unterstützt: direkte MP3-URL (Download und Ablage) +app.post('/api/play-url', async (req: Request, res: Response) => { + try { + const { url, guildId, channelId, volume } = req.body as { url?: string; guildId?: string; channelId?: string; volume?: number }; + if (!url || !guildId || !channelId) return res.status(400).json({ error: 'url, guildId, channelId erforderlich' }); + + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return res.status(400).json({ error: 'Ungültige URL' }); + } + const pathname = parsed.pathname.toLowerCase(); + if (!pathname.endsWith('.mp3')) { + return res.status(400).json({ error: 'Nur direkte MP3-Links werden unterstützt.' }); + } + const fileName = path.basename(parsed.pathname); + const dest = path.join(SOUNDS_DIR, fileName); + const r = await fetch(url); + if (!r.ok) return res.status(400).json({ error: 'Download fehlgeschlagen' }); + const buf = Buffer.from(await r.arrayBuffer()); + fs.writeFileSync(dest, buf); + // Vor dem Abspielen normalisieren → sofort aus Cache + if (NORMALIZE_ENABLE) { + try { await normalizeToCache(dest); } catch (e) { console.warn('Norm after URL import failed:', e); } + } + try { + await playFilePath(guildId, channelId, dest, volume, path.basename(dest)); + } catch { + return res.status(500).json({ error: 'Abspielen fehlgeschlagen' }); + } + return res.json({ ok: true, saved: path.basename(dest) }); + } catch (e: any) { + console.error('play-url error:', e); + return res.status(500).json({ error: e?.message ?? 'Unbekannter Fehler' }); + } +}); + // Static Frontend ausliefern (Vite build) const webDistPath = path.resolve(__dirname, '../../web/dist'); if (fs.existsSync(webDistPath)) { @@ -1265,39 +1469,11 @@ if (fs.existsSync(webDistPath)) { app.listen(PORT, () => { console.log(`Server läuft auf http://0.0.0.0:${PORT}`); + + // Vollständige Cache-Synchronisation beim Start (Hintergrund) + syncNormCache(); }); -// --- Medien-URL abspielen --- -// Unterstützt: direkte MP3- oder WAV-URL (Download und Ablage) -app.post('/api/play-url', async (req: Request, res: Response) => { - try { - const { url, guildId, channelId, volume } = req.body as { url?: string; guildId?: string; channelId?: string; volume?: number }; - if (!url || !guildId || !channelId) return res.status(400).json({ error: 'url, guildId, channelId erforderlich' }); - - const lower = url.toLowerCase(); - if (lower.endsWith('.mp3') || lower.endsWith('.wav')) { - const fileName = path.basename(new URL(url).pathname); - const dest = path.join(SOUNDS_DIR, fileName); - const r = await fetch(url); - if (!r.ok) return res.status(400).json({ error: 'Download fehlgeschlagen' }); - const buf = Buffer.from(await r.arrayBuffer()); - fs.writeFileSync(dest, buf); - try { - await playFilePath(guildId, channelId, dest, volume, path.basename(dest)); - } catch { - return res.status(500).json({ error: 'Abspielen fehlgeschlagen' }); - } - return res.json({ ok: true, saved: path.basename(dest) }); - } - return res.status(400).json({ error: 'Nur MP3- oder WAV-Links werden unterstützt.' }); - } catch (e: any) { - console.error('play-url error:', e); - return res.status(500).json({ error: e?.message ?? 'Unbekannter Fehler' }); - } -}); - -// Upload endpoint removed (build reverted) - diff --git a/web/index.html b/web/index.html index cf00157..34f6580 100644 --- a/web/index.html +++ b/web/index.html @@ -1,21 +1,20 @@ - - - - Soundboard - - - - - - - -
- - - - + + + + + Jukebox + + + + + + + +
+ + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..dd263c5 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,1734 @@ +{ + "name": "discord-soundboard-web", + "version": "1.1.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "discord-soundboard-web", + "version": "1.1.1", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.4", + "vite": "^5.3.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001775", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", + "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/web/src/App.tsx b/web/src/App.tsx index 5032d73..dd144bb 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,109 +1,217 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import ReactDOM from 'react-dom'; -import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, clearBadges, updateCategory, deleteCategory, partyStart, partyStop, subscribeEvents, getSelectedChannels, setSelectedChannel } from './api'; -import type { VoiceChannelInfo, Sound, Category } from './types'; +import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'; +import { + fetchChannels, fetchSounds, fetchAnalytics, playSound, playUrl, setVolumeLive, getVolume, + adminStatus, adminLogin, adminLogout, adminDelete, adminRename, + fetchCategories, partyStart, partyStop, subscribeEvents, + getSelectedChannels, setSelectedChannel, uploadFile, +} from './api'; +import type { VoiceChannelInfo, Sound, Category, AnalyticsResponse } from './types'; import { getCookie, setCookie } from './cookies'; -export default function App() { - const [sounds, setSounds] = useState([]); - const [total, setTotal] = useState(0); - const [folders, setFolders] = useState>([]); - const [activeFolder, setActiveFolder] = useState('__all__'); - const [categories, setCategories] = useState([]); - const [activeCategoryId, setActiveCategoryId] = useState(''); - const [channels, setChannels] = useState([]); - const [query, setQuery] = useState(''); - const [fuzzy, setFuzzy] = useState(false); - const [selected, setSelected] = useState(''); - const selectedRef = useRef(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [info, setInfo] = useState(null); - const [showTop, setShowTop] = useState(false); - const [volume, setVolume] = useState(1); - const [favs, setFavs] = useState>({}); - const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'dark'); - const [isAdmin, setIsAdmin] = useState(false); - const [adminPwd, setAdminPwd] = useState(''); - const [selectedSet, setSelectedSet] = useState>({}); - const [assignCategoryId, setAssignCategoryId] = useState(''); - const [newCategoryName, setNewCategoryName] = useState(''); - const [editingCategoryId, setEditingCategoryId] = useState(''); - const [editingCategoryName, setEditingCategoryName] = useState(''); - const [showEmojiPicker, setShowEmojiPicker] = useState(false); - const emojiPickerRef = useRef(null); - const emojiTriggerRef = useRef(null); - const [emojiPos, setEmojiPos] = useState<{left:number; top:number}>({ left: 0, top: 0 }); - const EMOJIS = useMemo(()=>{ - // einfache, breite Auswahl gängiger Emojis; kann später erweitert/extern geladen werden - const groups = [ - '😀😁😂🤣😅😊🙂😉😍😘😜🤪🤗🤔🤩🥳😎😴🤤','😇🥰🥺😡🤬😱😭🙈🙉🙊💀👻🤖🎃','👍👎👏🙌🙏🤝💪🔥✨💥🎉🎊','❤️🧡💛💚💙💜🖤🤍🤎💖💘💝','⭐🌟🌈☀️🌙⚡❄️☔🌊🍀','🎵🎶🎧🎤🎸🥁🎹🎺🎻','🍕🍔🍟🌭🌮🍣🍺🍻🍷🥂','🐶🐱🐼🐸🦄🐧🐢🦖🐙','🚀🛸✈️🚁🚗🏎️🚓🚒','🏆🥇🥈🥉🎯🎮🎲🧩'] - return groups.join('').split(''); - }, []); +const THEMES = [ + { id: 'default', color: '#5865f2', label: 'Discord' }, + { id: 'purple', color: '#9b59b6', label: 'Midnight' }, + { id: 'forest', color: '#2ecc71', label: 'Forest' }, + { id: 'sunset', color: '#e67e22', label: 'Sunset' }, + { id: 'ocean', color: '#3498db', label: 'Ocean' }, +]; - function emojiToTwemojiUrl(emoji: string): string { - const codePoints = Array.from(emoji).map(ch => ch.codePointAt(0)!.toString(16)).join('-'); - // twemoji svg assets - return `https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/${codePoints}.svg`; - } - const [showBroccoli, setShowBroccoli] = useState(false); - const broccoliItems = useMemo(() => { - if (!(theme === '420' && showBroccoli)) return [] as Array<{top:number; left:number; duration:number; delay:number}>; - const items: Array<{top:number; left:number; duration:number; delay:number}> = []; - for (let i = 0; i < 20; i += 1) { - items.push({ - top: 5 + Math.random() * 90, // 5%..95% - left: 2 + Math.random() * 96, // 2%..98% - duration: 14 + Math.random() * 14, // 14s..28s - delay: Math.random() * 2 // 0..2s - }); - } - return items; - }, [theme, showBroccoli]); +const CAT_PALETTE = [ + '#3b82f6', '#f59e0b', '#8b5cf6', '#ec4899', '#14b8a6', + '#f97316', '#06b6d4', '#ef4444', '#a855f7', '#84cc16', + '#d946ef', '#0ea5e9', '#f43f5e', '#10b981', +]; + +type Tab = 'all' | 'favorites' | 'recent'; + +type UploadItem = { + id: string; + file: File; + status: 'waiting' | 'uploading' | 'done' | 'error'; + progress: number; + savedName?: string; + error?: string; +}; + +export default function App() { + /* ── Data ── */ + const [sounds, setSounds] = useState([]); + const [total, setTotal] = useState(0); + const [folders, setFolders] = useState>([]); + const [categories, setCategories] = useState([]); + const [analytics, setAnalytics] = useState({ + totalSounds: 0, + totalPlays: 0, + mostPlayed: [], + }); + + /* ── Navigation ── */ + const [activeTab, setActiveTab] = useState('all'); + const [activeFolder, setActiveFolder] = useState(''); + const [query, setQuery] = useState(''); + const [importUrl, setImportUrl] = useState(''); + const [importBusy, setImportBusy] = useState(false); + + /* ── Channels ── */ + const [channels, setChannels] = useState([]); + const [selected, setSelected] = useState(''); + const selectedRef = useRef(''); + const [channelOpen, setChannelOpen] = useState(false); + + /* ── Playback ── */ + const [volume, setVolume] = useState(1); + const [lastPlayed, setLastPlayed] = useState(''); + + /* ── Preferences ── */ + const [favs, setFavs] = useState>({}); + const [theme, setTheme] = useState(() => localStorage.getItem('jb-theme') || 'default'); + const [cardSize, setCardSize] = useState(() => parseInt(localStorage.getItem('jb-card-size') || '110')); + + /* ── Party ── */ + const [chaosMode, setChaosMode] = useState(false); const [partyActiveGuilds, setPartyActiveGuilds] = useState([]); - const selectedCount = useMemo(() => Object.values(selectedSet).filter(Boolean).length, [selectedSet]); - const [clock, setClock] = useState(() => new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Europe/Berlin' }).format(new Date())); - const [totalPlays, setTotalPlays] = useState(0); - const [mediaUrl, setMediaUrl] = useState(''); - const [chaosMode, setChaosMode] = useState(false); - const chaosTimeoutRef = useRef(null); - const chaosModeRef = useRef(false); + const chaosModeRef = useRef(false); + const volDebounceRef = useRef>(); + + /* ── Admin ── */ + const [isAdmin, setIsAdmin] = useState(false); + const [showAdmin, setShowAdmin] = useState(false); + const [adminPwd, setAdminPwd] = useState(''); + const [adminSounds, setAdminSounds] = useState([]); + const [adminLoading, setAdminLoading] = useState(false); + const [adminQuery, setAdminQuery] = useState(''); + const [adminSelection, setAdminSelection] = useState>({}); + const [renameTarget, setRenameTarget] = useState(''); + const [renameValue, setRenameValue] = useState(''); + + /* ── Drag & Drop Upload ── */ + const [isDragging, setIsDragging] = useState(false); + const [uploads, setUploads] = useState([]); + const [showUploads, setShowUploads] = useState(false); + const dragCounterRef = useRef(0); + const uploadDismissRef = useRef>(); + + /* ── UI ── */ + const [notification, setNotification] = useState<{ msg: string; type: 'info' | 'error' } | null>(null); + const [clock, setClock] = useState(''); + const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; sound: Sound } | null>(null); + const [refreshKey, setRefreshKey] = useState(0); + + /* ── Refs ── */ useEffect(() => { chaosModeRef.current = chaosMode; }, [chaosMode]); useEffect(() => { selectedRef.current = selected; }, [selected]); + /* ── Drag & Drop: globale Window-Listener ── */ + useEffect(() => { + const onDragEnter = (e: DragEvent) => { + if (Array.from(e.dataTransfer?.items ?? []).some(i => i.kind === 'file')) { + dragCounterRef.current++; + setIsDragging(true); + } + }; + const onDragLeave = () => { + dragCounterRef.current = Math.max(0, dragCounterRef.current - 1); + if (dragCounterRef.current === 0) setIsDragging(false); + }; + const onDragOver = (e: DragEvent) => e.preventDefault(); + const onDrop = (e: DragEvent) => { + e.preventDefault(); + dragCounterRef.current = 0; + setIsDragging(false); + const files = Array.from(e.dataTransfer?.files ?? []).filter(f => + /\.(mp3|wav)$/i.test(f.name) + ); + if (files.length) handleFileDrop(files); + }; + window.addEventListener('dragenter', onDragEnter); + window.addEventListener('dragleave', onDragLeave); + window.addEventListener('dragover', onDragOver); + window.addEventListener('drop', onDrop); + return () => { + window.removeEventListener('dragenter', onDragEnter); + window.removeEventListener('dragleave', onDragLeave); + window.removeEventListener('dragover', onDragOver); + window.removeEventListener('drop', onDrop); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isAdmin]); + + /* ── Helpers ── */ + const notify = useCallback((msg: string, type: 'info' | 'error' = 'info') => { + setNotification({ msg, type }); + setTimeout(() => setNotification(null), 3000); + }, []); + const soundKey = useCallback((s: Sound) => s.relativePath ?? s.fileName, []); + const isMp3Url = useCallback((value: string) => { + try { + const parsed = new URL(value.trim()); + return parsed.pathname.toLowerCase().endsWith('.mp3'); + } catch { + return false; + } + }, []); + + const guildId = selected ? selected.split(':')[0] : ''; + const channelId = selected ? selected.split(':')[1] : ''; + + const selectedChannel = useMemo(() => + channels.find(c => `${c.guildId}:${c.channelId}` === selected), + [channels, selected]); + + /* ── Clock ── */ + useEffect(() => { + const update = () => { + const now = new Date(); + const h = String(now.getHours()).padStart(2, '0'); + const m = String(now.getMinutes()).padStart(2, '0'); + const s = String(now.getSeconds()).padStart(2, '0'); + setClock(`${h}:${m}:${s}`); + }; + update(); + const id = setInterval(update, 1000); + return () => clearInterval(id); + }, []); + + /* ── Init ── */ useEffect(() => { (async () => { try { - const [c, selectedMap] = await Promise.all([fetchChannels(), getSelectedChannels()]); - setChannels(c); - let initial = ''; - if (c.length > 0) { - const firstGuild = c[0].guildId; - const serverCid = selectedMap[firstGuild]; - if (serverCid && c.find(x => x.guildId === firstGuild && x.channelId === serverCid)) { - initial = `${firstGuild}:${serverCid}`; - } else { - initial = `${c[0].guildId}:${c[0].channelId}`; - } + const [ch, selMap] = await Promise.all([fetchChannels(), getSelectedChannels()]); + setChannels(ch); + if (ch.length) { + const g = ch[0].guildId; + const serverCid = selMap[g]; + const match = serverCid && ch.find(x => x.guildId === g && x.channelId === serverCid); + setSelected(match ? `${g}:${serverCid}` : `${ch[0].guildId}:${ch[0].channelId}`); } - if (initial) setSelected(initial); - } catch (e: any) { - setError(e?.message || 'Fehler beim Laden der Channels'); - } - try { setIsAdmin(await adminStatus()); } catch {} - try { const cats = await fetchCategories(); setCategories(cats.categories || []); } catch {} - try { - const h = await fetch('/api/health').then(r => r.json()).catch(() => null); - if (h && typeof h.totalPlays === 'number') setTotalPlays(h.totalPlays); - } catch {} + } catch (e: any) { notify(e?.message || 'Channel-Fehler', 'error'); } + try { setIsAdmin(await adminStatus()); } catch { } + try { const c = await fetchCategories(); setCategories(c.categories || []); } catch { } })(); }, []); - // SSE: Partymode-Status global synchronisieren (sauberes Cleanup) + /* ── Theme ── */ + useEffect(() => { + if (theme === 'default') document.body.removeAttribute('data-theme'); + else document.body.setAttribute('data-theme', theme); + localStorage.setItem('jb-theme', theme); + }, [theme]); + + /* ── Card size ── */ + useEffect(() => { + const r = document.documentElement; + r.style.setProperty('--card-size', cardSize + 'px'); + const ratio = cardSize / 110; + r.style.setProperty('--card-emoji', Math.round(28 * ratio) + 'px'); + r.style.setProperty('--card-font', Math.max(9, Math.round(11 * ratio)) + 'px'); + localStorage.setItem('jb-card-size', String(cardSize)); + }, [cardSize]); + + /* ── SSE ── */ useEffect(() => { const unsub = subscribeEvents((msg) => { if (msg?.type === 'party') { - setPartyActiveGuilds((prev) => { + setPartyActiveGuilds(prev => { const s = new Set(prev); if (msg.active) s.add(msg.guildId); else s.delete(msg.guildId); return Array.from(s); @@ -112,712 +220,948 @@ export default function App() { setPartyActiveGuilds(Array.isArray(msg.party) ? msg.party : []); try { const sel = msg?.selected || {}; - const currentSelected = selectedRef.current || ''; - const gid = currentSelected ? currentSelected.split(':')[0] : ''; - if (gid && sel[gid]) { - const newVal = `${gid}:${sel[gid]}`; - setSelected(newVal); - } - } catch {} + const g = selectedRef.current?.split(':')[0]; + if (g && sel[g]) setSelected(`${g}:${sel[g]}`); + } catch { } try { const vols = msg?.volumes || {}; - const cur = selectedRef.current || ''; - const gid = cur ? cur.split(':')[0] : ''; - if (gid && typeof vols[gid] === 'number') { - const v = vols[gid]; - setVolume(v); - } - } catch {} + const g = selectedRef.current?.split(':')[0]; + if (g && typeof vols[g] === 'number') setVolume(vols[g]); + } catch { } + try { + const np = msg?.nowplaying || {}; + const g = selectedRef.current?.split(':')[0]; + if (g && typeof np[g] === 'string') setLastPlayed(np[g]); + } catch { } } else if (msg?.type === 'channel') { - try { - const gid = msg.guildId; - const cid = msg.channelId; - if (gid && cid) { - const currentSelected = selectedRef.current || ''; - const curGid = currentSelected ? currentSelected.split(':')[0] : ''; - if (curGid === gid) setSelected(`${gid}:${cid}`); - } - } catch {} + const g = selectedRef.current?.split(':')[0]; + if (msg.guildId === g) setSelected(`${msg.guildId}:${msg.channelId}`); } else if (msg?.type === 'volume') { - try { - const gid = msg.guildId; - const v = msg.volume; - const cur = selectedRef.current || ''; - const curGid = cur ? cur.split(':')[0] : ''; - if (gid && curGid === gid && typeof v === 'number') { - setVolume(v); - } - } catch {} + const g = selectedRef.current?.split(':')[0]; + if (msg.guildId === g && typeof msg.volume === 'number') setVolume(msg.volume); + } else if (msg?.type === 'nowplaying') { + const g = selectedRef.current?.split(':')[0]; + if (msg.guildId === g) setLastPlayed(msg.name || ''); } }); - return () => { try { unsub(); } catch {} }; + return () => { try { unsub(); } catch { } }; }, []); - // Aus aktivem Guild-Status die lokale Anzeige setzen useEffect(() => { - const gid = selected ? selected.split(':')[0] : ''; - setChaosMode(gid ? partyActiveGuilds.includes(gid) : false); + setChaosMode(guildId ? partyActiveGuilds.includes(guildId) : false); }, [selected, partyActiveGuilds]); - // Uhrzeit (Berlin) aktualisieren - useEffect(() => { - const fmt = new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Europe/Berlin' }); - const update = () => setClock(fmt.format(new Date())); - const id = setInterval(update, 1000); - update(); - return () => clearInterval(id); - }, []); - + /* ── Data Fetch ── */ useEffect(() => { (async () => { try { - const folderParam = activeFolder === '__favs__' ? '__all__' : activeFolder; - const s = await fetchSounds(query, folderParam, activeCategoryId || undefined, fuzzy); + let folderParam = '__all__'; + if (activeTab === 'recent') folderParam = '__recent__'; + else if (activeFolder) folderParam = activeFolder; + const s = await fetchSounds(query, folderParam, undefined, false); setSounds(s.items); setTotal(s.total); setFolders(s.folders); - } catch (e: any) { - setError(e?.message || 'Fehler beim Laden der Sounds'); - } + } catch (e: any) { notify(e?.message || 'Sounds-Fehler', 'error'); } })(); - }, [activeFolder, query, activeCategoryId, fuzzy]); + }, [activeTab, activeFolder, query, refreshKey]); - // Favoriten aus Cookie laden + useEffect(() => { + void loadAnalytics(); + }, [refreshKey]); + + /* ── Favs persistence ── */ useEffect(() => { const c = getCookie('favs'); - if (c) { - try { setFavs(JSON.parse(c)); } catch {} - } + if (c) try { setFavs(JSON.parse(c)); } catch { } }, []); - // Favoriten persistieren useEffect(() => { - try { setCookie('favs', JSON.stringify(favs)); } catch {} + try { setCookie('favs', JSON.stringify(favs)); } catch { } }, [favs]); - // Theme anwenden/persistieren + /* ── Volume sync ── */ useEffect(() => { - document.body.setAttribute('data-theme', theme); - if (import.meta.env.VITE_BUILD_CHANNEL === 'nightly') { - document.body.setAttribute('data-build', 'nightly'); - } else { - document.body.removeAttribute('data-build'); + if (selected) { + (async () => { + try { const v = await getVolume(guildId); setVolume(v); } catch { } + })(); } - localStorage.setItem('theme', theme); - }, [theme]); - - // Back-to-top Sichtbarkeit - useEffect(() => { - const onScroll = () => setShowTop(window.scrollY > 300); - onScroll(); - window.addEventListener('scroll', onScroll, { passive: true }); - return () => window.removeEventListener('scroll', onScroll); - }, []); - - // Live-Update für totalPlays Counter - useEffect(() => { - const updateTotalPlays = async () => { - try { - const h = await fetch('/api/health').then(r => r.json()).catch(() => null); - if (h && typeof h.totalPlays === 'number') setTotalPlays(h.totalPlays); - } catch {} - }; - - // Sofort beim Start laden - updateTotalPlays(); - - // Alle 5 Sekunden aktualisieren - const interval = setInterval(updateTotalPlays, 5000); - return () => clearInterval(interval); - }, []); - - useEffect(() => { - (async () => { - if (selected) { - localStorage.setItem('selectedChannel', selected); - // gespeicherte Lautstärke vom Server laden - try { - const [guildId] = selected.split(':'); - const v = await getVolume(guildId); - setVolume(v); - } catch {} - } - })(); }, [selected]); - // Server liefert bereits gefilterte (und ggf. fuzzy-sortierte) Ergebnisse - const filtered = sounds; + /* ── Close dropdowns on outside click ── */ + useEffect(() => { + const handler = () => { setChannelOpen(false); setCtxMenu(null); }; + document.addEventListener('click', handler); + return () => document.removeEventListener('click', handler); + }, []); + + useEffect(() => { + if (showAdmin && isAdmin) { + void loadAdminSounds(); + } + }, [showAdmin, isAdmin]); + + /* ── Actions ── */ + async function loadAnalytics() { + try { + const data = await fetchAnalytics(); + setAnalytics(data); + } catch { } + } + + async function handlePlay(s: Sound) { + if (!selected) return notify('Bitte einen Voice-Channel auswählen', 'error'); + try { + await playSound(s.name, guildId, channelId, volume, s.relativePath); + setLastPlayed(s.name); + void loadAnalytics(); + } catch (e: any) { notify(e?.message || 'Play fehlgeschlagen', 'error'); } + } + + async function handleUrlImport() { + const trimmed = importUrl.trim(); + if (!trimmed) return notify('Bitte einen MP3-Link eingeben', 'error'); + if (!selected) return notify('Bitte einen Voice-Channel auswählen', 'error'); + if (!isMp3Url(trimmed)) return notify('Nur direkte MP3-Links sind erlaubt', 'error'); + setImportBusy(true); + try { + await playUrl(trimmed, guildId, channelId, volume); + setImportUrl(''); + notify('MP3 importiert und abgespielt'); + setRefreshKey(k => k + 1); + await loadAnalytics(); + } catch (e: any) { + notify(e?.message || 'URL-Import fehlgeschlagen', 'error'); + } finally { + setImportBusy(false); + } + } + + async function handleFileDrop(files: File[]) { + if (!isAdmin) { + notify('Admin-Login erforderlich zum Hochladen', 'error'); + return; + } + if (uploadDismissRef.current) clearTimeout(uploadDismissRef.current); + + const items: UploadItem[] = files.map(f => ({ + id: Math.random().toString(36).slice(2), + file: f, + status: 'waiting', + progress: 0, + })); + setUploads(items); + setShowUploads(true); + + const updated = [...items]; + for (let i = 0; i < updated.length; i++) { + updated[i] = { ...updated[i], status: 'uploading' }; + setUploads([...updated]); + try { + const savedName = await uploadFile( + updated[i].file, + pct => { + updated[i] = { ...updated[i], progress: pct }; + setUploads([...updated]); + }, + ); + updated[i] = { ...updated[i], status: 'done', progress: 100, savedName }; + } catch (e: any) { + updated[i] = { ...updated[i], status: 'error', error: e?.message ?? 'Fehler' }; + } + setUploads([...updated]); + } + + // Sound-Liste aktualisieren + setRefreshKey(k => k + 1); + void loadAnalytics(); + + // Auto-Dismiss nach 3s + uploadDismissRef.current = setTimeout(() => { + setShowUploads(false); + setUploads([]); + }, 3500); + } + + async function handleStop() { + if (!selected) return; + setLastPlayed(''); + try { await fetch(`/api/stop?guildId=${encodeURIComponent(guildId)}`, { method: 'POST' }); } catch { } + } + + async function handleRandom() { + if (!displaySounds.length || !selected) return; + const rnd = displaySounds[Math.floor(Math.random() * displaySounds.length)]; + handlePlay(rnd); + } + + async function toggleParty() { + if (chaosMode) { + await handleStop(); + try { await partyStop(guildId); } catch { } + } else { + if (!selected) return notify('Bitte einen Channel auswählen', 'error'); + try { await partyStart(guildId, channelId); } catch { } + } + } + + async function handleChannelSelect(ch: VoiceChannelInfo) { + const v = `${ch.guildId}:${ch.channelId}`; + setSelected(v); + setChannelOpen(false); + try { await setSelectedChannel(ch.guildId, ch.channelId); } catch { } + } + + function toggleFav(key: string) { + setFavs(prev => ({ ...prev, [key]: !prev[key] })); + } + + async function loadAdminSounds() { + setAdminLoading(true); + try { + const data = await fetchSounds('', '__all__', undefined, false); + setAdminSounds(data.items || []); + } catch (e: any) { + notify(e?.message || 'Admin-Sounds konnten nicht geladen werden', 'error'); + } finally { + setAdminLoading(false); + } + } + + function toggleAdminSelection(path: string) { + setAdminSelection(prev => ({ ...prev, [path]: !prev[path] })); + } + + function startRename(sound: Sound) { + setRenameTarget(soundKey(sound)); + setRenameValue(sound.name); + } + + function cancelRename() { + setRenameTarget(''); + setRenameValue(''); + } + + async function submitRename() { + if (!renameTarget) return; + const baseName = renameValue.trim().replace(/\.(mp3|wav)$/i, ''); + if (!baseName) { + notify('Bitte einen gültigen Namen eingeben', 'error'); + return; + } + try { + await adminRename(renameTarget, baseName); + notify('Sound umbenannt'); + cancelRename(); + setRefreshKey(k => k + 1); + if (showAdmin) await loadAdminSounds(); + } catch (e: any) { + notify(e?.message || 'Umbenennen fehlgeschlagen', 'error'); + } + } + + async function deleteAdminPaths(paths: string[]) { + if (paths.length === 0) return; + try { + await adminDelete(paths); + notify(paths.length === 1 ? 'Sound gelöscht' : `${paths.length} Sounds gelöscht`); + setAdminSelection({}); + cancelRename(); + setRefreshKey(k => k + 1); + if (showAdmin) await loadAdminSounds(); + } catch (e: any) { + notify(e?.message || 'Löschen fehlgeschlagen', 'error'); + } + } + + async function handleAdminLogin() { + try { + const ok = await adminLogin(adminPwd); + if (ok) { + setIsAdmin(true); + setAdminPwd(''); + notify('Admin eingeloggt'); + } + else notify('Falsches Passwort', 'error'); + } catch { notify('Login fehlgeschlagen', 'error'); } + } + + async function handleAdminLogout() { + try { + await adminLogout(); + setIsAdmin(false); + setAdminSelection({}); + cancelRename(); + notify('Ausgeloggt'); + } catch { } + } + + /* ── Computed ── */ + const displaySounds = useMemo(() => { + if (activeTab === 'favorites') return sounds.filter(s => favs[s.relativePath ?? s.fileName]); + return sounds; + }, [sounds, activeTab, favs]); const favCount = useMemo(() => Object.values(favs).filter(Boolean).length, [favs]); - function toggleSelect(key: string, on?: boolean) { - setSelectedSet((prev) => ({ ...prev, [key]: typeof on === 'boolean' ? on : !prev[key] })); - } - function clearSelection() { - setSelectedSet({}); - } + const visibleFolders = useMemo(() => + folders.filter(f => !['__all__', '__recent__', '__top3__'].includes(f.key)), + [folders]); - async function handlePlay(name: string, rel?: string) { - setError(null); - if (!selected) return setError('Bitte einen Voice-Channel auswählen'); - const [guildId, channelId] = selected.split(':'); - try { - setLoading(true); - await playSound(name, guildId, channelId, volume, rel); - } catch (e: any) { - setError(e?.message || 'Play fehlgeschlagen'); - } finally { - setLoading(false); - } - } + const folderColorMap = useMemo(() => { + const m: Record = {}; + visibleFolders.forEach((f, i) => { m[f.key] = CAT_PALETTE[i % CAT_PALETTE.length]; }); + return m; + }, [visibleFolders]); - // CHAOS Mode Funktionen (zufällige Wiedergabe alle 1-3 Minuten) - const startChaosMode = async () => { - if (!selected || !sounds.length) return; + const firstOfInitial = useMemo(() => { + const seen = new Set(); + const result = new Set(); + displaySounds.forEach((s, idx) => { + const ch = s.name.charAt(0).toUpperCase(); + if (!seen.has(ch)) { seen.add(ch); result.add(idx); } + }); + return result; + }, [displaySounds]); - const playRandomSound = async () => { - const pool = sounds; - if (!pool.length || !selected) return; - const randomSound = pool[Math.floor(Math.random() * pool.length)]; - const [guildId, channelId] = selected.split(':'); - try { - await playSound(randomSound.name, guildId, channelId, volume, randomSound.relativePath); - } catch (e: any) { - console.error('Chaos sound play failed:', e); - } - }; + const channelsByGuild = useMemo(() => { + const groups: Record = {}; + channels.forEach(c => { + if (!groups[c.guildName]) groups[c.guildName] = []; + groups[c.guildName].push(c); + }); + return groups; + }, [channels]); - const scheduleNextPlay = async () => { - if (!chaosModeRef.current) return; - await playRandomSound(); - const delay = 30_000 + Math.floor(Math.random() * 60_000); // 30-90 Sekunden - chaosTimeoutRef.current = window.setTimeout(scheduleNextPlay, delay); - }; + const adminFilteredSounds = useMemo(() => { + const q = adminQuery.trim().toLowerCase(); + if (!q) return adminSounds; + return adminSounds.filter(s => { + const key = soundKey(s).toLowerCase(); + return s.name.toLowerCase().includes(q) + || (s.folder || '').toLowerCase().includes(q) + || key.includes(q); + }); + }, [adminQuery, adminSounds, soundKey]); - // Sofort ersten Sound abspielen - await playRandomSound(); - // Nächsten zufällig in 1-3 Minuten planen - const firstDelay = 30_000 + Math.floor(Math.random() * 60_000); - chaosTimeoutRef.current = window.setTimeout(scheduleNextPlay, firstDelay); - }; + const selectedAdminPaths = useMemo(() => + Object.keys(adminSelection).filter(k => adminSelection[k]), + [adminSelection]); - const stopChaosMode = async () => { - if (chaosTimeoutRef.current) { - clearTimeout(chaosTimeoutRef.current); - chaosTimeoutRef.current = null; - } - - // Alle Sounds stoppen (wie Panic Button) - if (selected) { - const [guildId] = selected.split(':'); - try { - await fetch(`/api/stop?guildId=${encodeURIComponent(guildId)}`, { method: 'POST' }); - } catch (e: any) { - console.error('Chaos stop failed:', e); - } - } - }; + const selectedVisibleCount = useMemo(() => + adminFilteredSounds.filter(s => !!adminSelection[soundKey(s)]).length, + [adminFilteredSounds, adminSelection, soundKey]); - const toggleChaosMode = async () => { - if (chaosMode) { - setChaosMode(false); - await stopChaosMode(); - // serverseitig stoppen - if (selected) { const [guildId] = selected.split(':'); try { await partyStop(guildId); } catch {} } - } else { - setChaosMode(true); - await startChaosMode(); - // serverseitig starten - if (selected) { const [guildId, channelId] = selected.split(':'); try { await partyStart(guildId, channelId); } catch {} } - } - }; + const allVisibleSelected = adminFilteredSounds.length > 0 && selectedVisibleCount === adminFilteredSounds.length; + const analyticsTop = analytics.mostPlayed.slice(0, 3); + const totalSoundsDisplay = analytics.totalSounds || total; - // Cleanup bei Komponenten-Unmount - useEffect(() => { - return () => { - if (chaosTimeoutRef.current) { - clearTimeout(chaosTimeoutRef.current); - } - }; - }, []); + const clockMain = clock.slice(0, 5); + const clockSec = clock.slice(5); + /* ════════════════════════════════════════════ + RENDER + ════════════════════════════════════════════ */ return ( - -
- {/* Floating Broccoli for 420 Theme */} - {theme === '420' && showBroccoli && ( - <> - {broccoliItems.map((b, idx) => ( -
- 🥦 -
- ))} - - )} -
-
-
-

- Jukebox 420 - {import.meta.env.VITE_BUILD_CHANNEL === 'nightly' && ( -
- • Nightly -
- )} -

-

{clock}

-
-
-
-
-

Sounds

-

{total}

-
-
-

Played

-

{totalPlays}

-
-
- - - -
-
-
+
+ {chaosMode &&
} -
-
-
- -
- setQuery(e.target.value)} /> - search -
-
-
- { - setSelected(v); - try { - const [gid, cid] = v.split(':'); - await setSelectedChannel(gid, cid); - } catch (e) { /* noop */ } - }} /> - folder_special -
-
- volume_up - { - const v = parseFloat(e.target.value); - setVolume(v); - // CSS-Variable setzen, um die Füllbreite zu steuern - const percent = `${Math.round(v * 100)}%`; - try { (e.target as HTMLInputElement).style.setProperty('--_fill', percent); } catch {} - if(selected){ const [guildId]=selected.split(':'); try{ await setVolumeLive(guildId, v);}catch{} } - }} - // Initiale Füllbreite, falls State geladen ist - style={{ ['--_fill' as any]: `${Math.round(volume*100)}%` }} - /> - {Math.round(volume*100)}% -
-
- setMediaUrl(e.target.value)} onKeyDown={async (e)=>{ if(e.key==='Enter'){ if(!selected){ setError('Bitte Voice-Channel wählen'); setInfo(null); return;} const [guildId,channelId]=selected.split(':'); try{ await playUrl(mediaUrl,guildId,channelId,volume); setError(null); setInfo('MP3 heruntergeladen und abgespielt.'); }catch(err:any){ setInfo(null); setError(err?.message||'Download fehlgeschlagen'); } } }} /> - link - -
-
-
- - palette - unfold_more -
- {theme === '420' && ( -
- setShowBroccoli(e.target.checked)} - className="w-4 h-4 accent-green-500" - /> - -
- )} -
+ {/* ═══ TOPBAR ═══ */} +
+
+
+ music_note
-
-
- {!isAdmin ? ( - <> -
- setAdminPwd(e.target.value)} - onKeyDown={async (e)=>{ - if(e.key === 'Enter') { - const ok = await adminLogin(adminPwd); - if(ok) { - setIsAdmin(true); - setAdminPwd(''); - } else { - alert('Login fehlgeschlagen'); - } - } - }} - /> - lock + Soundboard + + {/* Channel Dropdown */} +
e.stopPropagation()}> + + {channelOpen && ( +
+ {Object.entries(channelsByGuild).map(([guild, chs]) => ( + +
{guild}
+ {chs.map(ch => ( +
handleChannelSelect(ch)} + > + volume_up + {ch.channelName} +
+ ))} +
+ ))} + {channels.length === 0 && ( +
+ Keine Channels verfügbar
- - - ) : ( -
- Ausgewählt: {selectedCount} - {selectedCount > 0 && ( - - )} - {selectedCount === 1 && ( - { - const from = Object.entries(selectedSet).find(([,v])=>v)?.[0]; - if(!from) return; - try { - await adminRename(from, newName); - clearSelection(); - const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder, activeCategoryId || undefined, fuzzy); - setSounds(resp.items); setTotal(resp.total); setFolders(resp.folders); - } catch (e:any) { setError(e?.message||'Umbenennen fehlgeschlagen'); } - }} /> - )} - {/* Kategorien-Zuweisung */} - {selectedCount > 0 && ( - <> - - - - {/* Custom Emoji Feature entfernt */} - - - - )} - -
- - {/* Kategorien: anlegen/umbenennen/löschen */} - setNewCategoryName(e.target.value)} style={{maxWidth:200}} /> - - - - setEditingCategoryName(e.target.value)} style={{maxWidth:200}} /> - - - - -
- )} -
+ )} +
+ )}
- {error &&
{error}
} - {info &&
{info}
} +
+
{clockMain}{clockSec}
+
-
-
- - {folders.map(f=> { - const displayName = f.name.replace(/\s*\(\d+\)\s*$/, ''); - return ( - - ); - })} -
- {categories.length > 0 && ( -
- {categories.map(cat => ( - - ))} +
+ {lastPlayed && ( +
+
+
+
+
+ {lastPlayed}
)} + {selected && ( +
+ + Verbunden +
+ )} + +
+
+ + {/* ═══ TOOLBAR ═══ */} +
+
+ + + +
+ +
+ search + setQuery(e.target.value)} + /> + {query && ( + + )}
-
- {(activeFolder === '__favs__' ? filtered.filter((s) => !!favs[s.relativePath ?? s.fileName]) : filtered).map((s) => { - const key = `${s.relativePath ?? s.fileName}`; - const isFav = !!favs[key]; - return ( -
- {isAdmin && ( - { e.stopPropagation(); toggleSelect(key, e.target.checked); }} - /> - )} -
handlePlay(s.name, s.relativePath)}> - - {s.name} - {Array.isArray((s as any).badges) && (s as any).badges!.map((b:string, i:number)=> ( - {b==='new'?'🆕': b==='rocket'?'🚀': b} - ))} - -
- -
-
-
- ); - })} -
- {/* Footer intentionally left without version display */} -
- {showTop && ( - - )} - - ); -} +
+ link + setImportUrl(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') void handleUrlImport(); }} + /> + +
-type SelectProps = { - channels: VoiceChannelInfo[]; - value: string; - onChange: (v: string) => void; -}; +
-function CustomSelect({ channels, value, onChange }: SelectProps) { - const [open, setOpen] = useState(false); - const ref = useRef(null); - const triggerRef = useRef(null); - const [menuPos, setMenuPos] = useState<{ left: number; top: number; width: number }>({ left: 0, top: 0, width: 0 }); - useEffect(() => { - const close = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; - window.addEventListener('click', close); - return () => window.removeEventListener('click', close); - }, []); +
+ { + const newVol = volume > 0 ? 0 : 0.5; + setVolume(newVol); + if (guildId) setVolumeLive(guildId, newVol).catch(() => {}); + }} + > + {volume === 0 ? 'volume_off' : volume < 0.5 ? 'volume_down' : 'volume_up'} + + { + const v = parseFloat(e.target.value); + setVolume(v); + if (guildId) { + if (volDebounceRef.current) clearTimeout(volDebounceRef.current); + volDebounceRef.current = setTimeout(() => { + setVolumeLive(guildId, v).catch(() => {}); + }, 120); + } + }} + style={{ '--vol': `${Math.round(volume * 100)}%` } as React.CSSProperties} + /> + {Math.round(volume * 100)}% +
- useEffect(() => { - if (!open) return; - const update = () => { - const el = triggerRef.current; - if (!el) return; - const r = el.getBoundingClientRect(); - setMenuPos({ left: Math.round(r.left), top: Math.round(r.bottom + 6), width: Math.round(r.width) }); - }; - update(); - window.addEventListener('resize', update); - window.addEventListener('scroll', update, true); - return () => { - window.removeEventListener('resize', update); - window.removeEventListener('scroll', update, true); - }; - }, [open]); + - const current = channels.find(c => `${c.guildId}:${c.channelId}` === value); - - return ( -
- - {open && typeof document !== 'undefined' && ReactDOM.createPortal( -
- {channels.map((c) => { - const v = `${c.guildId}:${c.channelId}`; - const active = v === value; + {chaosMode ? 'celebration' : 'auto_awesome'} + {chaosMode ? 'Party!' : 'Party'} + + + + +
+ grid_view + setCardSize(parseInt(e.target.value))} + /> +
+ +
+ {THEMES.map(t => ( +
setTheme(t.id)} + /> + ))} +
+
+ +
+
+ library_music +
+ Sounds gesamt + {totalSoundsDisplay} +
+
+ +
+ leaderboard +
+ Most Played +
+ {analyticsTop.length === 0 ? ( + Noch keine Plays + ) : ( + analyticsTop.map((item, idx) => ( + + {idx + 1}. {item.name} ({item.count}) + + )) + )} +
+
+
+
+ + {/* ═══ FOLDER CHIPS ═══ */} + {activeTab === 'all' && visibleFolders.length > 0 && ( +
+ {visibleFolders.map(f => { + const color = folderColorMap[f.key] || '#888'; + const isActive = activeFolder === f.key; return ( ); })} -
, - document.body +
+ )} + + {/* ═══ MAIN ═══ */} +
+ {displaySounds.length === 0 ? ( +
+
{activeTab === 'favorites' ? '⭐' : '🔇'}
+
+ {activeTab === 'favorites' + ? 'Noch keine Favoriten' + : query + ? `Kein Sound für "${query}" gefunden` + : 'Keine Sounds vorhanden'} +
+
+ {activeTab === 'favorites' + ? 'Klick den Stern auf einem Sound!' + : 'Hier gibt\'s noch nichts zu hören.'} +
+
+ ) : ( +
+ {displaySounds.map((s, idx) => { + const key = s.relativePath ?? s.fileName; + const isFav = !!favs[key]; + const isPlaying = lastPlayed === s.name; + const isNew = s.isRecent || s.badges?.includes('new'); + const initial = s.name.charAt(0).toUpperCase(); + const showInitial = firstOfInitial.has(idx); + const folderColor = s.folder ? (folderColorMap[s.folder] || 'var(--accent)') : 'var(--accent)'; + + return ( +
{ + const card = e.currentTarget; + const rect = card.getBoundingClientRect(); + const ripple = document.createElement('div'); + ripple.className = 'ripple'; + const sz = Math.max(rect.width, rect.height); + ripple.style.width = ripple.style.height = sz + 'px'; + ripple.style.left = (e.clientX - rect.left - sz / 2) + 'px'; + ripple.style.top = (e.clientY - rect.top - sz / 2) + 'px'; + card.appendChild(ripple); + setTimeout(() => ripple.remove(), 500); + handlePlay(s); + }} + onContextMenu={e => { + e.preventDefault(); + e.stopPropagation(); + setCtxMenu({ + x: Math.min(e.clientX, window.innerWidth - 170), + y: Math.min(e.clientY, window.innerHeight - 140), + sound: s, + }); + }} + title={`${s.name}${s.folder ? ` (${s.folder})` : ''}`} + > + {isNew && NEU} + { e.stopPropagation(); toggleFav(key); }} + > + {isFav ? 'star' : 'star_border'} + + {showInitial && {initial}} + {s.name} + {s.folder && {s.folder}} +
+
+
+
+
+ ); + })} +
+ )} +
+ + {/* ═══ CONTEXT MENU ═══ */} + {ctxMenu && ( +
e.stopPropagation()} + > +
{ handlePlay(ctxMenu.sound); setCtxMenu(null); }}> + play_arrow + Abspielen +
+
{ + toggleFav(ctxMenu.sound.relativePath ?? ctxMenu.sound.fileName); + setCtxMenu(null); + }}> + + {favs[ctxMenu.sound.relativePath ?? ctxMenu.sound.fileName] ? 'star' : 'star_border'} + + Favorit +
+ {isAdmin && ( + <> +
+
{ + const path = ctxMenu.sound.relativePath ?? ctxMenu.sound.fileName; + await deleteAdminPaths([path]); + setCtxMenu(null); + }}> + delete + Löschen +
+ + )} +
+ )} + + {/* ═══ TOAST ═══ */} + {notification && ( +
+ + {notification.type === 'error' ? 'error_outline' : 'check_circle'} + + {notification.msg} +
+ )} + + {/* ═══ ADMIN PANEL ═══ */} + {showAdmin && ( +
{ if (e.target === e.currentTarget) setShowAdmin(false); }}> +
+

+ Admin + +

+ {!isAdmin ? ( +
+
+ + setAdminPwd(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleAdminLogin()} + placeholder="Admin-Passwort..." + /> +
+ +
+ ) : ( +
+
+

Eingeloggt als Admin

+
+ + +
+
+ +
+ + setAdminQuery(e.target.value)} + placeholder="Nach Name, Ordner oder Pfad filtern..." + /> +
+ +
+ + + +
+ +
+ {adminLoading ? ( +
Lade Sounds...
+ ) : adminFilteredSounds.length === 0 ? ( +
Keine Sounds gefunden.
+ ) : ( +
+ {adminFilteredSounds.map(sound => { + const key = soundKey(sound); + const editing = renameTarget === key; + return ( +
+ + +
+
{sound.name}
+
+ {sound.folder ? `Ordner: ${sound.folder}` : 'Root'} + {' · '} + {key} +
+ {editing && ( +
+ setRenameValue(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter') void submitRename(); + if (e.key === 'Escape') cancelRename(); + }} + placeholder="Neuer Name..." + /> + + +
+ )} +
+ + {!editing && ( +
+ + +
+ )} +
+ ); + })} +
+ )} +
+
+ )} +
+
+ )} + + {/* ── Drag & Drop Overlay ── */} + {isDragging && ( +
+
+ cloud_upload +
MP3 & WAV hier ablegen
+
Mehrere Dateien gleichzeitig möglich
+
+
+ )} + + {/* ── Upload-Queue ── */} + {showUploads && uploads.length > 0 && ( +
+
+ upload + + {uploads.every(u => u.status === 'done' || u.status === 'error') + ? `${uploads.filter(u => u.status === 'done').length} von ${uploads.length} hochgeladen` + : `Lade hoch… (${uploads.filter(u => u.status === 'done').length}/${uploads.length})`} + + +
+
+ {uploads.map(u => ( +
+ audio_file +
+
+ {u.savedName ?? u.file.name} +
+
{(u.file.size / 1024).toFixed(0)} KB
+
+ {(u.status === 'waiting' || u.status === 'uploading') && ( +
+
+
+ )} + + {u.status === 'done' ? 'check_circle' : + u.status === 'error' ? 'error' : + u.status === 'uploading' ? 'sync' : 'schedule'} + + {u.status === 'error' &&
{u.error}
} +
+ ))} +
+
)}
); } - -// Einfache ErrorBoundary, damit die Seite nicht blank wird und Fehler sichtbar sind -class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { error?: Error }>{ - constructor(props: { children: React.ReactNode }) { - super(props); - this.state = { error: undefined }; - } - static getDerivedStateFromError(error: Error) { return { error }; } - componentDidCatch(error: Error, info: any) { console.error('UI-ErrorBoundary:', error, info); } - render() { - if (this.state.error) { - return ( -
-

Es ist ein Fehler aufgetreten

-
{String(this.state.error.message || this.state.error)}
- -
- ); - } - return this.props.children as any; - } -} - -// Inline-Komponente für Umbenennen (nur bei genau 1 Selektion sichtbar) -type RenameInlineProps = { onSubmit: (newName: string) => void | Promise }; -function RenameInline({ onSubmit }: RenameInlineProps) { - const [val, setVal] = useState(''); - async function submit() { - const n = val.trim(); - if (!n) return; - await onSubmit(n); - setVal(''); - } - return ( -
- setVal(e.target.value)} - placeholder="Neuer Name" - onKeyDown={(e) => { if (e.key === 'Enter') void submit(); }} - style={{ color: '#000000' }} - /> - -
- ); -} - - - - - diff --git a/web/src/api.ts b/web/src/api.ts index cd61520..4cf8736 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -1,4 +1,4 @@ -import type { Sound, SoundsResponse, VoiceChannelInfo } from './types'; +import type { AnalyticsResponse, Sound, SoundsResponse, VoiceChannelInfo } from './types'; const API_BASE = import.meta.env.VITE_API_BASE_URL || '/api'; @@ -13,6 +13,12 @@ export async function fetchSounds(q?: string, folderKey?: string, categoryId?: s return res.json(); } +export async function fetchAnalytics(): Promise { + const res = await fetch(`${API_BASE}/analytics`); + if (!res.ok) throw new Error('Fehler beim Laden der Analytics'); + return res.json(); +} + // Kategorien export async function fetchCategories() { const res = await fetch(`${API_BASE}/categories`, { credentials: 'include' }); @@ -200,7 +206,34 @@ export async function playUrl(url: string, guildId: string, channelId: string, v } } -// uploadFile removed (build reverted) +/** Lädt eine einzelne MP3/WAV-Datei hoch. Gibt den gespeicherten Dateinamen zurück. */ +export function uploadFile( + file: File, + onProgress: (pct: number) => void, +): Promise { + return new Promise((resolve, reject) => { + const form = new FormData(); + form.append('files', file); + const xhr = new XMLHttpRequest(); + xhr.open('POST', `${API_BASE}/upload`); + xhr.upload.onprogress = e => { + if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100)); + }; + xhr.onload = () => { + if (xhr.status === 200) { + try { + const data = JSON.parse(xhr.responseText); + resolve(data.files?.[0]?.name ?? file.name); + } catch { resolve(file.name); } + } else { + try { reject(new Error(JSON.parse(xhr.responseText).error)); } + catch { reject(new Error(`HTTP ${xhr.status}`)); } + } + }; + xhr.onerror = () => reject(new Error('Netzwerkfehler')); + xhr.send(form); + }); +} diff --git a/web/src/styles.css b/web/src/styles.css index e904884..4fa09d6 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1,725 +1,2000 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap'); +/* ═══════════════════════════════════════════════════════════════ + JUKEBOX — Discord Soundboard + Design: Discord-style dark theme with Blurple accent + Font: DM Sans (display + body) + ═══════════════════════════════════════════════════════════════ */ -:root { color-scheme: dark; --range-track-h: 8px; --range-thumb-d: 20px; } -* { box-sizing: border-box; } -[data-theme="dark"] body, -body { +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&display=swap'); + +/* ──────────────────────────────────────────── + Theme Variables — Default (Discord Blurple) + ──────────────────────────────────────────── */ +:root { + --bg-deep: #1a1b1e; + --bg-primary: #1e1f22; + --bg-secondary: #2b2d31; + --bg-tertiary: #313338; + --bg-modifier-hover: rgba(79, 84, 92, .16); + --bg-modifier-active: rgba(79, 84, 92, .24); + --bg-modifier-selected: rgba(79, 84, 92, .32); + + --text-normal: #dbdee1; + --text-muted: #949ba4; + --text-faint: #6d6f78; + + --accent: #5865f2; + --accent-rgb: 88, 101, 242; + --accent-hover: #4752c4; + --accent-glow: rgba(88, 101, 242, .45); + + --green: #23a55a; + --red: #f23f42; + --yellow: #f0b232; + --white: #ffffff; + + --font: 'DM Sans', 'Outfit', 'gg sans', 'Noto Sans', Whitney, 'Helvetica Neue', Helvetica, Arial, sans-serif; + --radius: 8px; + --radius-lg: 12px; + --shadow-low: 0 1px 3px rgba(0, 0, 0, .24); + --shadow-med: 0 4px 12px rgba(0, 0, 0, .32); + --shadow-high: 0 8px 24px rgba(0, 0, 0, .4); + --transition: 150ms cubic-bezier(.4, 0, .2, 1); + + --card-size: 110px; + --card-emoji: 28px; + --card-font: 11px; + + color-scheme: dark; +} + +/* ── Theme: Midnight Purple ── */ +[data-theme="purple"] { + --bg-deep: #13111c; + --bg-primary: #1a1726; + --bg-secondary: #241f35; + --bg-tertiary: #2e2845; + --accent: #9b59b6; + --accent-rgb: 155, 89, 182; + --accent-hover: #8e44ad; + --accent-glow: rgba(155, 89, 182, .45); +} + +/* ── Theme: Forest ── */ +[data-theme="forest"] { + --bg-deep: #0f1a14; + --bg-primary: #142119; + --bg-secondary: #1c2e22; + --bg-tertiary: #253a2c; + --accent: #2ecc71; + --accent-rgb: 46, 204, 113; + --accent-hover: #27ae60; + --accent-glow: rgba(46, 204, 113, .4); +} + +/* ── Theme: Sunset ── */ +[data-theme="sunset"] { + --bg-deep: #1a1210; + --bg-primary: #231815; + --bg-secondary: #2f201c; + --bg-tertiary: #3d2a24; + --accent: #e67e22; + --accent-rgb: 230, 126, 34; + --accent-hover: #d35400; + --accent-glow: rgba(230, 126, 34, .4); +} + +/* ── Theme: Ocean ── */ +[data-theme="ocean"] { + --bg-deep: #0a1628; + --bg-primary: #0f1e33; + --bg-secondary: #162a42; + --bg-tertiary: #1e3652; + --accent: #3498db; + --accent-rgb: 52, 152, 219; + --accent-hover: #2980b9; + --accent-glow: rgba(52, 152, 219, .4); +} + +/* ──────────────────────────────────────────── + Reset & Base + ──────────────────────────────────────────── */ +*, *::before, *::after { + box-sizing: border-box; margin: 0; - font-family: ui-sans-serif, system-ui, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; - background: - radial-gradient(1200px 800px at 15% -10%, rgba(99,102,241,.25), transparent 60%), - radial-gradient(1200px 800px at 110% 10%, rgba(168,85,247,.22), transparent 60%), - linear-gradient(180deg, #0b1020 0%, #0f1530 100%); - min-height: 100vh; - color: #e7e7ee; + padding: 0; } - -.gradient-text { background: -webkit-linear-gradient(45deg, #333, #555); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } - -/* Platz für linke Icons in allen Themes erzwingen */ -.input-field.pl-10 { padding-left: 2.5rem !important; } - -/* Rainbow Chaos Theme */ -[data-theme="rainbow"] body { - background: - radial-gradient(1200px 800px at 0% 0%, rgba(255,99,132,.35), transparent 60%), - radial-gradient(1200px 800px at 100% 0%, rgba(54,162,235,.35), transparent 60%), - radial-gradient(1200px 800px at 0% 100%, rgba(255,206,86,.35), transparent 60%), - radial-gradient(1200px 800px at 100% 100%, rgba(75,192,192,.35), transparent 60%), - linear-gradient(180deg, #101018 0%, #121226 100%); -} -[data-theme="rainbow"] .controls.glass, -[data-theme="rainbow"] .tabs.glass, -[data-theme="rainbow"] .select-trigger, -[data-theme="rainbow"] .control input, -[data-theme="rainbow"] .control select, -[data-theme="rainbow"] .sound { - /* Abgerundete Rainbow-Rahmen wie Buttons */ - border-radius: 14px; - border: 1px solid transparent; - background: - linear-gradient(135deg, rgba(255,255,255,.16), rgba(255,255,255,.08)) padding-box, - linear-gradient(90deg, #ff6384, #36a2eb, #ffce56, #4bc0c0, #9966ff) border-box; - background-clip: padding-box, border-box; -} -[data-theme="rainbow"] .sound, -[data-theme="rainbow"] .select-trigger, -[data-theme="rainbow"] .control input, -[data-theme="rainbow"] .control select { border-radius: 14px; } -[data-theme="rainbow"] .tab { border-radius: 999px; } -[data-theme="rainbow"] .tab.active { background: linear-gradient(90deg, #ff6384AA, #36a2ebAA, #ffce56AA, #4bc0c0AA, #9966ffAA); } -[data-theme="rainbow"] .tabs.glass { border: none; background: transparent; box-shadow: none; } - -/* Rainbow Chaos (Stitch) */ -@keyframes rainbow-bg { 0%{background-position:0% 50%} 50%{background-position:100% 50%} 100%{background-position:0% 50%} } -[data-theme="rainbow"] body { background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); background-size: 400% 400%; animation: rainbow-bg 15s ease infinite; } -[data-theme="rainbow"] .control-panel { background-color: rgba(30,30,30,.75); border: 1px solid #3a3a3c; backdrop-filter: blur(10px); } -[data-theme="rainbow"] .tag-btn { padding: 8px 16px; border-radius: 9999px; font-size: .875rem; font-weight: 500; background: rgba(44,44,44,.8); color: #a0a0a0; border:1px solid transparent; transition: transform .3s; cursor: pointer; text-shadow: 0 1px 2px rgba(0,0,0,.5) } -[data-theme="rainbow"] .tag-btn:hover { background: rgba(58,58,58,.9); color: #fff; transform: scale(1.1); } -[data-theme="rainbow"] .tag-btn.active { background: linear-gradient(45deg,#ff00ff,#00ffff); color: #fff; font-weight:700; border:1px solid #fff; box-shadow: 0 0 15px rgba(255,0,255,.7), 0 0 15px rgba(0,255,255,.7); } -[data-theme="rainbow"] .input-field { width:100%; background: rgba(44,44,44,.8); border:1px solid #3a3a3c; border-radius:.5rem; padding:.5rem 1rem; color:#e0e0e0; outline:none; } -[data-theme="rainbow"] .input-field:focus { box-shadow: 0 0 10px #23a6d5, 0 0 5px #e73c7e; border-color:#fff; } -[data-theme="rainbow"] .sound-btn { background: rgba(30,30,30,.75); border:1px solid #3a3a3c; box-shadow: 0 1px 2px rgba(0,0,0,.2); backdrop-filter: blur(10px); transition: transform .2s ease, box-shadow .2s; } -[data-theme="rainbow"] .sound-btn:hover { transform: translateY(-2px) scale(1.05); box-shadow: 0 8px 30px rgba(0,0,0,.5); border-color:#fff; } -[data-theme="rainbow"] .gradient-text { background: -webkit-linear-gradient(45deg,#ff8a00,#e52e71,#9c27b0); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; text-shadow: 0 0 10px rgba(255,255,255,.2) } - -/* 420 Theme - Cannabis/Trippy */ -[data-theme="420"] body { - background: - radial-gradient(1200px 800px at 20% -20%, rgba(34,197,94,.4), transparent 60%), - radial-gradient(1200px 800px at 80% 20%, rgba(74,222,128,.3), transparent 60%), - radial-gradient(1200px 800px at 40% 80%, rgba(22,163,74,.3), transparent 60%), - linear-gradient(135deg, #22c55e, #16a34a, #15803d); - background-size: 400% 400%; - animation: cannabis-bg 20s ease infinite; - color: #f0fdf4; - font-family: 'Poppins', ui-sans-serif, system-ui, Segoe UI, Roboto, Helvetica, Arial; - position: relative; - overflow-x: hidden; -} - -/* Floating Broccoli Animation */ -[data-theme="420"]::before { - content: ''; - position: fixed; - top: 0; - left: 0; - width: 100%; +html, body { height: 100%; - pointer-events: none; - z-index: 1; + overflow: hidden; + background: var(--bg-deep); + color: var(--text-normal); + font-family: var(--font); + font-size: 14px; + line-height: 1.4; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + transition: background .4s ease, color .4s ease; } -/* Broccoli Elements */ -[data-theme="420"] .broccoli { - position: fixed; - font-size: 2rem; - opacity: 0.6; - z-index: 1; - pointer-events: none; - animation: float-broccoli 15s linear infinite; +button { + font-family: inherit; + cursor: pointer; + border: none; + background: none; + color: inherit; } -[data-theme="420"] .broccoli:nth-child(1) { - top: 10%; - left: 10%; - animation-delay: 0s; - animation-duration: 18s; +input, select { + font-family: inherit; + color: inherit; } -[data-theme="420"] .broccoli:nth-child(2) { - top: 20%; - right: 15%; - animation-delay: 0s; - animation-duration: 22s; +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--bg-tertiary); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #3f4147; } + +::selection { + background: var(--accent); + color: white; } -[data-theme="420"] .broccoli:nth-child(3) { - bottom: 25%; - left: 20%; - animation-delay: 0s; - animation-duration: 20s; -} - -[data-theme="420"] .broccoli:nth-child(4) { - bottom: 15%; - right: 10%; - animation-delay: 0s; - animation-duration: 25s; -} - -[data-theme="420"] .broccoli:nth-child(5) { - top: 50%; - left: 5%; - animation-delay: 0s; - animation-duration: 16s; -} - -[data-theme="420"] .broccoli:nth-child(6) { - top: 30%; - right: 5%; - animation-delay: 0s; - animation-duration: 19s; -} - -/* Broccoli Bounce Animation */ -@keyframes float-broccoli { - 0% { - transform: translate(0, 0) rotate(0deg); - } - 25% { - transform: translate(100px, -50px) rotate(90deg); - } - 50% { - transform: translate(200px, 0px) rotate(180deg); - } - 75% { - transform: translate(100px, 50px) rotate(270deg); - } - 100% { - transform: translate(0, 0) rotate(360deg); - } -} - -/* Ensure content stays above broccoli */ -[data-theme="420"] .container { +/* ──────────────────────────────────────────── + App Layout + ──────────────────────────────────────────── */ +.app { + display: flex; + flex-direction: column; + height: 100vh; position: relative; +} + +/* ──────────────────────────────────────────── + Top Bar + ──────────────────────────────────────────── */ +.topbar { + display: flex; + align-items: center; + padding: 0 20px; + height: 52px; + background: var(--bg-secondary); + border-bottom: 1px solid rgba(0, 0, 0, .24); z-index: 10; + flex-shrink: 0; + gap: 16px; + transition: background .4s ease; } -@keyframes cannabis-bg { - 0% { background-position: 0% 50% } - 50% { background-position: 100% 50% } - 100% { background-position: 0% 50% } +.topbar-left { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; } -[data-theme="420"] .control-panel { - background-color: rgba(17, 24, 39, 0.6); - border: 1px solid #4ade80; - backdrop-filter: blur(10px); +.app-logo { + width: 28px; + height: 28px; + background: var(--accent); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + transition: background .4s ease; } -[data-theme="420"] .tag-btn { - padding: 8px 16px; - border-radius: 9999px; - font-size: .875rem; - font-weight: 500; - background: rgba(17, 24, 39, 0.7); - color: #d1fae5; - border: 1px solid #4ade80; - transition: all .3s ease; +.app-title { + font-size: 16px; + font-weight: 700; + color: var(--white); + letter-spacing: -.02em; +} + +/* ── Clock ── */ +.clock-wrap { + flex: 1; + display: flex; + justify-content: center; +} + +.clock { + font-size: 22px; + font-weight: 700; + color: var(--text-normal); + letter-spacing: .02em; + font-variant-numeric: tabular-nums; + opacity: .9; +} + +.clock-seconds { + font-size: 14px; + color: var(--text-faint); + font-weight: 500; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +/* ── Channel Dropdown ── */ +.channel-dropdown { + position: relative; + flex-shrink: 0; +} + +.channel-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 12px 5px 10px; + border: 1px solid rgba(255, 255, 255, .08); + border-radius: 20px; + background: var(--bg-tertiary); + color: var(--text-normal); + font-family: var(--font); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all var(--transition); + white-space: nowrap; +} + +.channel-btn:hover { + background: var(--bg-modifier-selected); + border-color: rgba(255, 255, 255, .12); +} + +.channel-btn.open { + border-color: var(--accent); +} + +.channel-btn .cb-icon { + font-size: 16px; + color: var(--text-muted); +} + +.channel-btn .chevron { + font-size: 12px; + color: var(--text-faint); + transition: transform var(--transition); + margin-left: 2px; +} + +.channel-btn.open .chevron { + transform: rotate(180deg); +} + +.channel-status { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--green); + flex-shrink: 0; +} + +.channel-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + min-width: 220px; + background: var(--bg-deep); + border: 1px solid rgba(255, 255, 255, .08); + border-radius: var(--radius); + box-shadow: var(--shadow-high); + padding: 6px; + z-index: 100; + animation: ctx-in 100ms ease-out; +} + +.channel-menu-header { + padding: 6px 8px 4px; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--text-faint); +} + +.channel-option { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 10px; + border-radius: 4px; + font-size: 13px; + color: var(--text-muted); + cursor: pointer; + transition: all var(--transition); +} + +.channel-option:hover { + background: var(--bg-modifier-hover); + color: var(--text-normal); +} + +.channel-option.active { + background: var(--accent); + color: var(--white); +} + +.channel-option .co-icon { + font-size: 16px; + opacity: .7; +} + +/* ── Connection Indicator ── */ +.connection { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 20px; + background: rgba(35, 165, 90, .12); + font-size: 12px; + color: var(--green); + font-weight: 600; +} + +.conn-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--green); + box-shadow: 0 0 6px rgba(35, 165, 90, .6); + animation: pulse-dot 2s ease-in-out infinite; +} + +@keyframes pulse-dot { + 0%, 100% { box-shadow: 0 0 6px rgba(35, 165, 90, .5); } + 50% { box-shadow: 0 0 12px rgba(35, 165, 90, .8); } +} + +/* ── Admin Icon Button ── */ +.admin-btn-icon { + width: 32px; + height: 32px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-faint); + transition: all var(--transition); + font-size: 18px; +} + +.admin-btn-icon:hover { + background: var(--bg-modifier-hover); + color: var(--text-normal); +} + +.admin-btn-icon.active { + color: var(--accent); +} + +/* ──────────────────────────────────────────── + Toolbar + ──────────────────────────────────────────── */ +.toolbar { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 20px; + background: var(--bg-primary); + border-bottom: 1px solid rgba(0, 0, 0, .12); + flex-shrink: 0; + flex-wrap: wrap; + transition: background .4s ease; +} + +/* ── Category Tabs ── */ +.cat-tabs { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +.cat-tab { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + border-radius: 20px; + background: var(--bg-tertiary); + color: var(--text-muted); + font-family: var(--font); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all var(--transition); + white-space: nowrap; +} + +.cat-tab:hover { + background: var(--bg-modifier-selected); + color: var(--text-normal); +} + +.cat-tab.active { + background: var(--accent); + color: var(--white); +} + +.tab-count { + font-size: 10px; + font-weight: 700; + background: rgba(255, 255, 255, .15); + padding: 0 6px; + border-radius: 8px; + line-height: 1.6; +} + +/* ── Search ── */ +.search-wrap { + position: relative; + flex: 1; + max-width: 280px; + min-width: 140px; +} + +.search-wrap .search-icon { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + font-size: 15px; + color: var(--text-faint); + pointer-events: none; +} + +.search-input { + width: 100%; + height: 32px; + padding: 0 28px 0 32px; + border: 1px solid rgba(255, 255, 255, .06); + border-radius: 20px; + background: var(--bg-secondary); + color: var(--text-normal); + font-family: var(--font); + font-size: 13px; + outline: none; + transition: all var(--transition); +} + +.search-input::placeholder { + color: var(--text-faint); +} + +.search-input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-glow); +} + +.search-clear { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + width: 20px; + height: 20px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-faint); + transition: all var(--transition); +} + +.search-clear:hover { + background: var(--bg-tertiary); + color: var(--text-normal); +} + +.toolbar-spacer { + flex: 1; +} + +/* ── URL Import ── */ +.url-import-wrap { + display: flex; + align-items: center; + gap: 6px; + min-width: 240px; + max-width: 460px; + flex: 1; + padding: 4px 6px 4px 8px; + border-radius: 20px; + background: var(--bg-secondary); + border: 1px solid rgba(255, 255, 255, .08); +} + +.url-import-icon { + font-size: 15px; + color: var(--text-faint); + flex-shrink: 0; +} + +.url-import-input { + flex: 1; + min-width: 0; + height: 26px; + border: none; + background: transparent; + color: var(--text-normal); + font-size: 12px; + font-family: var(--font); + outline: none; +} + +.url-import-input::placeholder { + color: var(--text-faint); +} + +.url-import-btn { + height: 24px; + padding: 0 10px; + border-radius: 14px; + border: 1px solid rgba(var(--accent-rgb, 88, 101, 242), .45); + background: rgba(var(--accent-rgb, 88, 101, 242), .12); + color: var(--accent); + font-size: 11px; + font-weight: 700; + white-space: nowrap; + transition: all var(--transition); +} + +.url-import-btn:hover { + background: var(--accent); + border-color: var(--accent); + color: var(--white); +} + +.url-import-btn:disabled { + opacity: .5; + pointer-events: none; +} + +/* ── Toolbar Buttons ── */ +.tb-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: 1px solid rgba(255, 255, 255, .08); + border-radius: 20px; + background: var(--bg-tertiary); + color: var(--text-muted); + font-family: var(--font); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all var(--transition); + white-space: nowrap; +} + +.tb-btn:hover { + background: var(--bg-modifier-selected); + color: var(--text-normal); + border-color: rgba(255, 255, 255, .12); +} + +.tb-btn .tb-icon { + font-size: 15px; +} + +.tb-btn.random { + border-color: rgba(88, 101, 242, .3); + color: var(--accent); +} + +.tb-btn.random:hover { + background: var(--accent); + color: var(--white); + border-color: var(--accent); +} + +.tb-btn.party { + border-color: rgba(240, 178, 50, .3); + color: var(--yellow); +} + +.tb-btn.party:hover { + background: var(--yellow); + color: #1a1b1e; + border-color: var(--yellow); +} + +.tb-btn.party.active { + background: var(--yellow); + color: #1a1b1e; + border-color: var(--yellow); + animation: party-btn 600ms ease-in-out infinite alternate; +} + +@keyframes party-btn { + from { box-shadow: 0 0 8px rgba(240, 178, 50, .4); } + to { box-shadow: 0 0 20px rgba(240, 178, 50, .7); } +} + +.tb-btn.stop { + border-color: rgba(242, 63, 66, .3); + color: var(--red); +} + +.tb-btn.stop:hover { + background: var(--red); + color: var(--white); + border-color: var(--red); +} + +/* ── Size Slider ── */ +.size-control { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 20px; + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, .06); +} + +.size-control .sc-icon { + font-size: 14px; + color: var(--text-faint); +} + +.size-slider { + -webkit-appearance: none; + appearance: none; + width: 70px; + height: 3px; + border-radius: 2px; + background: var(--bg-modifier-selected); + outline: none; cursor: pointer; } -[data-theme="420"] .tag-btn:hover { - background: rgba(34, 197, 94, 0.2); - color: #f0fdf4; - transform: scale(1.05); +.size-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent); + cursor: pointer; + transition: transform var(--transition); } -[data-theme="420"] .tag-btn.active { - background: #22c55e; - color: white; - font-weight: 600; - border-color: #22c55e; - box-shadow: 0 0 15px rgba(34, 197, 94, 0.5); +.size-slider::-webkit-slider-thumb:hover { + transform: scale(1.3); } -[data-theme="420"] .input-field { - width: 100%; - background: rgba(30, 41, 59, 0.7); - border: 2px solid transparent; - border-radius: .5rem; - padding: .5rem 1rem; - color: #d1fae5; - outline: none; - transition: all .3s ease; +.size-slider::-moz-range-thumb { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent); + border: none; + cursor: pointer; } -[data-theme="420"] .input-field:focus { - border-color: #4ade80; - box-shadow: 0 0 0 3px rgba(74, 222, 128, 0.25); +/* ── Theme Selector ── */ +.theme-selector { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: 20px; + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, .06); } -[data-theme="420"] .sound-btn { - background: rgba(17, 24, 39, 0.7); - border: 1px solid #4ade80; - box-shadow: 0 1px 2px rgba(0,0,0,.2); - backdrop-filter: blur(10px); - transition: all .3s ease; - color: #d1fae5; -} - -[data-theme="420"] .sound-btn:hover { - background: #22c55e; - color: white; - transform: translateY(-2px) scale(1.02); - box-shadow: 0 8px 25px rgba(34, 197, 94, 0.4); -} - -[data-theme="420"] .gradient-text { - background: -webkit-linear-gradient(45deg, #22c55e, #4ade80, #16a34a); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - text-shadow: 0 0 10px rgba(34, 197, 94, 0.3); -} - -/* 420 Theme Header */ -[data-theme="420"] header { - background: rgba(17, 24, 39, 0.6); - border: 1px solid #4ade80; - backdrop-filter: blur(15px); - color: #f0fdf4; -} - -/* 420 Theme Volume Slider */ -[data-theme="420"] .volume-slider { - --range-accent: #22c55e; - --range-track-bg: rgba(30, 41, 59, 0.7); - accent-color: #22c55e; - border-radius: 5px; - height: 8px; - border: 1px solid #4ade80; -} - -[data-theme="420"] .volume-slider::-webkit-slider-thumb { - background: linear-gradient(45deg, #22c55e, #4ade80); - box-shadow: 0 0 10px rgba(34, 197, 94, 0.5); -} - -[data-theme="420"] .volume-slider::-moz-range-thumb { - background: linear-gradient(45deg, #22c55e, #4ade80); - box-shadow: 0 0 10px rgba(34, 197, 94, 0.5); -} - -/* 420 Theme Checkbox */ -[data-theme="420"] input[type="checkbox"] { - accent-color: #22c55e; +.theme-dot { width: 16px; height: 16px; - border-radius: 3px; - border: 2px solid #4ade80; - background: rgba(17, 24, 39, 0.7); + border-radius: 50%; + cursor: pointer; + transition: all var(--transition); + border: 2px solid transparent; +} + +.theme-dot:hover { + transform: scale(1.2); +} + +.theme-dot.active { + border-color: var(--white); + box-shadow: 0 0 6px rgba(255, 255, 255, .3); +} + +/* ── Analytics Strip ── */ +.analytics-strip { + display: flex; + align-items: stretch; + gap: 8px; + padding: 8px 20px; + background: var(--bg-primary); + border-bottom: 1px solid rgba(0, 0, 0, .12); + flex-shrink: 0; +} + +.analytics-card { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 12px; + background: var(--bg-secondary); + border: 1px solid rgba(255, 255, 255, .08); +} + +.analytics-card.analytics-wide { + flex: 1; + min-width: 0; +} + +.analytics-icon { + font-size: 18px; + color: var(--accent); + flex-shrink: 0; +} + +.analytics-copy { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.analytics-label { + font-size: 10px; + font-weight: 700; + letter-spacing: .04em; + text-transform: uppercase; + color: var(--text-faint); +} + +.analytics-value { + font-size: 18px; + line-height: 1; + font-weight: 800; + color: var(--text-normal); +} + +.analytics-top-list { + display: flex; + align-items: center; + gap: 6px; + overflow-x: auto; + scrollbar-width: none; +} + +.analytics-top-list::-webkit-scrollbar { + display: none; +} + +.analytics-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 8px; + border-radius: 999px; + background: rgba(var(--accent-rgb, 88, 101, 242), .15); + color: var(--accent); + font-size: 11px; + font-weight: 600; + white-space: nowrap; +} + +.analytics-muted { + color: var(--text-muted); + font-size: 12px; +} + +/* ──────────────────────────────────────────── + Category / Folder Strip + ──────────────────────────────────────────── */ +.category-strip { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 20px; + background: var(--bg-primary); + border-bottom: 1px solid rgba(0, 0, 0, .12); + overflow-x: auto; + flex-shrink: 0; + scrollbar-width: none; + transition: background .4s ease; +} + +.category-strip::-webkit-scrollbar { + display: none; +} + +.cat-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + background: var(--bg-secondary); + border: 1px solid rgba(255, 255, 255, .06); + white-space: nowrap; + cursor: pointer; + transition: all var(--transition); + flex-shrink: 0; +} + +.cat-chip:hover { + border-color: rgba(255, 255, 255, .12); + color: var(--text-normal); + background: var(--bg-tertiary); +} + +.cat-chip.active { + background: rgba(88, 101, 242, .1); +} + +.cat-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.cat-count { + font-size: 10px; + font-weight: 700; + opacity: .5; +} + +/* ──────────────────────────────────────────── + Main Grid Area + ──────────────────────────────────────────── */ +.main { + flex: 1; + overflow-y: auto; + padding: 16px 20px; + background: var(--bg-primary); + transition: background .4s ease; +} + +.sound-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--card-size), 1fr)); + gap: 8px; +} + +/* ──────────────────────────────────────────── + Sound Card + ──────────────────────────────────────────── */ +.sound-card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3px; + padding: 12px 6px 8px; + background: var(--bg-secondary); + border-radius: var(--radius-lg); + cursor: pointer; + transition: all var(--transition); + border: 2px solid transparent; + user-select: none; + overflow: hidden; + aspect-ratio: 1; + opacity: 0; + animation: card-enter 350ms ease-out forwards; +} + +.sound-card::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + opacity: 0; + transition: opacity var(--transition); + background: radial-gradient(ellipse at center, var(--accent-glow) 0%, transparent 70%); + pointer-events: none; +} + +.sound-card:hover { + background: var(--bg-tertiary); + transform: translateY(-3px); + box-shadow: var(--shadow-med), 0 0 20px var(--accent-glow); + border-color: rgba(88, 101, 242, .2); +} + +.sound-card:hover::before { + opacity: 1; +} + +.sound-card:active { + transform: translateY(0); + transition-duration: 50ms; +} + +.sound-card.playing { + border-color: var(--accent); + animation: card-enter 350ms ease-out forwards, playing-glow 1.2s ease-in-out infinite alternate; +} + +@keyframes playing-glow { + from { box-shadow: 0 0 4px var(--accent-glow); } + to { box-shadow: 0 0 16px var(--accent-glow); } +} + +@keyframes card-enter { + from { opacity: 0; transform: translateY(10px) scale(.97); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +/* ── Ripple Effect ── */ +.ripple { + position: absolute; + border-radius: 50%; + background: rgba(88, 101, 242, .3); + transform: scale(0); + animation: ripple-expand 500ms ease-out forwards; + pointer-events: none; +} + +@keyframes ripple-expand { + to { transform: scale(3); opacity: 0; } +} + +/* ── Sound Card Content ── */ +.sound-emoji { + font-size: var(--card-emoji); + font-weight: 800; + line-height: 1; + z-index: 1; + transition: transform var(--transition); + opacity: .7; + font-family: 'Syne', 'DM Sans', sans-serif; +} + +.sound-card:hover .sound-emoji { + transform: scale(1.15); + opacity: 1; +} + +.sound-card.playing .sound-emoji { + animation: emoji-bounce 400ms ease; + opacity: 1; +} + +@keyframes emoji-bounce { + 0%, 100% { transform: scale(1); } + 40% { transform: scale(1.3); } + 70% { transform: scale(.95); } +} + +.sound-name { + font-size: var(--card-font); + font-weight: 600; + text-align: center; + color: var(--text-normal); + z-index: 1; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0 4px; +} + +.sound-duration { + font-size: 9px; + color: var(--text-faint); + z-index: 1; + font-weight: 500; +} + +/* ── Favorite Star ── */ +.fav-star { + position: absolute; + top: 4px; + right: 4px; + opacity: 0; + transition: all var(--transition); + cursor: pointer; + z-index: 2; + color: var(--text-faint); + padding: 2px; + line-height: 1; +} + +.fav-star .fav-icon { + font-size: 14px; +} + +.sound-card:hover .fav-star { + opacity: .6; +} + +.fav-star:hover { + opacity: 1 !important; + color: var(--yellow); + transform: scale(1.2); +} + +.fav-star.active { + opacity: 1 !important; + color: var(--yellow); +} + +/* ── "New" Badge ── */ +.new-badge { + position: absolute; + top: 4px; + left: 4px; + font-size: 8px; + font-weight: 700; + background: var(--green); + color: white; + padding: 1px 5px; + border-radius: 6px; + text-transform: uppercase; + letter-spacing: .03em; + z-index: 2; +} + +/* ── Playing Wave Indicator ── */ +.playing-indicator { + position: absolute; + bottom: 3px; + left: 50%; + transform: translateX(-50%); + display: none; + gap: 2px; + align-items: flex-end; + height: 10px; +} + +.sound-card.playing .playing-indicator { + display: flex; +} + +.wave-bar { + width: 2px; + background: var(--accent); + border-radius: 1px; + animation: wave 600ms ease-in-out infinite alternate; +} + +.wave-bar:nth-child(1) { height: 3px; animation-delay: 0ms; } +.wave-bar:nth-child(2) { height: 7px; animation-delay: 150ms; } +.wave-bar:nth-child(3) { height: 5px; animation-delay: 300ms; } +.wave-bar:nth-child(4) { height: 9px; animation-delay: 100ms; } + +@keyframes wave { + from { height: 2px; } + to { height: 10px; } +} + +/* ──────────────────────────────────────────── + Empty State + ──────────────────────────────────────────── */ +.empty-state { + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + padding: 60px 20px; + text-align: center; +} + +.empty-state.visible { + display: flex; +} + +.empty-emoji { + font-size: 42px; +} + +.empty-title { + font-size: 15px; + font-weight: 700; + color: var(--text-normal); +} + +.empty-desc { + font-size: 13px; + color: var(--text-muted); + max-width: 260px; +} + +/* ── Now Playing (Topbar) ── */ +.now-playing { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 20px; + background: rgba(var(--accent-rgb, 88, 101, 242), .12); + border: 1px solid rgba(var(--accent-rgb, 88, 101, 242), .2); + font-size: 12px; + color: var(--text-muted); + max-width: 200px; + min-width: 0; + animation: np-fade-in 300ms ease; +} + +@keyframes np-fade-in { + from { opacity: 0; transform: translateX(10px); } + to { opacity: 1; transform: translateX(0); } +} + +.np-name { + color: var(--accent); + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.np-waves { + display: none; + gap: 1.5px; + align-items: flex-end; + height: 12px; + flex-shrink: 0; +} + +.np-waves.active { + display: flex; +} + +.np-wave-bar { + width: 2px; + background: var(--accent); + border-radius: 1px; + animation: wave 500ms ease-in-out infinite alternate; +} + +.np-wave-bar:nth-child(1) { height: 3px; animation-delay: 0ms; } +.np-wave-bar:nth-child(2) { height: 8px; animation-delay: 120ms; } +.np-wave-bar:nth-child(3) { height: 5px; animation-delay: 240ms; } +.np-wave-bar:nth-child(4) { height: 10px; animation-delay: 80ms; } + +/* ── Volume Control (Toolbar) ── */ +.volume-control { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 20px; + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, .06); +} + +.vol-icon { + font-size: 16px; + color: var(--text-faint); + cursor: pointer; + transition: color var(--transition); + user-select: none; +} + +.vol-icon:hover { + color: var(--text-normal); +} + +.vol-slider { + -webkit-appearance: none; + appearance: none; + width: 80px; + height: 3px; + border-radius: 2px; + background: linear-gradient(to right, var(--accent) 0%, var(--accent) var(--vol, 80%), var(--bg-modifier-selected) var(--vol, 80%)); + outline: none; cursor: pointer; } -[data-theme="420"] input[type="checkbox"]:checked { - background: #22c55e; - border-color: #22c55e; - box-shadow: 0 0 10px rgba(34, 197, 94, 0.3); +.vol-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent); + cursor: pointer; + transition: transform var(--transition); } -/* Dark (Stitch) */ -[data-theme="dark"] .control-panel { background-color:#1e1e1e; border:1px solid #3a3a3c } -[data-theme="dark"] .tag-btn { padding:8px 16px; border-radius:9999px; font-size:.875rem; font-weight:500; background:#2c2c2c; color:#a0a0a0; border:1px solid transparent; } -[data-theme="dark"] .tag-btn:hover { background:#3a3a3a; color:#e0e0e0 } -[data-theme="dark"] .tag-btn.active { background:#0a84ff; color:#fff; font-weight:600 } -[data-theme="dark"] .input-field { width:100%; background:#2c2c2c; border:1px solid #3a3a3c; border-radius:.5rem; padding:.5rem 1rem; color:#e0e0e0 } -[data-theme="dark"] .sound-btn { background:#1e1e1e; border:1px solid #3a3a3c; box-shadow:0 1px 2px rgba(0,0,0,.2) } -[data-theme="dark"] .sound-btn:hover { transform: translateY(-1px); box-shadow:0 4px 12px rgba(0,0,0,.4); border-color:#0a84ff } -[data-theme="dark"] .gradient-text { background: -webkit-linear-gradient(45deg,#e0e0e0,#a0a0a0); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent } - -.container { width: 90vw; max-width: none; margin: 0 auto; padding: 28px; } -/* Eigener Container ohne Tailwind-Kollision */ -.page-container { width: 90vw; max-width: none; margin: 0 auto; padding: 28px; } - -/* Nightly Build: volle Breite (mind. 90% der Anzeige), kein max-width-Limit */ -[data-build="nightly"] .container { - width: 90vw; - max-width: none; +.vol-slider::-webkit-slider-thumb:hover { + transform: scale(1.3); } -/* Partymode (ehem. CHAOS) Button Regenbogen-Animation */ -.chaos-rainbow { - background: linear-gradient(45deg, #ff0000, #ff8000, #ffff00, #80ff00, #00ff00, #00ff80, #00ffff, #0080ff, #0000ff, #8000ff, #ff00ff, #ff0080); +.vol-slider::-moz-range-thumb { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent); + border: none; + cursor: pointer; +} + +.vol-pct { + font-size: 11px; + color: var(--text-faint); + min-width: 28px; + text-align: right; + font-variant-numeric: tabular-nums; +} + +/* ──────────────────────────────────────────── + Party Mode Overlay + ──────────────────────────────────────────── */ +.party-overlay { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 50; + opacity: 0; + transition: opacity .3s ease; +} + +.party-overlay.active { + opacity: 1; + animation: party-hue 2s linear infinite; +} + +.party-overlay::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(45deg, + rgba(255, 0, 0, .04), + rgba(0, 255, 0, .04), + rgba(0, 0, 255, .04), + rgba(255, 255, 0, .04) + ); background-size: 400% 400%; - animation: chaos-rainbow-animation 2s ease-in-out infinite; + animation: party-grad 3s ease infinite; } -@keyframes chaos-rainbow-animation { +@keyframes party-grad { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } -/* Rainbow-Flash entfernt */ - -/* Neuer Header-Style basierend auf Google Stitch Design */ -header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 24px; - margin-bottom: 18px; - background: rgba(255, 255, 255, 0.05); - border-radius: 16px; - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); +@keyframes party-hue { + to { filter: hue-rotate(360deg); } } - - -/* Rainbow Theme Header */ -[data-theme="rainbow"] header { - background: rgba(30, 30, 30, 0.4); - border: 1px solid transparent; - backdrop-filter: blur(20px); - background: - linear-gradient(135deg, rgba(255,255,255,.1), rgba(255,255,255,.05)) padding-box, - linear-gradient(90deg, #ff6384, #36a2eb, #ffce56, #4bc0c0, #9966ff) border-box; - background-clip: padding-box, border-box; - color: #ffffff; -} -/* Rainbow Header: Sekundärtexte sichtbar machen */ -[data-theme="rainbow"] header .text-gray-400 { color: #ffffff !important; opacity: 1 !important; } -/* Header-Titel und Uhrzeit */ -header h1 { - margin: 0; - font-weight: 800; - letter-spacing: .3px; - color: inherit; +/* ──────────────────────────────────────────── + Context Menu + ──────────────────────────────────────────── */ +.ctx-menu { + position: fixed; + min-width: 160px; + background: var(--bg-deep); + border: 1px solid rgba(255, 255, 255, .06); + border-radius: var(--radius); + box-shadow: var(--shadow-high); + padding: 4px; + z-index: 1000; + animation: ctx-in 100ms ease-out; } -/* Titel-Effekte pro Theme */ -[data-theme="rainbow"] .site-title { - background: linear-gradient(45deg, #ff0000, #ff8000, #ffff00, #80ff00, #00ff00, #00ff80, #00ffff, #0080ff, #0000ff, #8000ff, #ff00ff, #ff0080); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - background-size: 400% 400%; - animation: chaos-rainbow-animation 2s ease-in-out infinite; +@keyframes ctx-in { + from { opacity: 0; transform: scale(.96) translateY(-4px); } + to { opacity: 1; transform: scale(1) translateY(0); } } -[data-theme="420"] .site-title { - color: #22C55E; +.ctx-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-radius: 4px; + font-size: 13px; + color: var(--text-normal); + cursor: pointer; + transition: all var(--transition); } -/* Nightly Badge Farbe (Fallback) */ -.nightly-badge { color: #ff4d4f; } +.ctx-item:hover { + background: var(--accent); + color: var(--white); +} -header p { - margin: 0; - opacity: .9; - color: inherit; +.ctx-item.danger { + color: var(--red); } -.clock { - font-size: 48px; - font-weight: 800; - letter-spacing: 1px; - line-height: 1; + +.ctx-item.danger:hover { + background: var(--red); + color: var(--white); } -.badge { - display: inline-flex; + +.ctx-item .ctx-icon { + font-size: 15px; +} + +.ctx-sep { + height: 1px; + background: rgba(255, 255, 255, .06); + margin: 3px 8px; +} + +/* ──────────────────────────────────────────── + Toast Notification + ──────────────────────────────────────────── */ +.toast { + position: fixed; + bottom: 64px; + left: 50%; + transform: translateX(-50%); + padding: 10px 20px; + border-radius: 20px; + font-size: 13px; + font-weight: 600; + z-index: 100; + display: flex; + align-items: center; + gap: 8px; + box-shadow: var(--shadow-high); + animation: toast-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); + pointer-events: none; +} + +.toast .toast-icon { + font-size: 16px; +} + +.toast.error { + background: var(--red); + color: white; +} + +.toast.info { + background: var(--green); + color: white; +} + +@keyframes toast-in { + from { transform: translate(-50%, 16px); opacity: 0; } + to { transform: translate(-50%, 0); opacity: 1; } +} + +/* ──────────────────────────────────────────── + Admin Panel Overlay + ──────────────────────────────────────────── */ +.admin-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, .6); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + z-index: 60; + display: flex; align-items: center; justify-content: center; - padding: 8px 12px; /* wie .tab */ - border-radius: 999px; - border: 1px solid rgba(255,255,255,.25); - background: linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,.06)); - color: #e7e7ee; - font-size: .875rem; /* 14px */ + animation: fade-in 200ms ease; +} + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +.admin-panel { + background: var(--bg-secondary); + border: 1px solid rgba(255, 255, 255, .08); + border-radius: var(--radius-lg); + padding: 28px; + width: 92%; + max-width: 920px; + max-height: min(88vh, 860px); + display: flex; + flex-direction: column; + box-shadow: var(--shadow-high); +} + +.admin-panel h3 { + font-size: 18px; + font-weight: 700; + margin-bottom: 16px; + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; +} + +.admin-close { + width: 28px; + height: 28px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + transition: all var(--transition); +} + +.admin-close:hover { + background: var(--bg-tertiary); + color: var(--text-normal); +} + +.admin-field { + margin-bottom: 16px; +} + +.admin-field label { + display: block; + font-size: 12px; font-weight: 600; + color: var(--text-muted); + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: .5px; +} + +.admin-field input { + width: 100%; + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, .08); + border-radius: 8px; + padding: 10px 12px; + font-size: 14px; + color: var(--text-normal); + font-family: var(--font); + transition: all var(--transition); +} + +.admin-field input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-glow); +} + +.admin-btn-action { + padding: 10px 20px; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + font-family: var(--font); + cursor: pointer; + transition: all var(--transition); line-height: 1; } -.controls { display: grid; grid-template-columns: 1fr minmax(240px, 320px) 260px 200px; gap: 12px; align-items: center; margin-bottom: 12px; } -.controls.row1 { z-index: 5000; } -.controls.row2 { grid-template-columns: minmax(400px, 1fr); z-index: 3000; } -.controls.row3 { grid-template-columns: auto auto; justify-content: flex-start; z-index: 2000; gap: 8px; } -.controls.glass { padding: 18px; position: relative; z-index: inherit; overflow: visible; } -.controls.glass { - backdrop-filter: saturate(140%) blur(20px); - background: linear-gradient(135deg, rgba(255,255,255,.14), rgba(255,255,255,.06)); - border: 1px solid rgba(255,255,255,.28); - border-right-color: rgba(255,255,255,.18); - border-bottom-color: rgba(255,255,255,.18); - padding: 14px; - border-radius: 18px; - box-shadow: 0 20px 40px rgba(0,0,0,.35), inset 0 1px 0 rgba(255,255,255,.25); -} -.control input, .control select { - width: 100%; - padding: 12px 14px; /* Standard ohne Icon */ - border-radius: 14px; - border: 1px solid rgba(255,255,255,.25); - background: linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,.06)); - color: #fff; - backdrop-filter: blur(18px); - box-shadow: inset 0 1px 0 rgba(255,255,255,.2); -} -.control select option { background-color: #0f1530; color: #e7e7ee; } -.control select optgroup { background-color: #0f1530; color: #c8c8d8; } - -/* Custom Select */ -.custom-select { position: relative; z-index: 10000; } -.select-trigger { - width: 100%; - text-align: left; - padding: 12px 14px; /* Standard ohne Icon */ - border-radius: 14px; - border: 1px solid rgba(255,255,255,.32); - background: linear-gradient(180deg, rgba(255,255,255,.22), rgba(255,255,255,.12)); - color: #e7e7ee; - backdrop-filter: blur(18px); - box-shadow: inset 0 1px 0 rgba(255,255,255,.2); -} -.select-trigger .chev { float: right; opacity: .8; } -.select-menu { - position: absolute; inset: auto 0 auto 0; top: calc(100% + 6px); - border-radius: 12px; - overflow: hidden; - border: 1px solid rgba(255,255,255,.28); - background: #0f1530; - box-shadow: 0 24px 48px rgba(0,0,0,.5); - max-height: 280px; overflow-y: auto; - z-index: 20000; -} -/* Emoji-Picker entfernt */ -.emoji-picker button:hover { filter: brightness(1.2); } -.select-item { - width: 100%; text-align: left; padding: 10px 12px; color: #e7e7ee; - background: transparent; border: 0; -} -.select-item:hover { background: rgba(255,255,255,.08); color: #fff; } -.select-item.active { background: rgba(255,255,255,.14); color: #fff; } - -/* Theme Select */ -.control.theme select { - padding: 12px 14px; - border-radius: 14px; - border: 1px solid rgba(255,255,255,.32); - background: linear-gradient(180deg, rgba(255,255,255,.22), rgba(255,255,255,.12)); - color: #e7e7ee; - backdrop-filter: blur(18px); - box-shadow: inset 0 1px 0 rgba(255,255,255,.2); -} - -/* Konkrete Felder mit Icons: zusätzliche linke Padding über Klasse steuern */ -.with-left-icon { padding-left: 40px !important; } -.control.theme select option { background: #0f1530; color: #e7e7ee; } -.control input::placeholder { color: #c8c8d8; } - -.control.volume { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center; } -.control.volume label { font-weight: 700; opacity: .9; } - -/* Volume Slider - Base Styles */ -.control.volume input[type="range"], -.volume-slider { - -webkit-appearance: none; - appearance: none; - width: 100%; - height: var(--range-track-h, 8px); - border-radius: 5px; - outline: none; - cursor: pointer; - /* Sichtbarer Füllbalken über ein Hintergrund-Gradient, Breite via --_fill gesteuert */ - background: linear-gradient(var(--range-accent), var(--range-accent)) 0/var(--_fill, 0%) 100% no-repeat, var(--range-track-bg, #2c2c2c); - vertical-align: middle; -} - -/* Tracks transparent halten, damit der Hintergrund-Gradient sichtbar ist */ -.control.volume input[type="range"]::-webkit-slider-runnable-track, -.volume-slider::-webkit-slider-runnable-track { height: 8px; background: transparent; border-radius: 5px; } -.control.volume input[type="range"]::-moz-range-track, -.volume-slider::-moz-range-track { height: 8px; background: transparent; border-radius: 5px; } -.control.volume input[type="range"]::-moz-range-progress, -.volume-slider::-moz-range-progress { background: transparent; } - -.control.volume input[type="range"]::-webkit-slider-thumb, -.volume-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: var(--range-thumb-d, 20px); - height: var(--range-thumb-d, 20px); - margin-top: calc((var(--range-track-h, 8px) - var(--range-thumb-d, 20px)) / 2); - border-radius: 50%; - cursor: pointer; +.admin-btn-action.primary { + background: var(--accent); + color: white; border: none; - box-shadow: 0 2px 6px rgba(0,0,0,0.3); } -.control.volume input[type="range"]::-moz-range-thumb, -.volume-slider::-moz-range-thumb { - width: 20px; - height: 20px; - border-radius: 50%; - cursor: pointer; - border: none; - box-shadow: 0 2px 6px rgba(0,0,0,0.3); +.admin-btn-action.primary:hover { + background: var(--accent-hover); } -/* Volume Slider - Dark Theme */ -[data-theme="dark"] .control.volume input[type="range"], -[data-theme="dark"] .volume-slider { - --range-accent: #0a84ff; - --range-track-bg: #2c2c2c; - accent-color: #0a84ff; - border-radius: 5px; - height: var(--range-track-h, 8px); +.admin-btn-action.outline { + background: transparent; + border: 1px solid rgba(255, 255, 255, .08); + color: var(--text-muted); } -[data-theme="dark"] .control.volume input[type="range"]::-webkit-slider-thumb, -[data-theme="dark"] .volume-slider::-webkit-slider-thumb { - background: #0a84ff; +.admin-btn-action.outline:hover { + border-color: rgba(255, 255, 255, .12); + color: var(--text-normal); } -[data-theme="dark"] .control.volume input[type="range"]::-moz-range-thumb, -[data-theme="dark"] .volume-slider::-moz-range-thumb { - background: #0a84ff; +.admin-btn-action.danger { + background: var(--red); + color: var(--white); + border: 1px solid var(--red); } - - -/* Volume Slider - Rainbow Theme */ -[data-theme="rainbow"] .control.volume input[type="range"], -[data-theme="rainbow"] .volume-slider { - --range-accent: #23a6d5; - --range-track-bg: rgba(44,44,44,.8); - accent-color: #23a6d5; - border-radius: 5px; - height: var(--range-track-h, 8px); - border: 1px solid #3a3a3c; +.admin-btn-action.danger:hover { + filter: brightness(1.06); } -[data-theme="rainbow"] .control.volume input[type="range"]::-webkit-slider-thumb, -[data-theme="rainbow"] .volume-slider::-webkit-slider-thumb { - background: linear-gradient(45deg, #23a6d5, #e73c7e); +.admin-btn-action.danger.ghost { + background: transparent; + color: var(--red); + border: 1px solid rgba(242, 63, 66, .5); } -[data-theme="rainbow"] .control.volume input[type="range"]::-moz-range-thumb, -[data-theme="rainbow"] .volume-slider::-moz-range-thumb { - background: linear-gradient(45deg, #23a6d5, #e73c7e); +.admin-btn-action.danger.ghost:hover { + background: rgba(242, 63, 66, .14); } -.error { background: rgba(255, 99, 99, .12); color: #ffd1d1; border: 1px solid rgba(255, 99, 99, .3); padding: 10px 12px; border-radius: 10px; margin-bottom: 12px; } +.admin-btn-action:disabled { + opacity: .5; + pointer-events: none; +} -.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; } - -/* Lineares, responsives Flow-Layout für Sounds */ -.sounds-flow { +.admin-shell { display: flex; - flex-wrap: wrap; - align-items: flex-start; + flex-direction: column; gap: 12px; + min-height: 0; } -.sounds-flow .sound-wrap { - position: relative; - flex: 0 0 auto; -} -/* Kartenbreite an Tabs angelehnt */ -.sounds-flow .sound-btn { - display: inline-flex; - width: auto; - max-width: 260px; /* verhindert überlange Buttons, mehr Cards pro Zeile */ - white-space: nowrap; /* einzeilig */ - padding: 12px 16px; /* gleichmäßiges Padding links/rechts */ - justify-content: center; /* Text zentrieren */ -} -/* Soundbutton-Text minimal kräftiger als 500 */ -.sounds-flow .sound-btn > span { font-weight: 501 !important; } -/* URL Input mit Download Button - Text soll nicht über Button laufen */ -.input-field.pl-10.with-left-icon { - padding-right: 100px !important; /* Platz für Download Button */ - text-overflow: ellipsis; +.admin-header-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + flex-wrap: wrap; +} + +.admin-status { + font-size: 13px; + color: var(--text-muted); +} + +.admin-actions-inline { + display: flex; + align-items: center; + gap: 8px; +} + +.admin-search-field { + margin-bottom: 0; +} + +.admin-bulk-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 8px 10px; + border-radius: 10px; + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, .08); + flex-wrap: wrap; +} + +.admin-select-all { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--text-muted); +} + +.admin-select-all input, +.admin-item-check input { + accent-color: var(--accent); +} + +.admin-list-wrap { + min-height: 260px; + max-height: 52vh; + overflow-y: auto; + border: 1px solid rgba(255, 255, 255, .08); + border-radius: 10px; + background: var(--bg-primary); +} + +.admin-list { + display: flex; + flex-direction: column; + gap: 6px; + padding: 6px; +} + +.admin-empty { + padding: 24px 12px; + text-align: center; + color: var(--text-muted); + font-size: 13px; +} + +.admin-item { + display: grid; + grid-template-columns: 28px minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + padding: 10px; + border-radius: 8px; + background: var(--bg-secondary); + border: 1px solid rgba(255, 255, 255, .06); +} + +.admin-item-main { + min-width: 0; +} + +.admin-item-name { + font-size: 14px; + font-weight: 600; + color: var(--text-normal); overflow: hidden; + text-overflow: ellipsis; white-space: nowrap; } -.sound-wrap { position: relative; display: block; } -.sound-wrap.row .sound { width: 100%; } -.row-check { width: 18px; height: 18px; accent-color: #60a5fa; } -.select-check { - position: absolute; - left: 8px; - top: 8px; - z-index: 5; - width: 18px; - height: 18px; - accent-color: #60a5fa; -} -.sound { - padding: 18px 16px; - border-radius: 14px; - border: 1px solid rgba(255,255,255,.18); - background: rgba(255,255,255,.08); - color: #fff; - cursor: pointer; - font-weight: 700; - letter-spacing: .2px; - box-shadow: 0 10px 30px rgba(0,0,0,.25); + +.admin-item-meta { + margin-top: 3px; + font-size: 11px; + color: var(--text-faint); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.sound:hover { filter: brightness(1.06); background: rgba(255,255,255,.1); } -.sound:disabled { opacity: 0.6; cursor: not-allowed; } - -.fav { - position: absolute; - top: 8px; - right: 10px; - background: rgba(0,0,0,.25); - color: #fff; - border: 1px solid rgba(255,255,255,.2); - border-radius: 999px; - width: 28px; - height: 28px; - display: grid; - place-items: center; - cursor: pointer; -} -.fav.active { background: #eab308; color: #111; border-color: transparent; } - -.hint { opacity: .7; padding: 24px 0; } - -/* Footer mit Version/Build-Kanal */ -.footer-info { - opacity: .7; - font-size: 12px; - padding: 16px 0 8px; +.admin-item-actions { + display: flex; + align-items: center; + gap: 6px; } -.tabs { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; } -.tab { +.admin-item-actions .admin-btn-action, +.admin-rename-row .admin-btn-action { padding: 8px 12px; - border-radius: 999px; - border: 1px solid rgba(255,255,255,.25); - background: linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,.06)); - color: #e7e7ee; + font-size: 12px; } -.tab:hover { background: rgba(255,255,255,.12); } -.tab.active { background: linear-gradient(135deg, rgba(168,85,247,.55), rgba(59,130,246,.55)); color: #fff; border-color: transparent; } +.admin-rename-row { + display: flex; + align-items: center; + gap: 6px; + margin-top: 8px; +} -/* Back to top */ -.back-to-top { +.admin-rename-row input { + flex: 1; + min-width: 120px; + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, .08); + border-radius: 8px; + padding: 8px 10px; + font-size: 13px; + color: var(--text-normal); + font-family: var(--font); + transition: all var(--transition); +} + +.admin-rename-row input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-glow); +} + +/* ──────────────────────────────────────────── + Responsive + ──────────────────────────────────────────── */ +@media (max-width: 700px) { + .toolbar { + gap: 6px; + padding: 8px 12px; + } + + .cat-tabs { + overflow-x: auto; + scrollbar-width: none; + } + + .cat-tabs::-webkit-scrollbar { + display: none; + } + + .search-wrap { + max-width: 100%; + min-width: 100%; + order: -1; + } + + .url-import-wrap { + max-width: 100%; + min-width: 100%; + order: -1; + } + + .size-control, + .theme-selector { + display: none; + } + + .main { + padding: 12px; + } + + .topbar { + padding: 0 12px; + gap: 8px; + } + + .channel-label { + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + } + + .clock { + font-size: 16px; + } + + .clock-seconds { + font-size: 11px; + } + + .tb-btn span:not(.tb-icon) { + display: none; + } + + .analytics-strip { + padding: 8px 12px; + flex-direction: column; + gap: 6px; + } + + .analytics-card.analytics-wide { + width: 100%; + } + + .admin-panel { + width: 96%; + padding: 16px; + max-height: 92vh; + } + + .admin-item { + grid-template-columns: 24px minmax(0, 1fr); + } + + .admin-item-actions { + grid-column: 1 / -1; + justify-content: flex-end; + } + + .admin-rename-row { + flex-wrap: wrap; + } +} + +@media (max-width: 480px) { + .connection { + display: none; + } + + .app-title { + display: none; + } + + .now-playing { + max-width: 120px; + } + + .toolbar .tb-btn { + padding: 6px 8px; + } + + .url-import-btn { + padding: 0 8px; + } +} + +/* ──────────────────────────────────────────── + Drag & Drop Overlay + ──────────────────────────────────────────── */ +.drop-overlay { position: fixed; - right: 24px; - bottom: 24px; - padding: 10px 14px; - border-radius: 999px; - border: 1px solid rgba(255,255,255,.25); - background: linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,.06)); - color: #e7e7ee; - box-shadow: 0 10px 30px rgba(0,0,0,.35); - z-index: 40000; + inset: 0; + background: rgba(0, 0, 0, .78); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + z-index: 300; + display: flex; + align-items: center; + justify-content: center; + animation: fade-in 120ms ease; + pointer-events: none; } -.back-to-top:hover { filter: brightness(1.1); } +.drop-zone { + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + padding: 64px 72px; + border-radius: 24px; + border: 2.5px dashed rgba(var(--accent-rgb), .55); + background: rgba(var(--accent-rgb), .07); + animation: drop-pulse 2.2s ease-in-out infinite; +} +@keyframes drop-pulse { + 0%, 100% { + border-color: rgba(var(--accent-rgb), .45); + box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0); + } + 50% { + border-color: rgba(var(--accent-rgb), .9); + box-shadow: 0 0 60px 12px rgba(var(--accent-rgb), .12); + } +} +.drop-icon { + font-size: 64px; + color: var(--accent); + animation: drop-bounce 1.8s ease-in-out infinite; +} +@keyframes drop-bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-8px); } +} -/* Tabs/Filter wie der Random-Button im Header (einheitlicher Stil über alle Themes) */ -[data-theme] .tag-btn { - padding: 12px 24px; - border-radius: 0.5rem; - background: #374151; /* bg-gray-700 */ - color: #ffffff; - font-size: .875rem; /* einheitliche Schriftgröße wie andere Themes */ +.drop-title { + font-size: 22px; font-weight: 700; - border: none; - transition: background-color .2s ease, transform .2s ease; -} -[data-theme] .tag-btn:hover { background: #4b5563; /* bg-gray-600 */ } -[data-theme] .tag-btn.active { - background: #4b5563; - box-shadow: 0 0 0 2px rgba(255,255,255,.08) inset; + color: var(--text-normal); + letter-spacing: -.3px; } -/* Aktives Tab farbig je nach Theme */ -[data-theme="dark"] .tag-btn.active { - background: #0a84ff !important; - color: #ffffff !important; +.drop-sub { + font-size: 13px; + color: var(--text-muted); } -[data-theme="rainbow"] .tag-btn.active { - background: linear-gradient(90deg, #ff6384, #36a2eb, #ffce56, #4bc0c0, #9966ff) !important; - color: #ffffff !important; - border: 1px solid transparent !important; + +/* ──────────────────────────────────────────── + Upload Queue (floating card) + ──────────────────────────────────────────── */ +.upload-queue { + position: fixed; + bottom: 24px; + right: 24px; + width: 340px; + background: var(--bg-secondary); + border: 1px solid rgba(255, 255, 255, .09); + border-radius: 14px; + box-shadow: 0 8px 40px rgba(0, 0, 0, .45); + z-index: 200; + overflow: hidden; + animation: slide-up 200ms cubic-bezier(.16,1,.3,1); +} + +@keyframes slide-up { + from { opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); } +} + +.uq-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 14px; + background: rgba(var(--accent-rgb), .12); + border-bottom: 1px solid rgba(255, 255, 255, .06); + font-size: 13px; + font-weight: 600; + color: var(--text-normal); +} + +.uq-header .material-icons { color: var(--accent); } + +.uq-close { + margin-left: auto; + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 50%; + border: none; + background: rgba(255,255,255,.06); + color: var(--text-muted); + cursor: pointer; + transition: background var(--transition), color var(--transition); +} +.uq-close:hover { background: rgba(255,255,255,.14); color: var(--text-normal); } + +.uq-list { + display: flex; + flex-direction: column; + max-height: 260px; + overflow-y: auto; + padding: 6px 0; +} + +.uq-item { + display: grid; + grid-template-columns: 20px 1fr auto 18px; + align-items: center; + gap: 8px; + padding: 8px 14px; + position: relative; +} + +.uq-item + .uq-item { + border-top: 1px solid rgba(255, 255, 255, .04); +} + +.uq-file-icon { + font-size: 18px; + color: var(--text-faint); +} + +.uq-info { + min-width: 0; +} + +.uq-name { + font-size: 12px; + font-weight: 500; + color: var(--text-normal); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.uq-size { + font-size: 10px; + color: var(--text-faint); + margin-top: 1px; +} + +.uq-progress-wrap { + grid-column: 1 / -1; + height: 3px; + background: rgba(255, 255, 255, .07); + border-radius: 2px; + overflow: hidden; + margin-top: 4px; +} + +/* Vertikaler layout-Trick: progress bar als extra row nach den anderen */ +.uq-item { + flex-wrap: wrap; +} + +.uq-progress-wrap { + width: 100%; + order: 10; +} + +.uq-progress-bar { + height: 100%; + background: var(--accent); + border-radius: 2px; + transition: width 120ms ease; +} + +.uq-status-icon { font-size: 16px; } +.uq-status-waiting .uq-status-icon { color: var(--text-faint); } +.uq-status-uploading .uq-status-icon { + color: var(--accent); + animation: spin 1s linear infinite; +} +.uq-status-done .uq-status-icon { color: var(--green); } +.uq-status-error .uq-status-icon { color: var(--red); } + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.uq-error { + grid-column: 2 / -1; + font-size: 10px; + color: var(--red); + margin-top: 2px; +} + +/* ──────────────────────────────────────────── + Utility + ──────────────────────────────────────────── */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } -[data-theme="420"] .tag-btn.active { - background: #22c55e !important; - color: #ffffff !important; - border-color: #22c55e !important; - box-shadow: 0 0 15px rgba(34, 197, 94, 0.5); -} \ No newline at end of file diff --git a/web/src/types.ts b/web/src/types.ts index 4cfbef3..919e8fe 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -25,6 +25,18 @@ export type VoiceChannelInfo = { export type Category = { id: string; name: string; color?: string; sort?: number }; +export type AnalyticsItem = { + name: string; + relativePath: string; + count: number; +}; + +export type AnalyticsResponse = { + totalSounds: number; + totalPlays: number; + mostPlayed: AnalyticsItem[]; +}; +