commit cdeaab4e0adc82e273f1f60d53603a7d8fd19c31 Author: Yuzu Date: Fri Oct 24 11:35:35 2025 +0700 ✨ feat: Initial implementation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d895f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +sample_data/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..24d7cc6 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..08bc027 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 YuzuZensai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2004e46 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# VRC-Circle diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..baed567 --- /dev/null +++ b/bun.lock @@ -0,0 +1,683 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "vrc-one", + "dependencies": { + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.0.6", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-opener": "^2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "i18next": "^25.6.0", + "i18next-browser-languagedetector": "^8.2.0", + "lucide-react": "^0.546.0", + "next-themes": "^0.4.6", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-i18next": "^16.1.0", + "react-router-dom": "^7.9.4", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@types/node": "^24.8.1", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^3", + "typescript": "~5.8.3", + "vite": "^7.0.4", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="], + + "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@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" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], + + "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="], + + "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.11", "", { "os": "android", "cpu": "arm" }, "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.11", "", { "os": "android", "cpu": "arm64" }, "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.11", "", { "os": "android", "cpu": "x64" }, "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.11", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.11", "", { "os": "linux", "cpu": "arm" }, "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.11", "", { "os": "linux", "cpu": "ia32" }, "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.11", "", { "os": "linux", "cpu": "x64" }, "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.11", "", { "os": "none", "cpu": "x64" }, "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.11", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.11", "", { "os": "openbsd", "cpu": "x64" }, "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.11", "", { "os": "sunos", "cpu": "x64" }, "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.5", "", { "os": "android", "cpu": "arm64" }, "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.5", "", { "os": "none", "cpu": "arm64" }, "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg=="], + + "@tauri-apps/api": ["@tauri-apps/api@2.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="], + + "@tauri-apps/cli": ["@tauri-apps/cli@2.8.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.8.4", "@tauri-apps/cli-darwin-x64": "2.8.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.8.4", "@tauri-apps/cli-linux-arm64-gnu": "2.8.4", "@tauri-apps/cli-linux-arm64-musl": "2.8.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.8.4", "@tauri-apps/cli-linux-x64-gnu": "2.8.4", "@tauri-apps/cli-linux-x64-musl": "2.8.4", "@tauri-apps/cli-win32-arm64-msvc": "2.8.4", "@tauri-apps/cli-win32-ia32-msvc": "2.8.4", "@tauri-apps/cli-win32-x64-msvc": "2.8.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-ejUZBzuQRcjFV+v/gdj/DcbyX/6T4unZQjMSBZwLzP/CymEjKcc2+Fc8xTORThebHDUvqoXMdsCZt8r+hyN15g=="], + + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.8.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BKu8HRkYV01SMTa7r4fLx+wjgtRK8Vep7lmBdHDioP6b8XH3q2KgsAyPWfEZaZIkZ2LY4SqqGARaE9oilNe0oA=="], + + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.8.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-imb9PfSd/7G6VAO7v1bQ2A3ZH4NOCbhGJFLchxzepGcXf9NKkfun157JH9mko29K6sqAwuJ88qtzbKCbWJTH9g=="], + + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.8.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Ml215UnDdl7/fpOrF1CNovym/KjtUbCuPgrcZ4IhqUCnhZdXuphud/JT3E8X97Y03TZ40Sjz8raXYI2ET0exzw=="], + + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.8.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pbcgBpMyI90C83CxE5REZ9ODyIlmmAPkkJXtV398X3SgZEIYy5TACYqlyyv2z5yKgD8F8WH4/2fek7+jH+ZXAw=="], + + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.8.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-zumFeaU1Ws5Ay872FTyIm7z8kfzEHu8NcIn8M6TxbJs0a7GRV21KBdpW1zNj2qy7HynnpQCqjAYXTUUmm9JAOw=="], + + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.8.4", "", { "os": "linux", "cpu": "none" }, "sha512-qiqbB3Zz6IyO201f+1ojxLj65WYj8mixL5cOMo63nlg8CIzsP23cPYUrx1YaDPsCLszKZo7tVs14pc7BWf+/aQ=="], + + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.8.4", "", { "os": "linux", "cpu": "x64" }, "sha512-TaqaDd9Oy6k45Hotx3pOf+pkbsxLaApv4rGd9mLuRM1k6YS/aw81YrsMryYPThrxrScEIUcmNIHaHsLiU4GMkw=="], + + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.8.4", "", { "os": "linux", "cpu": "x64" }, "sha512-ot9STAwyezN8w+bBHZ+bqSQIJ0qPZFlz/AyscpGqB/JnJQVDFQcRDmUPFEaAtt2UUHSWzN3GoTJ5ypqLBp2WQA=="], + + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.8.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-+2aJ/g90dhLiOLFSD1PbElXX3SoMdpO7HFPAZB+xot3CWlAZD1tReUFy7xe0L5GAR16ZmrxpIDM9v9gn5xRy/w=="], + + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.8.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-yj7WDxkL1t9Uzr2gufQ1Hl7hrHuFKTNEOyascbc109EoiAqCp0tgZ2IykQqOZmZOHU884UAWI1pVMqBhS/BfhA=="], + + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.8.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XuvGB4ehBdd7QhMZ9qbj/8icGEatDuBNxyYHbLKsTYh90ggUlPa/AtaqcC1Fo69lGkTmq9BOKrs1aWSi7xDonA=="], + + "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="], + + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + + "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "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" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.26.3", "", { "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.237", "", {}, "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], + + "i18next": ["i18next@25.6.0", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw=="], + + "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + + "node-releases": ["node-releases@2.0.25", "", {}, "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + + "react-i18next": ["react-i18next@16.1.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 25.5.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-10qpoODc0ntrWpZdko9CQnl2Xf4VJXUAS9sa7WOkTrQzbbfyoSSUZ3Rsnpgt8la6qCxeB61yzYjyW8U1g3hUUg=="], + + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-router": ["react-router@7.9.4", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA=="], + + "react-router-dom": ["react-router-dom@7.9.4", "", { "dependencies": { "react-router": "7.9.4" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], + + "tailwindcss": ["tailwindcss@3.4.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], + + "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + } +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..62f1aa7 --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..038d243 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + VRC Circle + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..69728ab --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "vrc-circle", + "author": "Yuzu ", + "license": "MIT", + "private": false, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri", + "generate:bindings": "cargo run --manifest-path src-tauri/Cargo.toml --bin vrc-circle -- --generate-bindings" + }, + "dependencies": { + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.0.6", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-opener": "^2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "i18next": "^25.6.0", + "i18next-browser-languagedetector": "^8.2.0", + "lucide-react": "^0.546.0", + "next-themes": "^0.4.6", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-i18next": "^16.1.0", + "react-router-dom": "^7.9.4", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@types/node": "^24.8.1", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^3", + "typescript": "~5.8.3", + "vite": "^7.0.4" + }, + "packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184" +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/tauri.svg b/public/tauri.svg new file mode 100644 index 0000000..31b62c9 --- /dev/null +++ b/public/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 0000000..b21bd68 --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..b396509 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,7110 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.3", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byte-unit" +version = "5.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +dependencies = [ + "rust_decimal", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.10.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.8", +] + +[[package]] +name = "cc" +version = "1.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.108", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.108", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.108", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.108", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dlopen2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.8", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "flate2" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.12.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls 0.23.34", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.10.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.12.0", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "litrs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +dependencies = [ + "value-bag", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-javascript-core", + "objc2-security", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "3.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "ouroboros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.12.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e0f6df8eaa422d97d72edcd152e1451618fed47fabbdbd5a8864167b1d4aff7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64 0.22.1", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.7", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.108", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "sea-orm" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8814e37dc25de54398ee62228323657520b7f29713b8e238649385dbe473ee0" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "futures", + "log", + "ouroboros", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "serde_json", + "sqlx", + "strum", + "thiserror 1.0.69", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-macros" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e115c6b078e013aa963cc2d38c196c2c40b05f03d0ac872fe06b6e0d5265603" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.108", + "unicode-ident", +] + +[[package]] +name = "sea-query" +version = "0.30.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4166a1e072292d46dc91f31617c2a1cdaf55a8be4b5c9f4bf2ba248e3ac4999b" +dependencies = [ + "bigdecimal", + "chrono", + "derivative", + "inherent", + "ordered-float", + "rust_decimal", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-binder" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36bbb68df92e820e4d5aeb17b4acd5cc8b5d18b2c36a4dd6f4626aabfa7ab1b9" +dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.0", + "schemars 0.9.0", + "schemars 1.0.4", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "bytemuck", + "cfg_aliases", + "core-graphics", + "foreign-types 0.5.0", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", + "raw-window-handle", + "redox_syscall", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "specta" +version = "2.0.0-rc.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971" +dependencies = [ + "paste", + "serde", + "specta-macros", + "thiserror 1.0.69", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0074b9e30ed84c6924eb63ad8d2fe71cdc82628525d84b1fcb1f2fd40676517" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "specta-serde" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77216504061374659e7245eac53d30c7b3e5fe64b88da97c753e7184b0781e63" +dependencies = [ + "specta", + "thiserror 1.0.69", +] + +[[package]] +name = "specta-typescript" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3220a0c365e51e248ac98eab5a6a32f544ff6f961906f09d3ee10903a4f52b2d" +dependencies = [ + "specta", + "specta-serde", + "thiserror 1.0.69", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash 0.8.12", + "atoi", + "bigdecimal", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener 2.5.3", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.12.0", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rust_decimal", + "rustls 0.21.12", + "rustls-pemfile", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64 0.21.7", + "bigdecimal", + "bitflags 2.10.0", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bigdecimal", + "bitflags 2.10.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "rand 0.8.5", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "time", + "tracing", + "url", + "urlencoding", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9871670c6711f50fddd4e20350be6b9dd6e6c2b5d77d8ee8900eb0d58cd837a" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs 6.0.0", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "http-range", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "specta", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.17", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a924b6c50fe83193f0f8b14072afa7c25b7a72752a2a73d9549b463f5fe91a38" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c1fe64c74cc40f90848281a90058a6db931eb400b60205840e09801ee30f190" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.108", + "tauri-utils", + "thiserror 2.0.17", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "260c5d2eb036b76206b9fca20b7be3614cfd21046c5396f7959e0e64a4b07f2f" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.108", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c1438bc7662acd16d508c919b3c087efd63669a4c75625dff829b1c75975ec" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786156aa8e89e03d271fbd3fe642207da8e65f3c961baa9e2930f332bf80a1f5" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2 0.6.3", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "929f5df216f5c02a9e894554401bcdab6eec3e39ec6a4a7731c7067fc8688a93" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-specta" +version = "2.0.0-rc.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23c0132dd3cf6064e5cd919b82b3f47780e9280e7b5910babfe139829b76655" +dependencies = [ + "heck 0.5.0", + "serde", + "serde_json", + "specta", + "specta-typescript", + "tauri", + "thiserror 2.0.17", +] + +[[package]] +name = "tauri-utils" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.17", + "toml 0.9.8", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" +dependencies = [ + "embed-resource", + "toml 0.9.8", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.34", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap 2.12.0", + "serde_core", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow 0.7.13", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.12.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.12.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap 2.12.0", + "toml_datetime 0.7.3", + "toml_parser", + "winnow 0.7.13", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow 0.7.13", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "value-bag" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vrc-circle" +version = "0.0.1" +dependencies = [ + "base64 0.22.1", + "chrono", + "directories", + "dirs 5.0.1", + "futures-util", + "http", + "log", + "reqwest", + "sea-orm", + "serde", + "serde_json", + "sha2", + "specta", + "specta-typescript", + "tauri", + "tauri-build", + "tauri-plugin-log", + "tauri-plugin-opener", + "tauri-specta", + "tokio", + "tokio-tungstenite", + "url", +] + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.108", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webview2-com" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +dependencies = [ + "thiserror 2.0.17", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wry" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" +dependencies = [ + "base64 0.22.1", + "block2 0.6.2", + "cookie", + "crossbeam-channel", + "dirs 6.0.0", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.4.1", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.13", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "zvariant" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.13", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.108", + "winnow 0.7.13", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..c57e9fc --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "vrc-circle" +version = "0.0.1" +description = "A Tauri App" +authors = ["Yuzu "] +edition = "2024" +default-run = "vrc-circle" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "vrc_one_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } +specta = { version = "2.0.0-rc.20", features = ["serde"] } +specta-typescript = "0.0.9" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[dependencies] +tauri = { version = "2", features = ["protocol-asset"] } +tauri-plugin-opener = "2" +tauri-plugin-log = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", features = ["json", "cookies"] } +base64 = "0.22" +tokio = { version = "1", features = ["full"] } +tokio-tungstenite = { version = "0.24", features = ["native-tls"] } +futures-util = "0.3" +http = "1.1" +directories = "5.0" +dirs = "5.0" +chrono = { version = "0.4", features = ["serde"] } +specta = { version = "2.0.0-rc.20", features = ["serde"] } +tauri-specta = { version = "2.0.0-rc.20", features = ["typescript"] } +specta-typescript = "0.0.9" +sea-orm = { version = "0.12", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"] } +log = "0.4" +sha2 = "0.10" +url = "2" diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..9d39928 --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,4 @@ +fn main() { + tauri_build::build() +} + diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..162100b --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,12 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "core:default", + "opener:default", + "core:path:default", + "core:image:default" + ] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..6be5e50 Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..e81bece Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000..a437dd5 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..0ca4f27 Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..b81f820 Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..624c7bf Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..c021d2b Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..6219700 Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..f9bc048 Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..d5fbfb2 Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..63440d7 Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..f3f705a Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..4556388 Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..12a5bce Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000..b3636e4 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..e1cd261 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/database_studio.rs b/src-tauri/src/database_studio.rs new file mode 100644 index 0000000..eae1051 --- /dev/null +++ b/src-tauri/src/database_studio.rs @@ -0,0 +1,256 @@ +// TODO: Improve this dev tool + +use sea_orm::{ + ConnectOptions, ConnectionTrait, Database, DatabaseConnection, DbBackend, Statement, +}; +use serde::{Deserialize, Serialize}; +use specta::Type; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct TableInfo { + pub name: String, + pub sql: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct ColumnInfo { + pub cid: i32, + pub name: String, + pub r#type: String, + pub notnull: i32, + pub dflt_value: Option, + pub pk: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct QueryResult { + pub columns: Vec, + #[specta(type = Vec>)] + pub rows: Vec>, + pub rows_affected: Option, +} + +pub struct DatabaseStudio { + db: DatabaseConnection, +} + +impl DatabaseStudio { + pub async fn new() -> Result { + // Use per-user local data directory (e.g. %LOCALAPPDATA% on Windows) + let base_dir = dirs::data_local_dir() + .ok_or("Failed to resolve local data directory")? + .join("vrc-circle"); + + std::fs::create_dir_all(&base_dir) + .map_err(|e| format!("Failed to create app data directory: {}", e))?; + + let db_path = base_dir.join("data.sqlite"); + let db_url = format!( + "sqlite://{}?mode=rwc", + db_path.to_string_lossy().replace('\\', "/") + ); + + let mut options = ConnectOptions::new(db_url); + options.sqlx_logging(false); + + let db = Database::connect(options) + .await + .map_err(|e| format!("Failed to connect to database: {}", e))?; + + Ok(Self { db }) + } + + pub async fn list_tables(&self) -> Result, String> { + let query = Statement::from_string( + DbBackend::Sqlite, + "SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" + .to_string(), + ); + + let result = self + .db + .query_all(query) + .await + .map_err(|e| format!("Failed to list tables: {}", e))?; + + let tables: Vec = result + .into_iter() + .map(|row| { + let name: String = row.try_get("", "name").unwrap_or_default(); + let sql: String = row.try_get("", "sql").unwrap_or_default(); + TableInfo { name, sql } + }) + .collect(); + + Ok(tables) + } + + pub async fn get_table_schema(&self, table_name: &str) -> Result, String> { + let query = Statement::from_string( + DbBackend::Sqlite, + format!("PRAGMA table_info('{}')", table_name), + ); + + let result = self + .db + .query_all(query) + .await + .map_err(|e| format!("Failed to get table schema: {}", e))?; + + let columns: Vec = result + .into_iter() + .map(|row| ColumnInfo { + cid: row.try_get("", "cid").unwrap_or_default(), + name: row.try_get("", "name").unwrap_or_default(), + r#type: row.try_get("", "type").unwrap_or_default(), + notnull: row.try_get("", "notnull").unwrap_or_default(), + dflt_value: row.try_get("", "dflt_value").ok(), + pk: row.try_get("", "pk").unwrap_or_default(), + }) + .collect(); + + Ok(columns) + } + + pub async fn get_table_data( + &self, + table_name: &str, + limit: Option, + offset: Option, + ) -> Result { + let limit_val = limit.unwrap_or(100); + let offset_val = offset.unwrap_or(0); + + let query_str = format!( + "SELECT * FROM {} LIMIT {} OFFSET {}", + table_name, limit_val, offset_val + ); + + let mut result = self.execute_query(&query_str).await?; + + // Get actual column names from schema and remap the data + let schema = self.get_table_schema(table_name).await?; + let actual_columns: Vec = schema.into_iter().map(|col| col.name).collect(); + + // Remap rows from column_0, column_1 to actual column names + let remapped_rows: Vec> = result + .rows + .into_iter() + .map(|row| { + let mut new_row = HashMap::new(); + for (idx, actual_col) in actual_columns.iter().enumerate() { + let key = format!("column_{}", idx); + if let Some(value) = row.get(&key) { + new_row.insert(actual_col.clone(), value.clone()); + } + } + new_row + }) + .collect(); + + result.columns = actual_columns; + result.rows = remapped_rows; + + Ok(result) + } + + pub async fn get_table_count(&self, table_name: &str) -> Result { + let query = Statement::from_string( + DbBackend::Sqlite, + format!("SELECT COUNT(*) as count FROM {}", table_name), + ); + + let result = self + .db + .query_one(query) + .await + .map_err(|e| format!("Failed to count rows: {}", e))?; + + if let Some(row) = result { + let count: i64 = row.try_get("", "count").unwrap_or_default(); + Ok(count as i32) + } else { + Ok(0) + } + } + + pub async fn execute_query(&self, query: &str) -> Result { + use sea_orm::TryGetable; + + let query_stmt = Statement::from_string(DbBackend::Sqlite, query.to_string()); + + let result = self + .db + .query_all(query_stmt) + .await + .map_err(|e| format!("Query execution failed: {}", e))?; + + if result.is_empty() { + return Ok(QueryResult { + columns: vec![], + rows: vec![], + rows_affected: Some(0), + }); + } + + // Extract column names and convert rows + let mut columns: Vec = Vec::new(); + let mut rows: Vec> = Vec::new(); + + for row in result { + // Get columns from first row by trying all possible indices + if columns.is_empty() { + let mut idx = 0; + loop { + // Try to get as String first, then fall back to other types + match String::try_get_by_index(&row, idx) { + Ok(_) => { + idx += 1; + } + Err(_) => { + // Try as i64 + match i64::try_get_by_index(&row, idx) { + Ok(_) => { + idx += 1; + } + Err(_) => break, + } + } + } + } + // We don't know column names yet, will be set by get_table_data + columns = (0..idx).map(|i| format!("column_{}", i)).collect(); + } + + // Convert row to HashMap with String values by index + let mut map = HashMap::new(); + for idx in 0..columns.len() { + // Try multiple types and convert to string + let str_value = if let Ok(val) = String::try_get_by_index(&row, idx) { + val + } else if let Ok(val) = i64::try_get_by_index(&row, idx) { + val.to_string() + } else if let Ok(val) = f64::try_get_by_index(&row, idx) { + val.to_string() + } else if let Ok(val) = bool::try_get_by_index(&row, idx) { + val.to_string() + } else if let Ok(Some(val)) = Option::::try_get_by_index(&row, idx) { + val + } else { + "NULL".to_string() + }; + + let key = format!("column_{}", idx); + map.insert(key, str_value); + } + rows.push(map); + } + + Ok(QueryResult { + columns, + rows, + rows_affected: None, + }) + } +} diff --git a/src-tauri/src/http_common.rs b/src-tauri/src/http_common.rs new file mode 100644 index 0000000..476ba71 --- /dev/null +++ b/src-tauri/src/http_common.rs @@ -0,0 +1,35 @@ +pub const USER_AGENT_STRING: &str = "VRC-Circle/0.0.1 contact@kirameki.cafe"; + +// Retry configuration +pub const MAX_REQUEST_RETRIES: u8 = 5; + +// Backoff timings (milliseconds) +pub const INITIAL_BACKOFF: u64 = 500; +pub const MAX_BACKOFF: u64 = 10_000; + +// Rate limiting +pub const MAX_DOWNLOADS_PER_SECOND: u32 = 10; + +// Common API Request headers builder +use reqwest::header::{ + ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue, ORIGIN, USER_AGENT, +}; + +pub fn build_api_headers(auth: Option<&str>, cookie: Option<&str>) -> HeaderMap { + let mut headers = HeaderMap::new(); + + headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_STRING)); + headers.insert(ACCEPT, HeaderValue::from_static("*/*")); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert(ORIGIN, HeaderValue::from_static("https://vrchat.com")); + + if let Some(auth_value) = auth { + headers.insert(AUTHORIZATION, HeaderValue::from_str(auth_value).unwrap()); + } + + if let Some(cookie_value) = cookie { + headers.insert("cookie", HeaderValue::from_str(cookie_value).unwrap()); + } + + headers +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..e8b7fc5 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,771 @@ +pub mod database_studio; +pub mod http_common; +pub mod log_manager; +pub mod store; +pub mod vrchat_api; +pub mod vrchat_status; +pub mod websocket; + +use database_studio::{ColumnInfo, DatabaseStudio, QueryResult, TableInfo}; +use log::info; +use log_manager::{LogEntry, LogManager}; +use std::sync::Arc; +use store::{AccountStore, AppSettings, ImageCacheStore, SettingsStore, StoredAccount, UserStore}; +use tauri::{Manager, State}; +use tauri_specta::{Builder as SpectaBuilder, collect_commands}; +use tokio::sync::Mutex; +use vrchat_api::{ + AgeVerificationStatus, AvatarPerformance, AvatarStyles, Badge, DeveloperType, DiscordDetails, + FriendRequestStatus, GoogleDetails, LimitedAvatar, LimitedUserFriend, LimitedWorld, + LoginCredentials, LoginResult, OrderOption, PastDisplayName, PerformanceRatings, ReleaseStatus, + SteamDetails, TwoFactorMethod, UnityPackageSummary, UpdateStatusRequest, User, UserState, + UserStatus, VRCError, VRChatClient, +}; +use vrchat_status::{StatusPage, SystemStatus, VRChatStatusResponse}; +use websocket::VRChatWebSocket; + +#[cfg(debug_assertions)] +use specta_typescript::Typescript; + +// Application State +struct AppState { + vrchat_client: Arc>, + account_store: AccountStore, + websocket: Arc>, + user_store: UserStore, + settings_store: SettingsStore, + #[allow(dead_code)] + image_cache: Arc, +} + +// VRChat API Commands +#[tauri::command] +#[specta::specta] +async fn vrchat_login( + email: String, + password: String, + state: State<'_, AppState>, +) -> Result { + let credentials = LoginCredentials { email, password }; + let client = state.vrchat_client.lock().await; + + client.login(&credentials).await +} + +#[tauri::command] +#[specta::specta] +async fn vrchat_verify_2fa( + code: String, + method: String, + state: State<'_, AppState>, +) -> Result { + let two_fa_method = TwoFactorMethod::from_str(&method) + .ok_or_else(|| VRCError::invalid_input(format!("Invalid 2FA method: {}", method)))?; + + let client = state.vrchat_client.lock().await; + + client.verify_two_factor(&code, two_fa_method).await +} + +#[tauri::command] +#[specta::specta] +async fn vrchat_get_current_user(state: State<'_, AppState>) -> Result { + if let Some(user) = state.user_store.get_current_user().await { + return Ok(user); + } + + let client = state.vrchat_client.lock().await; + let user = client.get_current_user().await?; + + state.user_store.set_current_user(user.clone()).await; + + Ok(user) +} + +#[tauri::command] +#[specta::specta] +async fn vrchat_update_status( + status: UserStatus, + status_description: String, + state: State<'_, AppState>, +) -> Result { + let request = UpdateStatusRequest { + status, + status_description, + }; + + let client = state.vrchat_client.lock().await; + let user = client.update_status(&request).await?; + drop(client); + + state.user_store.set_current_user(user.clone()).await; + + Ok(user) +} + +#[tauri::command] +#[specta::specta] +async fn vrchat_logout(state: State<'_, AppState>) -> Result<(), VRCError> { + let websocket = state.websocket.lock().await; + websocket.stop().await; + drop(websocket); + + state.user_store.clear_all().await; + + let client = state.vrchat_client.lock().await; + client.logout().await +} + +#[tauri::command] +#[specta::specta] +async fn websocket_start(state: State<'_, AppState>) -> Result<(), VRCError> { + let client = state.vrchat_client.lock().await; + let (auth_cookie, two_factor_cookie) = client.export_cookies().await; + drop(client); + + log::debug!( + "WebSocket starting with cookies - auth: {:?}, 2fa: {:?}", + auth_cookie + .as_ref() + .map(|c| format!("{}...", &c.chars().take(20).collect::())), + two_factor_cookie + .as_ref() + .map(|c| format!("{}...", &c.chars().take(20).collect::())) + ); + + let websocket = state.websocket.lock().await; + websocket.set_cookies(auth_cookie, two_factor_cookie).await; + websocket.start().await +} + +#[tauri::command] +#[specta::specta] +async fn websocket_stop(state: State<'_, AppState>) -> Result<(), VRCError> { + let websocket = state.websocket.lock().await; + websocket.stop().await; + Ok(()) +} + +#[tauri::command] +#[specta::specta] +async fn vrchat_get_online_friends( + state: State<'_, AppState>, +) -> Result, VRCError> { + let cached_friends = state.user_store.get_all_friends().await; + + if !cached_friends.is_empty() { + return Ok(cached_friends); + } + + let client = state.vrchat_client.lock().await; + let friends = client.get_all_friends().await?; + + state.user_store.set_friends(friends.clone()).await; + Ok(friends) +} + +#[tauri::command] +#[specta::specta] +async fn vrchat_get_uploaded_worlds( + state: State<'_, AppState>, +) -> Result, VRCError> { + let client = state.vrchat_client.lock().await; + client.get_uploaded_worlds().await +} + +#[tauri::command] +#[specta::specta] +async fn vrchat_get_uploaded_avatars( + state: State<'_, AppState>, +) -> Result, VRCError> { + let client = state.vrchat_client.lock().await; + client.get_uploaded_avatars().await +} + +#[tauri::command] +#[specta::specta] +async fn get_online_friends( + state: State<'_, AppState>, +) -> Result, VRCError> { + Ok(state.user_store.get_all_friends().await) +} + +#[tauri::command] +#[specta::specta] +async fn check_image_cached( + url: String, + state: State<'_, AppState>, +) -> Result, VRCError> { + if url.trim().is_empty() { + return Ok(None); + } + + let cache = state.image_cache.clone(); + if let Some(path) = cache.get_cached_path(&url).await { + Ok(Some(path.to_string_lossy().to_string())) + } else { + Ok(None) + } +} + +#[tauri::command] +#[specta::specta] +async fn cache_image(url: String, state: State<'_, AppState>) -> Result { + if url.trim().is_empty() { + return Err(VRCError::invalid_input("Image URL is empty".to_string())); + } + + info!("Caching image: {}", url); + + let client = state.vrchat_client.lock().await; + let (auth_cookie, two_factor_cookie) = client.export_cookies().await; + drop(client); + + let cookies = match (auth_cookie, two_factor_cookie) { + (Some(auth), Some(two_fa)) => Some(format!("{}; {}", auth, two_fa)), + (Some(auth), None) => Some(auth), + (None, Some(two_fa)) => Some(two_fa), + (None, None) => None, + }; + + let cache = state.image_cache.clone(); + let path = cache + .get_or_fetch(&url, cookies) + .await + .map_err(VRCError::unknown)?; + info!("Image cached to: {}", path.display()); + + Ok(path.to_string_lossy().to_string()) +} + +#[tauri::command] +#[specta::specta] +async fn get_cache_directory(state: State<'_, AppState>) -> Result { + let cache = state.image_cache.clone(); + let dir = cache.get_cache_dir(); + Ok(dir.to_string_lossy().to_string()) +} + +#[tauri::command] +#[specta::specta] +async fn get_all_friends(state: State<'_, AppState>) -> Result, VRCError> { + Ok(state.user_store.get_all_friends().await) +} + +#[tauri::command] +#[specta::specta] +async fn get_user( + user_id: String, + state: State<'_, AppState>, +) -> Result, VRCError> { + Ok(state.user_store.get_user(&user_id).await) +} + +#[tauri::command] +#[specta::specta] +async fn get_user_by_id(user_id: String, state: State<'_, AppState>) -> Result { + if let Some(cached_user) = state.user_store.get_full_user(&user_id).await { + log::debug!("get_user_by_id: Returning cached user for {}", user_id); + return Ok(cached_user); + } + + log::info!("get_user_by_id: Fetching user {} from API", user_id); + + let client = state.vrchat_client.lock().await; + let user = client.get_user_by_id(&user_id).await?; + drop(client); + + log::info!("get_user_by_id: Successfully fetched user {}", user_id); + + state.user_store.cache_full_user(user.clone()).await; + + Ok(user) +} + +#[tauri::command] +#[specta::specta] +async fn is_friend(user_id: String, state: State<'_, AppState>) -> Result { + Ok(state.user_store.is_friend(&user_id).await) +} + +#[tauri::command] +#[specta::specta] +async fn is_user_online(user_id: String, state: State<'_, AppState>) -> Result { + Ok(state.user_store.is_user_online(&user_id).await) +} + +#[tauri::command] +#[specta::specta] +async fn vrchat_check_session(state: State<'_, AppState>) -> Result { + let client = state.vrchat_client.lock().await; + Ok(client.has_valid_session().await) +} + +#[tauri::command] +#[specta::specta] +async fn vrchat_clear_session(state: State<'_, AppState>) -> Result<(), VRCError> { + let client = state.vrchat_client.lock().await; + client.clear_cookies().await; + drop(client); + + state.user_store.clear_all().await; + + state + .account_store + .clear_last_active_account() + .await + .map_err(|e| VRCError::unknown(e))?; + + Ok(()) +} + +// Account Management Commands +#[tauri::command] +#[specta::specta] +async fn save_current_account(user: User, state: State<'_, AppState>) -> Result<(), VRCError> { + let client = state.vrchat_client.lock().await; + let (auth_cookie, two_factor_cookie) = client.export_cookies().await; + + let avatar_override = user + .user_icon + .clone() + .or_else(|| user.profile_pic_override.clone()) + .or_else(|| user.profile_pic_override_thumbnail.clone()); + + let avatar_fallback = user + .current_avatar_thumbnail_image_url + .clone() + .or_else(|| user.current_avatar_image_url.clone()); + + let account = StoredAccount { + user_id: user.id.clone(), + username: user.username.clone(), + display_name: user.display_name.clone(), + avatar_url: avatar_override.clone().or_else(|| avatar_fallback.clone()), + avatar_fallback_url: avatar_fallback, + auth_cookie, + two_factor_cookie, + last_login: chrono::Utc::now().to_rfc3339(), + }; + + state + .account_store + .save_account(account) + .await + .map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn get_all_accounts(state: State<'_, AppState>) -> Result, VRCError> { + state + .account_store + .get_all_accounts() + .await + .map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn switch_account(user_id: String, state: State<'_, AppState>) -> Result { + state.user_store.clear_all().await; + + let account = state + .account_store + .get_account(&user_id) + .await + .map_err(|e| VRCError::unknown(e))? + .ok_or_else(|| VRCError::invalid_input("Account not found"))?; + + state + .account_store + .set_active_account(&user_id) + .await + .map_err(|e| VRCError::unknown(e))?; + + let client = state.vrchat_client.lock().await; + client + .import_cookies(account.auth_cookie, account.two_factor_cookie) + .await; + + match client.get_current_user().await { + Ok(user) => { + state.user_store.set_current_user(user.clone()).await; + Ok(user) + } + Err(err) => { + // TODO: Handle if switching account fails + Err(err) + } + } +} + +#[tauri::command] +#[specta::specta] +async fn remove_account(user_id: String, state: State<'_, AppState>) -> Result<(), VRCError> { + state + .account_store + .remove_account(&user_id) + .await + .map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn clear_all_accounts(state: State<'_, AppState>) -> Result<(), VRCError> { + state + .account_store + .clear_all_accounts() + .await + .map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn load_last_account(state: State<'_, AppState>) -> Result, VRCError> { + let account = match state + .account_store + .get_last_active_account() + .await + .map_err(|e| VRCError::unknown(e))? + { + Some(acc) => acc, + None => return Ok(None), + }; + + let client = state.vrchat_client.lock().await; + client + .import_cookies(account.auth_cookie, account.two_factor_cookie) + .await; + + match client.get_current_user().await { + Ok(user) => Ok(Some(user)), + Err(_) => { + // TODO: Handle if last account cannot be loaded + // client.clear_cookies().await; + Ok(None) + } + } +} + +// Settings Commands +#[tauri::command] +#[specta::specta] +async fn get_settings(state: State<'_, AppState>) -> Result { + state + .settings_store + .get_settings() + .await + .map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn save_settings(settings: AppSettings, state: State<'_, AppState>) -> Result<(), VRCError> { + info!( + "Saving settings: developer_mode={}", + settings.developer_mode + ); + state + .settings_store + .save_settings(settings) + .await + .map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn get_developer_mode(state: State<'_, AppState>) -> Result { + state + .settings_store + .get_developer_mode() + .await + .map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn set_developer_mode(enabled: bool, state: State<'_, AppState>) -> Result<(), VRCError> { + info!("Setting developer mode: {}", enabled); + state + .settings_store + .set_developer_mode(enabled) + .await + .map_err(|e| VRCError::unknown(e)) +} + +// Log Commands +#[tauri::command] +#[specta::specta] +async fn get_backend_logs() -> Result, VRCError> { + LogManager::read_logs().map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn clear_backend_logs() -> Result<(), VRCError> { + info!("Clearing backend logs"); + LogManager::clear_logs().map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn export_backend_logs() -> Result { + LogManager::export_logs().map_err(|e| VRCError::unknown(e)) +} + +// VRChat Service Status Commands +#[tauri::command] +#[specta::specta] +async fn get_vrchat_status() -> Result { + vrchat_status::fetch_vrchat_status().await +} + +// Database Studio Commands +#[tauri::command] +#[specta::specta] +async fn db_list_tables() -> Result, VRCError> { + let studio = DatabaseStudio::new() + .await + .map_err(|e| VRCError::unknown(e))?; + + studio.list_tables().await.map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn db_get_table_schema(table_name: String) -> Result, VRCError> { + let studio = DatabaseStudio::new() + .await + .map_err(|e| VRCError::unknown(e))?; + + studio + .get_table_schema(&table_name) + .await + .map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn db_get_table_data( + table_name: String, + limit: Option, + offset: Option, +) -> Result { + let studio = DatabaseStudio::new() + .await + .map_err(|e| VRCError::unknown(e))?; + + studio + .get_table_data(&table_name, limit, offset) + .await + .map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn db_get_table_count(table_name: String) -> Result { + let studio = DatabaseStudio::new() + .await + .map_err(|e| VRCError::unknown(e))?; + + studio + .get_table_count(&table_name) + .await + .map_err(|e| VRCError::unknown(e)) +} + +#[tauri::command] +#[specta::specta] +async fn db_execute_query(query: String) -> Result { + let studio = DatabaseStudio::new() + .await + .map_err(|e| VRCError::unknown(e))?; + + studio + .execute_query(&query) + .await + .map_err(|e| VRCError::unknown(e)) +} + +// Binding Generation +fn create_specta_builder() -> SpectaBuilder { + SpectaBuilder::::new() + .commands(collect_commands![ + vrchat_login, + vrchat_verify_2fa, + vrchat_get_current_user, + vrchat_update_status, + vrchat_logout, + vrchat_get_online_friends, + vrchat_get_uploaded_worlds, + vrchat_get_uploaded_avatars, + get_online_friends, + get_all_friends, + get_user, + get_user_by_id, + is_friend, + is_user_online, + vrchat_check_session, + vrchat_clear_session, + websocket_start, + websocket_stop, + save_current_account, + get_all_accounts, + switch_account, + remove_account, + clear_all_accounts, + load_last_account, + get_settings, + save_settings, + get_developer_mode, + set_developer_mode, + get_backend_logs, + clear_backend_logs, + export_backend_logs, + get_vrchat_status, + db_list_tables, + db_get_table_schema, + db_get_table_data, + db_get_table_count, + db_execute_query, + check_image_cached, + cache_image, + get_cache_directory, + ]) + // Core VRChat API types + .typ::() + .typ::() + .typ::() + .typ::() + // Enum types + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + // User-related types + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + .typ::() + // World types + .typ::() + .typ::() + // Avatar types + .typ::() + .typ::() + .typ::() + // Store types + .typ::() + .typ::() + // Log types + .typ::() + // Database types + .typ::() + .typ::() + .typ::() + // Status types + .typ::() + .typ::() + .typ::() +} + +pub fn generate_bindings() { + use std::{fs, fs::File, io::Write}; + eprintln!("Generating TypeScript bindings..."); + + let specta_builder = create_specta_builder(); + let formatter = Typescript::default(); + let bindings = specta_builder + .export_str(&formatter) + .expect("Failed to generate TypeScript bindings"); + let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let output_path = manifest_dir + .join("..") + .join("src") + .join("types") + .join("bindings.ts"); + + if let Some(parent) = output_path.parent() { + fs::create_dir_all(parent).expect("Failed to create bindings directory"); + } + + let mut file = File::create(&output_path).expect("Failed to create TypeScript bindings file"); + file.write_all( + b"// @ts-nocheck\n// This file is auto-generated by Specta. Do not edit manually.\n\n", + ) + .expect("Failed to write bindings header"); + file.write_all(bindings.as_bytes()) + .expect("Failed to write TypeScript bindings"); + + formatter.format(&output_path).ok(); + eprintln!( + "Successfully generated bindings at {}", + output_path.display() + ); +} + +// Application Entry Point +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + let specta_builder = create_specta_builder(); + + #[cfg(debug_assertions)] + { + eprintln!("Auto-generating TypeScript bindings in development mode..."); + generate_bindings(); + } + + let vrchat_client = VRChatClient::new().expect("Failed to create VRChat client"); + let account_store = + tauri::async_runtime::block_on(AccountStore::new()).expect("Failed to create AccountStore"); + let settings_store = tauri::async_runtime::block_on(SettingsStore::new()) + .expect("Failed to create SettingsStore"); + let image_cache = Arc::new( + tauri::async_runtime::block_on(ImageCacheStore::new()) + .expect("Failed to create ImageCacheStore"), + ); + let user_store = UserStore::new(); + + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .plugin( + tauri_plugin_log::Builder::new() + .target(tauri_plugin_log::Target::new( + tauri_plugin_log::TargetKind::Stdout, + )) + .target(tauri_plugin_log::Target::new( + tauri_plugin_log::TargetKind::LogDir { + file_name: Some("vrc-circle".to_string()), + }, + )) + .level(log::LevelFilter::Info) + .build(), + ) + .invoke_handler(specta_builder.invoke_handler()) + .setup(move |app| { + // Initialize WebSocket with app handle and UserStore + let websocket = VRChatWebSocket::new(app.handle().clone(), user_store.clone()); + + let app_state = AppState { + vrchat_client: Arc::new(Mutex::new(vrchat_client)), + account_store, + websocket: Arc::new(Mutex::new(websocket)), + user_store, + settings_store, + image_cache: image_cache.clone(), + }; + + app.manage(app_state); + specta_builder.mount_events(app); + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/log_manager.rs b/src-tauri/src/log_manager.rs new file mode 100644 index 0000000..868e1c1 --- /dev/null +++ b/src-tauri/src/log_manager.rs @@ -0,0 +1,101 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; +use std::fs; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct LogEntry { + pub timestamp: String, + pub level: String, + pub source: String, + pub module: String, + pub message: String, +} + +pub struct LogManager; + +impl LogManager { + pub fn get_log_file_path() -> Result { + let log_dir = dirs::data_local_dir() + .ok_or("Failed to get local data directory")? + .join("cafe.kirameki.vrc-circle") + .join("logs"); + + fs::create_dir_all(&log_dir) + .map_err(|e| format!("Failed to create logs directory: {}", e))?; + + Ok(log_dir.join("vrc-circle.log")) + } + + pub fn read_logs() -> Result, String> { + let log_path = Self::get_log_file_path()?; + + if !log_path.exists() { + return Ok(Vec::new()); + } + + let content = + fs::read_to_string(&log_path).map_err(|e| format!("Failed to read log file: {}", e))?; + + let mut entries = Vec::new(); + + for line in content.lines() { + if line.trim().is_empty() { + continue; + } + + // Parse log line format: "2024-01-01 12:00:00 [INFO] module: message" + if let Some(entry) = Self::parse_log_line(line) { + entries.push(entry); + } + } + + Ok(entries) + } + + fn parse_log_line(line: &str) -> Option { + // tauri-plugin-log format + // Example: "2024-01-01 12:00:00.123 [INFO] vrc_circle: message" + + let parts: Vec<&str> = line.splitn(4, ' ').collect(); + if parts.len() < 4 { + return None; + } + + let timestamp = format!("{} {}", parts[0], parts[1]); + let level_part = parts[2].trim_matches(|c| c == '[' || c == ']'); + let rest = parts[3]; + + // Split module and message by ": " + let (module, message) = if let Some(idx) = rest.find(": ") { + let (mod_part, msg_part) = rest.split_at(idx); + (mod_part.to_string(), msg_part[2..].to_string()) + } else { + ("unknown".to_string(), rest.to_string()) + }; + + Some(LogEntry { + timestamp, + level: level_part.to_uppercase(), + source: "backend".to_string(), + module, + message, + }) + } + + pub fn clear_logs() -> Result<(), String> { + let log_path = Self::get_log_file_path()?; + + if log_path.exists() { + fs::remove_file(&log_path).map_err(|e| format!("Failed to clear log file: {}", e))?; + } + + Ok(()) + } + + pub fn export_logs() -> Result { + let entries = Self::read_logs()?; + serde_json::to_string_pretty(&entries) + .map_err(|e| format!("Failed to serialize logs: {}", e)) + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..0f2d3b1 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,17 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::env; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() > 1 && args[1] == "--generate-bindings" { + eprintln!("Generating TypeScript bindings..."); + vrc_one_lib::generate_bindings(); + eprintln!("Bindings generated successfully!"); + return; + } + + vrc_one_lib::run() +} diff --git a/src-tauri/src/store/account_store.rs b/src-tauri/src/store/account_store.rs new file mode 100644 index 0000000..7e222c6 --- /dev/null +++ b/src-tauri/src/store/account_store.rs @@ -0,0 +1,272 @@ +use sea_orm::sea_query::{Expr, OnConflict}; +use sea_orm::{ + ActiveModelTrait, ActiveValue::Set, ColumnTrait, ConnectionTrait, DatabaseConnection, + EntityTrait, QueryFilter, QueryOrder, Schema, Statement, TransactionTrait, +}; +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct StoredAccount { + pub user_id: String, + pub username: String, + pub display_name: String, + #[serde(default)] + pub avatar_url: Option, + #[serde(default)] + pub avatar_fallback_url: Option, + pub auth_cookie: Option, + pub two_factor_cookie: Option, + pub last_login: String, +} + +mod account_entity { + use sea_orm::entity::prelude::*; + use sea_orm::ActiveModelBehavior; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "accounts")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub user_id: String, + pub username: String, + pub display_name: String, + pub avatar_url: Option, + pub avatar_fallback_url: Option, + pub auth_cookie: Option, + pub two_factor_cookie: Option, + pub last_login: String, + #[sea_orm(column_type = "Boolean", default_value = 0)] + pub last_active: bool, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} +} + +use account_entity::{ + ActiveModel as AccountActiveModel, Column as AccountColumn, Entity as AccountEntity, + Model as AccountModel, +}; + +pub struct AccountStore { + db: DatabaseConnection, +} + +impl AccountStore { + pub async fn new() -> Result { + let db = crate::store::connect_db("accounts").await?; + + let store = Self { db }; + store.init_schema().await?; + + Ok(store) + } + + async fn init_schema(&self) -> Result<(), String> { + let backend = self.db.get_database_backend(); + let schema = Schema::new(backend); + + let create_table = schema + .create_table_from_entity(AccountEntity) + .if_not_exists() + .to_owned(); + + let statement: Statement = backend.build(&create_table); + self.db + .execute(statement) + .await + .map_err(|e| format!("Failed to initialize accounts table: {}", e))?; + + Ok(()) + } + + pub async fn save_account(&self, account: StoredAccount) -> Result<(), String> { + let mut txn = self + .db + .begin() + .await + .map_err(|e| format!("Failed to begin save transaction: {}", e))?; + + AccountEntity::update_many() + .col_expr(AccountColumn::LastActive, Expr::value(false)) + .exec(&mut txn) + .await + .map_err(|e| format!("Failed to clear last active flag: {}", e))?; + + let mut active_model = to_active_model(account); + active_model.last_active = Set(true); + + AccountEntity::insert(active_model) + .on_conflict( + OnConflict::column(AccountColumn::UserId) + .update_columns([ + AccountColumn::Username, + AccountColumn::DisplayName, + AccountColumn::AvatarUrl, + AccountColumn::AvatarFallbackUrl, + AccountColumn::AuthCookie, + AccountColumn::TwoFactorCookie, + AccountColumn::LastLogin, + AccountColumn::LastActive, + ]) + .to_owned(), + ) + .exec(&mut txn) + .await + .map_err(|e| format!("Failed to upsert account: {}", e))?; + + txn.commit() + .await + .map_err(|e| format!("Failed to commit account save: {}", e)) + } + + pub async fn get_account(&self, user_id: &str) -> Result, String> { + let account = AccountEntity::find_by_id(user_id.to_string()) + .one(&self.db) + .await + .map_err(|e| format!("Failed to load account: {}", e))?; + + Ok(account.map(StoredAccount::from)) + } + + pub async fn get_last_active_account(&self) -> Result, String> { + let account = AccountEntity::find() + .filter(AccountColumn::LastActive.eq(true)) + .one(&self.db) + .await + .map_err(|e| format!("Failed to load last active account: {}", e))?; + + Ok(account.map(StoredAccount::from)) + } + + pub async fn get_all_accounts(&self) -> Result, String> { + let accounts = AccountEntity::find() + .order_by_desc(AccountColumn::LastLogin) + .all(&self.db) + .await + .map_err(|e| format!("Failed to load accounts: {}", e))?; + + Ok(accounts.into_iter().map(StoredAccount::from).collect()) + } + + pub async fn remove_account(&self, user_id: &str) -> Result<(), String> { + let mut txn = self + .db + .begin() + .await + .map_err(|e| format!("Failed to begin remove transaction: {}", e))?; + + let target = AccountEntity::find_by_id(user_id.to_string()) + .one(&mut txn) + .await + .map_err(|e| format!("Failed to find account: {}", e))?; + + if target.is_none() { + txn.rollback().await.ok(); + return Ok(()); + } + + let was_active = target.as_ref().map(|acc| acc.last_active).unwrap_or(false); + + AccountEntity::delete_by_id(user_id.to_string()) + .exec(&mut txn) + .await + .map_err(|e| format!("Failed to delete account: {}", e))?; + + if was_active { + AccountEntity::update_many() + .col_expr(AccountColumn::LastActive, Expr::value(false)) + .exec(&mut txn) + .await + .map_err(|e| format!("Failed to clear last active flag: {}", e))?; + } + + txn.commit() + .await + .map_err(|e| format!("Failed to commit account removal: {}", e)) + } + + pub async fn set_active_account(&self, user_id: &str) -> Result<(), String> { + let mut txn = self + .db + .begin() + .await + .map_err(|e| format!("Failed to begin activation transaction: {}", e))?; + + let account = AccountEntity::find_by_id(user_id.to_string()) + .one(&mut txn) + .await + .map_err(|e| format!("Failed to find account: {}", e))?; + + let Some(model) = account else { + txn.rollback().await.ok(); + return Err("Account not found".to_string()); + }; + + AccountEntity::update_many() + .col_expr(AccountColumn::LastActive, Expr::value(false)) + .exec(&mut txn) + .await + .map_err(|e| format!("Failed to clear last active flags: {}", e))?; + + let mut active_model: AccountActiveModel = model.into(); + active_model.last_active = Set(true); + active_model + .update(&mut txn) + .await + .map_err(|e| format!("Failed to mark account active: {}", e))?; + + txn.commit() + .await + .map_err(|e| format!("Failed to commit account activation: {}", e)) + } + + pub async fn clear_all_accounts(&self) -> Result<(), String> { + AccountEntity::delete_many() + .exec(&self.db) + .await + .map_err(|e| format!("Failed to clear accounts: {}", e))?; + Ok(()) + } + + pub async fn clear_last_active_account(&self) -> Result<(), String> { + AccountEntity::update_many() + .col_expr(AccountColumn::LastActive, Expr::value(false)) + .exec(&self.db) + .await + .map_err(|e| format!("Failed to clear last active account: {}", e))?; + Ok(()) + } +} + +fn to_active_model(account: StoredAccount) -> AccountActiveModel { + AccountActiveModel { + user_id: Set(account.user_id), + username: Set(account.username), + display_name: Set(account.display_name), + avatar_url: Set(account.avatar_url), + avatar_fallback_url: Set(account.avatar_fallback_url), + auth_cookie: Set(account.auth_cookie), + two_factor_cookie: Set(account.two_factor_cookie), + last_login: Set(account.last_login), + last_active: Set(false), + } +} + +impl From for StoredAccount { + fn from(model: AccountModel) -> Self { + Self { + user_id: model.user_id, + username: model.username, + display_name: model.display_name, + avatar_url: model.avatar_url, + avatar_fallback_url: model.avatar_fallback_url, + auth_cookie: model.auth_cookie, + two_factor_cookie: model.two_factor_cookie, + last_login: model.last_login, + } + } +} diff --git a/src-tauri/src/store/db.rs b/src-tauri/src/store/db.rs new file mode 100644 index 0000000..d7e9a68 --- /dev/null +++ b/src-tauri/src/store/db.rs @@ -0,0 +1,24 @@ +use sea_orm::{ConnectOptions, Database, DatabaseConnection}; + +pub async fn connect_db(component: &str) -> Result { + // Use per-user local data directory (this is %LOCALAPPDATA% on Windows) + let base_dir = dirs::data_local_dir() + .ok_or("Failed to resolve local data directory")? + .join("vrc-circle"); + + std::fs::create_dir_all(&base_dir) + .map_err(|e| format!("Failed to create app data directory: {}", e))?; + + let db_path = base_dir.join("data.sqlite"); + let db_url = format!( + "sqlite://{}?mode=rwc", + db_path.to_string_lossy().replace('\\', "/") + ); + + let mut options = ConnectOptions::new(db_url); + options.sqlx_logging(false); + + Database::connect(options) + .await + .map_err(|e| format!("Failed to connect to {} database: {}", component, e)) +} diff --git a/src-tauri/src/store/image_cache.rs b/src-tauri/src/store/image_cache.rs new file mode 100644 index 0000000..80dc849 --- /dev/null +++ b/src-tauri/src/store/image_cache.rs @@ -0,0 +1,328 @@ +use crate::http_common::{ + INITIAL_BACKOFF as INITIAL_BACKOFF_MS, MAX_BACKOFF as MAX_BACKOFF_MS, MAX_DOWNLOADS_PER_SECOND, + MAX_REQUEST_RETRIES, USER_AGENT_STRING, +}; +use reqwest::{ + Client, + header::{COOKIE, USER_AGENT}, +}; +use sha2::{Digest, Sha256}; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::fs; +use tokio::io::AsyncWriteExt; +use tokio::sync::Mutex; +use tokio::time::sleep; + +const INITIAL_BACKOFF: Duration = Duration::from_millis(INITIAL_BACKOFF_MS); +const MAX_BACKOFF: Duration = Duration::from_millis(MAX_BACKOFF_MS); + +pub struct ImageCacheStore { + base_dir: PathBuf, + client: Client, + in_flight: Arc>>, + rate_limiter: Arc>, +} + +struct RateLimiter { + last_reset: Instant, + tokens: u32, + max_tokens: u32, +} + +impl RateLimiter { + fn new(max_tokens: u32) -> Self { + Self { + last_reset: Instant::now(), + tokens: max_tokens, + max_tokens, + } + } + + async fn acquire(&mut self) { + loop { + // Refill tokens if duration has passed + let now = Instant::now(); + if now.duration_since(self.last_reset) >= Duration::from_secs(1) { + self.tokens = self.max_tokens; + self.last_reset = now; + } + + // If we have tokens, consume one and return + if self.tokens > 0 { + self.tokens -= 1; + return; + } + + // Otherwise, sleep until the next refill + let time_until_refill = + Duration::from_secs(1).saturating_sub(now.duration_since(self.last_reset)); + if time_until_refill > Duration::from_millis(0) { + sleep(time_until_refill).await; + } + } + } +} + +impl ImageCacheStore { + pub async fn new() -> Result { + // Use per-user local data directory (this is %LOCALAPPDATA% on Windows) + let base_dir = dirs::data_local_dir() + .ok_or("Failed to resolve local data directory")? + .join("vrc-circle"); + + let cache_dir = base_dir.join("cache").join("files"); + + log::info!("Image cache directory: {}", cache_dir.display()); + + fs::create_dir_all(&cache_dir) + .await + .map_err(|e| format!("Failed to create cache directory: {}", e))?; + + let client = Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .map_err(|e| format!("Failed to create cache HTTP client: {}", e))?; + + Ok(Self { + base_dir: cache_dir, + client, + in_flight: Arc::new(Mutex::new(HashSet::new())), + rate_limiter: Arc::new(Mutex::new(RateLimiter::new(MAX_DOWNLOADS_PER_SECOND))), + }) + } + + pub fn get_cache_dir(&self) -> &Path { + &self.base_dir + } + + pub async fn get_cached_path(&self, url: &str) -> Option { + if url.trim().is_empty() { + return None; + } + + let (file_name, extension) = Self::hash_filename(url); + let mut file_path = self.base_dir.join(&file_name); + if let Some(ext) = extension { + file_path.set_extension(ext); + } + + if fs::metadata(&file_path).await.is_ok() { + Some(file_path) + } else { + None + } + } + + pub async fn get_or_fetch( + &self, + url: &str, + auth_cookies: Option, + ) -> Result { + log::info!("[CACHE] get_or_fetch called for: {}", url); + + if url.trim().is_empty() { + return Err("Image URL is empty".to_string()); + } + + let (file_name, extension) = Self::hash_filename(url); + let mut file_path = self.base_dir.join(&file_name); + if let Some(ext) = extension { + file_path.set_extension(ext); + } + + log::info!("[CACHE] Computed file path: {}", file_path.display()); + + // If cache hit + if fs::metadata(&file_path).await.is_ok() { + log::info!("[CACHE] HIT: {}", file_path.display()); + return Ok(file_path); + } + + // Rate limiter, wait for a token before acquiring the in-flight lock + { + let mut limiter = self.rate_limiter.lock().await; + limiter.acquire().await; + } + + // Deduplicate, if another download is in progress for this URL, wait for it to complete + { + let mut in_flight = self.in_flight.lock().await; + if in_flight.contains(url) { + drop(in_flight); + log::debug!("Download already in progress for: {}, waiting...", url); + + // Poll for status + for _ in 0..60 { + // Wait up to 30 seconds (60 * 500ms) + sleep(Duration::from_millis(500)).await; + if fs::metadata(&file_path).await.is_ok() { + log::debug!( + "Download completed by another request: {}", + file_path.display() + ); + return Ok(file_path); + } + } + return Err(format!("Timeout waiting for concurrent download: {}", url)); + } + + // Mark this URL as in-flight + in_flight.insert(url.to_string()); + } + + log::info!("Cache MISS: Downloading {}", url); + let result = self.do_download(url, auth_cookies, &file_path).await; + + // Remove from in-flight set + { + let mut in_flight = self.in_flight.lock().await; + in_flight.remove(url); + } + + result + } + + async fn do_download( + &self, + url: &str, + auth_cookies: Option, + file_path: &Path, + ) -> Result { + let tmp_path = file_path.with_extension("tmp"); + + if let Some(parent) = tmp_path.parent() { + fs::create_dir_all(parent) + .await + .map_err(|e| format!("Failed to prepare cache directory: {}", e))?; + } + + let bytes = self.download_with_retry(url, auth_cookies).await?; + log::info!("Downloaded {} bytes from {}", bytes.len(), url); + + let mut file = fs::File::create(&tmp_path) + .await + .map_err(|e| format!("Failed to create cache file: {}", e))?; + file.write_all(&bytes) + .await + .map_err(|e| format!("Failed to write cache file: {}", e))?; + file.flush() + .await + .map_err(|e| format!("Failed to flush cache file: {}", e))?; + + fs::rename(&tmp_path, &file_path) + .await + .map_err(|e| format!("Failed to finalize cache file: {}", e))?; + + log::info!("Cached to: {}", file_path.display()); + + Ok(file_path.to_path_buf()) + } + + fn hash_filename(url: &str) -> (String, Option) { + let mut hasher = Sha256::new(); + hasher.update(url.as_bytes()); + let hash = format!("{:x}", hasher.finalize()); + + let extension = url::Url::parse(url).ok().and_then(|parsed| { + Path::new(parsed.path()) + .file_name() + .and_then(|name| name.to_str()) + .map(|name| name.split('?').next().unwrap_or(name)) + .and_then(|name| Path::new(name).extension()) + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_lowercase()) + }); + + (hash, extension) + } + + async fn download_with_retry( + &self, + url: &str, + auth_cookies: Option, + ) -> Result, String> { + let mut attempt = 0; + let mut backoff = INITIAL_BACKOFF; + + loop { + let mut request = self.client.get(url).header(USER_AGENT, USER_AGENT_STRING); + + // Only pass cookies if URL is from VRChat domains to prevent leaking credentials + let is_vrchat_domain = url.starts_with("https://api.vrchat.cloud/") + || url.starts_with("https://files.vrchat.cloud/") + || url.starts_with("https://assets.vrchat.com/") + || url.starts_with("https://d348imysud55la.cloudfront.net/"); + + if is_vrchat_domain { + if let Some(ref cookies) = auth_cookies { + request = request.header(COOKIE, cookies); + } + } + + let response = request.send().await; + match response { + Ok(resp) => { + let status = resp.status(); + log::debug!("[CACHE] HTTP {}: {}", status.as_u16(), url); + + if status.is_success() { + return resp.bytes().await.map(|bytes| bytes.to_vec()).map_err(|e| { + log::error!("[CACHE] Failed to read bytes: {}", e); + format!("Failed to read image bytes: {}", e) + }); + } + + if status.as_u16() == 429 || status.is_server_error() { + log::warn!("[CACHE] Retryable error {} for: {}", status.as_u16(), url); + if attempt >= MAX_REQUEST_RETRIES { + return Err(format!( + "Image request failed after retries (status {}): {}", + status.as_u16(), + url + )); + } + + let wait = Self::retry_after_seconds(&resp).unwrap_or(backoff); + sleep(wait).await; + attempt += 1; + backoff = (backoff * 2).min(MAX_BACKOFF); + continue; + } + + log::error!("[CACHE] HTTP error {}: {}", status.as_u16(), url); + return Err(format!( + "Failed to download image (status {}): {}", + status.as_u16(), + url + )); + } + Err(err) => { + log::error!( + "[CACHE] Network error: {} (attempt {}/{})", + err, + attempt + 1, + MAX_REQUEST_RETRIES + ); + if attempt >= MAX_REQUEST_RETRIES { + return Err(format!("Failed to download image: {}", err)); + } + sleep(backoff).await; + attempt += 1; + backoff = (backoff * 2).min(MAX_BACKOFF); + } + } + } + } + + fn retry_after_seconds(response: &reqwest::Response) -> Option { + response + .headers() + .get("retry-after") + .and_then(|value| value.to_str().ok()) + .and_then(|seconds| seconds.parse::().ok()) + .map(Duration::from_secs) + } +} diff --git a/src-tauri/src/store/mod.rs b/src-tauri/src/store/mod.rs new file mode 100644 index 0000000..4a9d13e --- /dev/null +++ b/src-tauri/src/store/mod.rs @@ -0,0 +1,11 @@ +pub mod account_store; +pub mod image_cache; +pub mod settings_store; +pub mod user_store; +pub mod db; + +pub use account_store::{AccountStore, StoredAccount}; +pub use image_cache::ImageCacheStore; +pub use settings_store::{AppSettings, SettingsStore}; +pub use user_store::UserStore; +pub use db::connect_db; diff --git a/src-tauri/src/store/settings_store.rs b/src-tauri/src/store/settings_store.rs new file mode 100644 index 0000000..eb57d0a --- /dev/null +++ b/src-tauri/src/store/settings_store.rs @@ -0,0 +1,135 @@ +use sea_orm::sea_query::OnConflict; +use sea_orm::{ + ActiveValue::Set, ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait, QueryFilter, + Schema, Statement, +}; +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct AppSettings { + pub developer_mode: bool, +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + developer_mode: false, + } + } +} + +mod settings_entity { + use sea_orm::ActiveModelBehavior; + use sea_orm::entity::prelude::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "settings")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub key: String, + pub value: String, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} +} + +use settings_entity::{ + ActiveModel as SettingsActiveModel, Column as SettingsColumn, Entity as SettingsEntity, +}; + +pub struct SettingsStore { + db: DatabaseConnection, +} + +impl SettingsStore { + pub async fn new() -> Result { + let db = crate::store::connect_db("settings").await?; + let store = Self { db }; + store.init_schema().await?; + + Ok(store) + } + + async fn init_schema(&self) -> Result<(), String> { + let backend = self.db.get_database_backend(); + let schema = Schema::new(backend); + + let create_table = schema + .create_table_from_entity(SettingsEntity) + .if_not_exists() + .to_owned(); + + let statement: Statement = backend.build(&create_table); + self.db + .execute(statement) + .await + .map_err(|e| format!("Failed to initialize settings table: {}", e))?; + + Ok(()) + } + + async fn get_setting(&self, key: &str, default: &str) -> Result { + let settings_row = SettingsEntity::find() + .filter(SettingsColumn::Key.eq(key)) + .one(&self.db) + .await + .map_err(|e| format!("Failed to load setting '{}': {}", key, e))?; + + Ok(settings_row + .map(|row| row.value) + .unwrap_or_else(|| default.to_string())) + } + + async fn set_setting(&self, key: &str, value: &str) -> Result<(), String> { + let active_model = SettingsActiveModel { + key: Set(key.to_string()), + value: Set(value.to_string()), + }; + + SettingsEntity::insert(active_model) + .on_conflict( + OnConflict::column(SettingsColumn::Key) + .update_column(SettingsColumn::Value) + .to_owned(), + ) + .exec(&self.db) + .await + .map_err(|e| format!("Failed to save setting '{}': {}", key, e))?; + + Ok(()) + } + + pub async fn get_settings(&self) -> Result { + let developer_mode = self.get_setting("developer_mode", "false").await?; + + Ok(AppSettings { + developer_mode: developer_mode == "true", + }) + } + + pub async fn save_settings(&self, settings: AppSettings) -> Result<(), String> { + self.set_setting( + "developer_mode", + if settings.developer_mode { + "true" + } else { + "false" + }, + ) + .await + } + + pub async fn get_developer_mode(&self) -> Result { + let value = self.get_setting("developer_mode", "false").await?; + Ok(value == "true") + } + + pub async fn set_developer_mode(&self, enabled: bool) -> Result<(), String> { + self.set_setting("developer_mode", if enabled { "true" } else { "false" }) + .await + } +} diff --git a/src-tauri/src/store/user_store.rs b/src-tauri/src/store/user_store.rs new file mode 100644 index 0000000..8c88b7c --- /dev/null +++ b/src-tauri/src/store/user_store.rs @@ -0,0 +1,651 @@ +use crate::vrchat_api::types::{LimitedUserFriend, User, UserStatus}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UserRelationship { + CurrentUser, + Friend, + Known, + Unknown, +} + +fn parse_user_status(status: &str) -> UserStatus { + match status { + "active" => UserStatus::Active, + "join me" => UserStatus::JoinMe, + "ask me" => UserStatus::AskMe, + "busy" => UserStatus::Busy, + _ => UserStatus::Offline, // Default fallback + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Platform { + StandaloneWindows, + Android, + Web, + Other(String), +} + +fn parse_platform(platform: &str) -> Platform { + match platform { + "standalonewindows" => Platform::StandaloneWindows, + "android" => Platform::Android, + "web" => Platform::Web, + other => Platform::Other(other.to_string()), + } +} + +/// Cached user representation +#[derive(Debug, Clone)] +pub struct CachedUser { + pub id: String, + pub display_name: String, + pub username: Option, + pub user_icon: Option, + pub profile_pic_override: Option, + pub profile_pic_override_thumbnail: Option, + pub current_avatar_image_url: Option, + pub current_avatar_thumbnail_image_url: Option, + pub bio: Option, + pub status: Option, + pub status_description: Option, + pub location: Option, + pub platform: Option, + pub relationship: UserRelationship, + pub full_user: Option, + pub friend_data: Option, + pub last_updated: std::time::Instant, +} + +impl CachedUser { + /// Create from LimitedUserFriend (friend list entry) + pub fn from_friend(friend: LimitedUserFriend) -> Self { + Self { + id: friend.id.clone(), + display_name: friend.display_name.clone(), + username: None, + user_icon: friend.user_icon.clone(), + profile_pic_override: friend.profile_pic_override.clone(), + profile_pic_override_thumbnail: friend.profile_pic_override_thumbnail.clone(), + current_avatar_image_url: friend.current_avatar_image_url.clone(), + current_avatar_thumbnail_image_url: friend.current_avatar_thumbnail_image_url.clone(), + bio: Some(friend.bio.clone()), + status: Some(friend.status.clone()), + status_description: Some(friend.status_description.clone()), + location: friend.location.clone(), + platform: Some(parse_platform(&friend.platform)), + relationship: UserRelationship::Friend, + full_user: None, + friend_data: Some(friend), + last_updated: std::time::Instant::now(), + } + } + + /// Create from User (full user object) + pub fn from_user(user: User, relationship: UserRelationship) -> Self { + Self { + id: user.id.clone(), + display_name: user.display_name.clone(), + username: if user.username.is_empty() { + None + } else { + Some(user.username.clone()) + }, + user_icon: user.user_icon.clone(), + profile_pic_override: user.profile_pic_override.clone(), + profile_pic_override_thumbnail: user.profile_pic_override_thumbnail.clone(), + current_avatar_image_url: user.current_avatar_image_url.clone(), + current_avatar_thumbnail_image_url: user.current_avatar_thumbnail_image_url.clone(), + bio: Some(user.bio.clone()), + status: Some(user.status.clone()), + status_description: Some(user.status_description.clone()), + location: user.location.clone(), + platform: Some(parse_platform(&user.platform)), + relationship, + full_user: Some(user), + friend_data: None, + last_updated: std::time::Instant::now(), + } + } + + /// Update from friend data + pub fn update_from_friend(&mut self, friend: LimitedUserFriend) { + self.display_name = friend.display_name.clone(); + self.user_icon = friend.user_icon.clone(); + self.profile_pic_override = friend.profile_pic_override.clone(); + self.profile_pic_override_thumbnail = friend.profile_pic_override_thumbnail.clone(); + self.current_avatar_image_url = friend.current_avatar_image_url.clone(); + self.current_avatar_thumbnail_image_url = friend.current_avatar_thumbnail_image_url.clone(); + self.bio = Some(friend.bio.clone()); + self.status = Some(friend.status.clone()); + self.status_description = Some(friend.status_description.clone()); + self.location = friend.location.clone(); + self.platform = Some(parse_platform(&friend.platform)); + self.friend_data = Some(friend); + self.last_updated = std::time::Instant::now(); + } + + pub fn is_online(&self) -> bool { + self.location.as_ref().map_or(false, |loc| { + loc != "offline" && loc != "private" && !loc.is_empty() + }) + } + + /// Get as LimitedUserFriend if available + pub fn as_friend(&self) -> Option<&LimitedUserFriend> { + self.friend_data.as_ref() + } + + pub fn age_seconds(&self) -> u64 { + self.last_updated.elapsed().as_secs() + } +} + +/// Minimal representation of a current-user update emitted by the pipeline websocket. +#[derive(Debug, Clone)] +pub struct CurrentUserPipelineUpdate { + pub id: String, + pub display_name: String, + pub username: String, + pub status: String, + pub status_description: String, + pub bio: String, + pub user_icon: Option, + pub profile_pic_override: Option, + pub profile_pic_override_thumbnail: Option, + pub current_avatar: Option, + pub current_avatar_asset_url: Option, + pub current_avatar_image_url: Option, + pub current_avatar_thumbnail_image_url: Option, + pub fallback_avatar: Option, + pub tags: Vec, +} + +#[derive(Clone)] +pub struct UserStore { + users: Arc>>, + current_user_id: Arc>>, +} + +impl UserStore { + pub fn new() -> Self { + Self { + users: Arc::new(RwLock::new(HashMap::new())), + current_user_id: Arc::new(RwLock::new(None)), + } + } + + /// Current User Management + pub async fn set_current_user(&self, user: User) { + let user_id = user.id.clone(); + let cached_user = CachedUser::from_user(user, UserRelationship::CurrentUser); + + let mut users = self.users.write().await; + users.insert(user_id.clone(), cached_user); + drop(users); + + let mut current = self.current_user_id.write().await; + *current = Some(user_id.clone()); + + log::info!("UserStore: Set current user to {}", user_id); + } + + pub async fn get_current_user(&self) -> Option { + let current_id = self.current_user_id.read().await; + let user_id = current_id.as_ref()?.clone(); + drop(current_id); + + let users = self.users.read().await; + users.get(&user_id)?.full_user.clone() + } + + pub async fn get_current_user_id(&self) -> Option { + let current = self.current_user_id.read().await; + current.clone() + } + + pub async fn clear_current_user(&self) { + let mut current = self.current_user_id.write().await; + *current = None; + log::info!("UserStore: Cleared current user"); + } + + // Friend Management + + // TODO: Refactor this code and function name + /// Initialize friends list (from REST API on startup) + pub async fn set_friends(&self, friends: Vec) { + let mut users = self.users.write().await; + + // Mark all existing friends as non-friends first + for user in users.values_mut() { + if user.relationship == UserRelationship::Friend { + user.relationship = UserRelationship::Known; + } + } + + // Add/update friends + for friend in friends { + let user_id = friend.id.clone(); + + if let Some(existing) = users.get_mut(&user_id) { + existing.update_from_friend(friend); + existing.relationship = UserRelationship::Friend; + } else { + users.insert(user_id, CachedUser::from_friend(friend)); + } + } + + let friend_count = users + .values() + .filter(|u| u.relationship == UserRelationship::Friend) + .count(); + + log::info!("UserStore: Initialized {} friends", friend_count); + } + + /// Upsert friend + pub async fn upsert_friend(&self, friend: LimitedUserFriend) { + let user_id = friend.id.clone(); + + // Check if this is the current user + // Of course, don't add yourself to friends list here + let current_id = self.current_user_id.read().await; + if let Some(ref current) = *current_id { + if current == &user_id { + log::debug!( + "UserStore: Skipping upsert_friend for current user {}", + user_id + ); + drop(current_id); + + // But still update other data if available + let mut users = self.users.write().await; + if let Some(existing) = users.get_mut(&user_id) { + if let Some(location) = friend.location.clone() { + existing.location = Some(location.clone()); + + if let Some(full_user) = existing.full_user.as_mut() { + full_user.location = Some(location); + } + } + + if !friend.platform.is_empty() { + existing.platform = Some(parse_platform(&friend.platform)); + + if let Some(full_user) = existing.full_user.as_mut() { + full_user.platform = friend.platform.clone(); + } + } + + { + existing.status = Some(friend.status.clone()); + + if let Some(full_user) = existing.full_user.as_mut() { + full_user.status = friend.status; + } + } + + if !friend.status_description.is_empty() { + existing.status_description = Some(friend.status_description.clone()); + + if let Some(full_user) = existing.full_user.as_mut() { + full_user.status_description = friend.status_description.clone(); + } + } + + existing.friend_data = Some(friend); + existing.last_updated = std::time::Instant::now(); + } + return; + } + } + drop(current_id); + + let mut users = self.users.write().await; + + if let Some(existing) = users.get_mut(&user_id) { + existing.update_from_friend(friend); + existing.relationship = UserRelationship::Friend; + } else { + users.insert(user_id.clone(), CachedUser::from_friend(friend)); + } + + log::debug!("UserStore: Upserted friend {}", user_id); + } + + /// Mark a friend as offline + pub async fn set_friend_offline(&self, user_id: &str) { + let mut users = self.users.write().await; + + if let Some(user) = users.get_mut(user_id) { + user.location = Some("offline".to_string()); + user.last_updated = std::time::Instant::now(); + + if let Some(ref mut friend_data) = user.friend_data { + friend_data.location = Some("offline".to_string()); + friend_data.platform = String::new(); + } + + log::debug!("UserStore: User {} went offline", user_id); + } + } + + /// Update friend's location + pub async fn update_user_location( + &self, + user_id: &str, + location: String, + platform: Option, + ) { + let mut users = self.users.write().await; + + if let Some(user) = users.get_mut(user_id) { + user.location = Some(location.clone()); + if let Some(plat) = platform { + user.platform = Some(parse_platform(&plat)); + } + user.last_updated = std::time::Instant::now(); + + if let Some(ref mut friend_data) = user.friend_data { + friend_data.location = Some(location.clone()); + if let Some(ref plat) = user.platform { + friend_data.platform = match plat { + Platform::StandaloneWindows => "standalonewindows".to_string(), + Platform::Android => "android".to_string(), + Platform::Web => "web".to_string(), + Platform::Other(s) => s.clone(), + }; + } + } + + log::trace!( + "UserStore: User {} location updated to {}", + user_id, + location + ); + } + } + + /// Remove a friend when the relationship is terminated :( + pub async fn remove_friend(&self, user_id: &str) { + let mut users = self.users.write().await; + + if let Some(user) = users.get_mut(user_id) { + user.relationship = UserRelationship::Known; + user.friend_data = None; + user.location = None; + user.platform = None; + user.last_updated = std::time::Instant::now(); + + log::info!("UserStore: Removed friend {}", user_id); + } else { + log::debug!("UserStore: Attempted to remove unknown friend {}", user_id); + } + } + + /// Patch the cached current-user record with data streamed from the websocket. + pub async fn apply_current_user_update(&self, patch: CurrentUserPipelineUpdate) { + use std::time::Instant; + + { + let mut users = self.users.write().await; + let entry = users.entry(patch.id.clone()).or_insert_with(|| CachedUser { + id: patch.id.clone(), + display_name: patch.display_name.clone(), + username: Some(patch.username.clone()), + user_icon: patch.user_icon.clone(), + profile_pic_override: patch.profile_pic_override.clone(), + profile_pic_override_thumbnail: patch.profile_pic_override_thumbnail.clone(), + current_avatar_image_url: patch.current_avatar_image_url.clone(), + current_avatar_thumbnail_image_url: patch + .current_avatar_thumbnail_image_url + .clone(), + bio: Some(patch.bio.clone()), + status: Some(parse_user_status(&patch.status)), + status_description: Some(patch.status_description.clone()), + location: None, + platform: None, + relationship: UserRelationship::CurrentUser, + full_user: None, + friend_data: None, + last_updated: Instant::now(), + }); + + entry.display_name = patch.display_name.clone(); + entry.username = Some(patch.username.clone()); + entry.user_icon = patch.user_icon.clone(); + entry.profile_pic_override = patch.profile_pic_override.clone(); + entry.profile_pic_override_thumbnail = patch.profile_pic_override_thumbnail.clone(); + entry.current_avatar_image_url = patch.current_avatar_image_url.clone(); + entry.current_avatar_thumbnail_image_url = + patch.current_avatar_thumbnail_image_url.clone(); + entry.bio = Some(patch.bio.clone()); + entry.status = Some(parse_user_status(&patch.status)); + entry.status_description = Some(patch.status_description.clone()); + entry.relationship = UserRelationship::CurrentUser; + entry.last_updated = Instant::now(); + + if let Some(full_user) = entry.full_user.as_mut() { + full_user.display_name = patch.display_name.clone(); + full_user.username = patch.username.clone(); + full_user.status = parse_user_status(&patch.status); + full_user.status_description = patch.status_description.clone(); + full_user.bio = patch.bio.clone(); + full_user.user_icon = patch.user_icon.clone(); + full_user.profile_pic_override = patch.profile_pic_override.clone(); + full_user.profile_pic_override_thumbnail = + patch.profile_pic_override_thumbnail.clone(); + full_user.current_avatar = patch.current_avatar.clone(); + full_user.current_avatar_image_url = patch.current_avatar_image_url.clone(); + full_user.current_avatar_thumbnail_image_url = + patch.current_avatar_thumbnail_image_url.clone(); + full_user.fallback_avatar = patch.fallback_avatar.clone(); + full_user.tags = patch.tags.clone(); + } + } + + let mut current_id = self.current_user_id.write().await; + *current_id = Some(patch.id); + } + + /// Get all friends (online and offline) + pub async fn get_all_friends(&self) -> Vec { + let users = self.users.read().await; + users + .values() + .filter(|u| u.relationship == UserRelationship::Friend) + .filter_map(|u| u.friend_data.clone()) + .collect() + } + + /// Get all online friends + pub async fn get_online_friends(&self) -> Vec { + let users = self.users.read().await; + users + .values() + .filter(|u| u.relationship == UserRelationship::Friend) + .filter(|u| u.is_online()) + .filter_map(|u| u.friend_data.clone()) + .collect() + } + + /// Get count of online friends + pub async fn get_online_friend_count(&self) -> usize { + let users = self.users.read().await; + users + .values() + .filter(|u| u.relationship == UserRelationship::Friend) + .filter(|u| u.is_online()) + .count() + } + + // General User Queries + + /// Get a user by ID (returns friend data if they're a friend) + pub async fn get_user(&self, user_id: &str) -> Option { + let users = self.users.read().await; + users.get(user_id)?.friend_data.clone() + } + + /// Get a full User object by ID + pub async fn get_full_user(&self, user_id: &str) -> Option { + let users = self.users.read().await; + users.get(user_id)?.full_user.clone() + } + + /// Cache a full User object (for non-current users) + pub async fn cache_full_user(&self, user: User) { + let user_id = user.id.clone(); + let mut users = self.users.write().await; + + if let Some(existing) = users.get_mut(&user_id) { + // Update existing cached user with full user data + let relationship = existing.relationship.clone(); + existing.display_name = user.display_name.clone(); + + // Only update username if it's not empty + // You only see usernames for yourself in the API + if !user.username.is_empty() { + existing.username = Some(user.username.clone()); + } + existing.user_icon = user.user_icon.clone(); + existing.profile_pic_override = user.profile_pic_override.clone(); + existing.profile_pic_override_thumbnail = user.profile_pic_override_thumbnail.clone(); + existing.current_avatar_image_url = user.current_avatar_image_url.clone(); + existing.current_avatar_thumbnail_image_url = + user.current_avatar_thumbnail_image_url.clone(); + existing.bio = Some(user.bio.clone()); + existing.status = Some(user.status.clone()); + existing.status_description = Some(user.status_description.clone()); + existing.location = user.location.clone(); + existing.platform = Some(parse_platform(&user.platform)); + existing.full_user = Some(user); + existing.last_updated = std::time::Instant::now(); + existing.relationship = relationship; + } else { + users.insert( + user_id.clone(), + CachedUser::from_user(user, UserRelationship::Known), + ); + } + + log::debug!("UserStore: Cached full user data for {}", user_id); + } + + /// Get cached user entry + pub async fn get_cached_user(&self, user_id: &str) -> Option { + let users = self.users.read().await; + users.get(user_id).cloned() + } + + /// Check if a user is a friend + pub async fn is_friend(&self, user_id: &str) -> bool { + let users = self.users.read().await; + users + .get(user_id) + .map_or(false, |u| u.relationship == UserRelationship::Friend) + } + + /// Check if a user is online + pub async fn is_user_online(&self, user_id: &str) -> bool { + let users = self.users.read().await; + users.get(user_id).map_or(false, |u| u.is_online()) + } + + /// Search users by display name (case-insensitive, partial match) + pub async fn search_users(&self, query: &str) -> Vec { + let users = self.users.read().await; + let query_lower = query.to_lowercase(); + + users + .values() + .filter(|u| u.display_name.to_lowercase().contains(&query_lower)) + .cloned() + .collect() + } + + // Cache Management + + /// Clear all cached users (keeps current user) + pub async fn clear_cache(&self) { + let current_id = self.current_user_id.read().await.clone(); + let mut users = self.users.write().await; + + if let Some(current_id) = current_id { + users.retain(|id, _| id == ¤t_id); // Keep only current user + } else { + users.clear(); + } + + log::info!("UserStore: Cleared cache"); + } + + /// Clear all data + pub async fn clear_all(&self) { + let mut users = self.users.write().await; + users.clear(); + drop(users); + + let mut current = self.current_user_id.write().await; + *current = None; + + log::info!("UserStore: Cleared all data"); + } + + /// Remove stale entries that are older than max_age_seconds + pub async fn remove_stale(&self, max_age_seconds: u64) { + let current_id = self.current_user_id.read().await.clone(); + let mut users = self.users.write().await; + + let before_count = users.len(); + users.retain(|id, user| { + // Keep current user and friends + if Some(id) == current_id.as_ref() || user.relationship == UserRelationship::Friend { + return true; + } + // Keep recent entries + user.age_seconds() < max_age_seconds + }); + + let removed = before_count - users.len(); + if removed > 0 { + log::info!("UserStore: Removed {} stale entries", removed); + } + } + + pub async fn get_stats(&self) -> UserStoreStats { + let users = self.users.read().await; + let current_id = self.current_user_id.read().await.clone(); + + UserStoreStats { + total_cached: users.len(), + friends: users + .values() + .filter(|u| u.relationship == UserRelationship::Friend) + .count(), + online_friends: users + .values() + .filter(|u| u.relationship == UserRelationship::Friend && u.is_online()) + .count(), + has_current_user: current_id.is_some(), + } + } +} + +impl Default for UserStore { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone)] +pub struct UserStoreStats { + pub total_cached: usize, + pub friends: usize, + pub online_friends: usize, + pub has_current_user: bool, +} diff --git a/src-tauri/src/vrchat_api/client.rs b/src-tauri/src/vrchat_api/client.rs new file mode 100644 index 0000000..1a979a7 --- /dev/null +++ b/src-tauri/src/vrchat_api/client.rs @@ -0,0 +1,710 @@ +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; +use reqwest::header::HeaderMap; +use reqwest::{Client, Request, RequestBuilder, Response}; +use std::collections::HashSet; +use std::sync::Arc; +use tokio::sync::Mutex; +use tokio::time::{Duration, sleep}; + +use crate::http_common::{INITIAL_BACKOFF, MAX_BACKOFF, MAX_REQUEST_RETRIES}; +use crate::vrchat_api::{ + error::{VRCError, VRCResult}, + types::*, +}; + +const API_BASE_URL: &str = "https://api.vrchat.cloud/api/1"; + +// Cookie Management +#[derive(Debug, Clone, Default)] +struct CookieStore { + auth_cookie: Option, + two_factor_cookie: Option, +} + +impl CookieStore { + fn to_header_value(&self) -> Option { + let mut parts = Vec::new(); + + if let Some(auth) = &self.auth_cookie { + parts.push(auth.clone()); + } + + if let Some(two_fa) = &self.two_factor_cookie { + parts.push(two_fa.clone()); + } + + if parts.is_empty() { + None + } else { + Some(parts.join("; ")) + } + } + + fn update_from_response(&mut self, response: &Response) { + for cookie_header in response.headers().get_all("set-cookie") { + if let Ok(cookie_str) = cookie_header.to_str() { + if let Some(cookie_pair) = cookie_str.split(';').next() { + if cookie_pair.starts_with("auth=") { + self.auth_cookie = Some(cookie_pair.to_string()); + } else if cookie_pair.starts_with("twoFactorAuth=") { + self.two_factor_cookie = Some(cookie_pair.to_string()); + } + } + } + } + } + + fn clear(&mut self) { + *self = Self::default(); + } + + fn has_auth(&self) -> bool { + self.auth_cookie.is_some() + } +} + +// VRChat HTTP Client for VRChat API requests +#[derive(Clone)] +pub struct VRChatClient { + http_client: Client, + cookies: Arc>, +} + +impl VRChatClient { + /// Create a new VRChat API client + pub fn new() -> VRCResult { + let http_client = Client::builder() + .cookie_store(false) + .build() + .map_err(|e| VRCError::network(format!("Failed to create HTTP client: {}", e)))?; + + Ok(Self { + http_client, + cookies: Arc::new(Mutex::new(CookieStore::default())), + }) + } + + // Authentication Methods + + /// Attempt to log in with email and password + pub async fn login(&self, credentials: &LoginCredentials) -> VRCResult { + let auth_header = Self::create_basic_auth(&credentials.email, &credentials.password); + let headers = self.build_headers(Some(&auth_header), None, None); + + let response = self + .execute_request( + self.http_client + .get(&format!("{}/auth/user", API_BASE_URL)) + .headers(headers), + ) + .await?; + + let mut cookies = self.cookies.lock().await; + cookies.update_from_response(&response); + drop(cookies); + + let status = response.status(); + + if status == 429 { + return Err(VRCError::rate_limit( + "Too many requests. Please wait before trying again.", + )); + } + + if !status.is_success() { + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Login failed".to_string()); + return Err(VRCError::auth(error_text)); + } + + let body = response.text().await?; + + // Check if 2FA is required + if let Ok(two_fa) = serde_json::from_str::(&body) { + if let Some(methods) = two_fa.requires_two_factor_auth { + return Ok(LoginResult::TwoFactorRequired { methods }); + } + } + + let user: User = serde_json::from_str(&body)?; + Ok(LoginResult::Success { user }) + } + + /// Verify two-factor authentication code + pub async fn verify_two_factor(&self, code: &str, method: TwoFactorMethod) -> VRCResult { + let cookie_header = { + let cookies = self.cookies.lock().await; + cookies.to_header_value() + }; + + let headers = self.build_headers(None, None, cookie_header.as_deref()); + + let request_body = TwoFactorCode { + code: code.to_string(), + }; + + let response = self + .execute_request( + self.http_client + .post(&format!( + "{}/auth/twofactorauth/{}/verify", + API_BASE_URL, + method.endpoint() + )) + .headers(headers) + .json(&request_body), + ) + .await?; + + let status = response.status(); + + let mut cookies = self.cookies.lock().await; + cookies.update_from_response(&response); + drop(cookies); + + if status == 429 { + return Err(VRCError::rate_limit( + "Too many requests. Please wait before trying again.", + )); + } + + if !status.is_success() && status.as_u16() != 400 { + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Verification failed".to_string()); + return Err(VRCError::auth(error_text)); + } + + let body = response.text().await?; + let verify_response: TwoFactorVerifyResponse = serde_json::from_str(&body)?; + + Ok(verify_response.verified) + } + + /// Get the currently authenticated user + pub async fn get_current_user(&self) -> VRCResult { + let cookie_header = { + let cookies = self.cookies.lock().await; + cookies.to_header_value() + }; + + let headers = self.build_headers(None, None, cookie_header.as_deref()); + + let response = self + .execute_request( + self.http_client + .get(&format!("{}/auth/user", API_BASE_URL)) + .headers(headers), + ) + .await?; + + let status = response.status(); + + if !status.is_success() { + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Failed to get user".to_string()); + return Err(VRCError::http(status.as_u16(), error_text)); + } + + let body = response.text().await?; + let user: User = serde_json::from_str(&body)?; + + Ok(user) + } + + /// Update the user's status and status description + pub async fn update_status(&self, request: &UpdateStatusRequest) -> VRCResult { + // First get current user to get their userId + let current_user = self.get_current_user().await?; + + let cookie_header = { + let cookies = self.cookies.lock().await; + cookies.to_header_value() + }; + + let cookie = cookie_header.ok_or_else(|| VRCError::auth("Not authenticated"))?; + let headers = self.build_headers(None, None, Some(&cookie)); + + let response = self + .execute_request( + self.http_client + .put(&format!("{}/users/{}", API_BASE_URL, current_user.id)) + .headers(headers) + .json(request), + ) + .await?; + + let status = response.status(); + + if !status.is_success() { + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Failed to update status".to_string()); + return Err(VRCError::http(status.as_u16(), error_text)); + } + + let user: User = response.json().await?; + Ok(user) + } + + /// Log out the current user + pub async fn logout(&self) -> VRCResult<()> { + let cookie_header = { + let cookies = self.cookies.lock().await; + cookies.to_header_value() + }; + + if let Some(cookie) = cookie_header { + let headers = self.build_headers(None, None, Some(&cookie)); + + let _ = self + .execute_request( + self.http_client + .put(&format!("{}/logout", API_BASE_URL)) + .headers(headers), + ) + .await; + } + + let mut cookies = self.cookies.lock().await; + cookies.clear(); + + Ok(()) + } + + /// Get online friends list + pub async fn get_online_friends(&self) -> VRCResult> { + self.fetch_friends(false).await + } + + pub async fn get_all_friends(&self) -> VRCResult> { + let mut combined = Vec::new(); + let mut seen = HashSet::new(); + + let mut online = self.fetch_friends(false).await?; + for friend in online.drain(..) { + seen.insert(friend.id.clone()); + combined.push(friend); + } + + let mut offline = self.fetch_friends(true).await?; + for friend in offline.drain(..) { + if seen.insert(friend.id.clone()) { + combined.push(friend); + } + } + + Ok(combined) + } + + async fn fetch_friends(&self, offline: bool) -> VRCResult> { + let cookie_header = { + let cookies = self.cookies.lock().await; + cookies.to_header_value() + }; + + let cookie = cookie_header.ok_or_else(|| VRCError::auth("Not authenticated"))?; + let mut results = Vec::new(); + let mut offset = 0usize; + const PAGE_SIZE: usize = 100; + + loop { + let headers = self.build_headers(None, None, Some(&cookie)); + let response = self + .execute_request( + self.http_client + .get(&format!( + "{}/auth/user/friends?offline={}&n={}&offset={}", + API_BASE_URL, offline, PAGE_SIZE, offset + )) + .headers(headers), + ) + .await?; + + if !response.status().is_success() { + return Err(VRCError::http( + response.status().as_u16(), + "Failed to fetch friends", + )); + } + + let page: Vec = response.json().await?; + let count = page.len(); + if count == 0 { + break; + } + + results.extend(page.into_iter()); + + if count < PAGE_SIZE { + break; + } + + sleep(Duration::from_secs(1)).await; + offset += PAGE_SIZE; + } + + Ok(results) + } + + /// Fetch all worlds uploaded by the authenticated user + pub async fn get_uploaded_worlds(&self) -> VRCResult> { + let cookie_header = { + let cookies = self.cookies.lock().await; + cookies.to_header_value() + }; + + let cookie = cookie_header.ok_or_else(|| VRCError::auth("Not authenticated"))?; + let mut worlds = Vec::new(); + let mut offset: usize = 0; + const PAGE_SIZE: usize = 100; + + loop { + let headers = self.build_headers(None, None, Some(&cookie)); + let url = format!( + "{}/worlds?user=me&n={}&offset={}&order=descending&sort=updated", + API_BASE_URL, PAGE_SIZE, offset + ); + + let response = self + .execute_request(self.http_client.get(&url).headers(headers)) + .await?; + + if !response.status().is_success() { + return Err(VRCError::http( + response.status().as_u16(), + "Failed to fetch uploaded worlds", + )); + } + + let mut page: Vec = response.json().await?; + let count = page.len(); + + if count == 0 { + break; + } + + // Backfill statistics that might be missing from the list endpoint + for idx in 0..page.len() { + if page[idx].visits.is_none() + || page[idx].favorites.is_none() + || page[idx].popularity.is_none() + || page[idx].occupants.is_none() + || page[idx].capacity.is_none() + || page[idx].recommended_capacity.is_none() + { + match self.get_world_details(&page[idx].id).await { + Ok(details) => { + if details.visits.is_some() { + page[idx].visits = details.visits; + } + if details.favorites.is_some() { + page[idx].favorites = details.favorites; + } + if details.popularity.is_some() { + page[idx].popularity = details.popularity; + } + if details.occupants.is_some() { + page[idx].occupants = details.occupants; + } + if details.capacity.is_some() { + page[idx].capacity = details.capacity; + } + if details.recommended_capacity.is_some() { + page[idx].recommended_capacity = details.recommended_capacity; + } + if details.heat.is_some() { + page[idx].heat = details.heat; + } + if details.organization.is_some() { + page[idx].organization = details.organization; + } + } + Err(err) => { + log::warn!( + "Failed to load additional details for world {}: {}", + page[idx].id, + err + ); + } + } + } + } + + offset += count; + worlds.append(&mut page); + + if count < PAGE_SIZE { + break; + } + } + + Ok(worlds) + } + + /// Fetch all avatars uploaded by the authenticated user + pub async fn get_uploaded_avatars(&self) -> VRCResult> { + let cookie_header = { + let cookies = self.cookies.lock().await; + cookies.to_header_value() + }; + + let cookie = cookie_header.ok_or_else(|| VRCError::auth("Not authenticated"))?; + let mut avatars = Vec::new(); + let mut offset: usize = 0; + const PAGE_SIZE: usize = 100; + + loop { + let headers = self.build_headers(None, None, Some(&cookie)); + let url = format!( + "{}/avatars?user=me&releaseStatus=all&sort=updated&order=descending&n={}&offset={}", + API_BASE_URL, PAGE_SIZE, offset + ); + + let response = self + .execute_request(self.http_client.get(&url).headers(headers)) + .await?; + + if !response.status().is_success() { + return Err(VRCError::http( + response.status().as_u16(), + "Failed to fetch uploaded avatars", + )); + } + + let mut page: Vec = response.json().await?; + let count = page.len(); + + if count == 0 { + break; + } + + offset += count; + avatars.append(&mut page); + + if count < PAGE_SIZE { + break; + } + } + + Ok(avatars) + } + + // TODO: Have to analyses consequences of rate limits when fetching world details in a loop + /// Fetch additional details for a specific world + pub async fn get_world_details(&self, world_id: &str) -> VRCResult { + let cookie_header = { + let cookies = self.cookies.lock().await; + cookies.to_header_value() + }; + + let cookie = cookie_header.ok_or_else(|| VRCError::auth("Not authenticated"))?; + let headers = self.build_headers(None, None, Some(&cookie)); + + let response = self + .execute_request( + self.http_client + .get(&format!("{}/worlds/{}", API_BASE_URL, world_id)) + .headers(headers), + ) + .await?; + + if !response.status().is_success() { + return Err(VRCError::http( + response.status().as_u16(), + format!("Failed to fetch world {}", world_id), + )); + } + + let world: LimitedWorld = response.json().await?; + Ok(world) + } + + /// Fetch full user data by user ID + pub async fn get_user_by_id(&self, user_id: &str) -> VRCResult { + let cookie_header = { + let cookies = self.cookies.lock().await; + cookies.to_header_value() + }; + + let cookie = cookie_header.ok_or_else(|| VRCError::auth("Not authenticated"))?; + let headers = self.build_headers(None, None, Some(&cookie)); + + let response = self + .execute_request( + self.http_client + .get(&format!("{}/users/{}", API_BASE_URL, user_id)) + .headers(headers), + ) + .await?; + + let status = response.status(); + + if !status.is_success() { + let error_body = response + .text() + .await + .unwrap_or_else(|_| "Unable to read error response".to_string()); + log::error!( + "Failed to fetch user {}: HTTP {} - {}", + user_id, + status.as_u16(), + error_body + ); + return Err(VRCError::http( + status.as_u16(), + format!("Failed to fetch user {}: {}", user_id, error_body), + )); + } + + let body = response.text().await?; + log::debug!("User API response for {}: {}", user_id, body); + + let user: User = serde_json::from_str(&body).map_err(|e| { + log::error!("Failed to parse user JSON: {}", e); + VRCError::unknown(format!("Failed to parse user data: {}", e)) + })?; + + Ok(user) + } + + // Session Management + + /// Check if the client has a valid session + pub async fn has_valid_session(&self) -> bool { + let cookies = self.cookies.lock().await; + cookies.has_auth() + } + + /// Export cookies for storage + pub async fn export_cookies(&self) -> (Option, Option) { + let cookies = self.cookies.lock().await; + ( + cookies.auth_cookie.clone(), + cookies.two_factor_cookie.clone(), + ) + } + + /// Import previously stored cookies + pub async fn import_cookies(&self, auth: Option, two_factor: Option) { + let mut cookies = self.cookies.lock().await; + cookies.auth_cookie = auth; + cookies.two_factor_cookie = two_factor; + } + + /// Clear all stored cookies + pub async fn clear_cookies(&self) { + let mut cookies = self.cookies.lock().await; + cookies.clear(); + } + + // Private Helper Methods + + fn create_basic_auth(email: &str, password: &str) -> String { + let credentials = format!("{}:{}", email, password); + let encoded = BASE64.encode(credentials.as_bytes()); + format!("Basic {}", encoded) + } + + fn build_headers( + &self, + auth: Option<&str>, + _referer: Option<&str>, + cookie: Option<&str>, + ) -> HeaderMap { + crate::http_common::build_api_headers(auth, cookie) + } + + async fn execute_request(&self, builder: RequestBuilder) -> VRCResult { + let request = builder + .build() + .map_err(|e| VRCError::network(format!("Failed to build request: {}", e)))?; + self.send_with_retry(request).await + } + + async fn send_with_retry(&self, request: Request) -> VRCResult { + let mut attempt: u8 = 0; + let mut backoff = Duration::from_millis(INITIAL_BACKOFF); + + loop { + let req = request + .try_clone() + .ok_or_else(|| VRCError::network("Failed to clone request for retry attempts"))?; + + match self.http_client.execute(req).await { + Ok(response) => { + let status = response.status(); + + if status.as_u16() == 429 { + if attempt >= MAX_REQUEST_RETRIES { + return Err(VRCError::rate_limit( + "Too many requests. Please wait before trying again.", + )); + } + + let wait = Self::extract_retry_after(&response).unwrap_or(backoff); + drop(response); + sleep(wait).await; + attempt += 1; + backoff = (backoff * 2).min(Duration::from_millis(MAX_BACKOFF)); + continue; + } + + if status.is_server_error() { + if attempt >= MAX_REQUEST_RETRIES { + return Err(VRCError::http( + status.as_u16(), + status + .canonical_reason() + .unwrap_or("Server error") + .to_string(), + )); + } + + let wait = Self::extract_retry_after(&response).unwrap_or(backoff); + drop(response); + sleep(wait).await; + attempt += 1; + backoff = (backoff * 2).min(Duration::from_millis(MAX_BACKOFF)); + continue; + } + + return Ok(response); + } + Err(err) => { + if attempt >= MAX_REQUEST_RETRIES { + return Err(VRCError::network(format!( + "Request failed after retries: {}", + err + ))); + } + + sleep(backoff).await; + attempt += 1; + backoff = (backoff * 2).min(Duration::from_millis(MAX_BACKOFF)); + } + } + } + } + + fn extract_retry_after(response: &Response) -> Option { + response + .headers() + .get("retry-after") + .and_then(|value| value.to_str().ok()) + .and_then(|header| header.parse::().ok()) + .map(Duration::from_secs) + } +} + +impl Default for VRChatClient { + fn default() -> Self { + Self::new().expect("Failed to create default VRChatClient") + } +} diff --git a/src-tauri/src/vrchat_api/error.rs b/src-tauri/src/vrchat_api/error.rs new file mode 100644 index 0000000..a82355b --- /dev/null +++ b/src-tauri/src/vrchat_api/error.rs @@ -0,0 +1,129 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; +use std::fmt; + +/// Result type alias for VRChat API operations +pub type VRCResult = Result; + +/// Main error type for VRChat API operations +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(tag = "type", content = "data")] +pub enum VRCError { + /// Network-related errors + Network(String), + + /// HTTP errors with status code + Http { status: u16, message: String }, + + /// Authentication errors + Authentication(String), + + /// Rate limiting error + RateLimit(String), + + /// JSON parsing errors + Parse(String), + + /// Invalid input or request + InvalidInput(String), + + /// Unknown or unexpected errors + Unknown(String), +} + +impl VRCError { + /// Create a new HTTP error + pub fn http(status: u16, message: impl Into) -> Self { + Self::Http { + status, + message: message.into(), + } + } + + /// Create a new network error + pub fn network(message: impl Into) -> Self { + Self::Network(message.into()) + } + + /// Create a new authentication error + pub fn auth(message: impl Into) -> Self { + Self::Authentication(message.into()) + } + + /// Create a new rate limit error + pub fn rate_limit(message: impl Into) -> Self { + Self::RateLimit(message.into()) + } + + /// Create a new parse error + pub fn parse(message: impl Into) -> Self { + Self::Parse(message.into()) + } + + /// Create a new invalid input error + pub fn invalid_input(message: impl Into) -> Self { + Self::InvalidInput(message.into()) + } + + /// Create an unknown error + pub fn unknown(message: impl Into) -> Self { + Self::Unknown(message.into()) + } + + /// Get the error message + pub fn message(&self) -> &str { + match self { + Self::Network(msg) + | Self::Http { message: msg, .. } + | Self::Authentication(msg) + | Self::RateLimit(msg) + | Self::Parse(msg) + | Self::InvalidInput(msg) + | Self::Unknown(msg) => msg, + } + } + + /// Get the HTTP status code if applicable + pub fn status_code(&self) -> Option { + match self { + Self::Http { status, .. } => Some(*status), + _ => None, + } + } +} + +impl fmt::Display for VRCError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Network(msg) => write!(f, "Network error: {}", msg), + Self::Http { status, message } => write!(f, "HTTP {} error: {}", status, message), + Self::Authentication(msg) => write!(f, "Authentication error: {}", msg), + Self::RateLimit(msg) => write!(f, "Rate limit: {}", msg), + Self::Parse(msg) => write!(f, "Parse error: {}", msg), + Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), + Self::Unknown(msg) => write!(f, "Unknown error: {}", msg), + } + } +} + +impl std::error::Error for VRCError {} + +/// Convert reqwest errors to VRCError +impl From for VRCError { + fn from(err: reqwest::Error) -> Self { + if err.is_timeout() { + VRCError::network("Request timed out") + } else if err.is_connect() { + VRCError::network("Failed to connect to VRChat API") + } else { + VRCError::network(err.to_string()) + } + } +} + +/// Convert serde_json errors to VRCError +impl From for VRCError { + fn from(err: serde_json::Error) -> Self { + VRCError::parse(format!("JSON parsing failed: {}", err)) + } +} diff --git a/src-tauri/src/vrchat_api/mod.rs b/src-tauri/src/vrchat_api/mod.rs new file mode 100644 index 0000000..80d0915 --- /dev/null +++ b/src-tauri/src/vrchat_api/mod.rs @@ -0,0 +1,8 @@ +pub mod client; +pub mod error; +pub mod types; + +// Re-export common types +pub use client::VRChatClient; +pub use error::{VRCError, VRCResult}; +pub use types::*; diff --git a/src-tauri/src/vrchat_api/types/auth.rs b/src-tauri/src/vrchat_api/types/auth.rs new file mode 100644 index 0000000..2bd0bc2 --- /dev/null +++ b/src-tauri/src/vrchat_api/types/auth.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; +use super::enums::UserStatus; + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct LoginCredentials { + pub email: String, + pub password: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(tag = "type")] +pub enum LoginResult { + Success { user: super::user::User }, + TwoFactorRequired { methods: Vec }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TwoFactorAuthResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub requires_two_factor_auth: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TwoFactorCode { + pub code: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TwoFactorVerifyResponse { + pub verified: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct UpdateStatusRequest { + pub status: UserStatus, + pub status_description: String, +} diff --git a/src-tauri/src/vrchat_api/types/avatar.rs b/src-tauri/src/vrchat_api/types/avatar.rs new file mode 100644 index 0000000..8b3520c --- /dev/null +++ b/src-tauri/src/vrchat_api/types/avatar.rs @@ -0,0 +1,69 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +use super::enums::ReleaseStatus; + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Default)] +#[serde(rename_all = "camelCase")] +pub struct AvatarPerformance { + #[serde(default)] + pub android: Option, + #[serde(default)] + pub ios: Option, + #[serde(default)] + pub standalonewindows: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Default)] +#[serde(rename_all = "camelCase")] +pub struct AvatarStyles { + #[serde(default)] + pub primary: Option, + #[serde(default)] + pub secondary: Option, +} + +use crate::vrchat_api::types::world::UnityPackageSummary; + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct LimitedAvatar { + pub id: String, + pub name: String, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub author_id: Option, + #[serde(default)] + pub author_name: Option, + #[serde(default)] + pub image_url: Option, + #[serde(default)] + pub thumbnail_image_url: Option, + #[serde(default)] + pub asset_url: Option, + #[serde(default)] + pub unity_package_url: Option, + #[serde(default)] + pub release_status: ReleaseStatus, + #[serde(default)] + pub featured: Option, + #[serde(default)] + pub searchable: Option, + #[serde(default)] + pub listing_date: Option, + #[serde(default)] + pub created_at: Option, + #[serde(default)] + pub updated_at: Option, + #[serde(default)] + pub version: Option, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub performance: Option, + #[serde(default)] + pub styles: Option, + #[serde(default)] + pub unity_packages: Vec, +} diff --git a/src-tauri/src/vrchat_api/types/enums.rs b/src-tauri/src/vrchat_api/types/enums.rs new file mode 100644 index 0000000..5d924b2 --- /dev/null +++ b/src-tauri/src/vrchat_api/types/enums.rs @@ -0,0 +1,254 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +/// User's current status +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "lowercase")] +pub enum UserStatus { + /// User is online and active + Active, + /// User is online and auto accepting invitations to join + #[serde(rename = "join me")] + JoinMe, + /// User is online but is hiding their location and requires invitation to join + #[serde(rename = "ask me")] + AskMe, + /// User is busy + Busy, + /// User is offline + Offline, +} + +impl Default for UserStatus { + fn default() -> Self { + UserStatus::Offline + } +} + +impl std::fmt::Display for UserStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UserStatus::Active => write!(f, "active"), + UserStatus::JoinMe => write!(f, "join me"), + UserStatus::AskMe => write!(f, "ask me"), + UserStatus::Busy => write!(f, "busy"), + UserStatus::Offline => write!(f, "offline"), + } + } +} + +/// Release status of avatars and worlds +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "lowercase")] +pub enum ReleaseStatus { + /// Publicly released + Public, + /// Private/restricted access + Private, + /// Hidden from listings + Hidden, + // TODO: Should this be here? It's not really a status, more of a filter. + /// Filter for all statuses + All, +} + +impl Default for ReleaseStatus { + fn default() -> Self { + ReleaseStatus::Public + } +} + +impl std::fmt::Display for ReleaseStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReleaseStatus::Public => write!(f, "public"), + ReleaseStatus::Private => write!(f, "private"), + ReleaseStatus::Hidden => write!(f, "hidden"), + ReleaseStatus::All => write!(f, "all"), + } + } +} + +/// User's developer type/staff level +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "lowercase")] +pub enum DeveloperType { + /// Normal user + None, + /// Trusted user + Trusted, + /// VRChat Developer/Staff + Internal, + /// VRChat Moderator + Moderator, +} + +impl Default for DeveloperType { + fn default() -> Self { + DeveloperType::None + } +} + +impl std::fmt::Display for DeveloperType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DeveloperType::None => write!(f, "none"), + DeveloperType::Trusted => write!(f, "trusted"), + DeveloperType::Internal => write!(f, "internal"), + DeveloperType::Moderator => write!(f, "moderator"), + } + } +} + +/// Age verification status +/// `verified` is obsolete. according to the unofficial docs, Users who have verified and are 18+ can switch to `plus18` status. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "lowercase")] +pub enum AgeVerificationStatus { + /// Age verification status is hidden + Hidden, + /// Legacy verified status (obsolete) + Verified, + /// User is verified to be 18+ + #[serde(rename = "18+")] + Plus18, +} + +impl Default for AgeVerificationStatus { + fn default() -> Self { + AgeVerificationStatus::Hidden + } +} + +impl std::fmt::Display for AgeVerificationStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AgeVerificationStatus::Hidden => write!(f, "hidden"), + AgeVerificationStatus::Verified => write!(f, "verified"), + AgeVerificationStatus::Plus18 => write!(f, "18+"), + } + } +} + +/// Friend request status +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Type)] +pub enum FriendRequestStatus { + /// No friend request + #[serde(rename = "")] + None, + /// Outgoing friend request pending + #[serde(rename = "outgoing")] + Outgoing, + /// Incoming friend request pending + #[serde(rename = "incoming")] + Incoming, + /// Completed friend request + #[serde(rename = "completed")] + Completed, +} + +impl Default for FriendRequestStatus { + fn default() -> Self { + FriendRequestStatus::None + } +} + +impl std::fmt::Display for FriendRequestStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FriendRequestStatus::None => write!(f, ""), + FriendRequestStatus::Outgoing => write!(f, "outgoing"), + FriendRequestStatus::Incoming => write!(f, "incoming"), + FriendRequestStatus::Completed => write!(f, "completed"), + } + } +} + +/// State of the user +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "lowercase")] +pub enum UserState { + /// User is offline + Offline, + /// User is active + Active, + /// User is online + Online, +} + +impl Default for UserState { + fn default() -> Self { + UserState::Offline + } +} + +impl std::fmt::Display for UserState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UserState::Offline => write!(f, "offline"), + UserState::Active => write!(f, "active"), + UserState::Online => write!(f, "online"), + } + } +} + +/// Avatar performance ratings +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] +pub enum PerformanceRatings { + /// No rating + None, + /// Excellent performance + Excellent, + /// Good performance + Good, + /// Medium performance + Medium, + /// Poor performance + Poor, + /// Very poor performance + VeryPoor, +} + +impl Default for PerformanceRatings { + fn default() -> Self { + PerformanceRatings::None + } +} + +impl std::fmt::Display for PerformanceRatings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PerformanceRatings::None => write!(f, "None"), + PerformanceRatings::Excellent => write!(f, "Excellent"), + PerformanceRatings::Good => write!(f, "Good"), + PerformanceRatings::Medium => write!(f, "Medium"), + PerformanceRatings::Poor => write!(f, "Poor"), + PerformanceRatings::VeryPoor => write!(f, "VeryPoor"), + } + } +} + +/// Sort order for API queries +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "lowercase")] +pub enum OrderOption { + /// Ascending order + Ascending, + /// Descending order + Descending, +} + +impl Default for OrderOption { + fn default() -> Self { + OrderOption::Descending + } +} + +impl std::fmt::Display for OrderOption { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OrderOption::Ascending => write!(f, "ascending"), + OrderOption::Descending => write!(f, "descending"), + } + } +} diff --git a/src-tauri/src/vrchat_api/types/mod.rs b/src-tauri/src/vrchat_api/types/mod.rs new file mode 100644 index 0000000..7fb50a7 --- /dev/null +++ b/src-tauri/src/vrchat_api/types/mod.rs @@ -0,0 +1,14 @@ +pub mod auth; +pub mod avatar; +pub mod enums; +pub mod two_factor; +pub mod user; +pub mod world; + +// Re-export common types at crate level +pub use auth::*; +pub use avatar::*; +pub use enums::*; +pub use two_factor::*; +pub use user::*; +pub use world::*; diff --git a/src-tauri/src/vrchat_api/types/two_factor.rs b/src-tauri/src/vrchat_api/types/two_factor.rs new file mode 100644 index 0000000..c52a5a7 --- /dev/null +++ b/src-tauri/src/vrchat_api/types/two_factor.rs @@ -0,0 +1,22 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TwoFactorMethod { + EmailOtp, + Totp, +} + +impl TwoFactorMethod { + pub fn endpoint(&self) -> &'static str { + match self { + Self::EmailOtp => "emailotp", + Self::Totp => "totp", + } + } + + pub fn from_str(s: &str) -> Option { + match s.to_lowercase().as_str() { + "emailotp" => Some(Self::EmailOtp), + "totp" | "otp" => Some(Self::Totp), + _ => None, + } + } +} diff --git a/src-tauri/src/vrchat_api/types/user.rs b/src-tauri/src/vrchat_api/types/user.rs new file mode 100644 index 0000000..103e85f --- /dev/null +++ b/src-tauri/src/vrchat_api/types/user.rs @@ -0,0 +1,297 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +use super::enums::{UserStatus, DeveloperType, AgeVerificationStatus, FriendRequestStatus}; + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct User { + pub id: String, + #[serde(default)] + pub username: String, + pub display_name: String, + #[serde(default)] + pub accepted_privacy_version: Option, + #[serde(default)] + pub accepted_tos_version: Option, + #[serde(default)] + pub account_deletion_date: Option, + #[serde(default)] + pub state: String, + #[serde(default)] + pub status: UserStatus, + #[serde(default)] + pub status_description: String, + #[serde(default)] + pub status_first_time: Option, + #[serde(default)] + pub status_history: Vec, + #[serde(default)] + pub bio: String, + #[serde(default)] + pub bio_links: Vec, + #[serde(default)] + pub age_verification_status: AgeVerificationStatus, + #[serde(default)] + pub age_verified: Option, + #[serde(default)] + pub is_adult: Option, + #[serde(default)] + pub date_joined: Option, + #[serde(default)] + pub last_login: Option, + #[serde(default)] + pub last_activity: Option, + #[serde(default)] + pub last_platform: Option, + #[serde(default)] + pub last_mobile: Option, + #[serde(default)] + pub platform: String, + #[serde(default)] + pub platform_history: Vec, + #[serde(default)] + pub location: Option, + #[serde(default)] + pub traveling_to_world: Option, + #[serde(default)] + pub traveling_to_location: Option, + #[serde(default)] + pub traveling_to_instance: Option, + #[serde(default)] + pub home_location: Option, + #[serde(default)] + pub instance_id: Option, + #[serde(default)] + pub world_id: Option, + #[serde(default)] + pub allow_avatar_copying: Option, + #[serde(default)] + pub two_factor_auth_enabled: Option, + #[serde(default)] + pub two_factor_auth_enabled_date: Option, + #[serde(default)] + pub current_avatar: Option, + #[serde(default)] + pub fallback_avatar: Option, + #[serde(default)] + pub current_avatar_tags: Vec, + #[serde(default)] + pub profile_pic_override: Option, + #[serde(default)] + pub profile_pic_override_thumbnail: Option, + #[serde(default)] + pub user_icon: Option, + #[serde(default)] + pub current_avatar_image_url: Option, + #[serde(default)] + pub current_avatar_thumbnail_image_url: Option, + #[serde(default)] + pub banner_id: Option, + #[serde(default)] + pub banner_url: Option, + #[serde(default)] + pub pronouns: Option, + #[serde(default)] + pub languages: Option>, + #[serde(default)] + pub pronouns_history: Vec, + #[serde(default)] + pub friends: Vec, + #[serde(default)] + pub friend_group_names: Vec, + #[serde(default)] + pub friend_key: Option, + #[serde(default)] + pub friend_request_status: FriendRequestStatus, + #[serde(default)] + pub past_display_names: Option>, + #[serde(default)] + pub badges: Option>, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub is_friend: Option, + #[serde(default)] + pub note: Option, + #[serde(default)] + pub developer_type: DeveloperType, + #[serde(default)] + pub is_booping_enabled: Option, + #[serde(default)] + pub receive_mobile_invitations: Option, + #[serde(default)] + pub hide_content_filter_settings: Option, + #[serde(default)] + pub has_birthday: Option, + #[serde(default)] + pub has_email: Option, + #[serde(default)] + pub has_pending_email: Option, + #[serde(default)] + pub has_logged_in_from_client: Option, + #[serde(default)] + pub unsubscribe: Option, + #[serde(default)] + pub updated_at: Option, + #[serde(default)] + pub email_verified: Option, + #[serde(default)] + pub obfuscated_email: Option, + #[serde(default)] + pub user_language: Option, + #[serde(default)] + pub user_language_code: Option, + #[serde(default)] + pub discord_id: Option, + #[serde(default)] + pub discord_details: Option, + #[serde(default)] + pub google_id: Option, + #[serde(default)] + pub google_details: Option, + #[serde(default)] + pub steam_id: Option, + #[serde(default)] + pub steam_details: Option, + #[serde(default)] + pub oculus_id: Option, + #[serde(default)] + pub pico_id: Option, + #[serde(default)] + pub vive_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct DiscordDetails { + #[serde(default)] + pub global_name: Option, + #[serde(default)] + pub id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct GoogleDetails { + #[serde(default)] + pub email_matches: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct SteamDetails { + #[serde(default)] + pub avatar: Option, + #[serde(default)] + pub avatarfull: Option, + #[serde(default)] + pub avatarhash: Option, + #[serde(default)] + pub avatarmedium: Option, + #[serde(default)] + pub communityvisibilitystate: Option, + #[serde(default)] + pub gameextrainfo: Option, + #[serde(default)] + pub gameid: Option, + #[serde(default)] + pub loccountrycode: Option, + #[serde(default)] + pub locstatecode: Option, + #[serde(default)] + pub personaname: Option, + #[serde(default)] + pub personastate: Option, + #[serde(default)] + pub personastateflags: Option, + #[serde(default)] + pub primaryclanid: Option, + #[serde(default)] + pub profilestate: Option, + #[serde(default)] + pub profileurl: Option, + #[serde(default)] + pub steamid: Option, + #[serde(default)] + pub timecreated: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct PastDisplayName { + pub display_name: String, + #[serde(default)] + pub updated_at: Option, + #[serde(default)] + pub reverted: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct Badge { + pub badge_id: String, + #[serde(default)] + pub badge_name: String, + #[serde(default)] + pub badge_description: String, + #[serde(default)] + pub assigned_at: Option, + #[serde(default)] + pub showcased: bool, + #[serde(default)] + pub badge_image_url: Option, + #[serde(default)] + pub updated_at: Option, + #[serde(default)] + pub hidden: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct LimitedUserFriend { + pub id: String, + pub display_name: String, + #[serde(default)] + pub bio: String, + #[serde(default)] + pub bio_links: Vec, + #[serde(default)] + pub current_avatar_image_url: Option, + #[serde(default)] + pub current_avatar_thumbnail_image_url: Option, + #[serde(default)] + pub current_avatar_tags: Vec, + #[serde(default)] + pub developer_type: DeveloperType, + #[serde(default)] + pub friend_key: Option, + #[serde(default)] + pub is_friend: bool, + #[serde(default)] + pub image_url: Option, + #[serde(default)] + pub last_platform: Option, + #[serde(default)] + pub location: Option, + #[serde(default)] + pub last_login: Option, + #[serde(default)] + pub last_activity: Option, + #[serde(default)] + pub last_mobile: Option, + #[serde(default)] + pub platform: String, + #[serde(default)] + pub profile_pic_override: Option, + #[serde(default)] + pub profile_pic_override_thumbnail: Option, + #[serde(default)] + pub status: UserStatus, + #[serde(default)] + pub status_description: String, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub user_icon: Option, +} diff --git a/src-tauri/src/vrchat_api/types/world.rs b/src-tauri/src/vrchat_api/types/world.rs new file mode 100644 index 0000000..d54c23d --- /dev/null +++ b/src-tauri/src/vrchat_api/types/world.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +use super::enums::ReleaseStatus; + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct UnityPackageSummary { + #[serde(default)] + pub id: Option, + #[serde(default)] + pub asset_url: Option, + #[serde(default)] + pub asset_version: Option, + #[serde(default)] + pub platform: Option, + #[serde(default)] + pub unity_version: Option, + #[serde(default)] + pub created_at: Option, + #[serde(default)] + pub performance_rating: Option, + #[serde(default)] + pub scan_status: Option, + #[serde(default)] + pub variant: Option, + #[serde(default)] + pub unity_sort_number: Option, + #[serde(default)] + pub impostorizer_version: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct LimitedWorld { + pub id: String, + pub name: String, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub author_id: Option, + #[serde(default)] + pub author_name: Option, + #[serde(default)] + pub image_url: Option, + #[serde(default)] + pub thumbnail_image_url: Option, + #[serde(default)] + pub release_status: ReleaseStatus, + #[serde(default)] + pub publication_date: Option, + #[serde(default)] + pub created_at: Option, + #[serde(default)] + pub updated_at: Option, + #[serde(default)] + pub labs_publication_date: Option, + #[serde(default)] + pub visits: Option, + #[serde(default)] + pub favorites: Option, + #[serde(default)] + pub popularity: Option, + #[serde(default)] + pub occupants: Option, + #[serde(default)] + pub capacity: Option, + #[serde(default)] + pub recommended_capacity: Option, + #[serde(default)] + pub heat: Option, + #[serde(default)] + pub organization: Option, + #[serde(default)] + pub preview_youtube_id: Option, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub unity_packages: Vec, +} diff --git a/src-tauri/src/vrchat_status.rs b/src-tauri/src/vrchat_status.rs new file mode 100644 index 0000000..6f4c767 --- /dev/null +++ b/src-tauri/src/vrchat_status.rs @@ -0,0 +1,100 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +const VRCHAT_STATUS_URL: &str = "https://status.vrchat.com/api/v2/status.json"; + +/// Response from VRChat status API +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct VRChatStatusResponse { + pub page: StatusPage, + pub status: SystemStatus, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct StatusPage { + pub id: String, + pub name: String, + pub url: String, + #[serde(rename = "time_zone")] + pub time_zone: String, + #[serde(rename = "updated_at")] + pub updated_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct SystemStatus { + /// Indicator of system status + pub indicator: StatusIndicator, + /// Human-readable status description + pub description: String, +} + +impl SystemStatus { + /// Check if the system is operating normally + pub fn is_healthy(&self) -> bool { + matches!(self.indicator, StatusIndicator::None) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "lowercase")] +pub enum StatusIndicator { + None, + Minor, + Major, + Critical, +} + +/// Fetch current VRChat service status +pub async fn fetch_vrchat_status() -> Result { + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .map_err(|e| format!("Failed to create HTTP client: {}", e))?; + + let response = client + .get(VRCHAT_STATUS_URL) + .send() + .await + .map_err(|e| format!("Failed to fetch status: {}", e))?; + + if !response.status().is_success() { + return Err(format!("HTTP error: {}", response.status())); + } + + let response_text = response + .text() + .await + .map_err(|e| format!("Failed to read response text: {}", e))?; + + log::debug!("VRChat status API response: {}", response_text); + + let status = serde_json::from_str::(&response_text).map_err(|e| { + format!( + "Failed to parse status response: {}. Response was: {}", + e, response_text + ) + })?; + + Ok(status) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_severity_levels() { + let healthy = SystemStatus { + indicator: StatusIndicator::None, + description: "All Systems Operational".to_string(), + }; + assert!(healthy.is_healthy()); + + let major = SystemStatus { + indicator: StatusIndicator::Major, + description: "Partial System Outage".to_string(), + }; + assert!(!major.is_healthy()); + } +} diff --git a/src-tauri/src/websocket/client.rs b/src-tauri/src/websocket/client.rs new file mode 100644 index 0000000..892cf44 --- /dev/null +++ b/src-tauri/src/websocket/client.rs @@ -0,0 +1,536 @@ +use futures_util::StreamExt; +use http::Request; +use std::sync::Arc; +use tauri::{AppHandle, Emitter}; +use tokio::sync::Mutex; +use tokio::time::Duration; +use tokio_tungstenite::{ + connect_async, + tungstenite::{Message, handshake::client::generate_key}, +}; + +use super::types::*; +use crate::store::{UserStore, user_store::CurrentUserPipelineUpdate}; +use crate::vrchat_api::error::{VRCError, VRCResult}; + +const PIPELINE_BASE_URL: &str = "wss://pipeline.vrchat.cloud/"; +const PIPELINE_HOST: &str = "pipeline.vrchat.cloud"; +use crate::http_common::USER_AGENT_STRING; +// const HEARTBEAT_INTERVAL_SECS: u64 = 30; + +pub struct VRChatWebSocket { + auth_cookie: Arc>>, + two_factor_cookie: Arc>>, + app_handle: AppHandle, + running: Arc>, + user_store: UserStore, +} + +impl VRChatWebSocket { + pub fn new(app_handle: AppHandle, user_store: UserStore) -> Self { + Self { + auth_cookie: Arc::new(Mutex::new(None)), + two_factor_cookie: Arc::new(Mutex::new(None)), + app_handle, + running: Arc::new(Mutex::new(false)), + user_store, + } + } + + pub fn get_user_store(&self) -> UserStore { + self.user_store.clone() + } + + pub async fn set_cookies( + &self, + auth_cookie: Option, + two_factor_cookie: Option, + ) { + let mut auth = self.auth_cookie.lock().await; + *auth = auth_cookie; + drop(auth); + + let mut two_fa = self.two_factor_cookie.lock().await; + *two_fa = two_factor_cookie; + } + + pub async fn start(&self) -> VRCResult<()> { + let mut running = self.running.lock().await; + if *running { + return Ok(()); + } + *running = true; + drop(running); + + let auth_cookie = self.auth_cookie.clone(); + let two_factor_cookie = self.two_factor_cookie.clone(); + let app_handle = self.app_handle.clone(); + let running = self.running.clone(); + let user_store = self.user_store.clone(); + + tokio::spawn(async move { + Self::run_connection_loop( + auth_cookie, + two_factor_cookie, + app_handle, + running, + user_store, + ) + .await; + }); + + Ok(()) + } + + pub async fn stop(&self) { + let mut running = self.running.lock().await; + *running = false; + } + + async fn run_connection_loop( + auth_cookie: Arc>>, + two_factor_cookie: Arc>>, + app_handle: AppHandle, + running: Arc>, + user_store: UserStore, + ) { + let mut reconnect_delay = 2; + const MAX_RECONNECT_DELAY: u64 = 60; + + loop { + { + let is_running = running.lock().await; + if !*is_running { + break; + } + } + + let cookies = { + let auth = auth_cookie.lock().await; + let two_fa = two_factor_cookie.lock().await; + (auth.clone(), two_fa.clone()) + }; + + if cookies.0.is_none() { + tokio::time::sleep(Duration::from_secs(5)).await; + continue; + } + + match Self::connect_and_listen( + &cookies.0.unwrap(), + cookies.1.as_deref(), + &app_handle, + &running, + &user_store, + ) + .await + { + Ok(_) => { + // Reset delay on successful connection + reconnect_delay = 2; + } + Err(e) => { + log::error!("WebSocket error: {:?}", e); + } + } + + { + let is_running = running.lock().await; + if !*is_running { + break; + } + } + + // Exponential backoff for reconnection + log::debug!("Reconnecting WebSocket in {} seconds...", reconnect_delay); + tokio::time::sleep(Duration::from_secs(reconnect_delay)).await; + reconnect_delay = (reconnect_delay * 2).min(MAX_RECONNECT_DELAY); + } + } + + async fn connect_and_listen( + auth_cookie: &str, + two_factor_cookie: Option<&str>, + app_handle: &AppHandle, + running: &Arc>, + user_store: &UserStore, + ) -> VRCResult<()> { + let auth_cookie_value = auth_cookie.split(';').next().unwrap_or(auth_cookie).trim(); + let auth_token = auth_cookie_value + .splitn(2, '=') + .nth(1) + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + VRCError::invalid_input( + "Auth cookie missing auth token required for pipeline WebSocket", + ) + })?; + let websocket_url = format!("{PIPELINE_BASE_URL}?authToken={}", auth_token); + + log::debug!("Attempting WebSocket connection to: {}", websocket_url); + log::trace!( + "Using auth cookie (first 20 chars): {}...", + &auth_cookie_value.chars().take(20).collect::() + ); + + // Build Cookie + let mut cookie_parts = vec![auth_cookie_value.to_string()]; + if let Some(two_fa) = two_factor_cookie { + let two_fa_value = two_fa.split(';').next().unwrap_or(two_fa).trim(); + if !two_fa_value.is_empty() { + cookie_parts.push(two_fa_value.to_string()); + } + } + let cookie_header = cookie_parts.join("; "); + + // Build WebSocket request + let ws_key = generate_key(); + let request = Request::builder() + .method("GET") + .uri(&websocket_url) + .header("Host", PIPELINE_HOST) + .header("User-Agent", USER_AGENT_STRING) + .header("Connection", "Upgrade") + .header("Upgrade", "websocket") + .header("Sec-WebSocket-Version", "13") + .header("Sec-WebSocket-Key", ws_key) + .header("Cookie", cookie_header) + .body(()) + .map_err(|e| VRCError::network(format!("Failed to build WebSocket request: {}", e)))?; + + let (ws_stream, response) = connect_async(request) + .await + .map_err(|e| VRCError::network(format!("WebSocket connection failed: {}", e)))?; + + log::debug!( + "WebSocket handshake response status: {:?}", + response.status() + ); + + log::info!("WebSocket connected"); + let _ = app_handle.emit("websocket-connected", ()); + + let (_write, mut read) = ws_stream.split(); + + // Ping task + // Disabled, as VRChat pipeline seems to not require pings. + // let running_ping = running.clone(); + // let ping_task = tokio::spawn(async move { + // let mut ping_interval = interval(Duration::from_secs(HEARTBEAT_INTERVAL_SECS)); + // loop { + // ping_interval.tick().await; + // { + // let is_running = running_ping.lock().await; + // if !*is_running { + // break; + // } + // } + // if write.send(Message::Ping(vec![])).await.is_err() { + // break; + // } + // } + // }); + + // Main message loop + while let Some(msg) = read.next().await { + { + let is_running = running.lock().await; + if !*is_running { + break; + } + } + + match msg { + Ok(Message::Text(text)) => { + if let Err(e) = Self::handle_message(&text, app_handle, user_store).await { + log::error!("Error handling WebSocket message: {:?}", e); + } + } + Ok(Message::Close(_)) => { + log::info!("WebSocket closed by server"); + break; + } + Err(e) => { + log::error!("WebSocket read error: {:?}", e); + break; + } + _ => {} + } + } + + //ping_task.abort(); + let _ = app_handle.emit("websocket-disconnected", ()); + Ok(()) + } + + async fn handle_message( + text: &str, + app_handle: &AppHandle, + user_store: &UserStore, + ) -> VRCResult<()> { + log::trace!("WebSocket Message Received: {}", text); + + // Parse the outer envelope + let message: WebSocketMessage = serde_json::from_str(text) + .map_err(|e| VRCError::parse(format!("Failed to parse WebSocket message: {}", e)))?; + + // Handle different message types + match message { + WebSocketMessage::Notification(payload) => { + let payload = payload.into_inner(); + log::trace!( + "Notification event: {}", + payload.kind.as_deref().unwrap_or("unknown") + ); + let _ = app_handle.emit("vrchat-notification", &payload); + } + WebSocketMessage::ResponseNotification(payload) => { + let payload = payload.into_inner(); + log::trace!( + "Notification response: notification={}, response={}", + payload.notification_id, + payload.response_id + ); + let _ = app_handle.emit("vrchat-notification-response", &payload); + } + WebSocketMessage::SeeNotification(notification_id) => { + let notification_id = notification_id.into_inner(); + log::trace!("Notification seen: {}", notification_id); + let _ = app_handle.emit("vrchat-notification-see", ¬ification_id); + } + WebSocketMessage::HideNotification(notification_id) => { + let notification_id = notification_id.into_inner(); + log::trace!("Notification hide requested: {}", notification_id); + let _ = app_handle.emit("vrchat-notification-hide", ¬ification_id); + } + WebSocketMessage::ClearNotification => { + log::trace!("Notification clear requested"); + let _ = app_handle.emit("vrchat-notification-clear", ()); + } + WebSocketMessage::NotificationV2(payload) => { + let payload = payload.into_inner(); + log::trace!("Notification v2: {}", payload.kind); + let _ = app_handle.emit("vrchat-notification-v2", &payload); + } + WebSocketMessage::NotificationV2Update(payload) => { + let payload = payload.into_inner(); + log::trace!("Notification v2 update: {}", payload.id); + let _ = app_handle.emit("vrchat-notification-v2-update", &payload); + } + WebSocketMessage::NotificationV2Delete(payload) => { + let payload = payload.into_inner(); + log::trace!("Notification v2 delete: {} ids", payload.ids.len()); + let _ = app_handle.emit("vrchat-notification-v2-delete", &payload); + } + WebSocketMessage::FriendAdd(payload) => { + let content = payload.into_inner(); + log::info!( + "Friend added: {} ({})", + content.user.display_name, + content.user_id + ); + user_store.upsert_friend(content.user.clone()).await; + let event = FriendUpdateEvent { + user_id: content.user_id.clone(), + user: content.user.clone(), + }; + let _ = app_handle.emit("friend-added", &event); + let _ = app_handle.emit("friend-update", &event); + } + WebSocketMessage::FriendDelete(payload) => { + let content = payload.into_inner(); + log::info!("Friend removed: {}", content.user_id); + user_store.remove_friend(&content.user_id).await; + let event = FriendRemovedEvent { + user_id: content.user_id.clone(), + }; + let _ = app_handle.emit("friend-removed", &event); + } + WebSocketMessage::FriendUpdate(payload) => { + let content = payload.into_inner(); + log::debug!( + "Friend updated: {} ({})", + content.user.display_name, + content.user_id + ); + user_store.upsert_friend(content.user.clone()).await; + let event = FriendUpdateEvent { + user_id: content.user_id, + user: content.user, + }; + let _ = app_handle.emit("friend-update", &event); + } + WebSocketMessage::FriendOnline(payload) => { + let content = payload.into_inner(); + log::info!( + "Friend online: {} ({})", + content.user.display_name, + content.user_id + ); + user_store.upsert_friend(content.user.clone()).await; + if let Some(location) = content.location.clone() { + user_store + .update_user_location(&content.user_id, location, content.platform.clone()) + .await; + } + let event = FriendOnlineEvent { + user_id: content.user_id, + user: content.user, + }; + let _ = app_handle.emit("friend-online", &event); + } + WebSocketMessage::FriendActive(payload) => { + let content = payload.into_inner(); + log::debug!( + "Friend active: {} ({})", + content.user.display_name, + content.user_id + ); + user_store.upsert_friend(content.user.clone()).await; + let event = FriendOnlineEvent { + user_id: content.user_id.clone(), + user: content.user.clone(), + }; + let _ = app_handle.emit("friend-active", &event); + let _ = app_handle.emit("friend-online", &event); + } + WebSocketMessage::FriendOffline(payload) => { + let content = payload.into_inner(); + log::info!("Friend offline: {}", content.user_id); + user_store.set_friend_offline(&content.user_id).await; + let event = FriendOfflineEvent { + user_id: content.user_id, + }; + let _ = app_handle.emit("friend-offline", &event); + } + WebSocketMessage::FriendLocation(payload) => { + let content = payload.into_inner(); + log::debug!( + "Friend location: {} -> {}", + content.user_id, + content.location + ); + if let Some(user) = content.user.clone() { + user_store.upsert_friend(user).await; + } + let platform = content.user.as_ref().map(|friend| friend.platform.clone()); + user_store + .update_user_location(&content.user_id, content.location.clone(), platform) + .await; + let _ = app_handle.emit("friend-location", &content); + } + WebSocketMessage::UserUpdate(payload) => { + let content = payload.into_inner(); + log::debug!("Current user update for {}", content.user_id); + let user = content.user.clone(); + let patch = CurrentUserPipelineUpdate { + id: user.id.clone(), + display_name: user.display_name.clone(), + username: user.username.clone(), + status: user.status.clone(), + status_description: user.status_description.clone(), + bio: user.bio.clone(), + user_icon: user.user_icon.clone(), + profile_pic_override: user.profile_pic_override.clone(), + profile_pic_override_thumbnail: user + .profile_pic_override_thumbnail_image_url + .clone(), + current_avatar: user.current_avatar.clone(), + current_avatar_asset_url: user.current_avatar_asset_url.clone(), + current_avatar_image_url: user.current_avatar_image_url.clone(), + current_avatar_thumbnail_image_url: user + .current_avatar_thumbnail_image_url + .clone(), + fallback_avatar: user.fallback_avatar.clone(), + tags: user.tags.clone(), + }; + user_store.apply_current_user_update(patch).await; + let _ = app_handle.emit("user-update", &content); + } + WebSocketMessage::UserLocation(payload) => { + let content = payload.into_inner(); + log::debug!( + "Current user location: {} -> {}", + content.user_id, + content.location + ); + if let Some(user) = content.user.clone() { + user_store.upsert_friend(user).await; + } + let platform = content.user.as_ref().map(|friend| friend.platform.clone()); + user_store + .update_user_location(&content.user_id, content.location.clone(), platform) + .await; + let _ = app_handle.emit("user-location", &content); + } + WebSocketMessage::UserBadgeAssigned(payload) => { + let payload = payload.into_inner(); + log::info!("Badge assigned: {}", payload.badge.badge_id); + let _ = app_handle.emit("user-badge-assigned", &payload); + } + WebSocketMessage::UserBadgeUnassigned(payload) => { + let payload = payload.into_inner(); + log::info!("Badge unassigned: {}", payload.badge_id); + let _ = app_handle.emit("user-badge-unassigned", &payload); + } + WebSocketMessage::ContentRefresh(payload) => { + let payload = payload.into_inner(); + log::debug!( + "Content refresh: {} {}", + payload.content_type, + payload.action_type.as_deref().unwrap_or("") + ); + let _ = app_handle.emit("content-refresh", &payload); + } + WebSocketMessage::ModifiedImageUpdate(payload) => { + let payload = payload.into_inner(); + log::debug!("Image modified: {}", payload.file_id); + let _ = app_handle.emit("modified-image-update", &payload); + } + WebSocketMessage::InstanceQueueJoined(payload) => { + let payload = payload.into_inner(); + log::info!( + "Instance queue joined: {} (position {})", + payload.instance_location, + payload.position + ); + let _ = app_handle.emit("instance-queue-joined", &payload); + } + WebSocketMessage::InstanceQueueReady(payload) => { + let payload = payload.into_inner(); + log::info!( + "Instance queue ready: {} (expiry {})", + payload.instance_location, + payload.expiry + ); + let _ = app_handle.emit("instance-queue-ready", &payload); + } + WebSocketMessage::GroupJoined(payload) => { + let payload = payload.into_inner(); + log::info!("Group joined: {}", payload.group_id); + let _ = app_handle.emit("group-joined", &payload); + } + WebSocketMessage::GroupLeft(payload) => { + let payload = payload.into_inner(); + log::info!("Group left: {}", payload.group_id); + let _ = app_handle.emit("group-left", &payload); + } + WebSocketMessage::GroupMemberUpdated(payload) => { + let payload = payload.into_inner(); + log::debug!("Group member updated event received"); + let _ = app_handle.emit("group-member-updated", &payload); + } + WebSocketMessage::GroupRoleUpdated(payload) => { + let payload = payload.into_inner(); + log::debug!("Group role updated event received"); + let _ = app_handle.emit("group-role-updated", &payload); + } + WebSocketMessage::Unknown => { + log::debug!("Unknown WebSocket message type"); + } + } + + Ok(()) + } +} diff --git a/src-tauri/src/websocket/mod.rs b/src-tauri/src/websocket/mod.rs new file mode 100644 index 0000000..941526b --- /dev/null +++ b/src-tauri/src/websocket/mod.rs @@ -0,0 +1,5 @@ +pub mod client; +pub mod types; + +pub use client::VRChatWebSocket; +pub use types::*; diff --git a/src-tauri/src/websocket/types.rs b/src-tauri/src/websocket/types.rs new file mode 100644 index 0000000..eb6bb91 --- /dev/null +++ b/src-tauri/src/websocket/types.rs @@ -0,0 +1,505 @@ +use crate::vrchat_api::types::LimitedUserFriend; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; +use specta::Type; +use std::collections::BTreeMap; + +/// Wrapper that transparently handles VRChat's double-encoded message payloads. +/// Some events ship their "content" field as a JSON string containing another +/// JSON document. Others already provide structured JSON. This helper will +/// attempt to deserialize the content value directly and, if that fails, +/// attempt to parse the inner string as JSON and deserialize that instead. +#[derive(Debug, Clone)] +pub struct DoubleEncoded { + inner: T, +} + +impl DoubleEncoded { + pub fn into_inner(self) -> T { + self.inner + } + + pub fn as_inner(&self) -> &T { + &self.inner + } +} + +impl Serialize for DoubleEncoded +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.inner.serialize(serializer) + } +} + +impl<'de, T> Deserialize<'de> for DoubleEncoded +where + T: DeserializeOwned, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = Value::deserialize(deserializer)?; + let inner = decode_value::(value).map_err(serde::de::Error::custom)?; + Ok(Self { inner }) + } +} + +fn decode_value(value: Value) -> Result +where + T: DeserializeOwned, +{ + match serde_json::from_value::(value.clone()) { + Ok(result) => Ok(result), + Err(primary_err) => { + if let Value::String(raw) = value { + // Attempt to parse the string as JSON and then deserialize to the target type. + match serde_json::from_str::(&raw) { + Ok(decoded) => serde_json::from_value::(decoded), + Err(_) => Err(primary_err), + } + } else { + Err(primary_err) + } + } + } +} + +/// WebSocket message envelope for pipeline events. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "content")] +pub enum WebSocketMessage { + #[serde(rename = "notification")] + Notification(DoubleEncoded), + #[serde(rename = "response-notification")] + ResponseNotification(DoubleEncoded), + #[serde(rename = "see-notification")] + SeeNotification(DoubleEncoded), + #[serde(rename = "hide-notification")] + HideNotification(DoubleEncoded), + #[serde(rename = "clear-notification")] + ClearNotification, + #[serde(rename = "notification-v2")] + NotificationV2(DoubleEncoded), + #[serde(rename = "notification-v2-update")] + NotificationV2Update(DoubleEncoded), + #[serde(rename = "notification-v2-delete")] + NotificationV2Delete(DoubleEncoded), + #[serde(rename = "friend-add")] + FriendAdd(DoubleEncoded), + #[serde(rename = "friend-delete")] + FriendDelete(DoubleEncoded), + #[serde(rename = "friend-update")] + FriendUpdate(DoubleEncoded), + #[serde(rename = "friend-online")] + FriendOnline(DoubleEncoded), + #[serde(rename = "friend-active")] + FriendActive(DoubleEncoded), + #[serde(rename = "friend-offline")] + FriendOffline(DoubleEncoded), + #[serde(rename = "friend-location")] + FriendLocation(DoubleEncoded), + #[serde(rename = "user-update")] + UserUpdate(DoubleEncoded), + #[serde(rename = "user-location")] + UserLocation(DoubleEncoded), + #[serde(rename = "user-badge-assigned")] + UserBadgeAssigned(DoubleEncoded), + #[serde(rename = "user-badge-unassigned")] + UserBadgeUnassigned(DoubleEncoded), + #[serde(rename = "content-refresh")] + ContentRefresh(DoubleEncoded), + #[serde(rename = "modified-image-update")] + ModifiedImageUpdate(DoubleEncoded), + #[serde(rename = "instance-queue-joined")] + InstanceQueueJoined(DoubleEncoded), + #[serde(rename = "instance-queue-ready")] + InstanceQueueReady(DoubleEncoded), + #[serde(rename = "group-joined")] + GroupJoined(DoubleEncoded), + #[serde(rename = "group-left")] + GroupLeft(DoubleEncoded), + #[serde(rename = "group-member-updated")] + GroupMemberUpdated(DoubleEncoded), + #[serde(rename = "group-role-updated")] + GroupRoleUpdated(DoubleEncoded), + #[serde(other)] + Unknown, +} + +// Notification payloads +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct NotificationPayload { + #[serde(default)] + pub id: Option, + #[serde(rename = "type", default)] + pub kind: Option, + #[serde(default)] + pub category: Option, + #[serde(default)] + pub sender_user_id: Option, + #[serde(default)] + pub sender_username: Option, + #[serde(default)] + pub receiver_user_id: Option, + #[serde(default)] + pub message: Option, + #[serde(default)] + pub details: Option, + #[serde(default)] + pub image_url: Option, + #[serde(default)] + pub link: Option, + #[serde(default)] + pub link_text: Option, + #[serde(default)] + pub seen: Option, + #[serde(default)] + pub can_respond: Option, + #[serde(default)] + pub expires_at: Option, + #[serde(default)] + pub expiry_after_seen: Option, + #[serde(default)] + pub require_seen: Option, + #[serde(default)] + pub hide_after_seen: Option, + #[serde(default)] + pub created_at: Option, + #[serde(default)] + pub updated_at: Option, + #[serde(flatten)] + pub extra: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResponseNotificationContent { + pub notification_id: String, + pub receiver_id: String, + pub response_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct NotificationV2Payload { + pub id: String, + pub version: i32, + #[serde(rename = "type")] + pub kind: String, + pub category: String, + pub is_system: bool, + pub ignore_dnd: bool, + #[serde(default)] + pub sender_user_id: Option, + #[serde(default)] + pub sender_username: Option, + pub receiver_user_id: String, + #[serde(default)] + pub related_notifications_id: Option, + #[serde(default)] + pub title: Option, + #[serde(default)] + pub message: Option, + #[serde(default)] + pub image_url: Option, + #[serde(default)] + pub link: Option, + #[serde(default)] + pub link_text: Option, + #[serde(default)] + pub responses: Vec, + #[serde(default)] + pub expires_at: Option, + #[serde(default)] + pub expiry_after_seen: Option, + #[serde(default)] + pub require_seen: Option, + #[serde(default)] + pub seen: Option, + #[serde(default)] + pub can_delete: Option, + #[serde(default)] + pub created_at: Option, + #[serde(default)] + pub updated_at: Option, + #[serde(flatten)] + pub extra: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct NotificationV2Response { + #[serde(rename = "type", default)] + pub kind: Option, + #[serde(default)] + pub data: Option, + #[serde(default)] + pub icon: Option, + #[serde(default)] + pub text: Option, + #[serde(flatten)] + pub extra: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct NotificationV2UpdatePayload { + pub id: String, + pub version: i32, + #[serde(default)] + pub updates: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Default)] +#[serde(rename_all = "camelCase")] +pub struct NotificationV2DeletePayload { + #[serde(default)] + pub ids: Vec, + pub version: i32, +} + +// Friend events +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FriendAddContent { + #[serde(rename = "userId")] + pub user_id: String, + pub user: LimitedUserFriend, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FriendDeleteContent { + #[serde(rename = "userId")] + pub user_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct FriendUpdateContent { + #[serde(rename = "userId")] + pub user_id: String, + pub user: LimitedUserFriend, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct FriendOnlineContent { + #[serde(rename = "userId")] + pub user_id: String, + #[serde(default)] + pub platform: Option, + #[serde(default)] + pub location: Option, + #[serde(default)] + pub can_request_invite: Option, + pub user: LimitedUserFriend, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct FriendActiveContent { + #[serde(rename = "userid", alias = "userId")] + pub user_id: String, + #[serde(default)] + pub platform: Option, + pub user: LimitedUserFriend, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct FriendOfflineContent { + #[serde(rename = "userId")] + pub user_id: String, + #[serde(default)] + pub platform: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct FriendLocationContent { + #[serde(rename = "userId")] + pub user_id: String, + pub location: String, + #[serde(default)] + pub traveling_to_location: Option, + #[serde(default)] + pub world_id: Option, + #[serde(default)] + pub can_request_invite: Option, + #[serde(default)] + pub user: Option, +} + +// User events +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct UserUpdateContent { + #[serde(rename = "userId")] + pub user_id: String, + pub user: PipelineUserSummary, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct PipelineUserSummary { + #[serde(default)] + pub bio: String, + #[serde(default)] + pub current_avatar: Option, + #[serde(default)] + pub current_avatar_asset_url: Option, + #[serde(default)] + pub current_avatar_image_url: Option, + #[serde(default)] + pub current_avatar_thumbnail_image_url: Option, + pub display_name: String, + #[serde(default)] + pub fallback_avatar: Option, + pub id: String, + #[serde(default)] + pub profile_pic_override: Option, + #[serde(default)] + pub profile_pic_override_thumbnail_image_url: Option, + #[serde(default)] + pub status: String, + #[serde(default)] + pub status_description: String, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub user_icon: Option, + pub username: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct UserLocationContent { + #[serde(rename = "userId")] + pub user_id: String, + #[serde(default)] + pub user: Option, + pub location: String, + #[serde(default)] + pub instance: Option, + #[serde(default)] + pub traveling_to_location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UserBadgeAssignedContent { + pub badge: PipelineBadge, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PipelineBadge { + pub badge_id: String, + #[serde(default)] + pub badge_name: Option, + #[serde(default)] + pub badge_description: Option, + #[serde(default)] + pub badge_image_url: Option, + #[serde(default)] + pub assigned_at: Option, + #[serde(flatten)] + pub extra: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct UserBadgeUnassignedContent { + pub badge_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct ContentRefreshContent { + pub content_type: String, + pub file_id: Option, + #[serde(default)] + pub item_id: Option, + #[serde(default)] + pub item_type: Option, + #[serde(default)] + pub action_type: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct ModifiedImageUpdateContent { + pub file_id: String, + pub pixel_size: i64, + pub version_number: i64, + pub needs_processing: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct InstanceQueueJoinedContent { + pub instance_location: String, + pub position: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct InstanceQueueReadyContent { + pub instance_location: String, + pub expiry: String, +} + +// Group events +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct GroupChangedContent { + pub group_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GroupMemberUpdatedContent { + pub member: Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GroupRoleUpdatedContent { + pub role: Value, +} + +// Typed payloads for frontend events +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct FriendOnlineEvent { + pub user_id: String, + pub user: LimitedUserFriend, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct FriendOfflineEvent { + pub user_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct FriendUpdateEvent { + pub user_id: String, + pub user: LimitedUserFriend, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct FriendRemovedEvent { + pub user_id: String, +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..dbfd537 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "VRC Circle", + "version": "0.0.1", + "identifier": "cafe.kirameki.vrc-circle", + "build": { + "beforeDevCommand": "bun run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "bun run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "VRC Circle", + "width": 1200, + "height": 800 + } + ], + "security": { + "csp": null, + "assetProtocol": { + "enable": true, + "scope": ["**"] + } + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..ad60d9e --- /dev/null +++ b/src/App.css @@ -0,0 +1,64 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color: #0f0f0f; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; +} + +.loading-container { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.loading-spinner { + width: 50px; + height: 50px; + border: 4px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 16px; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.loading-container p { + font-size: 16px; + font-weight: 500; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6f6f6; + background-color: #2f2f2f; + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..4c09b0d --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,129 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { AuthProvider, useAuth } from './context/AuthContext'; +import { ThemeProvider } from './components/theme-provider'; +import { ToasterComponent } from './components/toaster'; +import { MainLayout } from './layouts/MainLayout'; +import { Login } from './pages/Login'; +import { Verify2FA } from './pages/Verify2FA'; +import { Home } from './pages/Home'; +import { Profile } from './pages/Profile'; +import { Worlds } from './pages/Worlds'; +import { Avatars } from './pages/Avatars'; +import { Settings } from './pages/Settings'; +import { Loader2 } from 'lucide-react'; +import './App.css'; + +function AppRoute({ children }: { children: React.ReactNode }) { + const { loading } = useAuth(); + + if (loading) { + return ( +
+
+ +

Loading...

+
+
+ ); + } + + return {children}; +} + +function PublicRoute({ children }: { children: React.ReactNode }) { + const { user, loading } = useAuth(); + + if (loading) { + return ( +
+
+ +

Loading...

+
+
+ ); + } + + if (user) { + return ; + } + + return <>{children}; +} + +function App() { + return ( + + + + + + + + + } + /> + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + + + + + ); +} + +export default App; diff --git a/src/components/AccountMenu.tsx b/src/components/AccountMenu.tsx new file mode 100644 index 0000000..78694c1 --- /dev/null +++ b/src/components/AccountMenu.tsx @@ -0,0 +1,481 @@ +import { useState, useEffect } from "react"; +import { useNavigate, useLocation } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { useAuth } from "@/context/AuthContext"; +import { AccountService } from "@/services/account"; +import { WebSocketService } from "@/services/websocket"; +import { VRChatService } from "@/services/vrchat"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + LogOut, + Users, + Settings, + Loader2, + CheckCircle2, + ChevronDown, + IdCard, + UserCircle, + CircleDot, + X, + ChevronUp, +} from "lucide-react"; +import { UserAvatar } from "@/components/UserAvatar"; +import type { UserStatus } from "@/types/bindings"; +import type { StoredAccount } from "@/types/bindings"; +import { accountsStore } from "@/stores"; +import { getStatusDotClass } from "@/lib/utils"; + +interface AccountMenuProps { + showThemeToggle?: boolean; +} + +export function AccountMenu({ + showThemeToggle: _showThemeToggle = false, +}: AccountMenuProps) { + const { user, logout, clearLocalSession, setUser } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); + const [accounts, setAccounts] = useState( + accountsStore.getSnapshot() ?? [] + ); + const [switchingAccount, setSwitchingAccount] = useState(null); + const [updatingStatus, setUpdatingStatus] = useState(false); + const [customStatusInput, setCustomStatusInput] = useState(""); + const [accountsExpanded, setAccountsExpanded] = useState(() => { + const switchable = + accountsStore.getSnapshot()?.filter((acc) => acc.user_id !== user?.id) ?? + []; + return switchable.length <= 3; + }); + + useEffect(() => { + const unsubscribe = accountsStore.subscribe((value) => { + setAccounts(value ?? []); + }); + + return () => { + unsubscribe(); + }; + }, []); + + const switchableAccounts = accounts.filter( + (account) => account.user_id !== user?.id + ); + + const handleLogout = async () => { + setIsLoading(true); + try { + await logout(); + navigate("/login"); + } finally { + setIsLoading(false); + } + }; + + const handleAddAccount = async () => { + setIsLoading(true); + try { + await clearLocalSession(); + setMenuOpen(false); + navigate("/login"); + } finally { + setIsLoading(false); + } + }; + + const handleViewProfile = () => { + setMenuOpen(false); + navigate("/profile"); + }; + + const handleOpenSettings = () => { + setMenuOpen(false); + navigate("/settings"); + }; + + const handleStatusChange = async ( + status: UserStatus | string, + statusDescription: string = "" + ) => { + if (!user || updatingStatus) return; + + setUpdatingStatus(true); + try { + const updatedUser = await VRChatService.updateStatus( + status, + statusDescription + ); + setUser(updatedUser); + setCustomStatusInput(""); + } catch (error) { + console.error("Failed to update status:", error); + toast.error(t("component.accountMenu.statusUpdateFailed")); + } finally { + setUpdatingStatus(false); + } + }; + + const handleCustomStatusSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (customStatusInput.trim()) { + await handleStatusChange( + user?.status || "active", + customStatusInput.trim() + ); + setMenuOpen(false); + } + }; + + const handleClearStatus = () => { + if (!user || updatingStatus) return; + handleStatusChange(user.status ?? "active", ""); + }; + + const handleCopyStatus = async () => { + if (!user?.statusDescription) return; + try { + await navigator.clipboard.writeText(user.statusDescription); + toast.success(t("component.accountMenu.statusCopied")); + } catch (error) { + console.error("Failed to copy status:", error); + toast.error(t("component.accountMenu.statusCopyFailed")); + } + }; + + const handleQuickSwitch = async (userId: string) => { + if (switchingAccount || userId === user?.id) { + return; + } + + setSwitchingAccount(userId); + try { + if (user) { + await accountsStore.saveFromUser(user); + } + + // Stop current WebSocket + await WebSocketService.stop(); + + // Switch account + const switchedUser = await AccountService.switchAccount(userId); + setUser(switchedUser); + + // Start WebSocket for new account + await WebSocketService.start(); + await accountsStore.refresh(); + + if (location.pathname === "/login") { + navigate("/"); + } + } catch (error) { + console.error("Failed to switch account", error); + toast.error(t("component.accountMenu.accountSwitchFailed")); + } finally { + setSwitchingAccount(null); + } + }; + + return ( + + + + + + + {user && ( + <> +
+
+ +
+

+ {user.displayName} +

+

+ @{user.username} +

+
+
+ +
+ + + + )} + + {user && ( + <> + {user.statusDescription && ( +
+ + {user.statusDescription} + + +
+ )} + + + + + {t("component.accountMenu.setStatus")} + {updatingStatus && ( + + )} + + + + handleStatusChange("active", user.statusDescription ?? "") + } + disabled={updatingStatus} + > +
+
+ {t("common.status.active")} +
+ {user.status === "active" && ( + + )} + + + handleStatusChange("join me", user.statusDescription ?? "") + } + disabled={updatingStatus} + > +
+
+ {t("common.status.joinMe")} +
+ {user.status === "join me" && ( + + )} + + + handleStatusChange("ask me", user.statusDescription ?? "") + } + disabled={updatingStatus} + > +
+
+ {t("common.status.askMe")} +
+ {user.status === "ask me" && ( + + )} + + + handleStatusChange("busy", user.statusDescription ?? "") + } + disabled={updatingStatus} + > +
+
+ {t("common.status.busy")} +
+ {user.status === "busy" && ( + + )} + + +
+
+ setCustomStatusInput(e.target.value)} + disabled={updatingStatus} + className="h-8 text-sm" + maxLength={32} + /> + +
+
+ + + + + + {t("component.accountMenu.viewProfile")} + + + + + )} + +
setAccountsExpanded(!accountsExpanded)} + > + + {t("component.accountMenu.switchAccount")} + + {accountsExpanded ? ( + + ) : ( + + )} +
+ {accountsExpanded && + switchableAccounts.map((account) => { + const isCurrent = account.user_id === user?.id; + const isSwitching = switchingAccount === account.user_id; + const disabled = + isCurrent || (switchingAccount !== null && !isSwitching); + const primaryAvatar = + account.avatar_url ?? account.avatar_fallback_url ?? undefined; + const fallbackAvatar = + account.avatar_fallback_url ?? account.avatar_url ?? undefined; + + return ( + { + e.preventDefault(); + handleQuickSwitch(account.user_id); + }} + onSelect={(e) => { + e.preventDefault(); + }} + disabled={disabled} + > + +
+ + {account.display_name || + t("component.accountMenu.unknownUser")} + + + @ + {account.username || + t("component.accountMenu.unknownUsername")} + +
+ {isCurrent ? ( + + ) : isSwitching ? ( + + ) : null} +
+ ); + })} + {accountsExpanded && ( + +
+ +
+
+ + {t("component.accountMenu.addAccount")} + +
+
+ )} + + + + + {t("component.accountMenu.settings")} + + + + ); +} diff --git a/src/components/AlertBarContainer.tsx b/src/components/AlertBarContainer.tsx new file mode 100644 index 0000000..3e287d4 --- /dev/null +++ b/src/components/AlertBarContainer.tsx @@ -0,0 +1,101 @@ +import { useState, useEffect } from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { AlertBar } from "./ui/alert-bar"; +import { alertStore } from "@/stores"; +import type { AlertStoreState } from "@/stores/alert-store"; +import { cn } from "@/lib/utils"; + +export function AlertBarContainer() { + // Force re-render every 30 seconds + // TODO: Remove this, no longer needed + const [, setNow] = useState(Date.now()); + useEffect(() => { + const interval = setInterval(() => setNow(Date.now()), 30000); + return () => clearInterval(interval); + }, []); + const [alertState, setAlertState] = useState( + alertStore.getSnapshot() + ); + + useEffect(() => { + const unsubscribe = alertStore.subscribe((state) => { + setAlertState(state); + }); + + return unsubscribe; + }, []); + + const { alerts, currentIndex } = alertState; + const currentAlert = alerts[currentIndex]; + + if (!currentAlert) return null; + + const hasMultipleAlerts = alerts.length > 1; + + const handleDismiss = () => { + if (currentAlert.dismissable) { + alertStore.removeAlert(currentAlert.id); + } + }; + + const handleClick = async () => { + if (currentAlert.onClick) { + try { + await currentAlert.onClick(); + } catch (error) { + console.error("Alert onClick error:", error); + } + } + }; + + return ( +
+ +
+ {/* Alert Message */} +
{currentAlert.message}
+ + {/* Navigation Controls (Only show if there are multiple alerts) */} + {hasMultipleAlerts && ( +
+ + + + {currentIndex + 1}/{alerts.length} + + + +
+ )} +
+
+
+ ); +} diff --git a/src/components/CachedImage.tsx b/src/components/CachedImage.tsx new file mode 100644 index 0000000..5118930 --- /dev/null +++ b/src/components/CachedImage.tsx @@ -0,0 +1,24 @@ +import { cn } from '@/lib/utils'; +import { useCachedImage } from '@/hooks/useCachedImage'; +import type { ImgHTMLAttributes } from 'react'; + +type CachedImageProps = ImgHTMLAttributes & { + src: string | null | undefined; +}; + +export function CachedImage({ src, className, ...rest }: CachedImageProps) { + const cachedSrc = useCachedImage(src); + + if (!src && !cachedSrc) { + return null; + } + + return ( + + ); +} + diff --git a/src/components/DevTools.tsx b/src/components/DevTools.tsx new file mode 100644 index 0000000..d1b8c6d --- /dev/null +++ b/src/components/DevTools.tsx @@ -0,0 +1,194 @@ +import { useState, useEffect, useRef } from "react"; +import { ChevronUp, ChevronDown } from "lucide-react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { StoreStudio } from "@/pages/StoreStudio"; +import { DatabaseStudio } from "@/pages/DatabaseStudio"; +import { Logs } from "@/pages/Logs"; +import { cn } from "@/lib/utils"; +import { developerModeStore } from "@/stores/developer-mode-store"; +import { useTranslation } from "react-i18next"; + +export function DevTools() { + const { t } = useTranslation(); + const [isDeveloperMode, setIsDeveloperMode] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + const [height, setHeight] = useState(400); + const [isResizing, setIsResizing] = useState(false); + const [shouldRenderContent, setShouldRenderContent] = useState(false); + const containerRef = useRef(null); + const headerRef = useRef(null); + const [headerHeight, setHeaderHeight] = useState(48); + + // TODO: Refactor this animation duration to a shared constant or something + // Keep this in sync with the tailwind duration used in classes below + const ANIMATION_DURATION = 300; + + useEffect(() => { + const unsubscribe = developerModeStore.subscribe(setIsDeveloperMode); + developerModeStore.ensure(); + return unsubscribe; + }, []); + + // Handle expanded content rendering with animation delay + useEffect(() => { + if (isExpanded) { + setShouldRenderContent(true); + } else { + // Delay unmounting to allow exit animation + height transition to finish + const timeout = setTimeout( + () => setShouldRenderContent(false), + ANIMATION_DURATION + 20 + ); + return () => clearTimeout(timeout); + } + }, [isExpanded]); + + // Measure the header height so we can animate between header-only (collapsed) + // and the expanded panel height. Use ResizeObserver to update if styles change. + useEffect(() => { + const el = headerRef.current; + if (!el) return; + + const update = () => setHeaderHeight(el.offsetHeight || 48); + update(); + + let ro: ResizeObserver | undefined; + if (typeof ResizeObserver !== "undefined") { + ro = new ResizeObserver(update); + ro.observe(el); + } else { + // Fallback if ResizeObserver somehow isn't supported + window.addEventListener("resize", update); + } + + return () => { + if (ro) ro.disconnect(); + else window.removeEventListener("resize", update); + }; + }, []); + + // Handle resize + useEffect(() => { + if (!isResizing) return; + + const handleMouseMove = (e: MouseEvent) => { + const newHeight = window.innerHeight - e.clientY; + const minHeight = 100; + const maxHeight = window.innerHeight - 100; + setHeight(Math.max(minHeight, Math.min(maxHeight, newHeight))); + }; + + const handleMouseUp = () => { + setIsResizing(false); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [isResizing]); + + // Don't render if developer mode is disabled + if (!isDeveloperMode) { + return null; + } + + return ( +
+
+ {/* Resize handle (only visible when expanded) */} + {isExpanded && ( +
setIsResizing(true)} + /> + )} + + {/* Toggle bar (The pull up tab) */} +
setIsExpanded(!isExpanded)} + className={cn( + "cursor-pointer hover:bg-accent/50 transition-colors flex items-center justify-center", + isExpanded + ? "px-4 py-2 border-b" + : "mx-auto w-64 px-4 py-1.5 rounded-t-lg bg-background/80 backdrop-blur-sm border border-b-0 shadow-lg" + )} + style={{ userSelect: "none" }} + > +
+ {isExpanded ? ( + + ) : ( + + )} + + {t("layout.sidebar.developerTools")} + +
+
+ + {/* Expanded content */} + {shouldRenderContent && ( +
+ + + + {t("layout.developerTools.tabs.logger")} + + + {t("layout.developerTools.tabs.databaseStudio")} + + + {t("layout.developerTools.tabs.storeStudio")} + + + +
+ + + + + + + + + + + +
+
+
+ )} +
+
+ ); +} diff --git a/src/components/FriendsSidebar.tsx b/src/components/FriendsSidebar.tsx new file mode 100644 index 0000000..b0bbb8e --- /dev/null +++ b/src/components/FriendsSidebar.tsx @@ -0,0 +1,433 @@ +import { useState, useEffect, useMemo, useCallback } from "react"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { cn, isUserOffline, getStatusDotClass } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Users, ChevronLeft, User, Loader2 } from "lucide-react"; +import { UserAvatar } from "@/components/UserAvatar"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useAuth } from "@/context/AuthContext"; +import { userStore } from "@/stores"; +import type { LimitedUserFriend, UserStatus } from "@/types/bindings"; + +const POLL_INTERVAL_MS = 5 * 60 * 1000; + +interface FriendsSidebarProps { + isOpen: boolean; + onToggle: () => void; +} + +export function FriendsSidebar({ isOpen, onToggle }: FriendsSidebarProps) { + const { user } = useAuth(); + const navigate = useNavigate(); + const { t } = useTranslation(); + const [friends, setFriends] = useState( + userStore.getFriendsSnapshot() + ); + const [loading, setLoading] = useState(false); + const [error, setError] = useState<"loadFailed" | null>(null); + + const describeLocation = useCallback( + (location: string): string => { + const normalized = location.toLowerCase(); + + if (!location || normalized === "offline") { + return t("layout.friendsSidebar.location.offline"); + } + + if (normalized === "private") { + return t("layout.friendsSidebar.location.private"); + } + + if (normalized.startsWith("traveling")) { + return t("layout.friendsSidebar.location.traveling"); + } + + if (normalized.startsWith("group:")) { + return t("layout.friendsSidebar.location.group"); + } + + if (normalized.includes(":")) { + return t("layout.friendsSidebar.location.instance"); + } + + return location; + }, + [t] + ); + + const getStatusText = useCallback( + (status?: UserStatus | null) => { + if (!status) { + return ""; + } + + switch (status) { + case "active": + return t("common.status.active"); + case "join me": + return t("common.status.joinMe"); + case "ask me": + return t("common.status.askMe"); + case "busy": + return t("common.status.busy"); + } + + return status; + }, + [t] + ); + + useEffect(() => { + let mounted = true; + + const handleUpdate = (value: LimitedUserFriend[] | null) => { + if (!mounted) { + return; + } + setFriends(value); + setLoading(false); + if (value) { + setError(null); + } + }; + + const unsubscribe = userStore.subscribeFriends(handleUpdate); + + if (!user) { + setFriends(null); + setLoading(false); + setError(null); + return () => { + mounted = false; + unsubscribe(); + }; + } + + // Only show loading if we don't already have data + const snapshot = userStore.getFriendsSnapshot(); + if (!snapshot) { + setLoading(true); + setError(null); + } + + userStore.ensureFriends().catch((err) => { + console.error("Failed to load friends:", err); + if (!mounted) { + return; + } + setError("loadFailed"); + setLoading(false); + }); + + return () => { + mounted = false; + unsubscribe(); + }; + }, [user?.id]); + + // Periodic refresh from backend (backend handles WebSocket updates to UserStore) + useEffect(() => { + if (!user) { + return; + } + + let disposed = false; + + const refreshFriends = async (showLoading = true) => { + // Don't show loading spinner if we already have data + const hasData = userStore.getFriendsSnapshot() !== null; + const shouldShowLoading = showLoading && !hasData; + + if (!disposed && shouldShowLoading) { + setLoading(true); + setError(null); + } else if (!disposed) { + setError(null); + } + try { + await userStore.refreshFriends(); + } catch (err) { + console.error("Failed to refresh friends:", err); + if (!disposed) { + setError("loadFailed"); + if (shouldShowLoading) { + setLoading(false); + } + } + return; + } + if (!disposed && shouldShowLoading) { + setLoading(false); + } + }; + + // Initial load + void refreshFriends(true); + + // Poll periodically to sync with backend UserStore + const poller = setInterval(() => { + void refreshFriends(false); + }, POLL_INTERVAL_MS); + + return () => { + disposed = true; + clearInterval(poller); + }; + }, [user?.id]); + + const getFriendSecondaryText = (friend: LimitedUserFriend) => { + const location = friend.location?.trim(); + if (location && location.toLowerCase() !== "offline") { + return describeLocation(location); + } + if (friend.platform?.toLowerCase() === "web") { + return t("layout.friendsSidebar.status.website"); + } + if (friend.statusDescription) { + return friend.statusDescription; + } + return getStatusText(friend.status); + }; + + const friendsList = friends ?? []; + + const { sections, onlineCount, compactFriends } = useMemo(() => { + const inWorld: LimitedUserFriend[] = []; + const active: LimitedUserFriend[] = []; + const offline: LimitedUserFriend[] = []; + + const isInWorld = (friend: LimitedUserFriend) => { + const location = friend.location?.toLowerCase() ?? ""; + if (!location || location === "offline") { + return false; + } + return true; + }; + + for (const friend of friendsList) { + if (isInWorld(friend)) { + inWorld.push(friend); + } else if (!isUserOffline(friend)) { + active.push(friend); + } else { + offline.push(friend); + } + } + + const sectionsData = [ + { + key: "in-world", + title: t("layout.friendsSidebar.sections.inWorld.title"), + friends: inWorld, + emptyMessage: t("layout.friendsSidebar.sections.inWorld.empty"), + }, + { + key: "active", + title: t("layout.friendsSidebar.sections.active.title"), + friends: active, + emptyMessage: t("layout.friendsSidebar.sections.active.empty"), + }, + { + key: "offline", + title: t("layout.friendsSidebar.sections.offline.title"), + friends: offline, + emptyMessage: t("layout.friendsSidebar.sections.offline.empty"), + }, + ]; + + const online = inWorld.length + active.length; + // For the collapsed view we want to show all friends (in-world, active, then offline) + // so the compact list should include offline friends as well. + const compactList = [...inWorld, ...active, ...offline]; + + return { + sections: sectionsData, + onlineCount: online, + compactFriends: compactList, + }; + }, [friendsList, t]); + + const renderFriendRow = (friend: LimitedUserFriend) => ( + + ); + + // Show all compact friends when collapsed (no slicing). + const compactFriendsToShow = compactFriends; + // Only show sections that actually contain friends in the expanded view + const visibleSections = sections.filter((s) => s.friends.length > 0); + + return ( +
+ {/* Header */} +
+ {isOpen && ( +
+ + + {t("layout.friendsSidebar.title")} + +
+ )} + +
+ + {/* Friends List (uses custom ScrollArea to avoid native scrollbar) */} + +
+ {loading ? ( +
+ +
+ ) : error ? ( +

+ {t(`layout.friendsSidebar.errors.${error}`)} +

+ ) : isOpen ? ( + visibleSections.length > 0 ? ( + visibleSections.map((section, index) => ( +
0 && "pt-4 border-t border-border/60" + )} + > +

+ {section.title} +

+
+ {section.friends.map((friend) => renderFriendRow(friend))} +
+
+ )) + ) : ( +

+ {t("layout.friendsSidebar.empty.collapsed")} +

+ ) + ) : ( + // Collapsed view - show avatars only with separators between sections +
+ {compactFriendsToShow.length > 0 ? ( + <> + {sections.map((section, sectionIndex) => { + if (section.friends.length === 0) return null; + + return ( +
+ {sectionIndex > 0 && ( +
+
+
+ )} +
+ {section.friends.map((friend) => ( + + ))} +
+
+ ); + })} + + ) : ( +

+ {t("layout.friendsSidebar.empty.collapsed")} +

+ )} +
+ )} +
+ + + {/* Footer - Online count */} + {isOpen && !loading && ( +
+ {t("layout.friendsSidebar.onlineCount", { count: onlineCount })} +
+ )} +
+ ); +} diff --git a/src/components/LoginRequired.tsx b/src/components/LoginRequired.tsx new file mode 100644 index 0000000..c2c56f0 --- /dev/null +++ b/src/components/LoginRequired.tsx @@ -0,0 +1,38 @@ +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Lock, LogIn } from "lucide-react"; + +export function LoginRequired() { + const { t } = useTranslation(); + const navigate = useNavigate(); + + return ( +
+
+ + +
+ +
+ + {t("component.loginRequired.title")} + + {/* + */} +
+ +

+ {t("component.loginRequired.bottomText")} +

+ +
+
+
+
+ ); +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..789c910 --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,40 @@ +import { useAuth } from "@/context/AuthContext"; +import { Button } from "@/components/ui/button"; +import { ThemeToggle } from "@/components/theme-toggle"; +import { Menu } from "lucide-react"; +import { AccountMenu } from "@/components/AccountMenu"; + +interface NavbarProps { + onMenuToggle: () => void; +} + +export function Navbar({ onMenuToggle }: NavbarProps) { + const { user } = useAuth(); + + if (!user) { + return null; + } + + return ( + + ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..e0acb4e --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,126 @@ +import { LucideIcon, Menu, ChevronLeft } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +interface NavigationItem { + id: string; + label: string; + icon: LucideIcon; + path: string; +} + +interface SidebarProps { + isOpen: boolean; + onToggle: () => void; + mainItems: NavigationItem[]; + bottomItems: NavigationItem[]; + isActive: (path: string) => boolean; + onNavigate: (path: string) => void; +} + +export function Sidebar({ + isOpen, + onToggle, + mainItems, + bottomItems, + isActive, + onNavigate, +}: SidebarProps) { + const { t } = useTranslation(); + + return ( + + ); +} diff --git a/src/components/UserAvatar.tsx b/src/components/UserAvatar.tsx new file mode 100644 index 0000000..203e492 --- /dev/null +++ b/src/components/UserAvatar.tsx @@ -0,0 +1,90 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { cn } from "@/lib/utils"; +import { getUserAvatarUrl, getUserInitials } from "@/lib/user"; +import type { User } from "@/types/bindings"; +import type { CSSProperties } from "react"; +import { useCachedImage } from "@/hooks/useCachedImage"; + +type MinimalUser = Pick< + User, + | "displayName" + | "userIcon" + | "profilePicOverride" + | "profilePicOverrideThumbnail" + | "currentAvatarImageUrl" + | "currentAvatarThumbnailImageUrl" +>; + +interface UserAvatarProps { + user: MinimalUser; + className?: string; + imageClassName?: string; + fallbackClassName?: string; + fallbackText?: string; + statusClassName?: string; + avatarClassName?: string; + statusSize?: string; + statusOffset?: string; + statusContainerClassName?: string; +} + +export function UserAvatar({ + user, + className, + imageClassName, + fallbackClassName, + fallbackText, + statusClassName, + avatarClassName, + statusSize, + statusOffset, + statusContainerClassName, +}: UserAvatarProps) { + const avatarUrl = getUserAvatarUrl(user); + const cachedSrc = useCachedImage(avatarUrl); + const displaySrc = cachedSrc ?? undefined; + const initials = fallbackText ?? getUserInitials(user.displayName); + const indicatorSize = statusSize ?? "38%"; + const indicatorOffset = statusOffset ?? "4%"; + const indicatorStyle: CSSProperties = { + width: indicatorSize, + height: indicatorSize, + bottom: indicatorOffset, + right: indicatorOffset, + }; + + return ( +
+ + {displaySrc ? ( + + ) : null} + + {initials} + + + {statusClassName ? ( + + + + ) : null} +
+ ); +} diff --git a/src/components/store-studio/StoreEditor.tsx b/src/components/store-studio/StoreEditor.tsx new file mode 100644 index 0000000..cb35080 --- /dev/null +++ b/src/components/store-studio/StoreEditor.tsx @@ -0,0 +1,134 @@ +// TODO: Localize all strings + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { toast } from "sonner"; +import { Save, X, AlertTriangle } from "lucide-react"; + +interface StoreEditorProps { + storeName: string; + initialValue: unknown; + onSave: (value: unknown) => void | Promise; + onCancel: () => void; + scopeLabel?: string; +} + +export function StoreEditor({ + storeName, + initialValue, + onSave, + onCancel, + scopeLabel, +}: StoreEditorProps) { + const [jsonText, setJsonText] = useState(() => + JSON.stringify(initialValue, null, 2) + ); + const [error, setError] = useState(null); + const [saving, setSaving] = useState(false); + + const handleSave = async () => { + try { + const parsed = JSON.parse(jsonText); + setError(null); + setSaving(true); + + await onSave(parsed); + toast.success(`${storeName} saved successfully`); + + onCancel(); + } catch (err) { + if (err instanceof SyntaxError) { + setError(`Invalid JSON: ${err.message}`); + toast.error("Invalid JSON"); + } else { + setError( + `Failed to save: ${ + err instanceof Error ? err.message : "Unknown error" + }` + ); + toast.error("Save failed"); + } + } finally { + setSaving(false); + } + }; + + const handleValidate = () => { + try { + JSON.parse(jsonText); + setError(null); + toast.success("Valid JSON"); + } catch (err) { + if (err instanceof SyntaxError) { + setError(`Invalid JSON: ${err.message}`); + toast.error("Invalid JSON"); + } + } + }; + + return ( + + +
+
+ + + {`${storeName} Editor`} {scopeLabel ? `(${scopeLabel})` : ""} + + + Edit the JSON for this store and save your changes. + +
+
+
+ +
+