Compare commits

..

38 Commits

Author SHA1 Message Date
zhom e388e2e85a refactor: don't allow portable build to be set as the default browser 2026-03-29 15:47:53 +04:00
zhom decfdfcfc7 chore: linting 2026-03-29 15:01:26 +04:00
zhom c516999f7a feat: portable build 2026-03-29 14:55:20 +04:00
zhom 1099459dbb fix: wayfern initial connection on macos doesn't timeout 2026-03-29 13:03:17 +04:00
zhom a3514df0d4 refactor: show app version in settings 2026-03-29 13:02:41 +04:00
zhom 0102cb6c06 chore: do not provide possible cause 2026-03-28 23:50:15 +04:00
zhom 612c6610ce chore: linting 2026-03-28 23:31:20 +04:00
zhom ba750a3401 chore: linting 2026-03-28 20:59:00 +04:00
zhom d0e3e15fd3 chore: linting 2026-03-28 20:55:10 +04:00
zhom 248927ae6f chore: linting 2026-03-28 14:05:45 +04:00
zhom 6d71dbc62c Merge pull request #255 from zhom/dependabot/npm_and_yarn/frontend-dependencies-9854c608ec
deps(deps): bump the frontend-dependencies group with 35 updates
2026-03-28 13:53:32 +04:00
dependabot[bot] 3f0029c778 deps(deps): bump the frontend-dependencies group with 35 updates
Bumps the frontend-dependencies group with 35 updates:

| Package | From | To |
| --- | --- | --- |
| [i18next](https://github.com/i18next/i18next) | `25.10.5` | `26.0.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.577.0` | `1.7.0` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.6.2` | `17.0.0` |
| [recharts](https://github.com/recharts/recharts) | `3.8.0` | `3.8.1` |
| [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.4.8` | `2.4.9` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.9.3` | `6.0.2` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1015.0` | `3.1019.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.1015.0` | `3.1019.0` |
| [@aws-sdk/core](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/core) | `3.973.24` | `3.973.25` |
| [@aws-sdk/credential-provider-env](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/credential-provider-env) | `3.972.22` | `3.972.23` |
| [@aws-sdk/credential-provider-http](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/credential-provider-http) | `3.972.24` | `3.972.25` |
| [@aws-sdk/credential-provider-ini](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/credential-provider-ini) | `3.972.24` | `3.972.26` |
| [@aws-sdk/credential-provider-login](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/credential-provider-login) | `3.972.24` | `3.972.26` |
| [@aws-sdk/credential-provider-node](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/credential-provider-node) | `3.972.25` | `3.972.27` |
| [@aws-sdk/credential-provider-process](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/credential-provider-process) | `3.972.22` | `3.972.23` |
| [@aws-sdk/credential-provider-sso](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/credential-provider-sso) | `3.972.24` | `3.972.26` |
| [@aws-sdk/credential-provider-web-identity](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/credential-provider-web-identity) | `3.972.24` | `3.972.26` |
| [@aws-sdk/middleware-flexible-checksums](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/middleware-flexible-checksums) | `3.974.4` | `3.974.5` |
| [@aws-sdk/middleware-recursion-detection](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/middleware-recursion-detection) | `3.972.8` | `3.972.9` |
| [@aws-sdk/middleware-sdk-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/middleware-sdk-s3) | `3.972.24` | `3.972.26` |
| [@aws-sdk/middleware-user-agent](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/middleware-user-agent) | `3.972.25` | `3.972.26` |
| [@aws-sdk/nested-clients](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/nested-clients) | `3.996.14` | `3.996.16` |
| [@aws-sdk/region-config-resolver](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/region-config-resolver) | `3.972.9` | `3.972.10` |
| [@aws-sdk/signature-v4-multi-region](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-multi-region) | `3.996.12` | `3.996.14` |
| [@aws-sdk/token-providers](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/token-providers) | `3.1015.0` | `3.1019.0` |
| [@aws-sdk/util-user-agent-node](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/util-user-agent-node) | `3.973.11` | `3.973.12` |
| [@aws-sdk/xml-builder](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/xml-builder) | `3.972.15` | `3.972.16` |
| [@biomejs/cli-darwin-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.4.8` | `2.4.9` |
| [@biomejs/cli-darwin-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.4.8` | `2.4.9` |
| [@biomejs/cli-linux-arm64-musl](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.4.8` | `2.4.9` |
| [@biomejs/cli-linux-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.4.8` | `2.4.9` |
| [@biomejs/cli-linux-x64-musl](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.4.8` | `2.4.9` |
| [@biomejs/cli-linux-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.4.8` | `2.4.9` |
| [@biomejs/cli-win32-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.4.8` | `2.4.9` |
| [@biomejs/cli-win32-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.4.8` | `2.4.9` |


Updates `i18next` from 25.10.5 to 26.0.0
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v25.10.5...v26.0.0)

Updates `lucide-react` from 0.577.0 to 1.7.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.7.0/packages/lucide-react)

Updates `react-i18next` from 16.6.2 to 17.0.0
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.6.2...v17.0.0)

Updates `recharts` from 3.8.0 to 3.8.1
- [Release notes](https://github.com/recharts/recharts/releases)
- [Changelog](https://github.com/recharts/recharts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/recharts/recharts/compare/v3.8.0...v3.8.1)

Updates `@biomejs/biome` from 2.4.8 to 2.4.9
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.9/packages/@biomejs/biome)

Updates `typescript` from 5.9.3 to 6.0.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2)

Updates `@aws-sdk/client-s3` from 3.1015.0 to 3.1019.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1019.0/clients/client-s3)

Updates `@aws-sdk/s3-request-presigner` from 3.1015.0 to 3.1019.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1019.0/packages/s3-request-presigner)

Updates `@aws-sdk/core` from 3.973.24 to 3.973.25
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/core/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/core)

Updates `@aws-sdk/credential-provider-env` from 3.972.22 to 3.972.23
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/credential-provider-env/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/credential-provider-env)

Updates `@aws-sdk/credential-provider-http` from 3.972.24 to 3.972.25
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/credential-provider-http/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/credential-provider-http)

Updates `@aws-sdk/credential-provider-ini` from 3.972.24 to 3.972.26
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/credential-provider-ini/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/credential-provider-ini)

Updates `@aws-sdk/credential-provider-login` from 3.972.24 to 3.972.26
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/credential-provider-login/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/credential-provider-login)

Updates `@aws-sdk/credential-provider-node` from 3.972.25 to 3.972.27
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/credential-provider-node/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/credential-provider-node)

Updates `@aws-sdk/credential-provider-process` from 3.972.22 to 3.972.23
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/credential-provider-process/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/credential-provider-process)

Updates `@aws-sdk/credential-provider-sso` from 3.972.24 to 3.972.26
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/credential-provider-sso/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/credential-provider-sso)

Updates `@aws-sdk/credential-provider-web-identity` from 3.972.24 to 3.972.26
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/credential-provider-web-identity/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/credential-provider-web-identity)

Updates `@aws-sdk/middleware-flexible-checksums` from 3.974.4 to 3.974.5
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/middleware-flexible-checksums/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/middleware-flexible-checksums)

Updates `@aws-sdk/middleware-recursion-detection` from 3.972.8 to 3.972.9
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/middleware-recursion-detection/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/middleware-recursion-detection)

Updates `@aws-sdk/middleware-sdk-s3` from 3.972.24 to 3.972.26
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/middleware-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/middleware-sdk-s3)

Updates `@aws-sdk/middleware-user-agent` from 3.972.25 to 3.972.26
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/middleware-user-agent/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/middleware-user-agent)

Updates `@aws-sdk/nested-clients` from 3.996.14 to 3.996.16
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages/nested-clients)

Updates `@aws-sdk/region-config-resolver` from 3.972.9 to 3.972.10
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/region-config-resolver/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/region-config-resolver)

Updates `@aws-sdk/signature-v4-multi-region` from 3.996.12 to 3.996.14
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages/signature-v4-multi-region)

Updates `@aws-sdk/token-providers` from 3.1015.0 to 3.1019.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/token-providers/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1019.0/packages/token-providers)

Updates `@aws-sdk/util-user-agent-node` from 3.973.11 to 3.973.12
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/util-user-agent-node/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/util-user-agent-node)

Updates `@aws-sdk/xml-builder` from 3.972.15 to 3.972.16
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/xml-builder/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/xml-builder)

Updates `@biomejs/cli-darwin-arm64` from 2.4.8 to 2.4.9
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.9/packages/@biomejs/biome)

Updates `@biomejs/cli-darwin-x64` from 2.4.8 to 2.4.9
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.9/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-arm64-musl` from 2.4.8 to 2.4.9
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.9/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-arm64` from 2.4.8 to 2.4.9
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.9/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-x64-musl` from 2.4.8 to 2.4.9
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.9/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-x64` from 2.4.8 to 2.4.9
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.9/packages/@biomejs/biome)

Updates `@biomejs/cli-win32-arm64` from 2.4.8 to 2.4.9
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.9/packages/@biomejs/biome)

Updates `@biomejs/cli-win32-x64` from 2.4.8 to 2.4.9
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.9/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: lucide-react
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: react-i18next
  dependency-version: 17.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: recharts
  dependency-version: 3.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/biome"
  dependency-version: 2.4.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1019.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.1019.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/core"
  dependency-version: 3.973.25
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/credential-provider-env"
  dependency-version: 3.972.23
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/credential-provider-http"
  dependency-version: 3.972.25
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/credential-provider-ini"
  dependency-version: 3.972.26
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/credential-provider-login"
  dependency-version: 3.972.26
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/credential-provider-node"
  dependency-version: 3.972.27
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/credential-provider-process"
  dependency-version: 3.972.23
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/credential-provider-sso"
  dependency-version: 3.972.26
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/credential-provider-web-identity"
  dependency-version: 3.972.26
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/middleware-flexible-checksums"
  dependency-version: 3.974.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/middleware-recursion-detection"
  dependency-version: 3.972.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/middleware-sdk-s3"
  dependency-version: 3.972.26
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/middleware-user-agent"
  dependency-version: 3.972.26
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/nested-clients"
  dependency-version: 3.996.16
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/region-config-resolver"
  dependency-version: 3.972.10
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/signature-v4-multi-region"
  dependency-version: 3.996.14
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/token-providers"
  dependency-version: 3.1019.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/util-user-agent-node"
  dependency-version: 3.973.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@aws-sdk/xml-builder"
  dependency-version: 3.972.16
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-arm64"
  dependency-version: 2.4.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-x64"
  dependency-version: 2.4.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64-musl"
  dependency-version: 2.4.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64"
  dependency-version: 2.4.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64-musl"
  dependency-version: 2.4.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64"
  dependency-version: 2.4.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-arm64"
  dependency-version: 2.4.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-x64"
  dependency-version: 2.4.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-28 09:52:43 +00:00
zhom fff1fe7087 Merge pull request #254 from zhom/dependabot/cargo/src-tauri/rust-dependencies-23f0da4b4d
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 13 updates
2026-03-28 13:48:55 +04:00
dependabot[bot] 1c971c664f deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 13 updates:

| Package | From | To |
| --- | --- | --- |
| [bzip2](https://github.com/trifectatechfoundation/bzip2-rs) | `0.5.2` | `0.6.1` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.22.0` | `1.23.0` |
| [sha1](https://github.com/RustCrypto/hashes) | `0.10.6` | `0.11.0` |
| [tao](https://github.com/tauri-apps/tao) | `0.34.8` | `0.35.0` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.57` | `1.2.58` |
| [embed-resource](https://github.com/nabijaczleweli/rust-embed-resource) | `3.0.7` | `3.0.8` |
| libredox | `0.1.14` | `0.1.15` |
| [mio](https://github.com/tokio-rs/mio) | `1.1.1` | `1.2.0` |
| [num-conv](https://github.com/jhpratt/num-conv) | `0.2.0` | `0.2.1` |
| [rust_decimal](https://github.com/paupino/rust-decimal) | `1.40.0` | `1.41.0` |
| [simd-adler32](https://github.com/mcountryman/simd-adler32) | `0.3.8` | `0.3.9` |
| [unicode-segmentation](https://github.com/unicode-rs/unicode-segmentation) | `1.12.0` | `1.13.2` |
| [zune-jpeg](https://github.com/etemesi254/zune-image) | `0.5.14` | `0.5.15` |


Updates `bzip2` from 0.5.2 to 0.6.1
- [Release notes](https://github.com/trifectatechfoundation/bzip2-rs/releases)
- [Commits](https://github.com/trifectatechfoundation/bzip2-rs/compare/v0.5.2...v0.6.1)

Updates `uuid` from 1.22.0 to 1.23.0
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.22.0...v1.23.0)

Updates `sha1` from 0.10.6 to 0.11.0
- [Commits](https://github.com/RustCrypto/hashes/compare/sha1-v0.10.6...sha1-v0.11.0)

Updates `tao` from 0.34.8 to 0.35.0
- [Release notes](https://github.com/tauri-apps/tao/releases)
- [Changelog](https://github.com/tauri-apps/tao/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/tauri-apps/tao/compare/tao-v0.34.8...tao-v0.35)

Updates `cc` from 1.2.57 to 1.2.58
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.57...cc-v1.2.58)

Updates `embed-resource` from 3.0.7 to 3.0.8
- [Release notes](https://github.com/nabijaczleweli/rust-embed-resource/releases)
- [Commits](https://github.com/nabijaczleweli/rust-embed-resource/compare/v3.0.7...v3.0.8)

Updates `libredox` from 0.1.14 to 0.1.15

Updates `mio` from 1.1.1 to 1.2.0
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v1.1.1...v1.2.0)

Updates `num-conv` from 0.2.0 to 0.2.1
- [Commits](https://github.com/jhpratt/num-conv/compare/v0.2.0...v0.2.1)

Updates `rust_decimal` from 1.40.0 to 1.41.0
- [Release notes](https://github.com/paupino/rust-decimal/releases)
- [Changelog](https://github.com/paupino/rust-decimal/blob/master/CHANGELOG.md)
- [Commits](https://github.com/paupino/rust-decimal/compare/1.40.0...1.41.0)

Updates `simd-adler32` from 0.3.8 to 0.3.9
- [Changelog](https://github.com/mcountryman/simd-adler32/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mcountryman/simd-adler32/commits/v0.3.9)

Updates `unicode-segmentation` from 1.12.0 to 1.13.2
- [Commits](https://github.com/unicode-rs/unicode-segmentation/compare/v1.12.0...v1.13.2)

Updates `zune-jpeg` from 0.5.14 to 0.5.15
- [Release notes](https://github.com/etemesi254/zune-image/releases)
- [Changelog](https://github.com/etemesi254/zune-image/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/etemesi254/zune-image/commits)

---
updated-dependencies:
- dependency-name: bzip2
  dependency-version: 0.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: uuid
  dependency-version: 1.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: sha1
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tao
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.58
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: embed-resource
  dependency-version: 3.0.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libredox
  dependency-version: 0.1.15
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: mio
  dependency-version: 1.2.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: num-conv
  dependency-version: 0.2.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rust_decimal
  dependency-version: 1.41.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: simd-adler32
  dependency-version: 0.3.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: unicode-segmentation
  dependency-version: 1.13.2
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: zune-jpeg
  dependency-version: 0.5.15
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-28 09:48:00 +00:00
zhom 0788797e3f Merge pull request #253 from zhom/dependabot/github_actions/github-actions-f059b8c920
ci(deps): bump the github-actions group with 8 updates
2026-03-28 13:22:06 +04:00
dependabot[bot] 8c338515b7 ci(deps): bump the github-actions group with 8 updates
Bumps the github-actions group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `4.3.1` | `6.0.2` |
| [google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml](https://github.com/google/osv-scanner-action) | `2.3.3` | `2.3.5` |
| [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) | `2.5.0` | `3.0.0` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `3.12.0` | `4.0.0` |
| [docker/login-action](https://github.com/docker/login-action) | `3.7.0` | `4.0.0` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `6.19.2` | `7.0.0` |
| [anomalyco/opencode](https://github.com/anomalyco/opencode) | `1.2.27` | `1.3.3` |
| [google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml](https://github.com/google/osv-scanner-action) | `2.3.3` | `2.3.5` |


Updates `actions/checkout` from 4.3.1 to 6.0.2
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v4.3.1...v6.0.2)

Updates `google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml` from 2.3.3 to 2.3.5
- [Release notes](https://github.com/google/osv-scanner-action/releases)
- [Commits](https://github.com/google/osv-scanner-action/compare/c5996e0193a3df57d695c1b8a1dec2a4c62e8730...c51854704019a247608d928f370c98740469d4b5)

Updates `dependabot/fetch-metadata` from 2.5.0 to 3.0.0
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/21025c705c08248db411dc16f3619e6b5f9ea21a...ffa630c65fa7e0ecfa0625b5ceda64399aea1b36)

Updates `docker/setup-buildx-action` from 3.12.0 to 4.0.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/8d2750c68a42422c14e847fe6c8ac0403b4cbd6f...4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd)

Updates `docker/login-action` from 3.7.0 to 4.0.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/c94ce9fb468520275223c153574b00df6fe4bcc9...b45d80f862d83dbcd57f89517bcf500b2ab88fb2)

Updates `docker/build-push-action` from 6.19.2 to 7.0.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/10e90e3645eae34f1e60eeb005ba3a3d33f178e8...d08e5c354a6adb9ed34480a06d141179aa583294)

Updates `anomalyco/opencode` from 1.2.27 to 1.3.3
- [Release notes](https://github.com/anomalyco/opencode/releases)
- [Commits](https://github.com/anomalyco/opencode/compare/4ee426ba549131c4903a71dfb6259200467aca81...54443bfb7e090ec3130dc972e689a3e5cc55a7f9)

Updates `google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml` from 2.3.3 to 2.3.5
- [Release notes](https://github.com/google/osv-scanner-action/releases)
- [Commits](https://github.com/google/osv-scanner-action/compare/c5996e0193a3df57d695c1b8a1dec2a4c62e8730...c51854704019a247608d928f370c98740469d4b5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml
  dependency-version: 2.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: dependabot/fetch-metadata
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/login-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/build-push-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: anomalyco/opencode
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml
  dependency-version: 2.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-28 09:05:04 +00:00
zhom a8c179fca7 docs: agents 2026-03-28 01:41:01 +04:00
zhom d0f436ce2d chore: commit doc changes directly and pretty discord notifications 2026-03-28 01:41:01 +04:00
zhom 4019701186 Merge pull request #252 from zhom/contributors-readme-action-7fGCZTC5jp
docs(contributor): contributors readme action update
2026-03-27 20:11:15 +04:00
github-actions[bot] 53f85abe24 docs(contributor): contrib-readme-action has updated readme 2026-03-27 16:08:11 +00:00
zhom 2aafb4c7a4 Merge pull request #249 from yb403/fix/sync-loop-circular-dependency
This fix prevents the file watcher from triggering a new sync when th…
2026-03-27 20:07:59 +04:00
zhom 00d5c655dc Merge pull request #251 from zhom/chore/update-flake-0.18.1
chore: update flake.nix for v0.18.1
2026-03-25 03:39:31 +04:00
zhom b12a704d9f Merge pull request #250 from zhom/docs/release-0.18.1
docs: release notes for v0.18.1
2026-03-25 03:39:20 +04:00
github-actions[bot] 0e134fd145 chore: update flake.nix for v0.18.1 [skip ci] 2026-03-24 23:08:33 +00:00
github-actions[bot] adcdc91de2 docs: update CHANGELOG.md and README.md for v0.18.1 [skip ci] 2026-03-24 23:08:31 +00:00
yb 880014d4c4 chore: fix linting and formatting 2026-03-24 22:50:28 +01:00
zhom 71f367f0ae docs: cleanup 2026-03-25 01:36:43 +04:00
zhom daa001cdf2 chore: version bump 2026-03-25 01:05:26 +04:00
zhom 17056360ab chore: require ai disclosure 2026-03-25 01:01:16 +04:00
zhom 80d5b77a80 chore: redeploy web on new release 2026-03-25 00:54:43 +04:00
zhom 701605fa73 chore: fix e2e in pr requests 2026-03-25 00:52:48 +04:00
zhom 19cb24f67f chore: issues get stale after 30 days 2026-03-25 00:33:20 +04:00
zhom c3fec3d095 docs: agents.md 2026-03-24 23:58:33 +04:00
zhom bb8b6ea0b7 chore: better issue validation 2026-03-24 23:58:21 +04:00
zhom a6dfc5664b refactor: run docker workflow on release 2026-03-24 21:12:52 +04:00
yb 001a292185 This fix prevents the file watcher from triggering a new sync when the client updates the last_sync timestamp in metadata.json. 2026-03-24 13:20:28 +01:00
github-actions[bot] c7d7ff19a7 chore: update flake.nix for v0.18.0 [skip ci] (#247)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-24 06:59:11 +00:00
zhom aec05fb725 docs: new star history url 2026-03-24 09:29:03 +04:00
113 changed files with 2577 additions and 1529 deletions
+5
View File
@@ -197,6 +197,7 @@ These are frequently overlooked issues that make UI look unprofessional:
Before delivering UI code, verify these items:
### Visual Quality
- [ ] No emojis used as icons (use SVG instead)
- [ ] All icons from consistent icon set (Heroicons/Lucide)
- [ ] Brand logos are correct (verified from Simple Icons)
@@ -204,24 +205,28 @@ Before delivering UI code, verify these items:
- [ ] Use theme colors directly (bg-primary) not var() wrapper
### Interaction
- [ ] All clickable elements have `cursor-pointer`
- [ ] Hover states provide clear visual feedback
- [ ] Transitions are smooth (150-300ms)
- [ ] Focus states visible for keyboard navigation
### Light/Dark Mode
- [ ] Light mode text has sufficient contrast (4.5:1 minimum)
- [ ] Glass/transparent elements visible in light mode
- [ ] Borders visible in both modes
- [ ] Test both modes before delivery
### Layout
- [ ] Floating elements have proper spacing from edges
- [ ] No content hidden behind fixed navbars
- [ ] Responsive at 320px, 768px, 1024px, 1440px
- [ ] No horizontal scroll on mobile
### Accessibility
- [ ] All images have alt text
- [ ] Form inputs have labels
- [ ] Color is not the only indicator
+7
View File
@@ -10,4 +10,11 @@
- [ ] Read [CONTRIBUTING.md](https://github.com/zhom/donutbrowser/blob/main/CONTRIBUTING.md)
- [ ] Ran `pnpm format && pnpm lint && pnpm test` locally and it passes
- [ ] I tested the changes myself by running the app locally
- [ ] Updated translations in all locale files (if UI text changed)
## AI usage
- [ ] I used AI to help write this PR
<!-- If you checked the box above, briefly explain how AI was used (e.g. "generated the test", "wrote the initial implementation", "full PR"). -->
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 #v4.4.0
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Contribute List
uses: akhilmhdh/contributors-readme-action@83ea0b4f1ac928fbfe88b9e8460a932a528eb79f #v2.3.11
env:
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
security-scan:
name: Security Vulnerability Scan
if: github.repository == 'zhom/donutbrowser' && github.actor == 'dependabot[bot]'
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@c5996e0193a3df57d695c1b8a1dec2a4c62e8730" # v2.3.3
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@c51854704019a247608d928f370c98740469d4b5" # v2.3.5
with:
scan-args: |-
-r
@@ -69,7 +69,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a #v2.5.0
uses: dependabot/fetch-metadata@ffa630c65fa7e0ecfa0625b5ceda64399aea1b36 #v3.0.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for minor and patch updates
+14 -12
View File
@@ -1,12 +1,16 @@
name: Build and Push donut-sync Docker Image
on:
release:
types: [published]
push:
branches: [main]
paths:
- "donut-sync/**"
workflow_call:
inputs:
tag:
description: "Docker tag (e.g., v1.0.0)"
required: true
type: string
workflow_dispatch:
inputs:
tag:
@@ -26,13 +30,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f #v3
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd #v4.0.0
- name: Log in to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 #v3
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 #v4.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -41,26 +45,24 @@ jobs:
id: tags
run: |
TAGS=""
INPUT_TAG="${{ inputs.tag }}"
if [ "${{ github.event_name }}" = "release" ]; then
# Stable release: tag with version and latest
VERSION="${{ github.event.release.tag_name }}"
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}"
if [ -n "$INPUT_TAG" ]; then
# Called from release workflow or manual dispatch
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${INPUT_TAG}"
TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
elif [ "${{ github.event_name }}" = "push" ]; then
# Push to main (nightly): tag with nightly and commit SHA
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly"
TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly-${SHORT_SHA}"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.tag }}"
fi
echo "tags=${TAGS}" >> "$GITHUB_OUTPUT"
echo "Tags: ${TAGS}"
- name: Build and push Docker image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 #v6
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 #v7.0.0
with:
context: .
file: ./donut-sync/Dockerfile
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Install Nix
uses: cachix/install-nix-action@a6f7623b2e2401f485f1eead77ced45bd99b09b0 #v31
+121 -24
View File
@@ -21,15 +21,18 @@ jobs:
if: github.repository == 'zhom/donutbrowser' && github.event_name == 'issues'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Check if first-time contributor
id: check-first-time
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
run: |
ISSUE_COUNT=$(gh api "/repos/${{ github.repository }}/issues" \
--jq "map(select(.user.login == \"$ISSUE_AUTHOR\" and .number != ${{ github.event.issue.number }})) | length" \
--paginate || echo "0")
ISSUE_COUNT=$(gh api "/repos/${{ github.repository }}/issues?state=all&creator=$ISSUE_AUTHOR&per_page=100" \
--jq "[.[] | select(.number != ${{ github.event.issue.number }}) ] | length" \
|| echo "0")
if [ "$ISSUE_COUNT" = "0" ]; then
echo "is_first_time=true" >> $GITHUB_OUTPUT
@@ -37,6 +40,67 @@ jobs:
echo "is_first_time=false" >> $GITHUB_OUTPUT
fi
- name: Build repo context and find related files
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
# Read project guidelines (contains repo structure)
cp CLAUDE.md /tmp/repo-context.txt
printf '%s' "$ISSUE_TITLE" > /tmp/issue-title.txt
printf '%s' "${ISSUE_BODY:-}" > /tmp/issue-body.txt
# List all source files for the AI to pick from
find . -type f \( -name "*.rs" -o -name "*.ts" -o -name "*.tsx" \) \
! -path "*/node_modules/*" ! -path "*/target/*" ! -path "*/.next/*" ! -path "*/dist/*" \
! -path "*/.git/*" ! -path "*/gen/*" ! -path "*/data/*" \
| sed 's|^\./||' | sort > /tmp/all-source-files.txt
- name: Select relevant files with AI
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
run: |
PAYLOAD=$(jq -n \
--rawfile title /tmp/issue-title.txt \
--rawfile body /tmp/issue-body.txt \
--rawfile files /tmp/all-source-files.txt \
'{
model: "anthropic/claude-opus-4.6",
messages: [
{
role: "system",
content: "You are a file selector for Donut Browser (Tauri + Next.js + Rust anti-detect browser). Given an issue and a list of source files, output ONLY the 10 most likely relevant file paths, one per line. No explanations, no numbering, just paths."
},
{
role: "user",
content: ("Issue: " + $title + "\n\n" + $body + "\n\nFiles:\n" + $files)
}
]
}')
RESPONSE=$(curl -fsSL https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
jq -r '.choices[0].message.content // empty' <<< "$RESPONSE" > /tmp/selected-files.txt
# Read the selected files in full (skip binary files)
echo "" > /tmp/file-contents.txt
while IFS= read -r filepath; do
filepath=$(echo "$filepath" | xargs)
[ -z "$filepath" ] && continue
if [ -f "$filepath" ] && file --mime "$filepath" | grep -q "text/"; then
echo "=== $filepath ===" >> /tmp/file-contents.txt
cat "$filepath" >> /tmp/file-contents.txt
echo "" >> /tmp/file-contents.txt
fi
done < /tmp/selected-files.txt
# Cap total context at 100KB
head -c 100000 /tmp/file-contents.txt > /tmp/file-context.txt
- name: Analyze issue with AI
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
@@ -50,25 +114,24 @@ jobs:
GREETING='This is a first-time contributor. Start your comment with: "Thanks for opening your first issue!"'
fi
# Write all user content to files to avoid shell escaping issues
printf '%s' "$ISSUE_TITLE" > /tmp/issue-title.txt
printf '%s' "${ISSUE_BODY:-}" > /tmp/issue-body.txt
printf '%s' "$ISSUE_AUTHOR" > /tmp/issue-author.txt
printf '%s' "$GREETING" > /tmp/greeting.txt
# Build the JSON payload entirely in jq — never interpolate user content in shell
PAYLOAD=$(jq -n \
--rawfile title /tmp/issue-title.txt \
--rawfile body /tmp/issue-body.txt \
--rawfile author /tmp/issue-author.txt \
--rawfile greeting /tmp/greeting.txt \
--rawfile repo_context /tmp/repo-context.txt \
--rawfile context /tmp/file-context.txt \
'{
model: "z-ai/glm-5",
max_tokens: 1024,
model: "anthropic/claude-opus-4.6",
messages: [
{
role: "system",
content: "You are a triage bot for Donut Browser (open-source anti-detect browser, Tauri + Next.js + Rust).\n\nAnalyze the issue and produce a single concise comment. Format:\n\n1. One sentence acknowledging what the user wants.\n2. A short **Action items** list - what specific info is missing or what the user should do next. Only include items that are actually missing. If the issue is complete, say so and skip this section.\n3. Suggest a label at the very end of your response on its own line in the exact format: Label: bug OR Label: enhancement\n\nRules:\n- Be brief. No filler, no generic tips, no templates.\n- If it is a bug report, check for: reproduction steps, OS/version, error messages. Only ask for what is actually missing.\n- If it is a feature request, check for: clear description of desired behavior, use case. Only ask for what is actually missing.\n- If the issue already has everything needed, just acknowledge it.\n- Never exceed 6 items total."
content: ("You are a triage bot for Donut Browser, an open-source anti-detect browser (Tauri desktop app: Rust backend + Next.js frontend).\n\nProject guidelines and structure:\n" + $repo_context + "\n\nYou have access to relevant source files for context.\n\nAnalyze the issue and produce a single comment. Your job is to collect missing information needed to diagnose the issue, NOT to guess the cause.\n\nFormat:\n\n1. One sentence acknowledging the issue.\n2. **Missing information** - Ask specific questions about what is missing from the report. Focus on reproducing the issue. Do NOT speculate about root causes or mention internal code/files — you will almost certainly be wrong without logs. Instead, ask for:\n - Exact steps to reproduce (if not provided)\n - Expected vs actual behavior (if unclear)\n - Error messages or screenshots (if not provided)\n - OS and app version (if not provided)\n - For bug reports: if logs are needed, tell the user EXACTLY how to get them:\n - macOS app logs: `~/Library/Logs/Donut Browser/`\n - Linux app logs: `~/.local/share/DonutBrowser/logs/`\n - Windows app logs: `%APPDATA%\\DonutBrowser\\logs\\`\n - Sync server logs: `docker logs <container>` or check the server console\n - Provide a ready-to-run shell command when possible.\n - For self-hosted sync issues: check if the user is using the latest Docker image (`docker pull donutbrowser/donut-sync:latest`).\n - Only ask for information that is actually missing. If the issue is already detailed, just acknowledge it.\n3. Suggest a label: `Label: bug` or `Label: enhancement` on its own line.\n\nRules:\n- Do NOT include a \"Possible cause\" section. Do not speculate about what code might be causing the issue.\n- Be brief and focused on collecting actionable information from the reporter.\n- If the issue already has everything needed (steps to reproduce, logs, version, OS), just acknowledge it.\n- Never exceed 15 lines.")
},
{
role: "user",
@@ -76,7 +139,8 @@ jobs:
(if ($greeting | length) > 0 then $greeting + "\n\n" else "" end) +
"Analyze this issue:\n\nTitle: " + $title +
"\nAuthor: " + $author +
"\n\nBody:\n" + $body
"\n\nBody:\n" + $body +
"\n\nRelevant source files:\n" + $context
)
}
]
@@ -87,7 +151,6 @@ jobs:
-H "Content-Type: application/json" \
-d "$PAYLOAD")
# Extract the comment using jq — never parse AI output in shell
jq -r '.choices[0].message.content // empty' <<< "$RESPONSE" > /tmp/ai-comment.txt
if [ ! -s /tmp/ai-comment.txt ]; then
@@ -102,7 +165,6 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
# Extract and strip the label line before posting
LABEL=$(grep -oP '^Label:\s*\K.*' /tmp/ai-comment.txt | tail -1 | tr '[:upper:]' '[:lower:]' | xargs)
sed -i '/^Label:/d' /tmp/ai-comment.txt
@@ -118,6 +180,9 @@ jobs:
if: github.repository == 'zhom/donutbrowser' && github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Check if first-time contributor
id: check-first-time
env:
@@ -134,6 +199,40 @@ jobs:
echo "is_first_time=false" >> $GITHUB_OUTPUT
fi
- name: Gather PR context
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Get changed files list
gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" \
--jq '.[] | "- \(.filename) (\(.status)) +\(.additions)/-\(.deletions)"' \
> /tmp/pr-files.txt
# Get the actual diff
gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" \
--header "Accept: application/vnd.github.diff" \
> /tmp/pr-diff-full.txt 2>/dev/null || true
head -c 20000 /tmp/pr-diff-full.txt > /tmp/pr-diff.txt
# Get CONTRIBUTING.md and README.md for context
cat CONTRIBUTING.md > /tmp/contributing.txt 2>/dev/null || echo "Not found" > /tmp/contributing.txt
head -50 README.md > /tmp/readme.txt 2>/dev/null || echo "Not found" > /tmp/readme.txt
# Read project guidelines (contains repo structure)
cp CLAUDE.md /tmp/repo-context.txt
# Read full contents of all changed files (skip binary)
echo "" > /tmp/related-file-contents.txt
gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --jq '.[].filename' | while IFS= read -r filepath; do
if [ -f "$filepath" ] && file --mime "$filepath" | grep -q "text/"; then
echo "=== $filepath (full file) ===" >> /tmp/related-file-contents.txt
cat "$filepath" >> /tmp/related-file-contents.txt
echo "" >> /tmp/related-file-contents.txt
fi
done
head -c 100000 /tmp/related-file-contents.txt > /tmp/pr-file-context.txt
- name: Analyze PR with AI
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -151,7 +250,6 @@ jobs:
GREETING='This is a first-time contributor. Start your comment with: "Thanks for your first PR!"'
fi
# Write all user content to files to avoid shell escaping issues
printf '%s' "$PR_TITLE" > /tmp/pr-title.txt
printf '%s' "${PR_BODY:-}" > /tmp/pr-body.txt
printf '%s' "$PR_AUTHOR" > /tmp/pr-author.txt
@@ -159,11 +257,6 @@ jobs:
printf '%s' "$PR_HEAD" > /tmp/pr-head.txt
printf '%s' "$GREETING" > /tmp/greeting.txt
gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" \
--jq '.[] | "- \(.filename) (\(.status)) +\(.additions)/-\(.deletions)"' \
> /tmp/pr-files.txt
# Build the JSON payload entirely in jq — never interpolate user content in shell
PAYLOAD=$(jq -n \
--rawfile title /tmp/pr-title.txt \
--rawfile body /tmp/pr-body.txt \
@@ -171,14 +264,17 @@ jobs:
--rawfile base /tmp/pr-base.txt \
--rawfile head /tmp/pr-head.txt \
--rawfile files /tmp/pr-files.txt \
--rawfile diff /tmp/pr-diff.txt \
--rawfile greeting /tmp/greeting.txt \
--rawfile repo_context /tmp/repo-context.txt \
--rawfile contributing /tmp/contributing.txt \
--rawfile file_context /tmp/pr-file-context.txt \
'{
model: "z-ai/glm-5",
max_tokens: 1024,
model: "anthropic/claude-opus-4.6",
messages: [
{
role: "system",
content: "You are a review bot for Donut Browser (open-source anti-detect browser, Tauri + Next.js + Rust).\n\nReview this PR and produce a single concise comment. Format:\n\n1. One sentence summarizing what this PR does.\n2. **Action items** - only list things that actually need to be fixed or addressed. If the PR looks good, say so and skip this section.\n\nRules:\n- Be brief. No filler, no praise padding.\n- Focus on: bugs, security issues, missing edge cases, breaking changes.\n- If the PR touches UI text or adds new strings, remind to update translation files in src/i18n/locales/.\n- If the PR modifies Tauri commands, remind to check the unused-commands test.\n- Do not nitpick style or formatting - the project has automated linting.\n- Never exceed 8 lines total."
content: ("You are a code review bot for Donut Browser, an open-source anti-detect browser (Tauri desktop app: Rust backend + Next.js frontend).\n\nProject guidelines and structure:\n" + $repo_context + "\n\nContributing guidelines:\n" + $contributing + "\n\nYou have access to the full changed files and the diff. Use them to give a substantive review.\n\nReview this PR and produce a single comment. Format:\n\n1. One sentence summarizing what this PR does and whether the approach is sound.\n2. **Code review** - Specific observations about the actual code changes. Mention file names and what you see in the diff. Look for:\n - Bugs or logic errors in the changed code\n - Security issues (SQL injection, path traversal, XSS, command injection)\n - Missing error handling or edge cases\n - Breaking changes to existing APIs or behavior\n - If UI text was added/changed, check if all 7 translation files (en, es, fr, ja, pt, ru, zh) in src/i18n/locales/ were updated\n - If Tauri commands were added/removed, the unused-commands test in lib.rs needs updating\n3. **Suggestions** - Concrete improvements if any. Skip if the PR looks good.\n\nRules:\n- Be substantive. Review the actual diff, not just the description.\n- Do NOT nitpick formatting or style — the project has automated linting (biome + clippy + rustfmt).\n- Do NOT just summarize the PR description back to the user — they wrote it, they know what it says.\n- If the PR is good, say so briefly.\n- Never exceed 20 lines.")
},
{
role: "user",
@@ -188,7 +284,9 @@ jobs:
"\nAuthor: " + $author +
"\nBase: " + $base + " <- Head: " + $head +
"\n\nDescription:\n" + $body +
"\n\nChanged files:\n" + $files
"\n\nChanged files:\n" + $files +
"\n\nDiff:\n" + $diff +
"\n\nFull file contents:\n" + $file_context
)
}
]
@@ -199,7 +297,6 @@ jobs:
-H "Content-Type: application/json" \
-d "$PAYLOAD")
# Extract the comment using jq — never parse AI output in shell
jq -r '.choices[0].message.content // empty' <<< "$RESPONSE" > /tmp/ai-comment.txt
if [ ! -s /tmp/ai-comment.txt ]; then
@@ -227,10 +324,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Run opencode
uses: anomalyco/opencode/github@4ee426ba549131c4903a71dfb6259200467aca81 #v1.2.27
uses: anomalyco/opencode/github@54443bfb7e090ec3130dc972e689a3e5cc55a7f9 #v1.3.3
env:
ZHIPU_API_KEY: ${{ secrets.ZHIPU_API_KEY }}
TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
run: git config --global core.autocrlf false
- name: Checkout repository code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 #v4.4.0
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
run: git config --global core.autocrlf false
- name: Checkout repository code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 #v4.4.0
+2 -2
View File
@@ -46,7 +46,7 @@ jobs:
scan-scheduled:
name: Scheduled Security Scan
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@c5996e0193a3df57d695c1b8a1dec2a4c62e8730" # v2.3.3
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@c51854704019a247608d928f370c98740469d4b5" # v2.3.5
with:
scan-args: |-
-r
@@ -58,7 +58,7 @@ jobs:
scan-pr:
name: PR Security Scan
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@c5996e0193a3df57d695c1b8a1dec2a4c62e8730" # v2.3.3
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@c51854704019a247608d928f370c98740469d4b5" # v2.3.5
with:
scan-args: |-
-r
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
security-scan:
name: Security Vulnerability Scan
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@c5996e0193a3df57d695c1b8a1dec2a4c62e8730" # v2.3.3
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@c51854704019a247608d928f370c98740469d4b5" # v2.3.5
with:
scan-args: |-
-r
@@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
fetch-depth: 0
+146 -29
View File
@@ -20,7 +20,7 @@ jobs:
security-scan:
if: github.repository == 'zhom/donutbrowser'
name: Security Vulnerability Scan
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@c5996e0193a3df57d695c1b8a1dec2a4c62e8730" # v2.3.3
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@c51854704019a247608d928f370c98740469d4b5" # v2.3.5
with:
scan-args: |-
-r
@@ -105,7 +105,7 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 #v4.4.0
@@ -225,6 +225,44 @@ jobs:
prerelease: false
args: ${{ matrix.args }}
- name: Create portable Windows ZIP
if: matrix.platform == 'windows-latest'
shell: bash
env:
TAG: ${{ github.ref_name }}
run: |
VERSION="${TAG#v}"
PORTABLE_DIR="Donut-Portable"
mkdir -p "$PORTABLE_DIR"
# Copy main executable
cp "src-tauri/target/${{ matrix.target }}/release/Donut.exe" "$PORTABLE_DIR/"
# Copy sidecar binaries
cp "src-tauri/target/${{ matrix.target }}/release/donut-proxy.exe" "$PORTABLE_DIR/"
cp "src-tauri/target/${{ matrix.target }}/release/donut-daemon.exe" "$PORTABLE_DIR/"
# Copy WebView2Loader if present
if [ -f "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" ]; then
cp "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" "$PORTABLE_DIR/"
fi
# Create .portable marker
touch "$PORTABLE_DIR/.portable"
# Create ZIP
7z a "Donut_${VERSION}_x64-portable.zip" "$PORTABLE_DIR"
- name: Upload portable ZIP to release
if: matrix.platform == 'windows-latest'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
VERSION="${TAG#v}"
gh release upload "$TAG" "Donut_${VERSION}_x64-portable.zip" --clobber
- name: Clean up Apple certificate
if: matrix.platform == 'macos-latest' && always()
run: |
@@ -237,8 +275,9 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
ref: main
fetch-depth: 0
@@ -345,7 +384,7 @@ jobs:
### Windows
[Download Windows Installer (x64)](${BASE}/Donut_${VERSION}_x64-setup.exe)
[Download Windows Installer (x64)](${BASE}/Donut_${VERSION}_x64-setup.exe) · [Portable (x64)](${BASE}/Donut_${VERSION}_x64-portable.zip)
### Linux
@@ -368,16 +407,28 @@ jobs:
/<!-- install-links-end -->/!d
}' README.md
- name: Commit release docs
- name: Create release docs PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
VERSION="${TAG#v}"
BRANCH="docs/release-${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
git add CHANGELOG.md README.md
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "docs: update CHANGELOG.md and README.md for ${{ github.ref_name }} [skip ci]"
git push origin main
git commit -m "docs: update CHANGELOG.md and README.md for ${TAG} [skip ci]"
git push origin "$BRANCH"
gh pr create \
--title "docs: release notes for ${TAG}" \
--body "Automated update of CHANGELOG.md and README.md download links for ${TAG}." \
--base main \
--head "$BRANCH"
gh pr merge "$BRANCH" --squash --admin
fi
- name: Update release notes
@@ -389,26 +440,98 @@ jobs:
notify-discord:
if: github.repository == 'zhom/donutbrowser'
needs: [release]
needs: [release, changelog]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
ref: main
fetch-depth: 0
- name: Generate changelog summary
env:
TAG: ${{ github.ref_name }}
run: |
PREV_TAG=$(git tag --sort=-version:refname \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| grep -v "^${TAG}$" \
| head -n 1)
if [ -z "$PREV_TAG" ]; then
PREV_TAG=$(git rev-list --max-parents=0 HEAD)
fi
strip_prefix() { echo "$1" | sed -E 's/^[a-z]+(\([^)]*\))?: //'; }
CHANGES=""
while IFS= read -r msg; do
[ -z "$msg" ] && continue
case "$msg" in
feat\(*\):*|feat:*) CHANGES="${CHANGES}• $(strip_prefix "$msg")\n" ;;
fix\(*\):*|fix:*) CHANGES="${CHANGES}• $(strip_prefix "$msg")\n" ;;
refactor\(*\):*|refactor:*) CHANGES="${CHANGES}• $(strip_prefix "$msg")\n" ;;
perf\(*\):*|perf:*) CHANGES="${CHANGES}• $(strip_prefix "$msg")\n" ;;
esac
done < <(git log --pretty=format:"%s" "${PREV_TAG}..${TAG}" --no-merges)
# Truncate to fit Discord embed (max 4096 chars)
if [ ${#CHANGES} -gt 3900 ]; then
CHANGES="${CHANGES:0:3900}\n..."
fi
if [ -z "$CHANGES" ]; then
CHANGES="See the full changelog on GitHub."
fi
printf '%s' "$CHANGES" > /tmp/discord-changes.txt
- name: Send Discord notification
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_STABLE_WEBHOOK_URL }}
TAG: ${{ github.ref_name }}
run: |
VERSION="${GITHUB_REF_NAME}"
VERSION="${TAG}"
RELEASE_URL="https://github.com/${GITHUB_REPOSITORY}/releases/tag/${VERSION}"
CHANGES=$(cat /tmp/discord-changes.txt)
curl -fsSL -H "Content-Type: application/json" \
-d "{
\"embeds\": [{
\"title\": \"Donut Browser ${VERSION} Released\",
\"url\": \"${RELEASE_URL}\",
\"description\": \"A new stable release of Donut Browser is available.\",
\"color\": 5814783
# Build JSON with jq to handle escaping
PAYLOAD=$(jq -n \
--arg title "Donut Browser ${VERSION} Released" \
--arg url "$RELEASE_URL" \
--arg changes "$CHANGES" \
--arg dl_mac_arm "https://github.com/'"${GITHUB_REPOSITORY}"'/releases/download/'"${VERSION}"'/Donut_'"${VERSION#v}"'_aarch64.dmg" \
--arg dl_mac_intel "https://github.com/'"${GITHUB_REPOSITORY}"'/releases/download/'"${VERSION}"'/Donut_'"${VERSION#v}"'_x64.dmg" \
--arg dl_win "https://github.com/'"${GITHUB_REPOSITORY}"'/releases/download/'"${VERSION}"'/Donut_'"${VERSION#v}"'_x64-setup.exe" \
--arg dl_linux "https://github.com/'"${GITHUB_REPOSITORY}"'/releases/download/'"${VERSION}"'/Donut_'"${VERSION#v}"'_amd64.AppImage" \
'{
embeds: [{
title: $title,
url: $url,
description: $changes,
color: 5814783,
fields: [
{ name: "Download", value: ("[macOS (Apple Silicon)](" + $dl_mac_arm + ") · [macOS (Intel)](" + $dl_mac_intel + ")\n[Windows x64](" + $dl_win + ") · [Linux x64](" + $dl_linux + ")"), inline: false }
],
footer: { text: "donutbrowser.com" }
}]
}" \
"$DISCORD_WEBHOOK_URL"
}')
curl -fsSL -H "Content-Type: application/json" -d "$PAYLOAD" "$DISCORD_WEBHOOK_URL"
deploy-website:
if: github.repository == 'zhom/donutbrowser'
needs: [release]
runs-on: ubuntu-latest
steps:
- name: Trigger Cloudflare Pages deployment
run: curl -fsSL -X POST "${{ secrets.CLOUDFLARE_WEB_DEPLOYMENT_HOOK }}"
docker:
if: github.repository == 'zhom/donutbrowser'
needs: [release]
uses: ./.github/workflows/docker-sync.yml
with:
tag: ${{ github.ref_name }}
secrets: inherit
update-flake:
if: github.repository == 'zhom/donutbrowser'
@@ -418,7 +541,7 @@ jobs:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
ref: main
@@ -435,19 +558,13 @@ jobs:
echo "Downloading x86_64 AppImage..."
curl -fsSL -o /tmp/amd64.AppImage "$AMD64_URL" || { echo "x86_64 AppImage not found"; exit 1; }
AMD64_HASH=$(nix-hash --type sha256 --base32 --flat /tmp/amd64.AppImage 2>/dev/null || sha256sum /tmp/amd64.AppImage | awk '{print $1}')
echo "Downloading aarch64 AppImage..."
curl -fsSL -o /tmp/aarch64.AppImage "$AARCH64_URL" || { echo "aarch64 AppImage not found"; exit 1; }
AARCH64_HASH=$(nix-hash --type sha256 --base32 --flat /tmp/aarch64.AppImage 2>/dev/null || sha256sum /tmp/aarch64.AppImage | awk '{print $1}')
# Convert to SRI format (sha256-<base64>) if we got hex
if echo "$AMD64_HASH" | grep -qE '^[0-9a-f]{64}$'; then
AMD64_HASH="sha256-$(echo "$AMD64_HASH" | xxd -r -p | base64 | tr -d '\n')"
fi
if echo "$AARCH64_HASH" | grep -qE '^[0-9a-f]{64}$'; then
AARCH64_HASH="sha256-$(echo "$AARCH64_HASH" | xxd -r -p | base64 | tr -d '\n')"
fi
# Compute SRI hashes (sha256-<base64>)
AMD64_HASH="sha256-$(sha256sum /tmp/amd64.AppImage | awk '{print $1}' | xxd -r -p | base64 | tr -d '\n')"
AARCH64_HASH="sha256-$(sha256sum /tmp/aarch64.AppImage | awk '{print $1}' | xxd -r -p | base64 | tr -d '\n')"
echo "AMD64_HASH=${AMD64_HASH}" >> "$GITHUB_ENV"
echo "AARCH64_HASH=${AARCH64_HASH}" >> "$GITHUB_ENV"
@@ -493,4 +610,4 @@ jobs:
--body "Automated update of flake.nix with new AppImage hashes for v${VERSION}." \
--base main \
--head "$BRANCH"
gh pr merge "$BRANCH" --auto --squash
gh pr merge "$BRANCH" --squash --admin
+58 -12
View File
@@ -19,7 +19,7 @@ jobs:
security-scan:
if: github.repository == 'zhom/donutbrowser'
name: Security Vulnerability Scan
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@c5996e0193a3df57d695c1b8a1dec2a4c62e8730" # v2.3.3
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@c51854704019a247608d928f370c98740469d4b5" # v2.3.5
with:
scan-args: |-
-r
@@ -104,7 +104,7 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 #v4.4.0
@@ -235,6 +235,34 @@ jobs:
prerelease: true
args: ${{ matrix.args }}
- name: Create portable Windows ZIP
if: matrix.platform == 'windows-latest'
shell: bash
run: |
PORTABLE_DIR="Donut-Portable"
mkdir -p "$PORTABLE_DIR"
cp "src-tauri/target/${{ matrix.target }}/release/Donut.exe" "$PORTABLE_DIR/"
cp "src-tauri/target/${{ matrix.target }}/release/donut-proxy.exe" "$PORTABLE_DIR/"
cp "src-tauri/target/${{ matrix.target }}/release/donut-daemon.exe" "$PORTABLE_DIR/"
if [ -f "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" ]; then
cp "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" "$PORTABLE_DIR/"
fi
touch "$PORTABLE_DIR/.portable"
7z a "Donut_x64-portable.zip" "$PORTABLE_DIR"
- name: Upload portable ZIP to release
if: matrix.platform == 'windows-latest'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NIGHTLY_TAG: "nightly-${{ steps.timestamp.outputs.timestamp }}"
run: |
gh release upload "$NIGHTLY_TAG" "Donut_x64-portable.zip" --clobber
- name: Clean up Apple certificate
if: matrix.platform == 'macos-latest' && always()
run: |
@@ -248,7 +276,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Generate nightly tag
id: tag
@@ -345,6 +373,14 @@ jobs:
--notes-file /tmp/nightly-notes.md \
--prerelease
deploy-website:
if: github.repository == 'zhom/donutbrowser'
needs: [update-nightly-release]
runs-on: ubuntu-latest
steps:
- name: Trigger Cloudflare Pages deployment
run: curl -fsSL -X POST "${{ secrets.CLOUDFLARE_WEB_DEPLOYMENT_HOOK }}"
notify-discord:
if: github.repository == 'zhom/donutbrowser'
needs: [update-nightly-release]
@@ -356,14 +392,24 @@ jobs:
run: |
COMMIT_SHORT=$(echo "${GITHUB_SHA}" | cut -c1-7)
RELEASE_URL="https://github.com/${GITHUB_REPOSITORY}/releases/tag/nightly"
COMMIT_URL="https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}"
curl -fsSL -H "Content-Type: application/json" \
-d "{
\"embeds\": [{
\"title\": \"Donut Browser Nightly Updated\",
\"url\": \"${RELEASE_URL}\",
\"description\": \"A new nightly build is available (${COMMIT_SHORT}).\",
\"color\": 16752128
PAYLOAD=$(jq -n \
--arg title "Donut Browser Nightly (${COMMIT_SHORT})" \
--arg url "$RELEASE_URL" \
--arg commit_url "$COMMIT_URL" \
--arg commit_short "$COMMIT_SHORT" \
'{
embeds: [{
title: $title,
url: $url,
color: 16752128,
fields: [
{ name: "Commit", value: ("[" + $commit_short + "](" + $commit_url + ")"), inline: true },
{ name: "Download", value: ("[Nightly Release](" + $url + ")"), inline: true }
],
footer: { text: "donutbrowser.com" }
}]
}" \
"$DISCORD_WEBHOOK_URL"
}')
curl -fsSL -H "Content-Type: application/json" -d "$PAYLOAD" "$DISCORD_WEBHOOK_URL"
+1 -1
View File
@@ -21,6 +21,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Spell Check Repo
uses: crate-ci/typos@631208b7aac2daa8b707f55e7331f9112b0e062d #v1.44.0
+4 -2
View File
@@ -16,7 +16,9 @@ jobs:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue has been inactive for 60 days. Please respond to keep it open."
stale-pr-message: "This pull request has been inactive for 60 days. Please respond to keep it open."
stale-issue-message: "This issue has been inactive for 30 days. Please respond to keep it open."
stale-pr-message: "This pull request has been inactive for 30 days. Please respond to keep it open."
stale-issue-label: "stale"
stale-pr-label: "stale"
days-before-stale: 30
days-before-close: 7
+2 -2
View File
@@ -32,7 +32,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v6.0.2
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 #v4.4.0
@@ -73,7 +73,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v6.0.2
- name: Start MinIO
run: |
+39
View File
@@ -37,6 +37,7 @@
"codesign",
"codesigning",
"commitish",
"coreutils",
"Crashpad",
"CTYPE",
"daijro",
@@ -58,6 +59,7 @@
"doctest",
"doesn",
"domcontentloaded",
"dont",
"donutbrowser",
"doorhanger",
"dpkg",
@@ -70,21 +72,31 @@
"esbuild",
"etree",
"fetchurl",
"findutils",
"firstrun",
"flate",
"fontconfig",
"freetype",
"fribidi",
"frontmost",
"fsprogs",
"geoip",
"getcwd",
"gettimezone",
"gifs",
"globset",
"gnugrep",
"gnumake",
"gnused",
"GOPATH",
"gsettings",
"harfbuzz",
"healthreport",
"hiddenimports",
"hkcu",
"hooksconfig",
"hookspath",
"hostable",
"Hoverable",
"icns",
"idlelib",
@@ -110,13 +122,33 @@
"libayatana",
"libc",
"libcairo",
"libdrm",
"libfuse",
"libgbm",
"libgdk",
"libglib",
"libglvnd",
"libgpg",
"libpango",
"librsvg",
"libsoup",
"libwebkit",
"libx",
"libxcb",
"libxcomposite",
"libxcursor",
"libxdamage",
"libxdo",
"libxext",
"libxfixes",
"libxi",
"libxinerama",
"libxkbcommon",
"libxrandr",
"libxrender",
"libxscrnsaver",
"libxshmfence",
"libxtst",
"localtime",
"lpdw",
"lxml",
@@ -134,6 +166,8 @@
"msys",
"muda",
"mypy",
"nixos",
"nixpkgs",
"noarchive",
"nobrowse",
"noconfirm",
@@ -143,9 +177,11 @@
"nomount",
"norestart",
"NSIS",
"nspr",
"ntfs",
"ntlm",
"numpy",
"numtide",
"objc",
"oneshot",
"opencode",
@@ -156,6 +192,7 @@
"oscpu",
"outpath",
"OVPN",
"pango",
"passout",
"patchelf",
"pathex",
@@ -163,12 +200,14 @@
"peerconnection",
"PHANDLER",
"pids",
"pipefail",
"pixbuf",
"pkexec",
"pkgs",
"pkill",
"plasmohq",
"platformdirs",
"pname",
"prefs",
"presign",
"PRIO",
+49
View File
@@ -1,5 +1,53 @@
# Project Guidelines
> **NOTE**: CLAUDE.md is a symlink to AGENTS.md — editing either file updates both.
> After significant changes (new modules, renamed files, new directories), re-evaluate the Repository Structure below and update it if needed.
## Repository Structure
```
donutbrowser/
├── src/ # Next.js frontend
│ ├── app/ # App router (page.tsx, layout.tsx)
│ ├── components/ # 50+ React components (dialogs, tables, UI)
│ ├── hooks/ # Event-driven React hooks
│ ├── i18n/locales/ # Translations (en, es, fr, ja, pt, ru, zh)
│ ├── lib/ # Utilities (themes, toast, browser-utils)
│ └── types.ts # Shared TypeScript interfaces
├── src-tauri/ # Rust backend (Tauri)
│ ├── src/
│ │ ├── lib.rs # Tauri command registration (100+ commands)
│ │ ├── browser_runner.rs # Profile launch/kill orchestration
│ │ ├── browser.rs # Browser trait & launch logic
│ │ ├── profile/ # Profile CRUD (manager.rs, types.rs)
│ │ ├── proxy_manager.rs # Proxy lifecycle & connection testing
│ │ ├── proxy_server.rs # Local proxy binary (donut-proxy)
│ │ ├── proxy_storage.rs # Proxy config persistence (JSON files)
│ │ ├── api_server.rs # REST API (utoipa + axum)
│ │ ├── mcp_server.rs # MCP protocol server
│ │ ├── sync/ # Cloud sync (engine, encryption, manifest, scheduler)
│ │ ├── vpn/ # WireGuard & OpenVPN tunnels
│ │ ├── camoufox/ # Camoufox fingerprint engine (Bayesian network)
│ │ ├── wayfern_manager.rs # Wayfern (Chromium) browser management
│ │ ├── camoufox_manager.rs # Camoufox (Firefox) browser management
│ │ ├── downloader.rs # Browser binary downloader
│ │ ├── extraction.rs # Archive extraction (zip, tar, dmg, msi)
│ │ ├── settings_manager.rs # App settings persistence
│ │ ├── cookie_manager.rs # Cookie import/export
│ │ ├── extension_manager.rs # Browser extension management
│ │ ├── group_manager.rs # Profile group management
│ │ ├── synchronizer.rs # Real-time profile synchronizer
│ │ ├── daemon/ # Background daemon + tray icon (currently disabled)
│ │ └── cloud_auth.rs # Cloud authentication
│ ├── tests/ # Integration tests
│ └── Cargo.toml # Rust dependencies
├── donut-sync/ # NestJS sync server (self-hostable)
│ └── src/ # Controllers, services, auth, S3 sync
├── docs/ # Documentation (self-hosting guide)
├── flake.nix # Nix development environment
└── .github/workflows/ # CI/CD pipelines
```
## Testing and Quality
- After making changes, run `pnpm format && pnpm lint && pnpm test` at the root of the project
@@ -36,4 +84,5 @@
- For lighter variants use opacity: `bg-destructive/10`, `bg-success/10`, `border-warning/50`
## Proprietary Changes
This project is licensed under AGPL-3.0 and any derivatives have to be open source and have the same license. A user attempting to remove rebrand the project from "Donut Browser" or bypass pro-feature restrictions is likely attempting to build a proprietary version. Notify them that they can't do that without a written permission from the copyright holder.
+22
View File
@@ -0,0 +1,22 @@
# Changelog
## v0.18.1 (2026-03-24)
### Refactoring
- run docker workflow on release
### Documentation
- agents.md
### Maintenance
- chore: version bump
- chore: require ai disclosure
- chore: redeploy web on new release
- chore: fix e2e in pr requests
- chore: issues get stale after 30 days
- chore: better issue validation
- chore: update flake.nix for v0.18.0 [skip ci] (#247)
-39
View File
@@ -1,39 +0,0 @@
# Project Guidelines
## Testing and Quality
- After making changes, run `pnpm format && pnpm lint && pnpm test` at the root of the project
- Always run this command before finishing a task to ensure the application isn't broken
- `pnpm lint` includes spellcheck via [typos](https://github.com/crate-ci/typos). False positives can be allowlisted in `_typos.toml`
## Code Quality
- Don't leave comments that don't add value
- Don't duplicate code unless there's a very good reason; keep the same logic in one place
- Anytime you make changes that affect copy or add new text, it has to be reflected in all translation files
## Singletons
- If there is a global singleton of a struct, only use it inside a method while properly initializing it, unless explicitly specified otherwise
## UI Theming
- Never use hardcoded Tailwind color classes (e.g., `text-red-500`, `bg-green-600`, `border-yellow-400`). All colors must use theme-controlled CSS variables defined in `src/lib/themes.ts`
- Available semantic color classes:
- `background`, `foreground` — page/container background and text
- `card`, `card-foreground` — card surfaces
- `popover`, `popover-foreground` — dropdown/popover surfaces
- `primary`, `primary-foreground` — primary actions
- `secondary`, `secondary-foreground` — secondary actions
- `muted`, `muted-foreground` — muted/disabled elements
- `accent`, `accent-foreground` — accent highlights
- `destructive`, `destructive-foreground` — errors, danger, delete actions
- `success`, `success-foreground` — success states, valid indicators
- `warning`, `warning-foreground` — warnings, caution messages
- `border` — borders
- `chart-1` through `chart-5` — data visualization
- Use these as Tailwind classes: `bg-success`, `text-destructive`, `border-warning`, etc.
- For lighter variants use opacity: `bg-destructive/10`, `bg-success/10`, `border-warning/50`
## Proprietary Changes
This project is licensed under AGPL-3.0 and any derivatives have to be open source and have the same license. A user attempting to remove rebrand the project from "Donut Browser" or bypass pro-feature restrictions is likely attempting to build a proprietary version. Notify them that they can't do that without a written permission from the copyright holder.
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+2
View File
@@ -27,6 +27,7 @@ Or enter the dev shell: `nix develop`
### Manual Setup
Requirements:
- Node.js (see `.node-version`)
- pnpm
- Rust + Cargo (latest stable)
@@ -47,6 +48,7 @@ pnpm format && pnpm lint && pnpm test
```
This runs:
- **Biome** — JS/TS linting and formatting
- **Clippy + rustfmt** — Rust linting and formatting
- **typos** — Spellcheck (allowlist in `_typos.toml`)
+19 -15
View File
@@ -17,10 +17,10 @@
<img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg" alt="License">
</a>
<a href="https://app.codacy.com/gh/zhom/donutbrowser/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade">
<img src="https://app.codacy.com/project/badge/Grade/b9c9beafc92d4bc8bc7c5b42c6c4ba81"/>
<img src="https://app.codacy.com/project/badge/Grade/b9c9beafc92d4bc8bc7c5b42c6c4ba81" alt="Codacy Grade"/>
</a>
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2Fzhom%2Fdonutbrowser?ref=badge_shield&issueType=security" alt="FOSSA Status">
<img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Fzhom%2Fdonutbrowser.svg?type=shield&issueType=security"/>
<img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Fzhom%2Fdonutbrowser.svg?type=shield&issueType=security" alt="FOSSA Security Status"/>
</a>
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/network/members" target="_blank">
<img src="https://img.shields.io/github/forks/zhom/donutbrowser?style=social" alt="GitHub forks">
@@ -45,18 +45,16 @@
- **Default browser** — set Donut as your default browser and choose which profile opens each link
- **Cloud sync** — sync profiles, proxies, and groups across devices (self-hostable)
- **E2E encryption** — optional end-to-end encrypted sync with a password only you know
- **Zero telemetry** — no tracking, no fingerprinting of your device, fully auditable open source code
- **Cross-platform** — macOS, Linux, and Windows
- **Zero telemetry** — no tracking or device fingerprinting
## Install
<!-- install-links-start -->
### macOS
| | Apple Silicon | Intel |
|---|---|---|
| **DMG** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.17.6/Donut_0.17.6_aarch64.dmg) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.17.6/Donut_0.17.6_x64.dmg) |
| **DMG** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.18.1/Donut_0.18.1_aarch64.dmg) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.18.1/Donut_0.18.1_x64.dmg) |
Or install via Homebrew:
@@ -66,16 +64,15 @@ brew install --cask donut
### Windows
[Download Windows Installer (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.17.6/Donut_0.17.6_x64-setup.exe)
[Download Windows Installer (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.18.1/Donut_0.18.1_x64-setup.exe)
### Linux
| Format | x86_64 | ARM64 |
|---|---|---|
| **deb** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.17.6/Donut_0.17.6_amd64.deb) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.17.6/Donut_0.17.6_arm64.deb) |
| **rpm** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.17.6/Donut-0.17.6-1.x86_64.rpm) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.17.6/Donut-0.17.6-1.aarch64.rpm) |
| **AppImage** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.17.6/Donut_0.17.6_amd64.AppImage) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.17.6/Donut_0.17.6_aarch64.AppImage) |
| **deb** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.18.1/Donut_0.18.1_amd64.deb) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.18.1/Donut_0.18.1_arm64.deb) |
| **rpm** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.18.1/Donut-0.18.1-1.x86_64.rpm) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.18.1/Donut-0.18.1-1.aarch64.rpm) |
| **AppImage** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.18.1/Donut_0.18.1_amd64.AppImage) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.18.1/Donut_0.18.1_aarch64.AppImage) |
<!-- install-links-end -->
Or install via package manager:
@@ -118,11 +115,11 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
## Star History
<a href="https://www.star-history.com/#zhom/donutbrowser&Date">
<a href="https://www.star-history.com/?repos=zhom%2Fdonutbrowser&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date" />
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=zhom/donutbrowser&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=zhom/donutbrowser&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=zhom/donutbrowser&type=date&legend=top-left" />
</picture>
</a>
@@ -146,6 +143,13 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
<sub><b>Hassiy</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/yb403">
<img src="https://avatars.githubusercontent.com/u/87396571?v=4" width="100;" alt="yb403"/>
<br />
<sub><b>yb403</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/drunkod">
<img src="https://avatars.githubusercontent.com/u/9677471?v=4" width="100;" alt="drunkod"/>
+13 -4
View File
@@ -1,12 +1,21 @@
FROM node:22-alpine AS builder
WORKDIR /build
COPY donut-sync/package.json donut-sync/tsconfig.json donut-sync/tsconfig.build.json ./
COPY donut-sync/src/ src/
RUN npm install
RUN npm run build
RUN npm prune --omit=dev
FROM node:22-alpine
WORKDIR /app
COPY package.json .
COPY dist/ dist/
COPY node_modules/ node_modules/
COPY --from=builder /build/package.json .
COPY --from=builder /build/dist/ dist/
COPY --from=builder /build/node_modules/ node_modules/
ENV NODE_ENV=production
EXPOSE 12342
USER node
CMD ["node", "dist/main"]
+9 -11
View File
@@ -2,8 +2,6 @@
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
@@ -28,33 +26,33 @@
## Project setup
```bash
$ pnpm install
pnpm install
```
## Compile and run the project
```bash
# development
$ pnpm run start
pnpm run start
# watch mode
$ pnpm run start:dev
pnpm run start:dev
# production mode
$ pnpm run start:prod
pnpm run start:prod
```
## Run tests
```bash
# unit tests
$ pnpm run test
pnpm run test
# e2e tests
$ pnpm run test:e2e
pnpm run test:e2e
# test coverage
$ pnpm run test:cov
pnpm run test:cov
```
## Deployment
@@ -64,8 +62,8 @@ When you're ready to deploy your NestJS application to production, there are som
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ pnpm install -g @nestjs/mau
$ mau deploy
pnpm install -g @nestjs/mau
mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
+3 -3
View File
@@ -15,11 +15,11 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "NODE_OPTIONS='--experimental-vm-modules' jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1015.0",
"@aws-sdk/s3-request-presigner": "^3.1015.0",
"@aws-sdk/client-s3": "^3.1019.0",
"@aws-sdk/s3-request-presigner": "^3.1019.0",
"@nestjs/common": "^11.1.17",
"@nestjs/config": "^4.0.3",
"@nestjs/core": "^11.1.17",
+3 -3
View File
@@ -27,7 +27,7 @@ export class AuthGuard implements CanActivate {
const request = context.switchToHttp().getRequest<Request>();
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
if (!authHeader?.startsWith("Bearer ")) {
throw new UnauthorizedException(
"Missing or invalid authorization header",
);
@@ -38,7 +38,7 @@ export class AuthGuard implements CanActivate {
// Try SYNC_TOKEN first (self-hosted mode)
const expectedToken = this.configService.get<string>("SYNC_TOKEN");
if (expectedToken && token === expectedToken) {
(request as any).user = {
(request as unknown as Record<string, unknown>).user = {
mode: "self-hosted",
prefix: "",
teamPrefix: null,
@@ -55,7 +55,7 @@ export class AuthGuard implements CanActivate {
algorithms: ["RS256"],
}) as jwt.JwtPayload;
(request as any).user = {
(request as unknown as Record<string, unknown>).user = {
mode: "cloud",
prefix: decoded.prefix || `users/${decoded.sub}/`,
teamPrefix: decoded.teamPrefix || null,
+1 -1
View File
@@ -39,7 +39,7 @@ export class SyncController {
constructor(private readonly syncService: SyncService) {}
private getUserContext(req: Request): UserContext {
return (req as any).user as UserContext;
return (req as unknown as Record<string, unknown>).user as UserContext;
}
@Post("stat")
+2 -1
View File
@@ -13,10 +13,11 @@
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
"types": ["jest", "node"],
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
+5 -5
View File
@@ -94,17 +94,17 @@
pkgConfigPath = lib.makeSearchPath "lib/pkgconfig" (
pkgConfigLibs ++ map lib.getDev pkgConfigLibs
);
releaseVersion = "0.17.6";
releaseVersion = "0.18.1";
releaseAppImage =
if system == "x86_64-linux" then
pkgs.fetchurl {
url = "https://github.com/zhom/donutbrowser/releases/download/v${releaseVersion}/Donut_0.17.6_amd64.AppImage";
hash = "sha256-Bmqmb0zgaC02DKC9gcyI/St9wfwFWTEoYybOb1LXiS0=";
url = "https://github.com/zhom/donutbrowser/releases/download/v0.18.1/Donut_0.18.1_amd64.AppImage";
hash = "sha256-+twOKfcM5qdV3+415/PecdQUgTTe+9xwL7/qu4kCxQI=";
}
else if system == "aarch64-linux" then
pkgs.fetchurl {
url = "https://github.com/zhom/donutbrowser/releases/download/v${releaseVersion}/Donut_0.17.6_aarch64.AppImage";
hash = "sha256-FOV0PlYw59gY1QSoFrUcixtUcnFt27EYAzZE/KNQUrM=";
url = "https://github.com/zhom/donutbrowser/releases/download/v0.18.1/Donut_0.18.1_aarch64.AppImage";
hash = "sha256-/Fj2euuxKzP6DxcV7sqShsNr6sy7Ck1iERtYcMt2hZQ=";
}
else
null;
+7 -7
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.18.0",
"version": "0.18.1",
"type": "module",
"scripts": {
"dev": "next dev --turbopack -p 12341",
@@ -57,23 +57,23 @@
"cmdk": "^1.1.1",
"color": "^5.0.3",
"flag-icons": "^7.5.0",
"i18next": "^25.10.5",
"lucide-react": "^0.577.0",
"i18next": "^26.0.0",
"lucide-react": "^1.7.0",
"motion": "^12.38.0",
"next": "^16.2.1",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-i18next": "^16.6.2",
"react-i18next": "^17.0.0",
"react-icons": "^5.6.0",
"recharts": "3.8.0",
"recharts": "3.8.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tauri-plugin-macos-permissions-api": "^2.3.0"
},
"devDependencies": {
"@biomejs/biome": "2.4.8",
"@biomejs/biome": "2.4.9",
"@tailwindcss/postcss": "^4.2.2",
"@tauri-apps/cli": "~2.10.1",
"@types/color": "^4.2.1",
@@ -86,7 +86,7 @@
"tailwindcss": "^4.2.2",
"ts-unused-exports": "^11.0.1",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3"
"typescript": "~6.0.2"
},
"packageManager": "pnpm@10.30.1",
"lint-staged": {
+201 -188
View File
@@ -84,11 +84,11 @@ importers:
specifier: ^7.5.0
version: 7.5.0
i18next:
specifier: ^25.10.5
version: 25.10.5(typescript@5.9.3)
specifier: ^26.0.0
version: 26.0.0(typescript@6.0.2)
lucide-react:
specifier: ^0.577.0
version: 0.577.0(react@19.2.4)
specifier: ^1.7.0
version: 1.7.0(react@19.2.4)
motion:
specifier: ^12.38.0
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -108,14 +108,14 @@ importers:
specifier: ^19.2.4
version: 19.2.4(react@19.2.4)
react-i18next:
specifier: ^16.6.2
version: 16.6.2(i18next@25.10.5(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
specifier: ^17.0.0
version: 17.0.0(i18next@26.0.0(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2)
react-icons:
specifier: ^5.6.0
version: 5.6.0(react@19.2.4)
recharts:
specifier: 3.8.0
version: 3.8.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1)
specifier: 3.8.1
version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1)
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -127,8 +127,8 @@ importers:
version: 2.3.0
devDependencies:
'@biomejs/biome':
specifier: 2.4.8
version: 2.4.8
specifier: 2.4.9
version: 2.4.9
'@tailwindcss/postcss':
specifier: ^4.2.2
version: 4.2.2
@@ -161,22 +161,22 @@ importers:
version: 4.2.2
ts-unused-exports:
specifier: ^11.0.1
version: 11.0.1(typescript@5.9.3)
version: 11.0.1(typescript@6.0.2)
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
typescript:
specifier: ~5.9.3
version: 5.9.3
specifier: ~6.0.2
version: 6.0.2
donut-sync:
dependencies:
'@aws-sdk/client-s3':
specifier: ^3.1015.0
version: 3.1015.0
specifier: ^3.1019.0
version: 3.1019.0
'@aws-sdk/s3-request-presigner':
specifier: ^3.1015.0
version: 3.1015.0
specifier: ^3.1019.0
version: 3.1019.0
'@nestjs/common':
specifier: ^11.1.17
version: 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -308,48 +308,48 @@ packages:
'@aws-crypto/util@5.2.0':
resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
'@aws-sdk/client-s3@3.1015.0':
resolution: {integrity: sha512-yo+Y+/fq5/E684SynTRO+VA3a+98MeE/hs7J52XpNI5SchOCSrLhLtcDKVASlGhHQdNLGLzblRgps1OZaf8sbA==}
'@aws-sdk/client-s3@3.1019.0':
resolution: {integrity: sha512-0pb9x7PPhS4oEi4c0rL3vzQQoXA4cWKtPuGga/UfVYLZ68yrqdq0NDKg0fr55qzdhNvWFCpmGx73g9Iyy03kkA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/core@3.973.24':
resolution: {integrity: sha512-vvf82RYQu2GidWAuQq+uIzaPz9V0gSCXVqdVzRosgl5rXcspXOpSD3wFreGGW6AYymPr97Z69kjVnLePBxloDw==}
'@aws-sdk/core@3.973.25':
resolution: {integrity: sha512-TNrx7eq6nKNOO62HWPqoBqPLXEkW6nLZQGwjL6lq1jZtigWYbK1NbCnT7mKDzbLMHZfuOECUt3n6CzxjUW9HWQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/crc64-nvme@3.972.5':
resolution: {integrity: sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-env@3.972.22':
resolution: {integrity: sha512-cXp0VTDWT76p3hyK5D51yIKEfpf6/zsUvMfaB8CkyqadJxMQ8SbEeVroregmDlZbtG31wkj9ei0WnftmieggLg==}
'@aws-sdk/credential-provider-env@3.972.23':
resolution: {integrity: sha512-EamaclJcCEaPHp6wiVknNMM2RlsPMjAHSsYSFLNENBM8Wz92QPc6cOn3dif6vPDQt0Oo4IEghDy3NMDCzY/IvA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-http@3.972.24':
resolution: {integrity: sha512-h694K7+tRuepSRJr09wTvQfaEnjzsKZ5s7fbESrVds02GT/QzViJ94/HCNwM7bUfFxqpPXHxulZfL6Cou0dwPg==}
'@aws-sdk/credential-provider-http@3.972.25':
resolution: {integrity: sha512-qPymamdPcLp6ugoVocG1y5r69ScNiRzb0hogX25/ij+Wz7c7WnsgjLTaz7+eB5BfRxeyUwuw5hgULMuwOGOpcw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-ini@3.972.24':
resolution: {integrity: sha512-O46fFmv0RDFWiWEA9/e6oW92BnsyAXuEgTTasxHligjn2RCr9L/DK773m/NoFaL3ZdNAUz8WxgxunleMnHAkeQ==}
'@aws-sdk/credential-provider-ini@3.972.26':
resolution: {integrity: sha512-xKxEAMuP6GYx2y5GET+d3aGEroax3AgGfwBE65EQAUe090lzyJ/RzxPX9s8v7Z6qAk0XwfQl+LrmH05X7YvTeg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-login@3.972.24':
resolution: {integrity: sha512-sIk8oa6AzDoUhxsR11svZESqvzGuXesw62Rl2oW6wguZx8i9cdGCvkFg+h5K7iucUZP8wyWibUbJMc+J66cu5g==}
'@aws-sdk/credential-provider-login@3.972.26':
resolution: {integrity: sha512-EFcM8RM3TUxnZOfMJo++3PnyxFu1fL/huzmn3Vh+8IWRgqZawUD3cRwwOr+/4bE9DpyHaLOWFAjY0lfK5X9ZkQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-node@3.972.25':
resolution: {integrity: sha512-m7dR0Dsva2P+VUpL+VkC0WwiDby5pgmWXkRVDB5rlwv0jXJrQJf7YMtCoM8Wjk0H9jPeCYOxOXXcIgp/qp5Alg==}
'@aws-sdk/credential-provider-node@3.972.27':
resolution: {integrity: sha512-jXpxSolfFnPVj6GCTtx3xIdWNoDR7hYC/0SbetGZxOC9UnNmipHeX1k6spVstf7eWJrMhXNQEgXC0pD1r5tXIg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-process@3.972.22':
resolution: {integrity: sha512-Os32s8/4gTZjBk5BtoS/cuTILaj+K72d0dVG7TCJX/fC4598cxwLDmf1AEHEpER5oL3K//yETjvFaz0V8oO5Xw==}
'@aws-sdk/credential-provider-process@3.972.23':
resolution: {integrity: sha512-IL/TFW59++b7MpHserjUblGrdP5UXy5Ekqqx1XQkERXBFJcZr74I7VaSrQT5dxdRMU16xGK4L0RQ5fQG1pMgnA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-sso@3.972.24':
resolution: {integrity: sha512-PaFv7snEfypU2yXkpvfyWgddEbDLtgVe51wdZlinhc2doubBjUzJZZpgwuF2Jenl1FBydMhNpMjD6SBUM3qdSA==}
'@aws-sdk/credential-provider-sso@3.972.26':
resolution: {integrity: sha512-c6ghvRb6gTlMznWhGxn/bpVCcp0HRaz4DobGVD9kI4vwHq186nU2xN/S7QGkm0lo0H2jQU8+dgpUFLxfTcwCOg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-web-identity@3.972.24':
resolution: {integrity: sha512-J6H4R1nvr3uBTqD/EeIPAskrBtET4WFfNhpFySr2xW7bVZOXpQfPjrLSIx65jcNjBmLXzWq8QFLdVoGxiGG/SA==}
'@aws-sdk/credential-provider-web-identity@3.972.26':
resolution: {integrity: sha512-cXcS3+XD3iwhoXkM44AmxjmbcKueoLCINr1e+IceMmCySda5ysNIfiGBGe9qn5EMiQ9Jd7pP0AGFtcd6OV3Lvg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/middleware-bucket-endpoint@3.972.8':
@@ -360,8 +360,8 @@ packages:
resolution: {integrity: sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/middleware-flexible-checksums@3.974.4':
resolution: {integrity: sha512-fhCbZXPAyy8btnNbnBlR7Cc1nD54cETSvGn2wey71ehsM89AKPO8Dpco9DBAAgvrUdLrdHQepBXcyX4vxC5OwA==}
'@aws-sdk/middleware-flexible-checksums@3.974.5':
resolution: {integrity: sha512-SPSvF0G1t8m8CcB0L+ClNFszzQOvXaxmRj25oRWDf6aU+TuN2PXPFAJ9A6lt1IvX4oGAqqbTdMPTYs/SSHUYYQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/middleware-host-header@3.972.8':
@@ -376,40 +376,40 @@ packages:
resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/middleware-recursion-detection@3.972.8':
resolution: {integrity: sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==}
'@aws-sdk/middleware-recursion-detection@3.972.9':
resolution: {integrity: sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/middleware-sdk-s3@3.972.24':
resolution: {integrity: sha512-4sXxVC/enYgMkZefNMOzU6C6KtAXEvwVJLgNcUx1dvROH6GvKB5Sm2RGnGzTp0/PwkibIyMw4kOzF8tbLfaBAQ==}
'@aws-sdk/middleware-sdk-s3@3.972.26':
resolution: {integrity: sha512-5q7UGSTtt7/KF0Os8wj2VZtlLxeWJVb0e2eDrDJlWot2EIxUNKDDMPFq/FowUqrwZ40rO2bu6BypxaKNvQhI+g==}
engines: {node: '>=20.0.0'}
'@aws-sdk/middleware-ssec@3.972.8':
resolution: {integrity: sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/middleware-user-agent@3.972.25':
resolution: {integrity: sha512-QxiMPofvOt8SwSynTOmuZfvvPM1S9QfkESBxB22NMHTRXCJhR5BygLl8IXfC4jELiisQgwsgUby21GtXfX3f/g==}
'@aws-sdk/middleware-user-agent@3.972.26':
resolution: {integrity: sha512-AilFIh4rI/2hKyyGN6XrB0yN96W2o7e7wyrPWCM6QjZM1mcC/pVkW3IWWRvuBWMpVP8Fg+rMpbzeLQ6dTM4gig==}
engines: {node: '>=20.0.0'}
'@aws-sdk/nested-clients@3.996.14':
resolution: {integrity: sha512-fSESKvh1VbfjtV3QMnRkCPZWkUbQof6T/DOpiLp33yP2wA+rbwwnZeG3XT3Ekljgw2I8X4XaQPnw+zSR8yxJ5Q==}
'@aws-sdk/nested-clients@3.996.16':
resolution: {integrity: sha512-L7Qzoj/qQU1cL5GnYLQP5LbI+wlLCLoINvcykR3htKcQ4tzrPf2DOs72x933BM7oArYj1SKrkb2lGlsJHIic3g==}
engines: {node: '>=20.0.0'}
'@aws-sdk/region-config-resolver@3.972.9':
resolution: {integrity: sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng==}
'@aws-sdk/region-config-resolver@3.972.10':
resolution: {integrity: sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/s3-request-presigner@3.1015.0':
resolution: {integrity: sha512-N8Axxt3VNXPPnujakUfwm5SvyoE+4dqeIdfPr2EXLgV8vruerHuH9fb9/Dr1lGYeaRjM161ye2d3Ko4TB7oZLg==}
'@aws-sdk/s3-request-presigner@3.1019.0':
resolution: {integrity: sha512-KFv5UaIORIF6MTmEc79MQTPQSnRZjUmOIaOzXn9g6ujtViQLIrNYJiaSmVw8LqK1ebcndS6L6s4bBdLd9AQVJA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/signature-v4-multi-region@3.996.12':
resolution: {integrity: sha512-abRObSqjVeKUUHIZfAp78PTYrEsxCgVKDs/YET357pzT5C02eDDEvmWyeEC2wglWcYC4UTbBFk22gd2YJUlCQg==}
'@aws-sdk/signature-v4-multi-region@3.996.14':
resolution: {integrity: sha512-4nZSrBr1NO+48HCM/6BRU8mnRjuHZjcpziCvLXZk5QVftwWz5Mxqbhwdz4xf7WW88buaTB8uRO2MHklSX1m0vg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/token-providers@3.1015.0':
resolution: {integrity: sha512-3OSD4y110nisRhHzFOjoEeHU4GQL4KpzkX9PxzWaiZe0Yg2+thZKM0Pn9DjYwezH5JYfh/K++xK/SE0IHGrmCQ==}
'@aws-sdk/token-providers@3.1019.0':
resolution: {integrity: sha512-OF+2RfRmUKyjzrRWlDcyju3RBsuqcrYDQ8TwrJg8efcOotMzuZN4U9mpVTIdATpmEc4lWNZBMSjPzrGm6JPnAQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/types@3.973.6':
@@ -435,8 +435,8 @@ packages:
'@aws-sdk/util-user-agent-browser@3.972.8':
resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==}
'@aws-sdk/util-user-agent-node@3.973.11':
resolution: {integrity: sha512-1qdXbXo2s5MMLpUvw00284LsbhtlQ4ul7Zzdn5n+7p4WVgCMLqhxImpHIrjSoc72E/fyc4Wq8dLtUld2Gsh+lA==}
'@aws-sdk/util-user-agent-node@3.973.12':
resolution: {integrity: sha512-8phW0TS8ntENJgDcFewYT/Q8dOmarpvSxEjATu2GUBAutiHr++oEGCiBUwxslCMNvwW2cAPZNT53S/ym8zm/gg==}
engines: {node: '>=20.0.0'}
peerDependencies:
aws-crt: '>=1.0.0'
@@ -444,8 +444,8 @@ packages:
aws-crt:
optional: true
'@aws-sdk/xml-builder@3.972.15':
resolution: {integrity: sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA==}
'@aws-sdk/xml-builder@3.972.16':
resolution: {integrity: sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==}
engines: {node: '>=20.0.0'}
'@aws/lambda-invoke-store@0.2.4':
@@ -621,59 +621,59 @@ packages:
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
'@biomejs/biome@2.4.8':
resolution: {integrity: sha512-ponn0oKOky1oRXBV+rlSaUlixUxf1aZvWC19Z41zBfUOUesthrQqL3OtiAlSB1EjFjyWpn98Q64DHelhA6jNlA==}
'@biomejs/biome@2.4.9':
resolution: {integrity: sha512-wvZW92FrwitTcacvCBT8xdAbfbxWfDLwjYMmU3djjqQTh7Ni4ZdiWIT/x5VcZ+RQuxiKzIOzi5D+dcyJDFZMsA==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@2.4.8':
resolution: {integrity: sha512-ARx0tECE8I7S2C2yjnWYLNbBdDoPdq3oyNLhMglmuctThwUsuzFWRKrHmIGwIRWKz0Mat9DuzLEDp52hGnrxGQ==}
'@biomejs/cli-darwin-arm64@2.4.9':
resolution: {integrity: sha512-d5G8Gf2RpH5pYwiHLPA+UpG3G9TLQu4WM+VK6sfL7K68AmhcEQ9r+nkj/DvR/GYhYox6twsHUtmWWWIKfcfQQA==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@2.4.8':
resolution: {integrity: sha512-Jg9/PsB9vDCJlANE8uhG7qDhb5w0Ix69D7XIIc8IfZPUoiPrbLm33k2Ig3NOJ/7nb3UbesFz3D1aDKm9DvzjhQ==}
'@biomejs/cli-darwin-x64@2.4.9':
resolution: {integrity: sha512-LNCLNgqDMG7BLdc3a8aY/dwKPK7+R8/JXJoXjCvZh2gx8KseqBdFDKbhrr7HCWF8SzNhbTaALhTBoh/I6rf9lA==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@2.4.8':
resolution: {integrity: sha512-Zo9OhBQDJ3IBGPlqHiTISloo5H0+FBIpemqIJdW/0edJ+gEcLR+MZeZozcUyz3o1nXkVA7++DdRKQT0599j9jA==}
'@biomejs/cli-linux-arm64-musl@2.4.9':
resolution: {integrity: sha512-8RCww5xnPn2wpK4L/QDGDOW0dq80uVWfppPxHIUg6mOs9B6gRmqPp32h1Ls3T8GnW8Wo5A8u7vpTwz4fExN+sw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-arm64@2.4.8':
resolution: {integrity: sha512-5CdrsJct76XG2hpKFwXnEtlT1p+4g4yV+XvvwBpzKsTNLO9c6iLlAxwcae2BJ7ekPGWjNGw9j09T5KGPKKxQig==}
'@biomejs/cli-linux-arm64@2.4.9':
resolution: {integrity: sha512-4adnkAUi6K4C/emPRgYznMOcLlUqZdXWM6aIui4VP4LraE764g6Q4YguygnAUoxKjKIXIWPteKMgRbN0wsgwcg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@biomejs/cli-linux-x64-musl@2.4.8':
resolution: {integrity: sha512-Gi8quv8MEuDdKaPFtS2XjEnMqODPsRg6POT6KhoP+VrkNb+T2ywunVB+TvOU0LX1jAZzfBr+3V1mIbBhzAMKvw==}
'@biomejs/cli-linux-x64-musl@2.4.9':
resolution: {integrity: sha512-5TD+WS9v5vzXKzjetF0hgoaNFHMcpQeBUwKKVi3JbG1e9UCrFuUK3Gt185fyTzvRdwYkJJEMqglRPjmesmVv4A==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-x64@2.4.8':
resolution: {integrity: sha512-PdKXspVEaMCQLjtZCn6vfSck/li4KX9KGwSDbZdgIqlrizJ2MnMcE3TvHa2tVfXNmbjMikzcfJpuPWH695yJrw==}
'@biomejs/cli-linux-x64@2.4.9':
resolution: {integrity: sha512-L10na7POF0Ks/cgLFNF1ZvIe+X4onLkTi5oP9hY+Rh60Q+7fWzKDDCeGyiHUFf1nGIa9dQOOUPGe2MyYg8nMSQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@biomejs/cli-win32-arm64@2.4.8':
resolution: {integrity: sha512-LoFatS0tnHv6KkCVpIy3qZCih+MxUMvdYiPWLHRri7mhi2vyOOs8OrbZBcLTUEWCS+ktO72nZMy4F96oMhkOHQ==}
'@biomejs/cli-win32-arm64@2.4.9':
resolution: {integrity: sha512-aDZr0RBC3sMGJOU10BvG7eZIlWLK/i51HRIfScE2lVhfts2dQTreowLiJJd+UYg/tHKxS470IbzpuKmd0MiD6g==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@2.4.8':
resolution: {integrity: sha512-vAn7iXDoUbqFXqVocuq1sMYAd33p8+mmurqJkWl6CtIhobd/O6moe4rY5AJvzbunn/qZCdiDVcveqtkFh1e7Hg==}
'@biomejs/cli-win32-x64@2.4.9':
resolution: {integrity: sha512-NS4g/2G9SoQ4ktKtz31pvyc/rmgzlcIDCGU/zWbmHJAqx6gcRj2gj5Q/guXhoWTzCUaQZDIqiCQXHS7BcGYc0w==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
@@ -3980,10 +3980,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
i18next@25.10.5:
resolution: {integrity: sha512-jRnF7eRNsdcnh7AASSgaU3lj/8lJZuHkfsouetnLEDH0xxE1vVi7qhiJ9RhdSPUyzg4ltb7P7aXsFlTk9sxL2w==}
i18next@26.0.0:
resolution: {integrity: sha512-+Dg27j7VH40WRy+Q010hj3jxlxDb3qFkA7EwuK4kx0+glLxEqZsxdB8MqliPBkTrXEuNlEfeosM3lucAHK+0tQ==}
peerDependencies:
typescript: ^5
typescript: ^5 || ^6
peerDependenciesMeta:
typescript:
optional: true
@@ -4434,8 +4434,8 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react@0.577.0:
resolution: {integrity: sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==}
lucide-react@1.7.0:
resolution: {integrity: sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -4735,6 +4735,10 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
@@ -4803,14 +4807,14 @@ packages:
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
react-i18next@16.6.2:
resolution: {integrity: sha512-/S/GPzElTqEi5o2kzd0/O2627hPDmE6OGhJCCwCfUaQ3syyu+kaYH8/PYFtZeWc25NzfxTN/2fD1QjvrTgrFfA==}
react-i18next@17.0.0:
resolution: {integrity: sha512-L7aqwOePCExt6nlF7000lN2YKWnR7IpSpQId9sj01798Xn3LAncBdTHKl9lA/nr+YrG78BTqWPJxq9mlrrmH7Q==}
peerDependencies:
i18next: '>= 25.6.2'
i18next: '>= 25.10.10'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
typescript: ^5
typescript: ^5 || ^6
peerDependenciesMeta:
react-dom:
optional: true
@@ -4881,8 +4885,8 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
recharts@3.8.0:
resolution: {integrity: sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==}
recharts@3.8.1:
resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==}
engines: {node: '>=18'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -5341,6 +5345,11 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
typescript@6.0.2:
resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==}
engines: {node: '>=14.17'}
hasBin: true
uglify-js@3.19.3:
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
engines: {node: '>=0.8.0'}
@@ -5659,29 +5668,29 @@ snapshots:
'@smithy/util-utf8': 2.3.0
tslib: 2.8.1
'@aws-sdk/client-s3@3.1015.0':
'@aws-sdk/client-s3@3.1019.0':
dependencies:
'@aws-crypto/sha1-browser': 5.2.0
'@aws-crypto/sha256-browser': 5.2.0
'@aws-crypto/sha256-js': 5.2.0
'@aws-sdk/core': 3.973.24
'@aws-sdk/credential-provider-node': 3.972.25
'@aws-sdk/core': 3.973.25
'@aws-sdk/credential-provider-node': 3.972.27
'@aws-sdk/middleware-bucket-endpoint': 3.972.8
'@aws-sdk/middleware-expect-continue': 3.972.8
'@aws-sdk/middleware-flexible-checksums': 3.974.4
'@aws-sdk/middleware-flexible-checksums': 3.974.5
'@aws-sdk/middleware-host-header': 3.972.8
'@aws-sdk/middleware-location-constraint': 3.972.8
'@aws-sdk/middleware-logger': 3.972.8
'@aws-sdk/middleware-recursion-detection': 3.972.8
'@aws-sdk/middleware-sdk-s3': 3.972.24
'@aws-sdk/middleware-recursion-detection': 3.972.9
'@aws-sdk/middleware-sdk-s3': 3.972.26
'@aws-sdk/middleware-ssec': 3.972.8
'@aws-sdk/middleware-user-agent': 3.972.25
'@aws-sdk/region-config-resolver': 3.972.9
'@aws-sdk/signature-v4-multi-region': 3.996.12
'@aws-sdk/middleware-user-agent': 3.972.26
'@aws-sdk/region-config-resolver': 3.972.10
'@aws-sdk/signature-v4-multi-region': 3.996.14
'@aws-sdk/types': 3.973.6
'@aws-sdk/util-endpoints': 3.996.5
'@aws-sdk/util-user-agent-browser': 3.972.8
'@aws-sdk/util-user-agent-node': 3.973.11
'@aws-sdk/util-user-agent-node': 3.973.12
'@smithy/config-resolver': 4.4.13
'@smithy/core': 3.23.12
'@smithy/eventstream-serde-browser': 4.2.12
@@ -5719,10 +5728,10 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/core@3.973.24':
'@aws-sdk/core@3.973.25':
dependencies:
'@aws-sdk/types': 3.973.6
'@aws-sdk/xml-builder': 3.972.15
'@aws-sdk/xml-builder': 3.972.16
'@smithy/core': 3.23.12
'@smithy/node-config-provider': 4.3.12
'@smithy/property-provider': 4.2.12
@@ -5740,17 +5749,17 @@ snapshots:
'@smithy/types': 4.13.1
tslib: 2.8.1
'@aws-sdk/credential-provider-env@3.972.22':
'@aws-sdk/credential-provider-env@3.972.23':
dependencies:
'@aws-sdk/core': 3.973.24
'@aws-sdk/core': 3.973.25
'@aws-sdk/types': 3.973.6
'@smithy/property-provider': 4.2.12
'@smithy/types': 4.13.1
tslib: 2.8.1
'@aws-sdk/credential-provider-http@3.972.24':
'@aws-sdk/credential-provider-http@3.972.25':
dependencies:
'@aws-sdk/core': 3.973.24
'@aws-sdk/core': 3.973.25
'@aws-sdk/types': 3.973.6
'@smithy/fetch-http-handler': 5.3.15
'@smithy/node-http-handler': 4.5.0
@@ -5761,16 +5770,16 @@ snapshots:
'@smithy/util-stream': 4.5.20
tslib: 2.8.1
'@aws-sdk/credential-provider-ini@3.972.24':
'@aws-sdk/credential-provider-ini@3.972.26':
dependencies:
'@aws-sdk/core': 3.973.24
'@aws-sdk/credential-provider-env': 3.972.22
'@aws-sdk/credential-provider-http': 3.972.24
'@aws-sdk/credential-provider-login': 3.972.24
'@aws-sdk/credential-provider-process': 3.972.22
'@aws-sdk/credential-provider-sso': 3.972.24
'@aws-sdk/credential-provider-web-identity': 3.972.24
'@aws-sdk/nested-clients': 3.996.14
'@aws-sdk/core': 3.973.25
'@aws-sdk/credential-provider-env': 3.972.23
'@aws-sdk/credential-provider-http': 3.972.25
'@aws-sdk/credential-provider-login': 3.972.26
'@aws-sdk/credential-provider-process': 3.972.23
'@aws-sdk/credential-provider-sso': 3.972.26
'@aws-sdk/credential-provider-web-identity': 3.972.26
'@aws-sdk/nested-clients': 3.996.16
'@aws-sdk/types': 3.973.6
'@smithy/credential-provider-imds': 4.2.12
'@smithy/property-provider': 4.2.12
@@ -5780,10 +5789,10 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/credential-provider-login@3.972.24':
'@aws-sdk/credential-provider-login@3.972.26':
dependencies:
'@aws-sdk/core': 3.973.24
'@aws-sdk/nested-clients': 3.996.14
'@aws-sdk/core': 3.973.25
'@aws-sdk/nested-clients': 3.996.16
'@aws-sdk/types': 3.973.6
'@smithy/property-provider': 4.2.12
'@smithy/protocol-http': 5.3.12
@@ -5793,14 +5802,14 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/credential-provider-node@3.972.25':
'@aws-sdk/credential-provider-node@3.972.27':
dependencies:
'@aws-sdk/credential-provider-env': 3.972.22
'@aws-sdk/credential-provider-http': 3.972.24
'@aws-sdk/credential-provider-ini': 3.972.24
'@aws-sdk/credential-provider-process': 3.972.22
'@aws-sdk/credential-provider-sso': 3.972.24
'@aws-sdk/credential-provider-web-identity': 3.972.24
'@aws-sdk/credential-provider-env': 3.972.23
'@aws-sdk/credential-provider-http': 3.972.25
'@aws-sdk/credential-provider-ini': 3.972.26
'@aws-sdk/credential-provider-process': 3.972.23
'@aws-sdk/credential-provider-sso': 3.972.26
'@aws-sdk/credential-provider-web-identity': 3.972.26
'@aws-sdk/types': 3.973.6
'@smithy/credential-provider-imds': 4.2.12
'@smithy/property-provider': 4.2.12
@@ -5810,20 +5819,20 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/credential-provider-process@3.972.22':
'@aws-sdk/credential-provider-process@3.972.23':
dependencies:
'@aws-sdk/core': 3.973.24
'@aws-sdk/core': 3.973.25
'@aws-sdk/types': 3.973.6
'@smithy/property-provider': 4.2.12
'@smithy/shared-ini-file-loader': 4.4.7
'@smithy/types': 4.13.1
tslib: 2.8.1
'@aws-sdk/credential-provider-sso@3.972.24':
'@aws-sdk/credential-provider-sso@3.972.26':
dependencies:
'@aws-sdk/core': 3.973.24
'@aws-sdk/nested-clients': 3.996.14
'@aws-sdk/token-providers': 3.1015.0
'@aws-sdk/core': 3.973.25
'@aws-sdk/nested-clients': 3.996.16
'@aws-sdk/token-providers': 3.1019.0
'@aws-sdk/types': 3.973.6
'@smithy/property-provider': 4.2.12
'@smithy/shared-ini-file-loader': 4.4.7
@@ -5832,10 +5841,10 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/credential-provider-web-identity@3.972.24':
'@aws-sdk/credential-provider-web-identity@3.972.26':
dependencies:
'@aws-sdk/core': 3.973.24
'@aws-sdk/nested-clients': 3.996.14
'@aws-sdk/core': 3.973.25
'@aws-sdk/nested-clients': 3.996.16
'@aws-sdk/types': 3.973.6
'@smithy/property-provider': 4.2.12
'@smithy/shared-ini-file-loader': 4.4.7
@@ -5861,12 +5870,12 @@ snapshots:
'@smithy/types': 4.13.1
tslib: 2.8.1
'@aws-sdk/middleware-flexible-checksums@3.974.4':
'@aws-sdk/middleware-flexible-checksums@3.974.5':
dependencies:
'@aws-crypto/crc32': 5.2.0
'@aws-crypto/crc32c': 5.2.0
'@aws-crypto/util': 5.2.0
'@aws-sdk/core': 3.973.24
'@aws-sdk/core': 3.973.25
'@aws-sdk/crc64-nvme': 3.972.5
'@aws-sdk/types': 3.973.6
'@smithy/is-array-buffer': 4.2.2
@@ -5897,7 +5906,7 @@ snapshots:
'@smithy/types': 4.13.1
tslib: 2.8.1
'@aws-sdk/middleware-recursion-detection@3.972.8':
'@aws-sdk/middleware-recursion-detection@3.972.9':
dependencies:
'@aws-sdk/types': 3.973.6
'@aws/lambda-invoke-store': 0.2.4
@@ -5905,9 +5914,9 @@ snapshots:
'@smithy/types': 4.13.1
tslib: 2.8.1
'@aws-sdk/middleware-sdk-s3@3.972.24':
'@aws-sdk/middleware-sdk-s3@3.972.26':
dependencies:
'@aws-sdk/core': 3.973.24
'@aws-sdk/core': 3.973.25
'@aws-sdk/types': 3.973.6
'@aws-sdk/util-arn-parser': 3.972.3
'@smithy/core': 3.23.12
@@ -5928,9 +5937,9 @@ snapshots:
'@smithy/types': 4.13.1
tslib: 2.8.1
'@aws-sdk/middleware-user-agent@3.972.25':
'@aws-sdk/middleware-user-agent@3.972.26':
dependencies:
'@aws-sdk/core': 3.973.24
'@aws-sdk/core': 3.973.25
'@aws-sdk/types': 3.973.6
'@aws-sdk/util-endpoints': 3.996.5
'@smithy/core': 3.23.12
@@ -5939,20 +5948,20 @@ snapshots:
'@smithy/util-retry': 4.2.12
tslib: 2.8.1
'@aws-sdk/nested-clients@3.996.14':
'@aws-sdk/nested-clients@3.996.16':
dependencies:
'@aws-crypto/sha256-browser': 5.2.0
'@aws-crypto/sha256-js': 5.2.0
'@aws-sdk/core': 3.973.24
'@aws-sdk/core': 3.973.25
'@aws-sdk/middleware-host-header': 3.972.8
'@aws-sdk/middleware-logger': 3.972.8
'@aws-sdk/middleware-recursion-detection': 3.972.8
'@aws-sdk/middleware-user-agent': 3.972.25
'@aws-sdk/region-config-resolver': 3.972.9
'@aws-sdk/middleware-recursion-detection': 3.972.9
'@aws-sdk/middleware-user-agent': 3.972.26
'@aws-sdk/region-config-resolver': 3.972.10
'@aws-sdk/types': 3.973.6
'@aws-sdk/util-endpoints': 3.996.5
'@aws-sdk/util-user-agent-browser': 3.972.8
'@aws-sdk/util-user-agent-node': 3.973.11
'@aws-sdk/util-user-agent-node': 3.973.12
'@smithy/config-resolver': 4.4.13
'@smithy/core': 3.23.12
'@smithy/fetch-http-handler': 5.3.15
@@ -5982,7 +5991,7 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/region-config-resolver@3.972.9':
'@aws-sdk/region-config-resolver@3.972.10':
dependencies:
'@aws-sdk/types': 3.973.6
'@smithy/config-resolver': 4.4.13
@@ -5990,9 +5999,9 @@ snapshots:
'@smithy/types': 4.13.1
tslib: 2.8.1
'@aws-sdk/s3-request-presigner@3.1015.0':
'@aws-sdk/s3-request-presigner@3.1019.0':
dependencies:
'@aws-sdk/signature-v4-multi-region': 3.996.12
'@aws-sdk/signature-v4-multi-region': 3.996.14
'@aws-sdk/types': 3.973.6
'@aws-sdk/util-format-url': 3.972.8
'@smithy/middleware-endpoint': 4.4.27
@@ -6001,19 +6010,19 @@ snapshots:
'@smithy/types': 4.13.1
tslib: 2.8.1
'@aws-sdk/signature-v4-multi-region@3.996.12':
'@aws-sdk/signature-v4-multi-region@3.996.14':
dependencies:
'@aws-sdk/middleware-sdk-s3': 3.972.24
'@aws-sdk/middleware-sdk-s3': 3.972.26
'@aws-sdk/types': 3.973.6
'@smithy/protocol-http': 5.3.12
'@smithy/signature-v4': 5.3.12
'@smithy/types': 4.13.1
tslib: 2.8.1
'@aws-sdk/token-providers@3.1015.0':
'@aws-sdk/token-providers@3.1019.0':
dependencies:
'@aws-sdk/core': 3.973.24
'@aws-sdk/nested-clients': 3.996.14
'@aws-sdk/core': 3.973.25
'@aws-sdk/nested-clients': 3.996.16
'@aws-sdk/types': 3.973.6
'@smithy/property-provider': 4.2.12
'@smithy/shared-ini-file-loader': 4.4.7
@@ -6057,16 +6066,16 @@ snapshots:
bowser: 2.14.1
tslib: 2.8.1
'@aws-sdk/util-user-agent-node@3.973.11':
'@aws-sdk/util-user-agent-node@3.973.12':
dependencies:
'@aws-sdk/middleware-user-agent': 3.972.25
'@aws-sdk/middleware-user-agent': 3.972.26
'@aws-sdk/types': 3.973.6
'@smithy/node-config-provider': 4.3.12
'@smithy/types': 4.13.1
'@smithy/util-config-provider': 4.2.2
tslib: 2.8.1
'@aws-sdk/xml-builder@3.972.15':
'@aws-sdk/xml-builder@3.972.16':
dependencies:
'@smithy/types': 4.13.1
fast-xml-parser: 5.5.8
@@ -6265,39 +6274,39 @@ snapshots:
'@bcoe/v8-coverage@0.2.3': {}
'@biomejs/biome@2.4.8':
'@biomejs/biome@2.4.9':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.4.8
'@biomejs/cli-darwin-x64': 2.4.8
'@biomejs/cli-linux-arm64': 2.4.8
'@biomejs/cli-linux-arm64-musl': 2.4.8
'@biomejs/cli-linux-x64': 2.4.8
'@biomejs/cli-linux-x64-musl': 2.4.8
'@biomejs/cli-win32-arm64': 2.4.8
'@biomejs/cli-win32-x64': 2.4.8
'@biomejs/cli-darwin-arm64': 2.4.9
'@biomejs/cli-darwin-x64': 2.4.9
'@biomejs/cli-linux-arm64': 2.4.9
'@biomejs/cli-linux-arm64-musl': 2.4.9
'@biomejs/cli-linux-x64': 2.4.9
'@biomejs/cli-linux-x64-musl': 2.4.9
'@biomejs/cli-win32-arm64': 2.4.9
'@biomejs/cli-win32-x64': 2.4.9
'@biomejs/cli-darwin-arm64@2.4.8':
'@biomejs/cli-darwin-arm64@2.4.9':
optional: true
'@biomejs/cli-darwin-x64@2.4.8':
'@biomejs/cli-darwin-x64@2.4.9':
optional: true
'@biomejs/cli-linux-arm64-musl@2.4.8':
'@biomejs/cli-linux-arm64-musl@2.4.9':
optional: true
'@biomejs/cli-linux-arm64@2.4.8':
'@biomejs/cli-linux-arm64@2.4.9':
optional: true
'@biomejs/cli-linux-x64-musl@2.4.8':
'@biomejs/cli-linux-x64-musl@2.4.9':
optional: true
'@biomejs/cli-linux-x64@2.4.8':
'@biomejs/cli-linux-x64@2.4.9':
optional: true
'@biomejs/cli-win32-arm64@2.4.8':
'@biomejs/cli-win32-arm64@2.4.9':
optional: true
'@biomejs/cli-win32-x64@2.4.8':
'@biomejs/cli-win32-x64@2.4.9':
optional: true
'@borewit/text-codec@0.2.2': {}
@@ -9439,9 +9448,9 @@ snapshots:
dependencies:
bser: 2.1.1
fdir@6.5.0(picomatch@4.0.3):
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.3
picomatch: 4.0.4
file-type@21.3.2:
dependencies:
@@ -9637,11 +9646,11 @@ snapshots:
husky@9.1.7: {}
i18next@25.10.5(typescript@5.9.3):
i18next@26.0.0(typescript@6.0.2):
dependencies:
'@babel/runtime': 7.29.2
optionalDependencies:
typescript: 5.9.3
typescript: 6.0.2
iconv-lite@0.7.2:
dependencies:
@@ -10235,7 +10244,7 @@ snapshots:
dependencies:
yallist: 3.1.1
lucide-react@0.577.0(react@19.2.4):
lucide-react@1.7.0(react@19.2.4):
dependencies:
react: 19.2.4
@@ -10483,6 +10492,8 @@ snapshots:
picomatch@4.0.3: {}
picomatch@4.0.4: {}
pirates@4.0.7: {}
pkg-dir@4.2.0:
@@ -10601,16 +10612,16 @@ snapshots:
react-fast-compare@3.2.2: {}
react-i18next@16.6.2(i18next@25.10.5(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3):
react-i18next@17.0.0(i18next@26.0.0(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2):
dependencies:
'@babel/runtime': 7.29.2
html-parse-stringify: 3.0.1
i18next: 25.10.5(typescript@5.9.3)
i18next: 26.0.0(typescript@6.0.2)
react: 19.2.4
use-sync-external-store: 1.6.0(react@19.2.4)
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
typescript: 5.9.3
typescript: 6.0.2
react-icons@5.6.0(react@19.2.4):
dependencies:
@@ -10664,7 +10675,7 @@ snapshots:
readdirp@4.1.2: {}
recharts@3.8.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1):
recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1):
dependencies:
'@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)
clsx: 2.1.1
@@ -11070,8 +11081,8 @@ snapshots:
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
tmpl@1.0.5: {}
@@ -11135,11 +11146,11 @@ snapshots:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
ts-unused-exports@11.0.1(typescript@5.9.3):
ts-unused-exports@11.0.1(typescript@6.0.2):
dependencies:
chalk: 4.1.2
tsconfig-paths: 3.15.0
typescript: 5.9.3
typescript: 6.0.2
tsconfig-paths-webpack-plugin@4.2.0:
dependencies:
@@ -11186,6 +11197,8 @@ snapshots:
typescript@5.9.3: {}
typescript@6.0.2: {}
uglify-js@3.19.3:
optional: true
@@ -11286,8 +11299,8 @@ snapshots:
vite@7.0.6(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
postcss: 8.5.8
rollup: 4.60.0
tinyglobby: 0.2.15
+143 -38
View File
@@ -158,7 +158,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -169,7 +169,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -820,6 +820,15 @@ dependencies = [
"bzip2-sys",
]
[[package]]
name = "bzip2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c"
dependencies = [
"libbz2-rs-sys",
]
[[package]]
name = "bzip2-sys"
version = "0.1.13+1.0.8"
@@ -920,9 +929,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.57"
version = "1.2.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1461,6 +1470,17 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]]
name = "dbus"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4"
dependencies = [
"libc",
"libdbus-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "deadpool"
version = "0.12.3"
@@ -1619,7 +1639,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -1694,7 +1714,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.18.0"
version = "0.18.1"
dependencies = [
"aes",
"aes-gcm",
@@ -1705,7 +1725,7 @@ dependencies = [
"base64 0.22.1",
"blake3",
"boringtun",
"bzip2",
"bzip2 0.6.1",
"cbc",
"chrono",
"chrono-tz",
@@ -1750,7 +1770,7 @@ dependencies = [
"smoltcp",
"sys-locale",
"sysinfo",
"tao",
"tao 0.35.0",
"tar",
"tauri",
"tauri-build",
@@ -1825,9 +1845,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "embed-resource"
version = "3.0.7"
version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47ec73ddcf6b7f23173d5c3c5a32b5507dc0a734de7730aa14abc5d5e296bb5f"
checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45"
dependencies = [
"cc",
"memchr",
@@ -1956,7 +1976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -3485,12 +3505,27 @@ dependencies = [
"once_cell",
]
[[package]]
name = "libbz2-rs-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
[[package]]
name = "libc"
version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "libdbus-sys"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
dependencies = [
"pkg-config",
]
[[package]]
name = "libfuzzer-sys"
version = "0.4.12"
@@ -3519,9 +3554,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
version = "0.1.14"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
dependencies = [
"bitflags 2.11.0",
"libc",
@@ -3770,9 +3805,9 @@ dependencies = [
[[package]]
name = "mio"
version = "1.1.1"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
@@ -3954,9 +3989,9 @@ dependencies = [
[[package]]
name = "num-conv"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]]
name = "num-derive"
@@ -4024,7 +4059,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
dependencies = [
"proc-macro-crate 3.5.0",
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -4126,6 +4161,16 @@ dependencies = [
"objc2-foundation",
]
[[package]]
name = "objc2-core-location"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
dependencies = [
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-text"
version = "0.3.2"
@@ -4219,8 +4264,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
dependencies = [
"bitflags 2.11.0",
"block2",
"objc2",
"objc2-cloud-kit",
"objc2-core-data",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-image",
"objc2-core-location",
"objc2-core-text",
"objc2-foundation",
"objc2-quartz-core",
"objc2-user-notifications",
]
[[package]]
name = "objc2-user-notifications"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
dependencies = [
"objc2",
"objc2-foundation",
]
@@ -4345,7 +4409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.45.0",
]
[[package]]
@@ -5568,9 +5632,9 @@ dependencies = [
[[package]]
name = "rust_decimal"
version = "1.40.0"
version = "1.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0"
checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a"
dependencies = [
"arrayvec",
"borsh",
@@ -5580,6 +5644,7 @@ dependencies = [
"rkyv",
"serde",
"serde_json",
"wasm-bindgen",
]
[[package]]
@@ -5607,7 +5672,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -6140,9 +6205,9 @@ dependencies = [
[[package]]
name = "simd-adler32"
version = "0.3.8"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simd_helpers"
@@ -6222,7 +6287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -6528,6 +6593,46 @@ dependencies = [
"x11-dl",
]
[[package]]
name = "tao"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159"
dependencies = [
"bitflags 2.11.0",
"block2",
"core-foundation 0.10.1",
"core-graphics",
"crossbeam-channel",
"dbus",
"dispatch2",
"dlopen2",
"dpi",
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
"jni",
"libc",
"log",
"ndk",
"ndk-sys",
"objc2",
"objc2-app-kit",
"objc2-foundation",
"objc2-ui-kit",
"once_cell",
"parking_lot",
"percent-encoding",
"raw-window-handle",
"tao-macros",
"unicode-segmentation",
"url",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",
]
[[package]]
name = "tao-macros"
version = "0.1.3"
@@ -6890,7 +6995,7 @@ dependencies = [
"percent-encoding",
"raw-window-handle",
"softbuffer",
"tao",
"tao 0.34.8",
"tauri-runtime",
"tauri-utils",
"url",
@@ -6956,10 +7061,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -7519,7 +7624,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -7607,9 +7712,9 @@ checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
[[package]]
name = "unicode-vo"
@@ -7767,9 +7872,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.22.0"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [
"getrandom 0.4.2",
"js-sys",
@@ -8117,7 +8222,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -8646,7 +8751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
dependencies = [
"cfg-if",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -9069,7 +9174,7 @@ checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
dependencies = [
"aes",
"arbitrary",
"bzip2",
"bzip2 0.5.2",
"constant_time_eq 0.3.1",
"crc32fast",
"crossbeam-utils",
@@ -9167,9 +9272,9 @@ dependencies = [
[[package]]
name = "zune-jpeg"
version = "0.5.14"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6"
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [
"zune-core",
]
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.18.0"
version = "0.18.1"
description = "Simple Yet Powerful Anti-Detect Browser"
authors = ["zhom@github"]
edition = "2021"
@@ -64,7 +64,7 @@ flate2 = "1"
lzma-rs = "0"
msi-extract = "0"
uuid = { version = "1.20", features = ["v4", "serde"] }
uuid = { version = "1.23", features = ["v4", "serde"] }
url = "2.5"
blake3 = "1"
globset = "0.4"
@@ -111,7 +111,7 @@ smoltcp = { version = "0.13", default-features = false, features = ["std", "medi
# Daemon dependencies (tray icon)
tray-icon = "0.21"
muda = "0.17"
tao = "0.34"
tao = "0.35"
image = "0.25"
dirs = "6"
crossbeam-channel = "0.5"
+4
View File
@@ -1604,6 +1604,10 @@ rm "{}"
#[tauri::command]
pub async fn check_for_app_updates() -> Result<Option<AppUpdateInfo>, String> {
if crate::app_dirs::is_portable() {
log::info!("App auto-updates disabled in portable mode");
return Ok(None);
}
// The disable_auto_updates setting controls app self-updates only
let disabled = crate::settings_manager::SettingsManager::instance()
.load_settings()
+26
View File
@@ -3,11 +3,29 @@ use std::path::PathBuf;
use std::sync::OnceLock;
static BASE_DIRS: OnceLock<BaseDirs> = OnceLock::new();
static PORTABLE_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
fn base_dirs() -> &'static BaseDirs {
BASE_DIRS.get_or_init(|| BaseDirs::new().expect("Failed to get base directories"))
}
/// Returns the portable base directory if a `.portable` marker exists next to the executable.
fn portable_dir() -> Option<&'static PathBuf> {
PORTABLE_DIR
.get_or_init(|| {
std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|p| p.to_path_buf()))
.filter(|dir| dir.join(".portable").exists())
})
.as_ref()
}
/// Returns true if the app is running in portable mode.
pub fn is_portable() -> bool {
portable_dir().is_some()
}
pub fn app_name() -> &'static str {
if cfg!(debug_assertions) {
"DonutBrowserDev"
@@ -28,6 +46,10 @@ pub fn data_dir() -> PathBuf {
return PathBuf::from(dir);
}
if let Some(dir) = portable_dir() {
return dir.join("data");
}
base_dirs().data_local_dir().join(app_name())
}
@@ -43,6 +65,10 @@ pub fn cache_dir() -> PathBuf {
return PathBuf::from(dir);
}
if let Some(dir) = portable_dir() {
return dir.join("cache");
}
base_dirs().cache_dir().join(app_name())
}
+3
View File
@@ -340,6 +340,9 @@ pub fn is_autostart_enabled() -> bool {
}
pub fn get_data_dir() -> Option<PathBuf> {
if crate::app_dirs::is_portable() {
return Some(crate::app_dirs::data_dir());
}
if let Some(proj_dirs) = ProjectDirs::from("com", "donutbrowser", "Donut Browser") {
Some(proj_dirs.data_dir().to_path_buf())
} else {
+2 -1
View File
@@ -85,7 +85,7 @@ use downloader::{cancel_download, download_browser};
use settings_manager::{
decline_launch_on_login, dismiss_window_resize_warning, enable_launch_on_login, get_app_settings,
get_sync_settings, get_system_language, get_table_sorting_settings,
get_sync_settings, get_system_info, get_system_language, get_table_sorting_settings,
get_window_resize_warning_dismissed, save_app_settings, save_sync_settings,
save_table_sorting_settings, should_show_launch_on_login_prompt,
};
@@ -1816,6 +1816,7 @@ pub fn run() {
get_table_sorting_settings,
save_table_sorting_settings,
get_system_language,
get_system_info,
dismiss_window_resize_warning,
get_window_resize_warning_dismissed,
clear_all_version_cache_and_refetch,
+1 -1
View File
@@ -21,7 +21,7 @@ pub enum SyncMode {
Encrypted,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct BrowserProfile {
pub id: uuid::Uuid,
pub name: String,
+36
View File
@@ -945,6 +945,42 @@ pub fn get_system_language() -> String {
.unwrap_or_else(|| "en".to_string())
}
#[derive(Debug, Serialize, Clone)]
pub struct SystemInfo {
pub app_version: String,
pub os: String,
pub arch: String,
pub portable: bool,
}
#[tauri::command]
pub fn get_system_info() -> SystemInfo {
let os = if cfg!(target_os = "macos") {
"macOS"
} else if cfg!(target_os = "windows") {
"Windows"
} else if cfg!(target_os = "linux") {
"Linux"
} else {
"Unknown"
};
let arch = if cfg!(target_arch = "x86_64") {
"x86_64"
} else if cfg!(target_arch = "aarch64") {
"aarch64"
} else {
"unknown"
};
SystemInfo {
app_version: crate::app_auto_updater::AppAutoUpdater::get_current_version(),
os: os.to_string(),
arch: arch.to_string(),
portable: crate::app_dirs::is_portable(),
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref SETTINGS_MANAGER: SettingsManager = SettingsManager::new();
+1
View File
@@ -793,6 +793,7 @@ impl SyncEngine {
let mut sanitized = profile.clone();
sanitized.process_id = None;
sanitized.last_launch = None;
sanitized.last_sync = None; // Avoid triggering sync loop on timestamp change
let json = serde_json::to_string_pretty(&sanitized)
.map_err(|e| SyncError::SerializationError(format!("Failed to serialize profile: {e}")))?;
+134 -2
View File
@@ -8,6 +8,7 @@ use std::path::Path;
use std::time::SystemTime;
use super::types::{SyncError, SyncResult};
use crate::profile::types::BrowserProfile;
/// Default exclude patterns for volatile browser profile files.
/// Patterns use `**/` prefix to match at any directory depth, since the sync
@@ -209,6 +210,39 @@ fn hash_file(path: &Path) -> Result<Option<String>, SyncError> {
Ok(Some(hasher.finalize().to_hex().to_string()))
}
/// Compute blake3 hash of metadata.json after sanitizing volatile fields.
/// This prevents infinite sync loops where updating last_sync triggers a new sync.
fn hash_sanitized_metadata(path: &Path) -> Result<Option<String>, SyncError> {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => {
return Err(SyncError::IoError(format!(
"Failed to read metadata at {}: {e}",
path.display()
)));
}
};
let mut profile: BrowserProfile = serde_json::from_str(&content).map_err(|e| {
SyncError::SerializationError(format!("Failed to parse metadata for hashing: {e}"))
})?;
// Sanitize volatile fields that should not trigger a re-sync
profile.last_sync = None;
profile.process_id = None;
profile.last_launch = None;
let sanitized_json = serde_json::to_string(&profile).map_err(|e| {
SyncError::SerializationError(format!("Failed to serialize sanitized metadata: {e}"))
})?;
let mut hasher = blake3::Hasher::new();
hasher.update(sanitized_json.as_bytes());
Ok(Some(hasher.finalize().to_hex().to_string()))
}
/// Get mtime as unix timestamp
/// Returns None if the file doesn't exist (was deleted)
fn get_mtime(path: &Path) -> Result<Option<i64>, SyncError> {
@@ -324,7 +358,19 @@ pub fn generate_manifest(
*max_mtime = (*max_mtime).max(mtime);
// Check cache for existing hash
let hash = if let Some(cached_hash) = cache.get(&relative_path, size, mtime) {
let hash = if relative_path == "metadata.json" {
// Special case: sanitize metadata.json before hashing to prevent sync loops
match hash_sanitized_metadata(&path)? {
Some(computed_hash) => computed_hash,
None => {
log::debug!(
"File disappeared during manifest generation, skipping: {}",
path.display()
);
continue;
}
}
} else if let Some(cached_hash) = cache.get(&relative_path, size, mtime) {
cached_hash.to_string()
} else {
match hash_file(&path)? {
@@ -592,7 +638,12 @@ mod tests {
fs::write(profile_dir.join("profile/Crashpad/report"), "exclude").unwrap();
// metadata.json at root
fs::write(profile_dir.join("metadata.json"), "keep").unwrap();
let profile = BrowserProfile::default();
fs::write(
profile_dir.join("metadata.json"),
serde_json::to_string(&profile).unwrap(),
)
.unwrap();
let mut cache = HashCache::default();
let manifest = generate_manifest("test-profile", &profile_dir, &mut cache).unwrap();
@@ -800,4 +851,85 @@ mod tests {
assert!(diff.files_to_delete_remote.is_empty());
assert!(diff.files_to_delete_local.is_empty());
}
#[test]
fn test_generate_manifest_sanitizes_metadata() {
let temp_dir = TempDir::new().unwrap();
let profile_dir = temp_dir.path().join("profile");
fs::create_dir_all(&profile_dir).unwrap();
let profile_id = uuid::Uuid::new_v4();
let metadata_path = profile_dir.join("metadata.json");
let profile = BrowserProfile {
id: profile_id,
name: "test-profile".to_string(),
last_sync: Some(100),
process_id: Some(1234),
..Default::default()
};
fs::write(&metadata_path, serde_json::to_string(&profile).unwrap()).unwrap();
let mut cache = HashCache::default();
let manifest1 = generate_manifest(&profile_id.to_string(), &profile_dir, &mut cache).unwrap();
let hash1 = manifest1
.files
.iter()
.find(|f| f.path == "metadata.json")
.unwrap()
.hash
.clone();
// Update volatile fields
let profile2 = BrowserProfile {
id: profile_id,
name: "test-profile".to_string(),
last_sync: Some(200),
process_id: Some(5678),
..Default::default()
};
fs::write(&metadata_path, serde_json::to_string(&profile2).unwrap()).unwrap();
let manifest2 = generate_manifest(&profile_id.to_string(), &profile_dir, &mut cache).unwrap();
let hash2 = manifest2
.files
.iter()
.find(|f| f.path == "metadata.json")
.unwrap()
.hash
.clone();
// Hash should be identical because volatile fields are sanitized
assert_eq!(
hash1, hash2,
"Metadata hash should be stable across last_sync/process_id updates"
);
// Change a non-volatile field
let profile3 = BrowserProfile {
id: profile_id,
name: "changed-name".to_string(),
last_sync: Some(200),
..Default::default()
};
fs::write(&metadata_path, serde_json::to_string(&profile3).unwrap()).unwrap();
let manifest3 = generate_manifest(&profile_id.to_string(), &profile_dir, &mut cache).unwrap();
let hash3 = manifest3
.files
.iter()
.find(|f| f.path == "metadata.json")
.unwrap()
.hash
.clone();
// Hash should be different because name changed
assert_ne!(
hash1, hash3,
"Metadata hash should change when non-volatile fields change"
);
}
}
+4 -2
View File
@@ -136,8 +136,10 @@ impl WayfernManager {
port: u16,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let url = format!("http://127.0.0.1:{port}/json/version");
let max_attempts = 50;
let delay = Duration::from_millis(100);
// On first launch, macOS Gatekeeper verifies the binary which can take 30+ seconds.
// Use a generous timeout (60s) to handle this.
let max_attempts = 120;
let delay = Duration::from_millis(500);
for attempt in 0..max_attempts {
match self.http_client.get(&url).send().await {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut",
"version": "0.18.0",
"version": "0.18.1",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",
+39 -21
View File
@@ -280,7 +280,7 @@ export default function Home() {
const [processingUrls, setProcessingUrls] = useState<Set<string>>(new Set());
const handleUrlOpen = useCallback(
async (url: string) => {
(url: string) => {
// Prevent duplicate processing of the same URL
if (processingUrls.has(url)) {
console.log("URL already being processed:", url);
@@ -324,7 +324,7 @@ export default function Home() {
const currentUrl = await getCurrent();
if (currentUrl && currentUrl.length > 0) {
console.log("Startup URL detected:", currentUrl[0]);
void handleUrlOpen(currentUrl[0]);
handleUrlOpen(currentUrl[0]);
}
} catch (error) {
console.error("Failed to check current URL:", error);
@@ -372,7 +372,7 @@ export default function Home() {
}
}, [proxiesError]);
const checkAllPermissions = useCallback(async () => {
const checkAllPermissions = useCallback(() => {
try {
// Wait for permissions to be initialized before checking
if (!isInitialized) {
@@ -413,13 +413,13 @@ export default function Home() {
// Listen for URL open events from the deep link handler (when app is already running)
await listen<string>("url-open-request", (event) => {
console.log("Received URL open request:", event.payload);
void handleUrlOpen(event.payload);
handleUrlOpen(event.payload);
});
// Listen for show profile selector events
await listen<string>("show-profile-selector", (event) => {
console.log("Received show profile selector request:", event.payload);
void handleUrlOpen(event.payload);
handleUrlOpen(event.payload);
});
// Listen for show create profile dialog events
@@ -437,7 +437,7 @@ export default function Home() {
// Listen for custom logo click events
const handleLogoUrlEvent = (event: CustomEvent) => {
console.log("Received logo URL event:", event.detail);
void handleUrlOpen(event.detail);
handleUrlOpen(event.detail);
};
window.addEventListener(
@@ -529,7 +529,7 @@ export default function Home() {
camoufoxConfig: profileData.camoufoxConfig,
wayfernConfig: profileData.wayfernConfig,
groupId:
profileData.groupId ||
profileData.groupId ??
(selectedGroupId !== "default" ? selectedGroupId : undefined),
ephemeral: profileData.ephemeral,
},
@@ -764,13 +764,13 @@ export default function Home() {
setCookieManagementDialogOpen(true);
}, []);
const handleGroupAssignmentComplete = useCallback(async () => {
const handleGroupAssignmentComplete = useCallback(() => {
// No need to manually reload - useProfileEvents will handle the update
setGroupAssignmentDialogOpen(false);
setSelectedProfilesForGroup([]);
}, []);
const handleProxyAssignmentComplete = useCallback(async () => {
const handleProxyAssignmentComplete = useCallback(() => {
// No need to manually reload - useProfileEvents will handle the update
setProxyAssignmentDialogOpen(false);
setSelectedProfilesForProxy([]);
@@ -810,7 +810,7 @@ export default function Home() {
let unlistenStatus: (() => void) | undefined;
let unlistenProgress: (() => void) | undefined;
const profilesWithTransfer = new Set<string>();
(async () => {
void (async () => {
try {
unlistenStatus = await listen<{
profile_id: string;
@@ -898,7 +898,7 @@ export default function Home() {
};
let cleanup: (() => void) | undefined;
setupListeners().then((cleanupFn) => {
void setupListeners().then((cleanupFn) => {
cleanup = cleanupFn;
});
@@ -995,7 +995,7 @@ export default function Home() {
// Check permissions when they are initialized
useEffect(() => {
if (isInitialized) {
void checkAllPermissions();
checkAllPermissions();
}
}, [isInitialized, checkAllPermissions]);
@@ -1093,7 +1093,9 @@ export default function Home() {
crossOsUnlocked={crossOsUnlocked}
syncUnlocked={syncUnlocked}
getProfileSyncInfo={getProfileSyncInfo}
onLaunchWithSync={(profile) => setSyncLeaderProfile(profile)}
onLaunchWithSync={(profile) => {
setSyncLeaderProfile(profile);
}}
/>
</div>
</main>
@@ -1167,7 +1169,9 @@ export default function Home() {
<CloneProfileDialog
isOpen={!!cloneProfile}
onClose={() => setCloneProfile(null)}
onClose={() => {
setCloneProfile(null);
}}
profile={cloneProfile}
/>
@@ -1197,7 +1201,9 @@ export default function Home() {
<ExtensionManagementDialog
isOpen={extensionManagementDialogOpen}
onClose={() => setExtensionManagementDialogOpen(false)}
onClose={() => {
setExtensionManagementDialogOpen(false);
}}
limitedMode={!crossOsUnlocked}
/>
@@ -1242,7 +1248,9 @@ export default function Home() {
selectedProfiles={selectedProfilesForCookies}
profiles={profiles}
runningProfiles={runningProfiles}
onCopyComplete={() => setSelectedProfilesForCookies([])}
onCopyComplete={() => {
setSelectedProfilesForCookies([]);
}}
/>
<CookieManagementDialog
@@ -1256,7 +1264,9 @@ export default function Home() {
<DeleteConfirmationDialog
isOpen={showBulkDeleteConfirmation}
onClose={() => setShowBulkDeleteConfirmation(false)}
onClose={() => {
setShowBulkDeleteConfirmation(false);
}}
onConfirm={confirmBulkDelete}
title="Delete Selected Profiles"
description={`This action cannot be undone. This will permanently delete ${selectedProfiles.length} profile${selectedProfiles.length !== 1 ? "s" : ""} and all associated data.`}
@@ -1279,7 +1289,9 @@ export default function Home() {
<SyncAllDialog
isOpen={syncAllDialogOpen}
onClose={() => setSyncAllDialogOpen(false)}
onClose={() => {
setSyncAllDialogOpen(false);
}}
/>
<ProfileSyncDialog
@@ -1289,7 +1301,9 @@ export default function Home() {
setCurrentProfileForSync(null);
}}
profile={currentProfileForSync}
onSyncConfigOpen={() => setSyncConfigDialogOpen(true)}
onSyncConfigOpen={() => {
setSyncConfigDialogOpen(true);
}}
/>
{/* Wayfern Terms and Conditions Dialog - shown if terms not accepted */}
@@ -1313,7 +1327,9 @@ export default function Home() {
{/* Launch on Login Dialog - shown on every startup until enabled or declined */}
<LaunchOnLoginDialog
isOpen={launchOnLoginDialogOpen}
onClose={() => setLaunchOnLoginDialogOpen(false)}
onClose={() => {
setLaunchOnLoginDialogOpen(false);
}}
/>
<WindowResizeWarningDialog
@@ -1328,7 +1344,9 @@ export default function Home() {
<SyncFollowerDialog
isOpen={syncLeaderProfile !== null}
onClose={() => setSyncLeaderProfile(null)}
onClose={() => {
setSyncLeaderProfile(null);
}}
leaderProfile={syncLeaderProfile}
allProfiles={profiles}
runningProfiles={runningProfiles}
-6
View File
@@ -46,12 +46,6 @@ export function BandwidthMiniChart({
return result;
}, [data]);
// Find max value for scaling
const _maxBandwidth = React.useMemo(() => {
const max = Math.max(...chartData.map((d) => d.bandwidth), 1);
return max;
}, [chartData]);
// Use external bandwidth if provided, otherwise calculate from last data point
const currentBandwidth =
externalBandwidth ?? chartData[chartData.length - 1]?.bandwidth ?? 0;
+9 -2
View File
@@ -69,7 +69,12 @@ export function CloneProfileDialog({
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) onClose();
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("profileInfo.clone.title")}</DialogTitle>
@@ -80,7 +85,9 @@ export function CloneProfileDialog({
<Input
ref={inputRef}
value={name}
onChange={(e) => setName(e.target.value)}
onChange={(e) => {
setName(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") void handleClone();
}}
+9 -3
View File
@@ -44,9 +44,15 @@ export function CommercialTrialModal({
<Dialog open={isOpen}>
<DialogContent
className="sm:max-w-md"
onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => {
e.preventDefault();
}}
onPointerDownOutside={(e) => {
e.preventDefault();
}}
onInteractOutside={(e) => {
e.preventDefault();
}}
>
<DialogHeader>
<DialogTitle>Commercial Trial Expired</DialogTitle>
+23 -17
View File
@@ -50,12 +50,13 @@ interface CookieCopyDialogProps {
onCopyComplete?: () => void;
}
type SelectionState = {
[domain: string]: {
type SelectionState = Record<
string,
{
allSelected: boolean;
cookies: Set<string>;
};
};
}
>;
export function CookieCopyDialog({
isOpen,
@@ -109,7 +110,7 @@ export function CookieCopyDialog({
const domainSelection = selection[domain];
if (domainSelection.allSelected) {
const domainData = cookieData?.domains.find((d) => d.domain === domain);
count += domainData?.cookie_count || 0;
count += domainData?.cookie_count ?? 0;
} else {
count += domainSelection.cookies.size;
}
@@ -148,7 +149,7 @@ export function CookieCopyDialog({
(domain: string, cookies: UnifiedCookie[]) => {
setSelection((prev) => {
const current = prev[domain];
const allSelected = current?.allSelected || false;
const allSelected = current.allSelected;
if (allSelected) {
const newSelection = { ...prev };
@@ -171,7 +172,7 @@ export function CookieCopyDialog({
const toggleCookie = useCallback(
(domain: string, cookieName: string, totalCookies: number) => {
setSelection((prev) => {
const current = prev[domain] || {
const current = prev[domain] ?? {
allSelected: false,
cookies: new Set<string>(),
};
@@ -412,7 +413,9 @@ export function CookieCopyDialog({
<Input
placeholder="Search domains or cookies..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onChange={(e) => {
setSearchQuery(e.target.value);
}}
className="pl-8"
/>
</div>
@@ -501,8 +504,8 @@ function DomainRow({
onToggleExpand,
}: DomainRowProps) {
const domainSelection = selection[domain.domain];
const isAllSelected = domainSelection?.allSelected || false;
const selectedCount = domainSelection?.cookies.size || 0;
const isAllSelected = domainSelection.allSelected;
const selectedCount = domainSelection.cookies.size;
const isPartial =
selectedCount > 0 && selectedCount < domain.cookie_count && !isAllSelected;
@@ -511,13 +514,17 @@ function DomainRow({
<div className="flex items-center gap-2 p-2 hover:bg-accent/50 rounded">
<Checkbox
checked={isAllSelected || isPartial}
onCheckedChange={() => onToggleDomain(domain.domain, domain.cookies)}
onCheckedChange={() => {
onToggleDomain(domain.domain, domain.cookies);
}}
className={isPartial ? "opacity-70" : ""}
/>
<button
type="button"
className="flex items-center gap-1 flex-1 text-left bg-transparent border-none cursor-pointer"
onClick={() => onToggleExpand(domain.domain)}
onClick={() => {
onToggleExpand(domain.domain);
}}
>
{isExpanded ? (
<LuChevronDown className="w-4 h-4" />
@@ -533,8 +540,7 @@ function DomainRow({
{isExpanded && (
<div className="ml-8 pl-2 border-l space-y-1">
{domain.cookies.map((cookie) => {
const isSelected =
domainSelection?.cookies.has(cookie.name) || false;
const isSelected = domainSelection.cookies.has(cookie.name);
return (
<div
key={`${domain.domain}-${cookie.name}`}
@@ -542,13 +548,13 @@ function DomainRow({
>
<Checkbox
checked={isSelected || isAllSelected}
onCheckedChange={() =>
onCheckedChange={() => {
onToggleCookie(
domain.domain,
cookie.name,
domain.cookie_count,
)
}
);
}}
/>
<span className="truncate">{cookie.name}</span>
</div>
+23 -17
View File
@@ -45,12 +45,13 @@ interface CookieManagementDialogProps {
initialTab?: "import" | "export";
}
type SelectionState = {
[domain: string]: {
type SelectionState = Record<
string,
{
allSelected: boolean;
cookies: Set<string>;
};
};
}
>;
const countCookies = (content: string): number => {
const trimmed = content.trim();
@@ -150,7 +151,7 @@ export function CookieManagementDialog({
const domainData = exportCookieData?.domains.find(
(d) => d.domain === domain,
);
count += domainData?.cookie_count || 0;
count += domainData?.cookie_count ?? 0;
} else {
count += ds.cookies.size;
}
@@ -309,7 +310,7 @@ export function CookieManagementDialog({
(domain: string, cookies: UnifiedCookie[]) => {
setExportSelection((prev) => {
const current = prev[domain];
if (current?.allSelected) {
if (current.allSelected) {
const next = { ...prev };
delete next[domain];
return next;
@@ -329,7 +330,7 @@ export function CookieManagementDialog({
const toggleCookie = useCallback(
(domain: string, cookieName: string, totalCookies: number) => {
setExportSelection((prev) => {
const current = prev[domain] || {
const current = prev[domain] ?? {
allSelected: false,
cookies: new Set<string>(),
};
@@ -485,7 +486,9 @@ export function CookieManagementDialog({
<Label>Format</Label>
<Select
value={format}
onValueChange={(v) => setFormat(v as "netscape" | "json")}
onValueChange={(v) => {
setFormat(v as "netscape" | "json");
}}
>
<SelectTrigger>
<SelectValue />
@@ -589,8 +592,8 @@ function ExportDomainRow({
onToggleExpand,
}: ExportDomainRowProps) {
const domainSelection = selection[domain.domain];
const isAllSelected = domainSelection?.allSelected || false;
const selectedCount = domainSelection?.cookies.size || 0;
const isAllSelected = domainSelection.allSelected;
const selectedCount = domainSelection.cookies.size;
const isPartial =
selectedCount > 0 && selectedCount < domain.cookie_count && !isAllSelected;
@@ -599,13 +602,17 @@ function ExportDomainRow({
<div className="flex items-center gap-2 p-1.5 hover:bg-accent/50 rounded">
<Checkbox
checked={isAllSelected || isPartial}
onCheckedChange={() => onToggleDomain(domain.domain, domain.cookies)}
onCheckedChange={() => {
onToggleDomain(domain.domain, domain.cookies);
}}
className={isPartial ? "opacity-70" : ""}
/>
<button
type="button"
className="flex items-center gap-1 flex-1 text-left text-sm bg-transparent border-none cursor-pointer"
onClick={() => onToggleExpand(domain.domain)}
onClick={() => {
onToggleExpand(domain.domain);
}}
>
{isExpanded ? (
<LuChevronDown className="w-3.5 h-3.5" />
@@ -621,8 +628,7 @@ function ExportDomainRow({
{isExpanded && (
<div className="ml-7 pl-2 border-l space-y-0.5">
{domain.cookies.map((cookie) => {
const isSelected =
domainSelection?.cookies.has(cookie.name) || false;
const isSelected = domainSelection.cookies.has(cookie.name);
return (
<div
key={`${domain.domain}-${cookie.name}`}
@@ -630,13 +636,13 @@ function ExportDomainRow({
>
<Checkbox
checked={isSelected || isAllSelected}
onCheckedChange={() =>
onCheckedChange={() => {
onToggleCookie(
domain.domain,
cookie.name,
domain.cookie_count,
)
}
);
}}
/>
<span className="truncate">{cookie.name}</span>
</div>
+3 -1
View File
@@ -80,7 +80,9 @@ export function CreateGroupDialog({
id="group-name"
placeholder="Enter group name..."
value={groupName}
onChange={(e) => setGroupName(e.target.value)}
onChange={(e) => {
setGroupName(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter" && groupName.trim()) {
void handleCreate();
+49 -29
View File
@@ -172,11 +172,13 @@ export function CreateProfileDialog({
useEffect(() => {
if (isOpen) {
invoke<{ id: string; name: string; extension_ids: string[] }[]>(
void invoke<{ id: string; name: string; extension_ids: string[] }[]>(
"list_extension_groups",
)
.then(setExtensionGroups)
.catch(() => setExtensionGroups([]));
.catch(() => {
setExtensionGroups([]);
});
}
}, [isOpen]);
const [releaseTypes, setReleaseTypes] = useState<BrowserReleaseTypes>();
@@ -553,7 +555,9 @@ export function CreateProfileDialog({
<div className="space-y-3 pt-8">
{/* Wayfern (Chromium) - First */}
<Button
onClick={() => handleBrowserSelect("wayfern")}
onClick={() => {
handleBrowserSelect("wayfern");
}}
className="flex gap-3 justify-start items-center p-4 w-full h-16 border-2 transition-colors hover:border-primary/50"
variant="outline"
>
@@ -577,7 +581,9 @@ export function CreateProfileDialog({
{/* Camoufox (Firefox) - Second */}
<Button
onClick={() => handleBrowserSelect("camoufox")}
onClick={() => {
handleBrowserSelect("camoufox");
}}
className="flex gap-3 justify-start items-center p-4 w-full h-16 border-2 transition-colors hover:border-primary/50"
variant="outline"
>
@@ -620,9 +626,9 @@ export function CreateProfileDialog({
return (
<Button
key={browser.value}
onClick={() =>
handleBrowserSelect(browser.value)
}
onClick={() => {
handleBrowserSelect(browser.value);
}}
className="flex gap-3 justify-start items-center p-4 w-full h-16 border-2 transition-colors hover:border-primary/50"
variant="outline"
>
@@ -657,14 +663,16 @@ export function CreateProfileDialog({
<Input
id="profile-name"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
onChange={(e) => {
setProfileName(e.target.value);
}}
onKeyDown={(e) => {
if (
e.key === "Enter" &&
!isCreateDisabled &&
!isCreating
) {
handleCreate();
void handleCreate();
}
}}
placeholder="Enter profile name"
@@ -677,9 +685,9 @@ export function CreateProfileDialog({
<Checkbox
id="ephemeral"
checked={ephemeral}
onCheckedChange={(checked) =>
setEphemeral(checked === true)
}
onCheckedChange={(checked) => {
setEphemeral(checked === true);
}}
/>
<Label htmlFor="ephemeral" className="font-medium">
{t("profiles.ephemeral")}
@@ -746,7 +754,9 @@ export function CreateProfileDialog({
})()}
</p>
<LoadingButton
onClick={() => handleDownload("wayfern")}
onClick={() => {
void handleDownload("wayfern");
}}
isLoading={isBrowserCurrentlyDownloading(
"wayfern",
)}
@@ -848,7 +858,9 @@ export function CreateProfileDialog({
})()}
</p>
<LoadingButton
onClick={() => handleDownload("camoufox")}
onClick={() => {
void handleDownload("camoufox");
}}
isLoading={isBrowserCurrentlyDownloading(
"camoufox",
)}
@@ -955,9 +967,9 @@ export function CreateProfileDialog({
})()}
</p>
<LoadingButton
onClick={() =>
handleDownload(selectedBrowser)
}
onClick={() => {
void handleDownload(selectedBrowser);
}}
isLoading={isBrowserCurrentlyDownloading(
selectedBrowser,
)}
@@ -1014,7 +1026,9 @@ export function CreateProfileDialog({
<RippleButton
size="sm"
variant="outline"
onClick={() => setShowProxyForm(true)}
onClick={() => {
setShowProxyForm(true);
}}
className="px-2 h-7 text-xs"
>
<GoPlus className="mr-1 w-3 h-3" /> Add Proxy
@@ -1153,12 +1167,12 @@ export function CreateProfileDialog({
<div className="space-y-2">
<Label>{t("extensions.extensionGroup")}</Label>
<Select
value={selectedExtensionGroupId || "none"}
onValueChange={(val) =>
value={selectedExtensionGroupId ?? "none"}
onValueChange={(val) => {
setSelectedExtensionGroupId(
val === "none" ? undefined : val,
)
}
);
}}
>
<SelectTrigger>
<SelectValue
@@ -1190,14 +1204,16 @@ export function CreateProfileDialog({
<Input
id="profile-name"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
onChange={(e) => {
setProfileName(e.target.value);
}}
onKeyDown={(e) => {
if (
e.key === "Enter" &&
!isCreateDisabled &&
!isCreating
) {
handleCreate();
void handleCreate();
}
}}
placeholder="Enter profile name"
@@ -1251,9 +1267,9 @@ export function CreateProfileDialog({
})()}
</p>
<LoadingButton
onClick={() =>
handleDownload(selectedBrowser)
}
onClick={() => {
void handleDownload(selectedBrowser);
}}
isLoading={isBrowserCurrentlyDownloading(
selectedBrowser,
)}
@@ -1305,7 +1321,9 @@ export function CreateProfileDialog({
<RippleButton
size="sm"
variant="outline"
onClick={() => setShowProxyForm(true)}
onClick={() => {
setShowProxyForm(true);
}}
className="px-2 h-7 text-xs"
>
<GoPlus className="mr-1 w-3 h-3" /> Add Proxy
@@ -1470,7 +1488,9 @@ export function CreateProfileDialog({
</DialogContent>
<ProxyFormDialog
isOpen={showProxyForm}
onClose={() => setShowProxyForm(false)}
onClose={() => {
setShowProxyForm(false);
}}
/>
</Dialog>
);
+6 -4
View File
@@ -363,15 +363,17 @@ export function UnifiedToast(props: ToastProps) {
</>
)}
{action &&
"onClick" in (action as any) &&
"label" in (action as any) && (
"onClick" in (action as { onClick?: () => void; label?: string }) &&
"label" in (action as { onClick?: () => void; label?: string }) && (
<div className="mt-2 w-full">
<RippleButton
size="sm"
className="ml-auto"
onClick={(action as any).onClick}
onClick={
(action as { onClick: () => void; label: string }).onClick
}
>
{(action as any).label}
{(action as { onClick: () => void; label: string }).label}
</RippleButton>
</div>
)}
+4 -2
View File
@@ -40,11 +40,13 @@ function DataTableActionBar<TData>({
}
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [table]);
const portalContainer =
portalContainerProp ?? (mounted ? globalThis.document?.body : null);
portalContainerProp ?? (mounted ? globalThis.document.body : null);
if (!portalContainer) return null;
+3 -3
View File
@@ -148,9 +148,9 @@ export function DeleteGroupDialog({
<Label>What should happen to these profiles?</Label>
<RadioGroup
value={deleteAction}
onValueChange={(value) =>
setDeleteAction(value as "move" | "delete")
}
onValueChange={(value) => {
setDeleteAction(value as "move" | "delete");
}}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="move" id="move" />
+3 -1
View File
@@ -90,7 +90,9 @@ export function EditGroupDialog({
id="group-name"
placeholder="Enter group name..."
value={groupName}
onChange={(e) => setGroupName(e.target.value)}
onChange={(e) => {
setGroupName(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter" && groupName.trim()) {
void handleUpdate();
@@ -137,7 +137,7 @@ export function ExtensionGroupAssignmentDialog({
</div>
) : (
<Select
value={selectedGroupId || "none"}
value={selectedGroupId ?? "none"}
onValueChange={(value) => {
setSelectedGroupId(value === "none" ? null : value);
}}
+46 -26
View File
@@ -197,9 +197,7 @@ export function ExtensionManagementDialog({
useEffect(() => {
if (isOpen) {
void loadData().then(() => {
// Icons will be loaded after extensions are set
});
void loadData();
}
}, [isOpen, loadData]);
@@ -562,7 +560,9 @@ export function ExtensionManagementDialog({
? "border-primary text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
onClick={() => setActiveTab("extensions")}
onClick={() => {
setActiveTab("extensions");
}}
disabled={limitedMode}
>
{t("extensions.extensionsTab")}
@@ -574,7 +574,9 @@ export function ExtensionManagementDialog({
? "border-primary text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
onClick={() => setActiveTab("groups")}
onClick={() => {
setActiveTab("groups");
}}
disabled={limitedMode}
>
{t("extensions.groupsTab")}
@@ -627,13 +629,15 @@ export function ExtensionManagementDialog({
<div className="flex gap-2">
<Input
value={extensionName}
onChange={(e) => setExtensionName(e.target.value)}
onChange={(e) => {
setExtensionName(e.target.value);
}}
placeholder={t("extensions.namePlaceholder")}
className="flex-1"
/>
<RippleButton
size="sm"
onClick={handleUpload}
onClick={() => void handleUpload()}
disabled={isUploading || !extensionName.trim()}
>
{isUploading
@@ -705,7 +709,7 @@ export function ExtensionManagementDialog({
<Checkbox
checked={ext.sync_enabled}
onCheckedChange={() =>
handleToggleExtSync(ext)
void handleToggleExtSync(ext)
}
disabled={isTogglingExtSync[ext.id]}
/>
@@ -745,7 +749,9 @@ export function ExtensionManagementDialog({
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
onClick={() => setExtensionToDelete(ext)}
onClick={() => {
setExtensionToDelete(ext);
}}
>
<LuTrash2 className="w-3.5 h-3.5" />
</Button>
@@ -769,7 +775,9 @@ export function ExtensionManagementDialog({
<Label>{t("extensions.groupsTab")}</Label>
<RippleButton
size="sm"
onClick={() => setShowCreateGroup(true)}
onClick={() => {
setShowCreateGroup(true);
}}
className="flex gap-2 items-center"
disabled={limitedMode}
>
@@ -783,7 +791,9 @@ export function ExtensionManagementDialog({
<div className="flex gap-2 items-center">
<Input
value={newGroupName}
onChange={(e) => setNewGroupName(e.target.value)}
onChange={(e) => {
setNewGroupName(e.target.value);
}}
placeholder={t("extensions.groupNamePlaceholder")}
className="flex-1"
onKeyDown={(e) => {
@@ -792,7 +802,7 @@ export function ExtensionManagementDialog({
/>
<RippleButton
size="sm"
onClick={handleCreateGroup}
onClick={() => void handleCreateGroup()}
disabled={!newGroupName.trim()}
>
{t("common.buttons.create")}
@@ -902,7 +912,7 @@ export function ExtensionManagementDialog({
<Checkbox
checked={group.sync_enabled}
onCheckedChange={() =>
handleToggleGroupSync(group)
void handleToggleGroupSync(group)
}
disabled={isTogglingGroupSync[group.id]}
/>
@@ -943,7 +953,9 @@ export function ExtensionManagementDialog({
<Button
variant="ghost"
size="sm"
onClick={() => setGroupToDelete(group)}
onClick={() => {
setGroupToDelete(group);
}}
>
<LuTrash2 className="w-4 h-4" />
</Button>
@@ -996,7 +1008,9 @@ export function ExtensionManagementDialog({
<Label>{t("common.labels.name")}</Label>
<Input
value={editGroupName}
onChange={(e) => setEditGroupName(e.target.value)}
onChange={(e) => {
setEditGroupName(e.target.value);
}}
placeholder={t("extensions.groupNamePlaceholder")}
/>
</div>
@@ -1007,9 +1021,9 @@ export function ExtensionManagementDialog({
<Label>{t("extensions.addToGroup")}</Label>
<Select
value=""
onValueChange={(extId) =>
setEditGroupExtensionIds((prev) => [...prev, extId])
}
onValueChange={(extId) => {
setEditGroupExtensionIds((prev) => [...prev, extId]);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("extensions.addToGroup")} />
@@ -1055,11 +1069,11 @@ export function ExtensionManagementDialog({
variant="ghost"
size="sm"
className="h-6 w-6 p-0 shrink-0"
onClick={() =>
onClick={() => {
setEditGroupExtensionIds((prev) =>
prev.filter((id) => id !== extId),
)
}
);
}}
>
<LuTrash2 className="w-3 h-3" />
</Button>
@@ -1083,7 +1097,7 @@ export function ExtensionManagementDialog({
{t("common.buttons.cancel")}
</Button>
<RippleButton
onClick={handleSaveGroupEdits}
onClick={() => void handleSaveGroupEdits()}
disabled={!editGroupName.trim()}
>
{t("common.buttons.save")}
@@ -1117,7 +1131,9 @@ export function ExtensionManagementDialog({
<Label>{t("common.labels.name")}</Label>
<Input
value={editExtensionName}
onChange={(e) => setEditExtensionName(e.target.value)}
onChange={(e) => {
setEditExtensionName(e.target.value);
}}
placeholder={t("extensions.namePlaceholder")}
onKeyDown={(e) => {
if (e.key === "Enter") void handleUpdateExtension();
@@ -1239,7 +1255,7 @@ export function ExtensionManagementDialog({
{t("common.buttons.cancel")}
</Button>
<RippleButton
onClick={handleUpdateExtension}
onClick={() => void handleUpdateExtension()}
disabled={!editExtensionName.trim()}
>
{t("common.buttons.save")}
@@ -1251,7 +1267,9 @@ export function ExtensionManagementDialog({
{/* Delete extension confirmation */}
<DeleteConfirmationDialog
isOpen={extensionToDelete !== null}
onClose={() => setExtensionToDelete(null)}
onClose={() => {
setExtensionToDelete(null);
}}
onConfirm={handleDeleteExtension}
title={t("extensions.deleteConfirmTitle")}
description={t("extensions.deleteConfirmDescription", {
@@ -1263,7 +1281,9 @@ export function ExtensionManagementDialog({
{/* Delete group confirmation */}
<DeleteConfirmationDialog
isOpen={groupToDelete !== null}
onClose={() => setGroupToDelete(null)}
onClose={() => {
setGroupToDelete(null);
}}
onConfirm={handleDeleteGroup}
title={t("extensions.deleteGroupConfirmTitle")}
description={t("extensions.deleteGroupConfirmDescription", {
+7 -3
View File
@@ -144,7 +144,9 @@ export function GroupAssignmentDialog({
size="sm"
variant="outline"
className="h-7 px-2 text-xs"
onClick={() => setCreateDialogOpen(true)}
onClick={() => {
setCreateDialogOpen(true);
}}
>
<GoPlus className="mr-1 w-3 h-3" /> Create Group
</RippleButton>
@@ -155,7 +157,7 @@ export function GroupAssignmentDialog({
</div>
) : (
<Select
value={selectedGroupId || "default"}
value={selectedGroupId ?? "default"}
onValueChange={(value) => {
setSelectedGroupId(value === "default" ? null : value);
}}
@@ -201,7 +203,9 @@ export function GroupAssignmentDialog({
</DialogContent>
<CreateGroupDialog
isOpen={createDialogOpen}
onClose={() => setCreateDialogOpen(false)}
onClose={() => {
setCreateDialogOpen(false);
}}
onGroupCreated={(group) => {
setGroups((prev) => [...prev, group]);
setSelectedGroupId(group.id);
+18 -6
View File
@@ -246,7 +246,9 @@ export function GroupManagementDialog({
<Label>Groups</Label>
<RippleButton
size="sm"
onClick={() => setCreateDialogOpen(true)}
onClick={() => {
setCreateDialogOpen(true);
}}
className="flex gap-2 items-center"
>
<GoPlus className="w-4 h-4" />
@@ -350,7 +352,9 @@ export function GroupManagementDialog({
<Button
variant="ghost"
size="sm"
onClick={() => handleEditGroup(group)}
onClick={() => {
handleEditGroup(group);
}}
>
<LuPencil className="w-4 h-4" />
</Button>
@@ -364,7 +368,9 @@ export function GroupManagementDialog({
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteGroup(group)}
onClick={() => {
handleDeleteGroup(group);
}}
>
<LuTrash2 className="w-4 h-4" />
</Button>
@@ -395,20 +401,26 @@ export function GroupManagementDialog({
<CreateGroupDialog
isOpen={createDialogOpen}
onClose={() => setCreateDialogOpen(false)}
onClose={() => {
setCreateDialogOpen(false);
}}
onGroupCreated={handleGroupCreated}
/>
<EditGroupDialog
isOpen={editDialogOpen}
onClose={() => setEditDialogOpen(false)}
onClose={() => {
setEditDialogOpen(false);
}}
group={selectedGroup}
onGroupUpdated={handleGroupUpdated}
/>
<DeleteGroupDialog
isOpen={deleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
onClose={() => {
setDeleteDialogOpen(false);
}}
group={selectedGroup}
onGroupDeleted={handleGroupDeleted}
/>
+17 -7
View File
@@ -166,7 +166,7 @@ function useLogoEasterEgg() {
};
}
type Props = {
interface Props {
onSettingsDialogOpen: (open: boolean) => void;
onProxyManagementDialogOpen: (open: boolean) => void;
onGroupManagementDialogOpen: (open: boolean) => void;
@@ -177,7 +177,7 @@ type Props = {
onExtensionManagementDialogOpen: (open: boolean) => void;
searchQuery: string;
onSearchQueryChange: (query: string) => void;
};
}
const HomeHeader = ({
onSettingsDialogOpen,
@@ -211,9 +211,15 @@ const HomeHeader = ({
type="button"
className="p-1 cursor-pointer select-none"
onClick={handleClick}
onPointerDown={() => setIsPressed(true)}
onPointerUp={() => setIsPressed(false)}
onPointerLeave={() => setIsPressed(false)}
onPointerDown={() => {
setIsPressed(true);
}}
onPointerUp={() => {
setIsPressed(false);
}}
onPointerLeave={() => {
setIsPressed(false);
}}
>
<Logo
key={wobbleKey}
@@ -238,14 +244,18 @@ const HomeHeader = ({
type="text"
placeholder={t("header.searchPlaceholder")}
value={searchQuery}
onChange={(e) => onSearchQueryChange(e.target.value)}
onChange={(e) => {
onSearchQueryChange(e.target.value);
}}
className="pr-8 pl-10 w-48"
/>
<LuSearch className="absolute left-3 top-1/2 w-4 h-4 transform -translate-y-1/2 text-muted-foreground" />
{searchQuery && (
<button
type="button"
onClick={() => onSearchQueryChange("")}
onClick={() => {
onSearchQueryChange("");
}}
className="absolute right-2 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
aria-label={t("header.clearSearch")}
>
+22 -16
View File
@@ -53,7 +53,7 @@ export function IntegrationsDialog({
});
const [apiServerPort, setApiServerPort] = useState<number | null>(null);
const [mcpConfig, setMcpConfig] = useState<McpConfig | null>(null);
const [_mcpRunning, setMcpRunning] = useState(false);
const [, setMcpRunning] = useState(false);
const [showApiToken, setShowApiToken] = useState(false);
const [showMcpToken, setShowMcpToken] = useState(false);
const [isApiStarting, setIsApiStarting] = useState(false);
@@ -119,12 +119,12 @@ export function IntegrationsDialog({
useEffect(() => {
if (isOpen) {
loadSettings();
loadApiServerStatus();
loadMcpConfig();
loadMcpServerStatus();
loadClaudeDesktopStatus();
loadClaudeCodeStatus();
void loadSettings();
void loadApiServerStatus();
void loadMcpConfig();
void loadMcpServerStatus();
void loadClaudeDesktopStatus();
void loadClaudeCodeStatus();
}
}, [
isOpen,
@@ -177,7 +177,7 @@ export function IntegrationsDialog({
settings: { ...settings, mcp_enabled: true, mcp_port: port },
});
setSettings(next);
loadMcpConfig();
void loadMcpConfig();
showSuccessToast(`MCP server started on port ${port}`);
} else {
await invoke("stop_mcp_server");
@@ -198,11 +198,13 @@ export function IntegrationsDialog({
}
};
const _obfuscateToken = (token: string) =>
"•".repeat(Math.min(token.length, 32));
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) onClose();
}}
>
<DialogContent className="max-w-xl max-h-[80vh] my-8 flex flex-col">
<DialogHeader className="shrink-0">
<DialogTitle>Integrations</DialogTitle>
@@ -221,7 +223,7 @@ export function IntegrationsDialog({
id="api-enabled"
checked={apiServerPort !== null}
disabled={isApiStarting}
onCheckedChange={handleApiToggle}
onCheckedChange={(checked) => void handleApiToggle(!!checked)}
/>
<div className="grid gap-1.5 leading-none">
<Label
@@ -269,7 +271,9 @@ export function IntegrationsDialog({
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
onClick={() => setShowApiToken(!showApiToken)}
onClick={() => {
setShowApiToken(!showApiToken);
}}
>
{showApiToken ? (
<EyeOff className="h-4 w-4" />
@@ -297,7 +301,7 @@ export function IntegrationsDialog({
id="mcp-enabled"
checked={settings.mcp_enabled && mcpConfig !== null}
disabled={!termsAccepted || isMcpStarting}
onCheckedChange={handleMcpToggle}
onCheckedChange={(checked) => void handleMcpToggle(!!checked)}
/>
<div className="grid gap-1.5 leading-none">
<Label
@@ -336,7 +340,9 @@ export function IntegrationsDialog({
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
onClick={() => setShowMcpToken(!showMcpToken)}
onClick={() => {
setShowMcpToken(!showMcpToken);
}}
>
{showMcpToken ? (
<EyeOff className="h-4 w-4" />
+9 -3
View File
@@ -62,9 +62,15 @@ export function LaunchOnLoginDialog({
<Dialog open={isOpen}>
<DialogContent
className="sm:max-w-sm"
onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => {
e.preventDefault();
}}
onPointerDownOutside={(e) => {
e.preventDefault();
}}
onInteractOutside={(e) => {
e.preventDefault();
}}
>
<DialogHeader>
<DialogTitle>Enable Launch on Login?</DialogTitle>
+42 -16
View File
@@ -62,13 +62,17 @@ export function LocationProxyDialog({
useEffect(() => {
if (!isOpen) return;
setIsLoadingCountries(true);
invoke<LocationItem[]>("cloud_get_countries")
.then((data) => setCountries(data))
void invoke<LocationItem[]>("cloud_get_countries")
.then((data) => {
setCountries(data);
})
.catch((err) => {
console.error("Failed to fetch countries:", err);
toast.error("Failed to load countries");
})
.finally(() => setIsLoadingCountries(false));
.finally(() => {
setIsLoadingCountries(false);
});
}, [isOpen]);
// Fetch regions when country changes
@@ -83,10 +87,18 @@ export function LocationProxyDialog({
setSelectedIsp("");
setCities([]);
setIsps([]);
invoke<LocationItem[]>("cloud_get_regions", { country: selectedCountry })
.then((data) => setRegions(data))
.catch((err) => console.error("Failed to fetch regions:", err))
.finally(() => setIsLoadingRegions(false));
void invoke<LocationItem[]>("cloud_get_regions", {
country: selectedCountry,
})
.then((data) => {
setRegions(data);
})
.catch((err) => {
console.error("Failed to fetch regions:", err);
})
.finally(() => {
setIsLoadingRegions(false);
});
}, [selectedCountry]);
// Fetch cities when country or region changes (cities can be loaded without region)
@@ -103,10 +115,16 @@ export function LocationProxyDialog({
if (selectedRegion) {
args.region = selectedRegion;
}
invoke<LocationItem[]>("cloud_get_cities", args)
.then((data) => setCities(data))
.catch((err) => console.error("Failed to fetch cities:", err))
.finally(() => setIsLoadingCities(false));
void invoke<LocationItem[]>("cloud_get_cities", args)
.then((data) => {
setCities(data);
})
.catch((err) => {
console.error("Failed to fetch cities:", err);
})
.finally(() => {
setIsLoadingCities(false);
});
}, [selectedCountry, selectedRegion]);
// Fetch ISPs when country/region/city changes
@@ -122,10 +140,16 @@ export function LocationProxyDialog({
};
if (selectedRegion) args.region = selectedRegion;
if (selectedCity) args.city = selectedCity;
invoke<LocationItem[]>("cloud_get_isps", args)
.then((data) => setIsps(data))
.catch((err) => console.error("Failed to fetch ISPs:", err))
.finally(() => setIsLoadingIsps(false));
void invoke<LocationItem[]>("cloud_get_isps", args)
.then((data) => {
setIsps(data);
})
.catch((err) => {
console.error("Failed to fetch ISPs:", err);
})
.finally(() => {
setIsLoadingIsps(false);
});
}, [selectedCountry, selectedRegion, selectedCity]);
// Auto-generate name from selections
@@ -302,7 +326,9 @@ export function LocationProxyDialog({
<Label>Name</Label>
<Input
value={proxyName}
onChange={(e) => setProxyName(e.target.value)}
onChange={(e) => {
setProxyName(e.target.value);
}}
placeholder="Proxy name"
/>
</div>
+17 -17
View File
@@ -19,9 +19,7 @@ export interface Option {
/** Group the options by providing key. */
[key: string]: string | boolean | undefined;
}
interface GroupOption {
[key: string]: Option[];
}
type GroupOption = Record<string, Option[]>;
interface MultipleSelectorProps {
value?: Option[];
@@ -77,12 +75,13 @@ export interface MultipleSelectorRef {
input: HTMLInputElement;
}
// eslint-disable-next-line react-refresh/only-export-components
export function useDebounce<T>(value: T, delay?: number): T {
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay ?? 500);
return () => {
clearTimeout(timer);
@@ -104,11 +103,11 @@ function transToGroupOption(options: Option[], groupBy?: string) {
const groupOption: GroupOption = {};
options.forEach((option) => {
const key = (option[groupBy] as string) || "";
const key = (option[groupBy] as string) ?? "";
if (!groupOption[key]) {
groupOption[key] = [option];
} else {
groupOption[key]?.push(option);
groupOption[key].push(option);
}
});
return groupOption;
@@ -197,12 +196,12 @@ const MultipleSelector = React.forwardRef<
const [open, setOpen] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(false);
const [selected, setSelected] = React.useState<Option[]>(value || []);
const [selected, setSelected] = React.useState<Option[]>(value ?? []);
const [options, setOptions] = React.useState<GroupOption>(
transToGroupOption(arrayDefaultOptions, groupBy),
);
const [inputValue, setInputValue] = React.useState("");
const debouncedSearchTerm = useDebounce(inputValue, delay || 500);
const debouncedSearchTerm = useDebounce(inputValue, delay ?? 500);
React.useImperativeHandle(
ref,
@@ -231,7 +230,7 @@ const MultipleSelector = React.forwardRef<
if (input.value === "" && selected.length > 0) {
const lastSelectOption = selected[selected.length - 1];
// If last item is fixed, we should not remove it.
if (!lastSelectOption?.fixed) {
if (!lastSelectOption.fixed) {
// biome-ignore lint/style/noNonNullAssertion: false positive
handleUnselect(selected.at(-1)!);
}
@@ -257,7 +256,7 @@ const MultipleSelector = React.forwardRef<
if (!arrayOptions || onSearch) {
return;
}
const newOption = transToGroupOption(arrayOptions || [], groupBy);
const newOption = transToGroupOption(arrayOptions, groupBy);
if (JSON.stringify(newOption) !== JSON.stringify(options)) {
setOptions(newOption);
}
@@ -267,7 +266,7 @@ const MultipleSelector = React.forwardRef<
const doSearch = async () => {
setIsLoading(true);
const res = await onSearch?.(debouncedSearchTerm);
setOptions(transToGroupOption(res || [], groupBy));
setOptions(transToGroupOption(res ?? [], groupBy));
setIsLoading(false);
};
@@ -284,7 +283,6 @@ const MultipleSelector = React.forwardRef<
};
void exec();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus, onSearch]);
const CreatableItem = () => {
@@ -414,14 +412,14 @@ const MultipleSelector = React.forwardRef<
badgeClassName,
)}
data-fixed={option.fixed}
data-disabled={disabled || undefined}
data-disabled={disabled ?? undefined}
>
{option.label ?? option.value}
<button
type="button"
className={cn(
"cursor-pointer ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2",
(disabled || option.fixed) && "hidden",
(disabled ?? option.fixed) && "hidden",
)}
onKeyDown={(e) => {
if (e.key === "Enter") {
@@ -432,7 +430,9 @@ const MultipleSelector = React.forwardRef<
e.preventDefault();
e.stopPropagation();
}}
onClick={() => handleUnselect(option)}
onClick={() => {
handleUnselect(option);
}}
>
<LuX className="w-3 h-3 text-muted-foreground hover:text-foreground" />
</button>
@@ -490,7 +490,7 @@ const MultipleSelector = React.forwardRef<
onFocus={(event) => {
setOpen(true);
if (triggerSearchOnFocus && onSearch) {
onSearch(debouncedSearchTerm);
void onSearch(debouncedSearchTerm);
}
inputProps?.onFocus?.(event);
}}
+3 -1
View File
@@ -156,7 +156,9 @@ export function PermissionDialog({
<LoadingButton
isLoading={isRequesting}
onClick={() => {
handleRequestPermission().catch(console.error);
handleRequestPermission().catch((err: unknown) => {
console.error(err);
});
}}
className="min-w-24"
>
+106 -61
View File
@@ -102,7 +102,7 @@ import { RippleButton } from "./ui/ripple";
// Stable table meta type to pass volatile state/handlers into TanStack Table without
// causing column definitions to be recreated on every render.
type TableMeta = {
interface TableMeta {
t: (key: string, options?: Record<string, unknown>) => string;
selectedProfiles: string[];
selectableCount: number;
@@ -216,14 +216,14 @@ type TableMeta = {
}
| undefined;
onLaunchWithSync: (profile: BrowserProfile) => void;
};
}
type SyncStatusDot = {
interface SyncStatusDot {
color: string;
tooltip: string;
animate: boolean;
encrypted: boolean;
};
}
function getProfileSyncStatusDot(
profile: BrowserProfile,
@@ -436,7 +436,9 @@ const TagsCell = React.memo<{
}
};
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
return () => {
document.removeEventListener("mousedown", handleClick);
};
}, [openTagsEditorFor, profile.id, setOpenTagsEditorFor]);
React.useEffect(() => {
@@ -444,7 +446,7 @@ const TagsCell = React.memo<{
// Focus the inner input of MultipleSelector on open
const inputEl = editorRef.current.querySelector("input");
if (inputEl) {
(inputEl as HTMLInputElement).focus();
inputEl.focus();
}
}
}, [openTagsEditorFor, profile.id]);
@@ -537,8 +539,12 @@ const TagsCell = React.memo<{
onKeyDown: (e) => {
if (e.key === "Escape") setOpenTagsEditorFor(null);
},
onFocus: () => setIsFocused(true),
onBlur: () => setIsFocused(false),
onFocus: () => {
setIsFocused(true);
},
onBlur: () => {
setIsFocused(false);
},
}}
/>
</div>
@@ -569,8 +575,12 @@ const NonHoverableTooltip = React.memo<{
<Tooltip open={isOpen} onOpenChange={setIsOpen}>
<TooltipTrigger
asChild
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
onMouseEnter={() => {
setIsOpen(true);
}}
onMouseLeave={() => {
setIsOpen(false);
}}
>
{children}
</TooltipTrigger>
@@ -578,8 +588,12 @@ const NonHoverableTooltip = React.memo<{
sideOffset={sideOffset}
alignOffset={alignOffset}
arrowOffset={horizontalOffset}
onPointerEnter={(e) => e.preventDefault()}
onPointerLeave={() => setIsOpen(false)}
onPointerEnter={(e) => {
e.preventDefault();
}}
onPointerLeave={() => {
setIsOpen(false);
}}
className="pointer-events-none"
style={
horizontalOffset !== 0
@@ -623,7 +637,7 @@ const NoteCell = React.memo<{
const onNoteChange = React.useCallback(
async (newNote: string | null) => {
const trimmedNote = newNote?.trim() || null;
const trimmedNote = newNote?.trim() ?? null;
setNoteOverrides((prev) => ({ ...prev, [profile.id]: trimmedNote }));
try {
await invoke<BrowserProfile>("update_profile_note", {
@@ -639,12 +653,12 @@ const NoteCell = React.memo<{
const editorRef = React.useRef<HTMLDivElement | null>(null);
const textareaRef = React.useRef<HTMLTextAreaElement | null>(null);
const [noteValue, setNoteValue] = React.useState(effectiveNote || "");
const [noteValue, setNoteValue] = React.useState(effectiveNote ?? "");
// Update local state when effective note changes (from outside)
React.useEffect(() => {
if (openNoteEditorFor !== profile.id) {
setNoteValue(effectiveNote || "");
setNoteValue(effectiveNote ?? "");
}
}, [effectiveNote, openNoteEditorFor, profile.id]);
@@ -678,13 +692,15 @@ const NoteCell = React.memo<{
target &&
!editorRef.current.contains(target)
) {
const currentValue = textareaRef.current?.value || "";
const currentValue = textareaRef.current?.value ?? "";
void onNoteChange(currentValue);
setOpenNoteEditorFor(null);
}
};
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
return () => {
document.removeEventListener("mousedown", handleClick);
};
}, [openNoteEditorFor, profile.id, setOpenNoteEditorFor, onNoteChange]);
React.useEffect(() => {
@@ -696,7 +712,7 @@ const NoteCell = React.memo<{
}
}, [openNoteEditorFor, profile.id]);
const displayNote = effectiveNote || "";
const displayNote = effectiveNote ?? "";
const trimmedNote =
displayNote.length > 12 ? `${displayNote.slice(0, 12)}...` : displayNote;
const showTooltip = displayNote.length > 12 || displayNote.length > 0;
@@ -716,7 +732,7 @@ const NoteCell = React.memo<{
)}
onClick={() => {
if (!isDisabled) {
setNoteValue(effectiveNote || "");
setNoteValue(effectiveNote ?? "");
setOpenNoteEditorFor(profile.id);
}
}}
@@ -734,7 +750,7 @@ const NoteCell = React.memo<{
{showTooltip && (
<TooltipContent className="max-w-[320px]">
<p className="whitespace-pre-wrap wrap-break-word">
{effectiveNote || "No Note"}
{effectiveNote ?? "No Note"}
</p>
</TooltipContent>
)}
@@ -760,7 +776,7 @@ const NoteCell = React.memo<{
onChange={handleTextareaChange}
onKeyDown={(e) => {
if (e.key === "Escape") {
setNoteValue(effectiveNote || "");
setNoteValue(effectiveNote ?? "");
setOpenNoteEditorFor(null);
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
void onNoteChange(noteValue);
@@ -1100,14 +1116,13 @@ export function ProfilesDataTable({
isUpdating,
launchingProfiles,
stoppingProfiles,
crossOsUnlocked,
);
// Listen for sync status events
React.useEffect(() => {
if (!browserState.isClient) return;
let unlisten: (() => void) | undefined;
(async () => {
void (async () => {
try {
unlisten = await listen<{
profile_id: string;
@@ -1168,8 +1183,12 @@ export function ProfilesDataTable({
};
void fetchTrafficSnapshots();
const interval = setInterval(fetchTrafficSnapshots, 1000);
return () => clearInterval(interval);
const interval = setInterval(() => {
void fetchTrafficSnapshots();
}, 1000);
return () => {
clearInterval(interval);
};
}, [browserState.isClient, runningCount, runningProfileIds]);
// Clean up snapshots for profiles that are no longer running
@@ -1196,7 +1215,7 @@ export function ProfilesDataTable({
React.useEffect(() => {
if (!browserState.isClient) return;
let unlisten: (() => void) | undefined;
(async () => {
void (async () => {
try {
unlisten = await listen<{ id: string; is_running: boolean }>(
"profile-running-changed",
@@ -1231,7 +1250,7 @@ export function ProfilesDataTable({
React.useEffect(() => {
if (!browserState.isClient) return;
let unlisten: (() => void) | undefined;
(async () => {
void (async () => {
try {
unlisten = await listen("stored-proxies-changed", () => {
// Also refresh tags on profile updates
@@ -1521,7 +1540,11 @@ export function ProfilesDataTable({
// Overflow actions
onAssignProfilesToGroup,
onCloneProfile,
onCloneProfile: onCloneProfile
? (profile: BrowserProfile) => {
void onCloneProfile(profile);
}
: undefined,
onConfigureCamoufox,
onCopyCookiesToProfile,
onOpenCookieManagement,
@@ -1553,7 +1576,11 @@ export function ProfilesDataTable({
// Synchronizer
getProfileSyncInfo: getProfileSyncInfo ?? (() => undefined),
onLaunchWithSync: onLaunchWithSync ?? (() => {}),
onLaunchWithSync:
onLaunchWithSync ??
(() => {
/* empty */
}),
}),
[
t,
@@ -1625,7 +1652,9 @@ export function ProfilesDataTable({
meta.selectedProfiles.length === meta.selectableCount &&
meta.selectableCount !== 0
}
onCheckedChange={(value) => meta.handleToggleAll(!!value)}
onCheckedChange={(value) => {
meta.handleToggleAll(!!value);
}}
aria-label="Select all"
className="cursor-pointer"
/>
@@ -1669,7 +1698,9 @@ export function ProfilesDataTable({
<button
type="button"
className="flex justify-center items-center p-0 border-none cursor-pointer"
onClick={() => meta.handleIconClick(profile.id)}
onClick={() => {
meta.handleIconClick(profile.id);
}}
aria-label="Select profile"
>
<span className="w-4 h-4 group">
@@ -1705,9 +1736,9 @@ export function ProfilesDataTable({
<span className="flex justify-center items-center w-4 h-4">
<Checkbox
checked={isSelected}
onCheckedChange={(value) =>
meta.handleCheckboxChange(profile.id, !!value)
}
onCheckedChange={(value) => {
meta.handleCheckboxChange(profile.id, !!value);
}}
aria-label="Select row"
className="w-4 h-4"
/>
@@ -1753,9 +1784,9 @@ export function ProfilesDataTable({
<span className="flex justify-center items-center w-4 h-4">
<Checkbox
checked={isSelected}
onCheckedChange={(value) =>
meta.handleCheckboxChange(profile.id, !!value)
}
onCheckedChange={(value) => {
meta.handleCheckboxChange(profile.id, !!value);
}}
aria-label="Select row"
className="w-4 h-4"
/>
@@ -1774,7 +1805,9 @@ export function ProfilesDataTable({
<button
type="button"
className="flex justify-center items-center p-0 border-none cursor-pointer"
onClick={() => meta.handleIconClick(profile.id)}
onClick={() => {
meta.handleIconClick(profile.id);
}}
aria-label="Select profile"
>
<span className="w-4 h-4 group">
@@ -1848,12 +1881,12 @@ export function ProfilesDataTable({
const syncInfo = meta.getProfileSyncInfo(profile.id);
const isLeader = syncInfo?.isLeader === true;
const isFollower = syncInfo?.isLeader === false;
const isDesynced = isFollower && syncInfo?.failedAtUrl != null;
const isDesynced = isFollower && syncInfo.failedAtUrl != null;
const stopTooltip = isLeader
? meta.t("profiles.synchronizer.stopLeader")
: isFollower
? meta.t("profiles.synchronizer.stopFollower", {
leaderName: syncInfo?.session.leader_profile_name ?? "",
leaderName: syncInfo.session.leader_profile_name ?? "",
})
: tooltipContent;
@@ -1920,7 +1953,7 @@ export function ProfilesDataTable({
onClick={() =>
isRunning
? void handleStop()
: handleProfileLaunch(profile)
: void handleProfileLaunch(profile)
}
>
{isLaunching || isStopping ? (
@@ -1951,9 +1984,9 @@ export function ProfilesDataTable({
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
onClick={() => {
column.toggleSorting(column.getIsSorted() === "asc");
}}
className="justify-start p-0 h-auto font-semibold text-left cursor-pointer"
>
Name
@@ -2102,10 +2135,10 @@ export function ProfilesDataTable({
<TagsCell
profile={profile}
isDisabled={isDisabled}
tagsOverrides={meta.tagsOverrides || {}}
allTags={meta.allTags || []}
tagsOverrides={meta.tagsOverrides ?? {}}
allTags={meta.allTags ?? []}
setAllTags={meta.setAllTags}
openTagsEditorFor={meta.openTagsEditorFor || null}
openTagsEditorFor={meta.openTagsEditorFor ?? null}
setOpenTagsEditorFor={meta.setOpenTagsEditorFor}
setTagsOverrides={meta.setTagsOverrides}
/>
@@ -2131,8 +2164,8 @@ export function ProfilesDataTable({
<NoteCell
profile={profile}
isDisabled={isDisabled}
noteOverrides={meta.noteOverrides || {}}
openNoteEditorFor={meta.openNoteEditorFor || null}
noteOverrides={meta.noteOverrides ?? {}}
openNoteEditorFor={meta.openNoteEditorFor ?? null}
setOpenNoteEditorFor={meta.setOpenNoteEditorFor}
setNoteOverrides={meta.setNoteOverrides}
/>
@@ -2196,12 +2229,12 @@ export function ProfilesDataTable({
? [...snapshot.recent_bandwidth]
: [];
const currentBandwidth =
(snapshot?.current_bytes_sent || 0) +
(snapshot?.current_bytes_received || 0);
(snapshot?.current_bytes_sent ?? 0) +
(snapshot?.current_bytes_received ?? 0);
return (
<BandwidthMiniChart
key={`${profile.id}-${snapshot?.last_update || 0}-${bandwidthData.length}`}
key={`${profile.id}-${snapshot?.last_update ?? 0}-${bandwidthData.length}`}
data={bandwidthData}
currentBandwidth={currentBandwidth}
onClick={() => meta.onOpenTrafficDialog?.(profile.id)}
@@ -2213,9 +2246,9 @@ export function ProfilesDataTable({
<div className="flex gap-2 items-center">
<Popover
open={isSelectorOpen}
onOpenChange={(open) =>
meta.setOpenProxySelectorFor(open ? profile.id : null)
}
onOpenChange={(open) => {
meta.setOpenProxySelectorFor(open ? profile.id : null);
}}
>
<Tooltip>
<TooltipTrigger asChild>
@@ -2468,7 +2501,9 @@ export function ProfilesDataTable({
variant="ghost"
className="p-0 w-8 h-8"
disabled={!meta.isClient}
onClick={() => setProfileForInfoDialog(profile)}
onClick={() => {
setProfileForInfoDialog(profile);
}}
>
<span className="sr-only">Profile info</span>
<LuInfo className="w-4 h-4" />
@@ -2598,7 +2633,9 @@ export function ProfilesDataTable({
</ScrollArea>
<DeleteConfirmationDialog
isOpen={profileToDelete !== null}
onClose={() => setProfileToDelete(null)}
onClose={() => {
setProfileToDelete(null);
}}
onConfirm={handleDelete}
title="Delete Profile"
description={`This action cannot be undone. This will permanently delete the profile "${profileToDelete?.name}" and all its associated data.`}
@@ -2618,7 +2655,9 @@ export function ProfilesDataTable({
return (
<ProfileInfoDialog
isOpen={profileForInfoDialog !== null}
onClose={() => setProfileForInfoDialog(null)}
onClose={() => {
setProfileForInfoDialog(null);
}}
profile={infoProfile}
storedProxies={storedProxies}
vpnConfigs={vpnConfigs}
@@ -2632,7 +2671,9 @@ export function ProfilesDataTable({
onCopyCookiesToProfile={onCopyCookiesToProfile}
onOpenCookieManagement={onOpenCookieManagement}
onAssignExtensionGroup={onAssignExtensionGroup}
onOpenBypassRules={(profile) => setBypassRulesProfile(profile)}
onOpenBypassRules={(profile) => {
setBypassRulesProfile(profile);
}}
onCloneProfile={onCloneProfile}
onLaunchWithSync={onLaunchWithSync}
onDeleteProfile={(profile) => {
@@ -2700,14 +2741,18 @@ export function ProfilesDataTable({
{trafficDialogProfile && (
<TrafficDetailsDialog
isOpen={trafficDialogProfile !== null}
onClose={() => setTrafficDialogProfile(null)}
onClose={() => {
setTrafficDialogProfile(null);
}}
profileId={trafficDialogProfile.id}
profileName={trafficDialogProfile.name}
/>
)}
<ProfileBypassRulesDialog
isOpen={bypassRulesProfile !== null}
onClose={() => setBypassRulesProfile(null)}
onClose={() => {
setBypassRulesProfile(null);
}}
profileId={bypassRulesProfile?.id ?? null}
initialRules={bypassRulesProfile?.proxy_bypass_rules ?? []}
/>
+59 -22
View File
@@ -131,7 +131,7 @@ export function ProfileInfoDialog({
setGroupName(null);
return;
}
(async () => {
void (async () => {
try {
const groups = await invoke<ProfileGroup[]>("get_groups");
const group = groups.find((g) => g.id === profile.group_id);
@@ -147,7 +147,7 @@ export function ProfileInfoDialog({
setExtensionGroupName(null);
return;
}
(async () => {
void (async () => {
try {
const group = await invoke<{ name: string } | null>(
"get_extension_group_for_profile",
@@ -195,7 +195,9 @@ export function ProfileInfoDialog({
try {
await navigator.clipboard.writeText(profile.id);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
setTimeout(() => {
setCopied(false);
}, 2000);
} catch {
// ignore
}
@@ -213,7 +215,7 @@ export function ProfileInfoDialog({
const hasNote = !!profile.note;
const showCrossOs = isCrossOsProfile(profile);
type ActionItem = {
interface ActionItem {
icon: React.ReactNode;
label: string;
onClick: () => void;
@@ -222,34 +224,41 @@ export function ProfileInfoDialog({
proBadge?: boolean;
runningBadge?: boolean;
hidden?: boolean;
};
}
const actions: ActionItem[] = [
{
icon: <LuGlobe className="w-4 h-4" />,
label: t("profiles.actions.viewNetwork"),
onClick: () => handleAction(() => onOpenTrafficDialog?.(profile.id)),
onClick: () => {
handleAction(() => onOpenTrafficDialog?.(profile.id));
},
disabled: isCrossOs,
},
{
icon: <LuRefreshCw className="w-4 h-4" />,
label: t("profiles.actions.syncSettings"),
onClick: () => handleAction(() => onOpenProfileSyncDialog?.(profile)),
onClick: () => {
handleAction(() => onOpenProfileSyncDialog?.(profile));
},
disabled: isCrossOs,
hidden: profile.ephemeral === true,
},
{
icon: <LuGroup className="w-4 h-4" />,
label: t("profiles.actions.assignToGroup"),
onClick: () =>
handleAction(() => onAssignProfilesToGroup?.([profile.id])),
onClick: () => {
handleAction(() => onAssignProfilesToGroup?.([profile.id]));
},
disabled: isDisabled,
runningBadge: isRunning,
},
{
icon: <LuFingerprint className="w-4 h-4" />,
label: t("profiles.actions.changeFingerprint"),
onClick: () => handleAction(() => onConfigureCamoufox?.(profile)),
onClick: () => {
handleAction(() => onConfigureCamoufox?.(profile));
},
disabled: isDisabled,
runningBadge: isRunning,
hidden: !isCamoufoxOrWayfern || !onConfigureCamoufox,
@@ -257,7 +266,9 @@ export function ProfileInfoDialog({
{
icon: <LuUsers className="w-4 h-4" />,
label: t("profiles.synchronizer.launchWithSync"),
onClick: () => handleAction(() => onLaunchWithSync?.(profile)),
onClick: () => {
handleAction(() => onLaunchWithSync?.(profile));
},
disabled: isDisabled || isRunning || !crossOsUnlocked,
proBadge: !crossOsUnlocked,
hidden: profile.browser !== "wayfern" || !onLaunchWithSync,
@@ -265,7 +276,9 @@ export function ProfileInfoDialog({
{
icon: <LuCopy className="w-4 h-4" />,
label: t("profiles.actions.copyCookiesToProfile"),
onClick: () => handleAction(() => onCopyCookiesToProfile?.(profile)),
onClick: () => {
handleAction(() => onCopyCookiesToProfile?.(profile));
},
disabled: isDisabled,
runningBadge: isRunning,
hidden:
@@ -276,7 +289,9 @@ export function ProfileInfoDialog({
{
icon: <LuCookie className="w-4 h-4" />,
label: t("profileInfo.actions.manageCookies"),
onClick: () => handleAction(() => onOpenCookieManagement?.(profile)),
onClick: () => {
handleAction(() => onOpenCookieManagement?.(profile));
},
disabled: isDisabled,
runningBadge: isRunning,
hidden:
@@ -287,7 +302,9 @@ export function ProfileInfoDialog({
{
icon: <LuSettings className="w-4 h-4" />,
label: t("profiles.actions.clone"),
onClick: () => handleAction(() => onCloneProfile?.(profile)),
onClick: () => {
handleAction(() => onCloneProfile?.(profile));
},
disabled: isDisabled,
runningBadge: isRunning,
hidden: profile.ephemeral === true,
@@ -295,7 +312,9 @@ export function ProfileInfoDialog({
{
icon: <LuPuzzle className="w-4 h-4" />,
label: t("profileInfo.actions.assignExtensionGroup"),
onClick: () => handleAction(() => onAssignExtensionGroup?.([profile.id])),
onClick: () => {
handleAction(() => onAssignExtensionGroup?.([profile.id]));
},
disabled: isDisabled || !crossOsUnlocked,
proBadge: !crossOsUnlocked,
runningBadge: isRunning && crossOsUnlocked,
@@ -304,12 +323,16 @@ export function ProfileInfoDialog({
{
icon: <LuShieldCheck className="w-4 h-4" />,
label: t("profileInfo.network.bypassRulesTitle"),
onClick: () => handleAction(() => onOpenBypassRules?.(profile)),
onClick: () => {
handleAction(() => onOpenBypassRules?.(profile));
},
},
{
icon: <LuTrash2 className="w-4 h-4" />,
label: t("profiles.actions.delete"),
onClick: () => handleAction(() => onDeleteProfile?.(profile)),
onClick: () => {
handleAction(() => onDeleteProfile?.(profile));
},
disabled: isDeleteDisabled,
destructive: true,
},
@@ -318,7 +341,12 @@ export function ProfileInfoDialog({
const visibleActions = actions.filter((a) => !a.hidden);
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) onClose();
}}
>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{t("profileInfo.title")}</DialogTitle>
@@ -443,7 +471,7 @@ export function ProfileInfoDialog({
>
{syncMode === "Disabled"
? t("sync.mode.disabled")
: syncStatus?.status === "syncing"
: syncStatus.status === "syncing"
? t("common.status.syncing")
: t("common.status.synced")}
</Badge>
@@ -585,7 +613,12 @@ export function ProfileBypassRulesDialog({
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) onClose();
}}
>
<DialogContent className="sm:max-w-lg max-h-[80vh] flex flex-col">
<DialogHeader className="shrink-0">
<DialogTitle>{t("profileInfo.network.bypassRulesTitle")}</DialogTitle>
@@ -598,7 +631,9 @@ export function ProfileBypassRulesDialog({
<div className="flex gap-2">
<Input
value={newRule}
onChange={(e) => setNewRule(e.target.value)}
onChange={(e) => {
setNewRule(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") handleAddRule();
}}
@@ -628,7 +663,9 @@ export function ProfileBypassRulesDialog({
<span className="font-mono text-xs truncate">{rule}</span>
<button
type="button"
onClick={() => handleRemoveRule(rule)}
onClick={() => {
handleRemoveRule(rule);
}}
className="text-muted-foreground hover:text-destructive transition-colors shrink-0"
>
<LuX className="w-3.5 h-3.5" />
+2 -4
View File
@@ -51,7 +51,7 @@ export function ProfileSelectorDialog({
const { profiles, runningProfiles: hookRunningProfiles } = useProfileEvents();
// Use external runningProfiles if provided, otherwise use hook's runningProfiles
const runningProfiles = externalRunningProfiles || hookRunningProfiles;
const runningProfiles = externalRunningProfiles ?? hookRunningProfiles;
const { storedProxies } = useProxyEvents();
@@ -60,9 +60,7 @@ export function ProfileSelectorDialog({
const [launchingProfiles, setLaunchingProfiles] = useState<Set<string>>(
new Set(),
);
const [stoppingProfiles, _setStoppingProfiles] = useState<Set<string>>(
new Set(),
);
const [stoppingProfiles] = useState<Set<string>>(new Set());
// Use shared browser state hook
const browserState = useBrowserState(
+6 -2
View File
@@ -53,7 +53,9 @@ export function ProxyExportDialog({ isOpen, onClose }: ProxyExportDialogProps) {
await navigator.clipboard.writeText(exportContent);
setCopied(true);
toast.success("Copied to clipboard");
setTimeout(() => setCopied(false), 2000);
setTimeout(() => {
setCopied(false);
}, 2000);
} catch (error) {
console.error("Failed to copy to clipboard:", error);
toast.error("Failed to copy to clipboard");
@@ -99,7 +101,9 @@ export function ProxyExportDialog({ isOpen, onClose }: ProxyExportDialogProps) {
<Label>Export Format</Label>
<RadioGroup
value={format}
onValueChange={(value) => setFormat(value as "json" | "txt")}
onValueChange={(value) => {
setFormat(value as "json" | "txt");
}}
className="flex gap-4"
>
<div className="flex items-center space-x-2">
+35 -30
View File
@@ -105,8 +105,8 @@ export function ProxyFormDialog({
proxy_type: editingProxy.proxy_settings.proxy_type,
host: editingProxy.proxy_settings.host,
port: editingProxy.proxy_settings.port,
username: editingProxy.proxy_settings.username || "",
password: editingProxy.proxy_settings.password || "",
username: editingProxy.proxy_settings.username ?? "",
password: editingProxy.proxy_settings.password ?? "",
});
}
} else {
@@ -250,7 +250,12 @@ export function ProxyFormDialog({
<div className="grid gap-4 py-4">
{!editingProxy && (
<Tabs value={mode} onValueChange={(v) => setMode(v as ProxyMode)}>
<Tabs
value={mode}
onValueChange={(v) => {
setMode(v as ProxyMode);
}}
>
<TabsList className="w-full">
<TabsTrigger value="regular" className="flex-1">
{t("proxies.tabs.regular")}
@@ -275,9 +280,9 @@ export function ProxyFormDialog({
<Input
id="proxy-name"
value={regularForm.name}
onChange={(e) =>
setRegularForm({ ...regularForm, name: e.target.value })
}
onChange={(e) => {
setRegularForm({ ...regularForm, name: e.target.value });
}}
placeholder="e.g. Office Proxy, Home VPN, etc."
disabled={isSubmitting}
/>
@@ -287,9 +292,9 @@ export function ProxyFormDialog({
<Label>{t("proxies.form.type")}</Label>
<Select
value={regularForm.proxy_type}
onValueChange={(value) =>
setRegularForm({ ...regularForm, proxy_type: value })
}
onValueChange={(value) => {
setRegularForm({ ...regularForm, proxy_type: value });
}}
disabled={isSubmitting}
>
<SelectTrigger>
@@ -311,9 +316,9 @@ export function ProxyFormDialog({
<Input
id="proxy-host"
value={regularForm.host}
onChange={(e) =>
setRegularForm({ ...regularForm, host: e.target.value })
}
onChange={(e) => {
setRegularForm({ ...regularForm, host: e.target.value });
}}
placeholder={t("proxies.form.hostPlaceholder")}
disabled={isSubmitting}
/>
@@ -325,12 +330,12 @@ export function ProxyFormDialog({
id="proxy-port"
type="number"
value={regularForm.port}
onChange={(e) =>
onChange={(e) => {
setRegularForm({
...regularForm,
port: parseInt(e.target.value, 10) || 0,
})
}
});
}}
placeholder={t("proxies.form.portPlaceholder")}
min="1"
max="65535"
@@ -348,12 +353,12 @@ export function ProxyFormDialog({
<Input
id="proxy-username"
value={regularForm.username}
onChange={(e) =>
onChange={(e) => {
setRegularForm({
...regularForm,
username: e.target.value,
})
}
});
}}
placeholder={t("proxies.form.usernamePlaceholder")}
disabled={isSubmitting}
/>
@@ -368,12 +373,12 @@ export function ProxyFormDialog({
id="proxy-password"
type="password"
value={regularForm.password}
onChange={(e) =>
onChange={(e) => {
setRegularForm({
...regularForm,
password: e.target.value,
})
}
});
}}
placeholder={t("proxies.form.passwordPlaceholder")}
disabled={isSubmitting}
/>
@@ -387,9 +392,9 @@ export function ProxyFormDialog({
<Input
id="dynamic-name"
value={dynamicForm.name}
onChange={(e) =>
setDynamicForm({ ...dynamicForm, name: e.target.value })
}
onChange={(e) => {
setDynamicForm({ ...dynamicForm, name: e.target.value });
}}
placeholder="e.g. My Tunnel"
disabled={isSubmitting}
/>
@@ -400,9 +405,9 @@ export function ProxyFormDialog({
<Input
id="dynamic-url"
value={dynamicForm.url}
onChange={(e) =>
setDynamicForm({ ...dynamicForm, url: e.target.value })
}
onChange={(e) => {
setDynamicForm({ ...dynamicForm, url: e.target.value });
}}
placeholder={t("proxies.dynamic.urlPlaceholder")}
disabled={isSubmitting}
/>
@@ -412,9 +417,9 @@ export function ProxyFormDialog({
<Label>{t("proxies.dynamic.format")}</Label>
<Select
value={dynamicForm.format}
onValueChange={(value) =>
setDynamicForm({ ...dynamicForm, format: value })
}
onValueChange={(value) => {
setDynamicForm({ ...dynamicForm, format: value });
}}
disabled={isSubmitting}
>
<SelectTrigger>
+9 -7
View File
@@ -69,7 +69,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
}, []);
const processContent = useCallback(
async (content: string, isJson: boolean, _filename: string = "") => {
async (content: string, isJson: boolean, _filename = "") => {
try {
if (isJson) {
setIsImporting(true);
@@ -180,7 +180,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
useEffect(() => {
if (!isOpen || step !== "dropzone") return;
const handlePaste = async (e: ClipboardEvent) => {
const handlePaste = (e: ClipboardEvent) => {
const text = e.clipboardData?.getData("text");
if (text) {
// Try to detect if it's JSON
@@ -189,7 +189,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
(trimmed.startsWith("{") && trimmed.endsWith("}")) ||
(trimmed.startsWith("[") && trimmed.endsWith("]"));
// Use "pasted.txt" as filename to trigger content-based detection
await processContent(text, isJson, "pasted.txt");
void processContent(text, isJson, "pasted.txt");
}
};
@@ -339,7 +339,9 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
id="name-prefix"
placeholder="Imported"
value={namePrefix}
onChange={(e) => setNamePrefix(e.target.value)}
onChange={(e) => {
setNamePrefix(e.target.value);
}}
/>
<p className="text-xs text-muted-foreground">
Proxies will be named &quot;{namePrefix || "Imported"} Proxy
@@ -408,9 +410,9 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
type="radio"
name={`format-${i}`}
checked={proxy.selectedFormat === format}
onChange={() =>
handleAmbiguousFormatSelect(i, format)
}
onChange={() => {
handleAmbiguousFormatSelect(i, format);
}}
className="accent-primary"
/>
<span className="text-xs">{format}</span>
+38 -20
View File
@@ -389,7 +389,9 @@ export function ProxyManagementDialog({
<RippleButton
size="sm"
variant="outline"
onClick={() => setShowImportDialog(true)}
onClick={() => {
setShowImportDialog(true);
}}
className="flex gap-2 items-center"
>
<LuUpload className="w-4 h-4" />
@@ -398,7 +400,9 @@ export function ProxyManagementDialog({
<RippleButton
size="sm"
variant="outline"
onClick={() => setShowExportDialog(true)}
onClick={() => {
setShowExportDialog(true);
}}
className="flex gap-2 items-center"
disabled={storedProxies.length === 0}
>
@@ -487,7 +491,7 @@ export function ProxyManagementDialog({
<Checkbox
checked={proxy.sync_enabled}
onCheckedChange={() =>
handleToggleSync(proxy)
void handleToggleSync(proxy)
}
disabled={
isTogglingSync[proxy.id] ||
@@ -542,9 +546,9 @@ export function ProxyManagementDialog({
<Button
variant="ghost"
size="sm"
onClick={() =>
handleEditProxy(proxy)
}
onClick={() => {
handleEditProxy(proxy);
}}
>
<LuPencil className="w-4 h-4" />
</Button>
@@ -559,9 +563,9 @@ export function ProxyManagementDialog({
<Button
variant="ghost"
size="sm"
onClick={() =>
handleDeleteProxy(proxy)
}
onClick={() => {
handleDeleteProxy(proxy);
}}
disabled={
(proxyUsage[proxy.id] ?? 0) > 0
}
@@ -604,7 +608,9 @@ export function ProxyManagementDialog({
<RippleButton
size="sm"
variant="outline"
onClick={() => setShowVpnImportDialog(true)}
onClick={() => {
setShowVpnImportDialog(true);
}}
className="flex gap-2 items-center"
>
<LuUpload className="w-4 h-4" />
@@ -690,7 +696,7 @@ export function ProxyManagementDialog({
<Checkbox
checked={vpn.sync_enabled}
onCheckedChange={() =>
handleToggleVpnSync(vpn)
void handleToggleVpnSync(vpn)
}
disabled={
isTogglingVpnSync[vpn.id] ||
@@ -728,7 +734,9 @@ export function ProxyManagementDialog({
<Button
variant="ghost"
size="sm"
onClick={() => handleEditVpn(vpn)}
onClick={() => {
handleEditVpn(vpn);
}}
>
<LuPencil className="w-4 h-4" />
</Button>
@@ -743,9 +751,9 @@ export function ProxyManagementDialog({
<Button
variant="ghost"
size="sm"
onClick={() =>
handleDeleteVpn(vpn)
}
onClick={() => {
handleDeleteVpn(vpn);
}}
disabled={
(vpnUsage[vpn.id] ?? 0) > 0
}
@@ -796,7 +804,9 @@ export function ProxyManagementDialog({
/>
<DeleteConfirmationDialog
isOpen={proxyToDelete !== null}
onClose={() => setProxyToDelete(null)}
onClose={() => {
setProxyToDelete(null);
}}
onConfirm={handleConfirmDelete}
title="Delete Proxy"
description={`This action cannot be undone. This will permanently delete the proxy "${proxyToDelete?.name ?? ""}".`}
@@ -805,11 +815,15 @@ export function ProxyManagementDialog({
/>
<ProxyImportDialog
isOpen={showImportDialog}
onClose={() => setShowImportDialog(false)}
onClose={() => {
setShowImportDialog(false);
}}
/>
<ProxyExportDialog
isOpen={showExportDialog}
onClose={() => setShowExportDialog(false)}
onClose={() => {
setShowExportDialog(false);
}}
/>
<VpnFormDialog
isOpen={showVpnForm}
@@ -818,7 +832,9 @@ export function ProxyManagementDialog({
/>
<DeleteConfirmationDialog
isOpen={vpnToDelete !== null}
onClose={() => setVpnToDelete(null)}
onClose={() => {
setVpnToDelete(null);
}}
onConfirm={handleConfirmDeleteVpn}
title="Delete VPN"
description={`This action cannot be undone. This will permanently delete the VPN "${vpnToDelete?.name ?? ""}".`}
@@ -827,7 +843,9 @@ export function ProxyManagementDialog({
/>
<VpnImportDialog
isOpen={showVpnImportDialog}
onClose={() => setShowVpnImportDialog(false)}
onClose={() => {
setShowVpnImportDialog(false);
}}
/>
</>
);
+113 -67
View File
@@ -124,6 +124,12 @@ export function SettingsDialog({
const [e2ePasswordConfirm, setE2ePasswordConfirm] = useState("");
const [e2eError, setE2eError] = useState("");
const [isSavingE2e, setIsSavingE2e] = useState(false);
const [systemInfo, setSystemInfo] = useState<{
app_version: string;
os: string;
arch: string;
portable: boolean;
} | null>(null);
const { t } = useTranslation();
const { setTheme } = useTheme();
@@ -209,7 +215,7 @@ export function SettingsDialog({
if (merged.theme === "custom" && merged.custom_theme) {
const matchingTheme = getThemeByColors(merged.custom_theme);
setCustomThemeState({
selectedThemeId: matchingTheme?.id || null,
selectedThemeId: matchingTheme?.id ?? null,
colors: merged.custom_theme,
});
} else if (merged.theme === "custom") {
@@ -226,6 +232,18 @@ export function SettingsDialog({
} catch {
setHasE2ePassword(false);
}
// Load system info
try {
const info = await invoke<{
app_version: string;
os: string;
arch: string;
portable: boolean;
}>("get_system_info");
setSystemInfo(info);
} catch {
setSystemInfo(null);
}
} catch (error) {
console.error("Failed to load settings:", error);
} finally {
@@ -235,19 +253,19 @@ export function SettingsDialog({
const applyCustomTheme = useCallback((vars: Record<string, string>) => {
const root = document.documentElement;
Object.entries(vars).forEach(([k, v]) =>
root.style.setProperty(k, v, "important"),
);
Object.entries(vars).forEach(([k, v]) => {
root.style.setProperty(k, v, "important");
});
}, []);
const clearCustomTheme = useCallback(() => {
const root = document.documentElement;
THEME_VARIABLES.forEach(({ key }) =>
root.style.removeProperty(key as string),
);
THEME_VARIABLES.forEach(({ key }) => {
root.style.removeProperty(key as string);
});
}, []);
const loadPermissions = useCallback(async () => {
const loadPermissions = useCallback(() => {
setIsLoadingPermissions(true);
try {
if (!isMacOS) {
@@ -378,28 +396,29 @@ export function SettingsDialog({
// Apply or clear custom variables only on Save
if (settings.theme === "custom") {
if (
customThemeState.colors &&
Object.keys(customThemeState.colors).length > 0
) {
if (Object.keys(customThemeState.colors).length > 0) {
try {
const root = document.documentElement;
// Clear any previous custom vars first
THEME_VARIABLES.forEach(({ key }) =>
root.style.removeProperty(key as string),
);
Object.entries(customThemeState.colors).forEach(([k, v]) =>
root.style.setProperty(k, v, "important"),
);
} catch {}
THEME_VARIABLES.forEach(({ key }) => {
root.style.removeProperty(key as string);
});
Object.entries(customThemeState.colors).forEach(([k, v]) => {
root.style.setProperty(k, v, "important");
});
} catch {
/* empty */
}
}
} else {
try {
const root = document.documentElement;
THEME_VARIABLES.forEach(({ key }) =>
root.style.removeProperty(key as string),
);
} catch {}
THEME_VARIABLES.forEach(({ key }) => {
root.style.removeProperty(key as string);
});
} catch {
/* empty */
}
}
// Save language if changed
@@ -458,7 +477,7 @@ export function SettingsDialog({
if (originalSettings.theme === "custom" && originalSettings.custom_theme) {
const matchingTheme = getThemeByColors(originalSettings.custom_theme);
setCustomThemeState({
selectedThemeId: matchingTheme?.id || null,
selectedThemeId: matchingTheme?.id ?? null,
colors: originalSettings.custom_theme,
});
}
@@ -481,8 +500,12 @@ export function SettingsDialog({
useEffect(() => {
if (isOpen) {
loadSettings().catch(console.error);
checkDefaultBrowserStatus().catch(console.error);
loadSettings().catch((err: unknown) => {
console.error(err);
});
checkDefaultBrowserStatus().catch((err: unknown) => {
console.error(err);
});
// Check if we're on macOS
const userAgent = navigator.userAgent;
@@ -492,12 +515,14 @@ export function SettingsDialog({
setIsLinux(isLin);
if (isMac) {
loadPermissions().catch(console.error);
loadPermissions();
}
// Set up interval to check default browser status
const intervalId = setInterval(() => {
checkDefaultBrowserStatus().catch(console.error);
checkDefaultBrowserStatus().catch((err: unknown) => {
console.error(err);
});
}, 500); // Check every 500ms
// Cleanup interval on component unmount or dialog close
@@ -612,7 +637,7 @@ export function SettingsDialog({
Theme Preset
</Label>
<Select
value={customThemeState.selectedThemeId || "custom"}
value={customThemeState.selectedThemeId ?? "custom"}
onValueChange={(value) => {
if (value === "custom") {
setCustomThemeState((prev) => ({
@@ -648,7 +673,7 @@ export function SettingsDialog({
<div className="grid grid-cols-4 gap-3">
{THEME_VARIABLES.map(({ key, label }) => {
const colorValue =
customThemeState.colors[key] || "#000000";
customThemeState.colors[key] ?? "#000000";
return (
<div
key={key}
@@ -683,7 +708,7 @@ export function SettingsDialog({
getThemeByColors(newColors);
setCustomThemeState({
selectedThemeId: matchingTheme?.id || null,
selectedThemeId: matchingTheme?.id ?? null,
colors: newColors,
});
}}
@@ -723,8 +748,10 @@ export function SettingsDialog({
Interface Language
</Label>
<Select
value={selectedLanguage || "system"}
onValueChange={(value) => setSelectedLanguage(value)}
value={selectedLanguage ?? "system"}
onValueChange={(value) => {
setSelectedLanguage(value);
}}
disabled={isLanguageLoading}
>
<SelectTrigger id="language-select">
@@ -746,34 +773,38 @@ export function SettingsDialog({
</p>
</div>
{/* Default Browser Section */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-base font-medium">Default Browser</Label>
<Badge variant={isDefaultBrowser ? "default" : "secondary"}>
{isDefaultBrowser ? "Active" : "Inactive"}
</Badge>
{/* Default Browser Section - hidden in portable mode */}
{!systemInfo?.portable && (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-base font-medium">Default Browser</Label>
<Badge variant={isDefaultBrowser ? "default" : "secondary"}>
{isDefaultBrowser ? "Active" : "Inactive"}
</Badge>
</div>
<LoadingButton
isLoading={isSettingDefault}
onClick={() => {
handleSetDefaultBrowser().catch((err: unknown) => {
console.error(err);
});
}}
disabled={isDefaultBrowser}
variant={isDefaultBrowser ? "outline" : "default"}
className="w-full"
>
{isDefaultBrowser
? "Already Default Browser"
: "Set as Default Browser"}
</LoadingButton>
<p className="text-xs text-muted-foreground">
When set as default, Donut Browser will handle web links and
allow you to choose which profile to use.
</p>
</div>
<LoadingButton
isLoading={isSettingDefault}
onClick={() => {
handleSetDefaultBrowser().catch(console.error);
}}
disabled={isDefaultBrowser}
variant={isDefaultBrowser ? "outline" : "default"}
className="w-full"
>
{isDefaultBrowser
? "Already Default Browser"
: "Set as Default Browser"}
</LoadingButton>
<p className="text-xs text-muted-foreground">
When set as default, Donut Browser will handle web links and allow
you to choose which profile to use.
</p>
</div>
)}
{/* Permissions Section - Only show on macOS */}
{isMacOS && (
@@ -818,7 +849,9 @@ export function SettingsDialog({
onClick={() => {
handleRequestPermission(
permission.permission_type,
).catch(console.error);
).catch((err: unknown) => {
console.error(err);
});
}}
>
Grant
@@ -1037,10 +1070,10 @@ export function SettingsDialog({
<div className="flex items-start space-x-3 p-3 rounded-lg border">
<Checkbox
id="disable-auto-updates"
checked={settings.disable_auto_updates || false}
onCheckedChange={(checked) =>
updateSetting("disable_auto_updates", checked as boolean)
}
checked={settings.disable_auto_updates ?? false}
onCheckedChange={(checked) => {
updateSetting("disable_auto_updates", checked as boolean);
}}
/>
<div className="space-y-1">
<Label
@@ -1059,7 +1092,9 @@ export function SettingsDialog({
<LoadingButton
isLoading={isClearingCache}
onClick={() => {
handleClearCache().catch(console.error);
handleClearCache().catch((err: unknown) => {
console.error(err);
});
}}
variant="outline"
className="w-full"
@@ -1073,6 +1108,15 @@ export function SettingsDialog({
version information for all browsers.
</p>
</div>
{/* System Info */}
{systemInfo && (
<div className="pt-2 border-t">
<p className="text-xs text-muted-foreground font-mono whitespace-pre-line select-all">
{`Donut Browser ${systemInfo.app_version}\n${systemInfo.os} ${systemInfo.arch}${systemInfo.portable ? " (portable)" : ""}`}
</p>
</div>
)}
</div>
<DialogFooter className="shrink-0">
@@ -1082,7 +1126,9 @@ export function SettingsDialog({
<LoadingButton
isLoading={isSaving}
onClick={() => {
handleSave().catch(console.error);
handleSave().catch((err: unknown) => {
console.error(err);
});
}}
disabled={isLoading || !hasChanges}
>
+190 -186
View File
@@ -77,7 +77,7 @@ function ObjectEditor({
const [jsonString, setJsonString] = useState("");
useEffect(() => {
setJsonString(JSON.stringify(value || {}, null, 2));
setJsonString(JSON.stringify(value ?? {}, null, 2));
}, [value]);
const handleChange = (newValue: string) => {
@@ -108,7 +108,9 @@ function ObjectEditor({
<Label>{title}</Label>
<Textarea
value={jsonString}
onChange={(e) => handleChange(e.target.value)}
onChange={(e) => {
handleChange(e.target.value);
}}
placeholder={`Enter ${title} as JSON`}
className="font-mono text-sm"
rows={6}
@@ -142,7 +144,7 @@ export function SharedCamoufoxConfigForm({
const handleGenerateFingerprint = async () => {
if (!profileVersion) return;
const browser = profileBrowser || browserType || "camoufox";
const browser = profileBrowser ?? browserType ?? "camoufox";
setIsGeneratingFingerprint(true);
try {
const configJson = JSON.stringify(config);
@@ -267,7 +269,9 @@ export function SharedCamoufoxConfigForm({
</div>
<Select
value={selectedOS}
onValueChange={(value: CamoufoxOS) => onConfigChange("os", value)}
onValueChange={(value: CamoufoxOS) => {
onConfigChange("os", value);
}}
disabled={readOnly}
>
<SelectTrigger>
@@ -301,10 +305,10 @@ export function SharedCamoufoxConfigForm({
<div className="flex items-center space-x-2">
<Checkbox
id="randomize-fingerprint"
checked={config.randomize_fingerprint_on_launch || false}
onCheckedChange={(checked) =>
onConfigChange("randomize_fingerprint_on_launch", checked)
}
checked={config.randomize_fingerprint_on_launch ?? false}
onCheckedChange={(checked) => {
onConfigChange("randomize_fingerprint_on_launch", checked);
}}
disabled={readOnly}
/>
<Label htmlFor="randomize-fingerprint" className="font-medium">
@@ -365,10 +369,10 @@ export function SharedCamoufoxConfigForm({
<div className="flex items-center space-x-2">
<Checkbox
id="block-images"
checked={config.block_images || false}
onCheckedChange={(checked) =>
onConfigChange("block_images", checked)
}
checked={config.block_images ?? false}
onCheckedChange={(checked) => {
onConfigChange("block_images", checked);
}}
/>
<Label htmlFor="block-images">
{t("fingerprint.blockImages")}
@@ -377,10 +381,10 @@ export function SharedCamoufoxConfigForm({
<div className="flex items-center space-x-2">
<Checkbox
id="block-webrtc"
checked={config.block_webrtc || false}
onCheckedChange={(checked) =>
onConfigChange("block_webrtc", checked)
}
checked={config.block_webrtc ?? false}
onCheckedChange={(checked) => {
onConfigChange("block_webrtc", checked);
}}
/>
<Label htmlFor="block-webrtc">
{t("fingerprint.blockWebRTC")}
@@ -389,10 +393,10 @@ export function SharedCamoufoxConfigForm({
<div className="flex items-center space-x-2">
<Checkbox
id="block-webgl"
checked={config.block_webgl || false}
onCheckedChange={(checked) =>
onConfigChange("block_webgl", checked)
}
checked={config.block_webgl ?? false}
onCheckedChange={(checked) => {
onConfigChange("block_webgl", checked);
}}
/>
<Label htmlFor="block-webgl">
{t("fingerprint.blockWebGL")}
@@ -410,13 +414,13 @@ export function SharedCamoufoxConfigForm({
<Label htmlFor="user-agent">{t("fingerprint.userAgent")}</Label>
<Input
id="user-agent"
value={fingerprintConfig["navigator.userAgent"] || ""}
onChange={(e) =>
value={fingerprintConfig["navigator.userAgent"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"navigator.userAgent",
e.target.value || undefined,
)
}
);
}}
placeholder="Mozilla/5.0..."
/>
</div>
@@ -424,13 +428,13 @@ export function SharedCamoufoxConfigForm({
<Label htmlFor="platform">{t("fingerprint.platform")}</Label>
<Input
id="platform"
value={fingerprintConfig["navigator.platform"] || ""}
onChange={(e) =>
value={fingerprintConfig["navigator.platform"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"navigator.platform",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., MacIntel, Win32"
/>
</div>
@@ -440,13 +444,13 @@ export function SharedCamoufoxConfigForm({
</Label>
<Input
id="app-version"
value={fingerprintConfig["navigator.appVersion"] || ""}
onChange={(e) =>
value={fingerprintConfig["navigator.appVersion"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"navigator.appVersion",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., 5.0 (Macintosh)"
/>
</div>
@@ -454,13 +458,13 @@ export function SharedCamoufoxConfigForm({
<Label htmlFor="oscpu">{t("fingerprint.osCpu")}</Label>
<Input
id="oscpu"
value={fingerprintConfig["navigator.oscpu"] || ""}
onChange={(e) =>
value={fingerprintConfig["navigator.oscpu"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"navigator.oscpu",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., Intel Mac OS X 10.15"
/>
</div>
@@ -472,14 +476,14 @@ export function SharedCamoufoxConfigForm({
id="hardware-concurrency"
type="number"
value={
fingerprintConfig["navigator.hardwareConcurrency"] || ""
fingerprintConfig["navigator.hardwareConcurrency"] ?? ""
}
onChange={(e) =>
onChange={(e) => {
updateFingerprintConfig(
"navigator.hardwareConcurrency",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 8"
/>
</div>
@@ -490,13 +494,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="max-touch-points"
type="number"
value={fingerprintConfig["navigator.maxTouchPoints"] || ""}
onChange={(e) =>
value={fingerprintConfig["navigator.maxTouchPoints"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"navigator.maxTouchPoints",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 0"
/>
</div>
@@ -505,13 +509,13 @@ export function SharedCamoufoxConfigForm({
{t("fingerprint.doNotTrack")}
</Label>
<Select
value={fingerprintConfig["navigator.doNotTrack"] || ""}
onValueChange={(value) =>
value={fingerprintConfig["navigator.doNotTrack"] ?? ""}
onValueChange={(value) => {
updateFingerprintConfig(
"navigator.doNotTrack",
value || undefined,
)
}
);
}}
>
<SelectTrigger>
<SelectValue
@@ -535,13 +539,13 @@ export function SharedCamoufoxConfigForm({
<Label htmlFor="language">{t("fingerprint.language")}</Label>
<Input
id="language"
value={fingerprintConfig["navigator.language"] || ""}
onChange={(e) =>
value={fingerprintConfig["navigator.language"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"navigator.language",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., en-US"
/>
</div>
@@ -559,13 +563,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="screen-width"
type="number"
value={fingerprintConfig["screen.width"] || ""}
onChange={(e) =>
value={fingerprintConfig["screen.width"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screen.width",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1920"
/>
</div>
@@ -576,13 +580,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="screen-height"
type="number"
value={fingerprintConfig["screen.height"] || ""}
onChange={(e) =>
value={fingerprintConfig["screen.height"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screen.height",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1080"
/>
</div>
@@ -593,13 +597,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="avail-width"
type="number"
value={fingerprintConfig["screen.availWidth"] || ""}
onChange={(e) =>
value={fingerprintConfig["screen.availWidth"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screen.availWidth",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1920"
/>
</div>
@@ -610,13 +614,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="avail-height"
type="number"
value={fingerprintConfig["screen.availHeight"] || ""}
onChange={(e) =>
value={fingerprintConfig["screen.availHeight"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screen.availHeight",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1055"
/>
</div>
@@ -627,13 +631,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="color-depth"
type="number"
value={fingerprintConfig["screen.colorDepth"] || ""}
onChange={(e) =>
value={fingerprintConfig["screen.colorDepth"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screen.colorDepth",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 30"
/>
</div>
@@ -644,13 +648,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="pixel-depth"
type="number"
value={fingerprintConfig["screen.pixelDepth"] || ""}
onChange={(e) =>
value={fingerprintConfig["screen.pixelDepth"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screen.pixelDepth",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 30"
/>
</div>
@@ -668,13 +672,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="outer-width"
type="number"
value={fingerprintConfig["window.outerWidth"] || ""}
onChange={(e) =>
value={fingerprintConfig["window.outerWidth"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"window.outerWidth",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1512"
/>
</div>
@@ -685,13 +689,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="outer-height"
type="number"
value={fingerprintConfig["window.outerHeight"] || ""}
onChange={(e) =>
value={fingerprintConfig["window.outerHeight"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"window.outerHeight",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 886"
/>
</div>
@@ -702,13 +706,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="inner-width"
type="number"
value={fingerprintConfig["window.innerWidth"] || ""}
onChange={(e) =>
value={fingerprintConfig["window.innerWidth"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"window.innerWidth",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1512"
/>
</div>
@@ -719,13 +723,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="inner-height"
type="number"
value={fingerprintConfig["window.innerHeight"] || ""}
onChange={(e) =>
value={fingerprintConfig["window.innerHeight"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"window.innerHeight",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 886"
/>
</div>
@@ -734,13 +738,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="screen-x"
type="number"
value={fingerprintConfig["window.screenX"] || ""}
onChange={(e) =>
value={fingerprintConfig["window.screenX"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"window.screenX",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 0"
/>
</div>
@@ -749,13 +753,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="screen-y"
type="number"
value={fingerprintConfig["window.screenY"] || ""}
onChange={(e) =>
value={fingerprintConfig["window.screenY"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"window.screenY",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 0"
/>
</div>
@@ -772,13 +776,13 @@ export function SharedCamoufoxConfigForm({
id="latitude"
type="number"
step="any"
value={fingerprintConfig["geolocation:latitude"] || ""}
onChange={(e) =>
value={fingerprintConfig["geolocation:latitude"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"geolocation:latitude",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
);
}}
placeholder="e.g., 41.0019"
/>
</div>
@@ -788,13 +792,13 @@ export function SharedCamoufoxConfigForm({
id="longitude"
type="number"
step="any"
value={fingerprintConfig["geolocation:longitude"] || ""}
onChange={(e) =>
value={fingerprintConfig["geolocation:longitude"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"geolocation:longitude",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
);
}}
placeholder="e.g., 28.9645"
/>
</div>
@@ -803,13 +807,13 @@ export function SharedCamoufoxConfigForm({
<Input
id="timezone"
type="text"
value={fingerprintConfig.timezone || ""}
onChange={(e) =>
value={fingerprintConfig.timezone ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"timezone",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., America/New_York"
/>
</div>
@@ -826,13 +830,13 @@ export function SharedCamoufoxConfigForm({
</Label>
<Input
id="locale-language"
value={fingerprintConfig["locale:language"] || ""}
onChange={(e) =>
value={fingerprintConfig["locale:language"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"locale:language",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., tr"
/>
</div>
@@ -840,13 +844,13 @@ export function SharedCamoufoxConfigForm({
<Label htmlFor="locale-region">{t("fingerprint.region")}</Label>
<Input
id="locale-region"
value={fingerprintConfig["locale:region"] || ""}
onChange={(e) =>
value={fingerprintConfig["locale:region"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"locale:region",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., TR"
/>
</div>
@@ -854,13 +858,13 @@ export function SharedCamoufoxConfigForm({
<Label htmlFor="locale-script">{t("fingerprint.script")}</Label>
<Input
id="locale-script"
value={fingerprintConfig["locale:script"] || ""}
onChange={(e) =>
value={fingerprintConfig["locale:script"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"locale:script",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., Latn"
/>
</div>
@@ -877,13 +881,13 @@ export function SharedCamoufoxConfigForm({
</Label>
<Input
id="webgl-vendor"
value={fingerprintConfig["webGl:vendor"] || ""}
onChange={(e) =>
value={fingerprintConfig["webGl:vendor"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"webGl:vendor",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., Mesa"
/>
</div>
@@ -893,13 +897,13 @@ export function SharedCamoufoxConfigForm({
</Label>
<Input
id="webgl-renderer"
value={fingerprintConfig["webGl:renderer"] || ""}
onChange={(e) =>
value={fingerprintConfig["webGl:renderer"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"webGl:renderer",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., llvmpipe, or similar"
/>
</div>
@@ -913,11 +917,11 @@ export function SharedCamoufoxConfigForm({
(fingerprintConfig["webGl:parameters"] as Record<
string,
unknown
>) || {}
}
onChange={(value) =>
updateFingerprintConfig("webGl:parameters", value)
>) ?? {}
}
onChange={(value) => {
updateFingerprintConfig("webGl:parameters", value);
}}
title={t("fingerprint.webglParameters")}
readOnly={readOnly}
/>
@@ -930,11 +934,11 @@ export function SharedCamoufoxConfigForm({
(fingerprintConfig["webGl2:parameters"] as Record<
string,
unknown
>) || {}
}
onChange={(value) =>
updateFingerprintConfig("webGl2:parameters", value)
>) ?? {}
}
onChange={(value) => {
updateFingerprintConfig("webGl2:parameters", value);
}}
title={t("fingerprint.webgl2Parameters")}
readOnly={readOnly}
/>
@@ -947,11 +951,11 @@ export function SharedCamoufoxConfigForm({
(fingerprintConfig["webGl:shaderPrecisionFormats"] as Record<
string,
unknown
>) || {}
}
onChange={(value) =>
updateFingerprintConfig("webGl:shaderPrecisionFormats", value)
>) ?? {}
}
onChange={(value) => {
updateFingerprintConfig("webGl:shaderPrecisionFormats", value);
}}
title={t("fingerprint.webglShaderPrecisionFormats")}
readOnly={readOnly}
/>
@@ -964,11 +968,11 @@ export function SharedCamoufoxConfigForm({
(fingerprintConfig["webGl2:shaderPrecisionFormats"] as Record<
string,
unknown
>) || {}
}
onChange={(value) =>
updateFingerprintConfig("webGl2:shaderPrecisionFormats", value)
>) ?? {}
}
onChange={(value) => {
updateFingerprintConfig("webGl2:shaderPrecisionFormats", value);
}}
title={t("fingerprint.webgl2ShaderPrecisionFormats")}
readOnly={readOnly}
/>
@@ -1000,12 +1004,12 @@ export function SharedCamoufoxConfigForm({
value: font,
}));
})()}
onChange={(selected: Option[]) =>
onChange={(selected: Option[]) => {
updateFingerprintConfig(
"fonts",
selected.map((s: Option) => s.value),
)
}
);
}}
placeholder="Add fonts..."
creatable
/>
@@ -1019,10 +1023,10 @@ export function SharedCamoufoxConfigForm({
<div className="flex items-center space-x-2">
<Checkbox
id="battery-charging"
checked={fingerprintConfig["battery:charging"] || false}
onCheckedChange={(checked) =>
updateFingerprintConfig("battery:charging", checked)
}
checked={fingerprintConfig["battery:charging"] ?? false}
onCheckedChange={(checked) => {
updateFingerprintConfig("battery:charging", checked);
}}
/>
<Label htmlFor="battery-charging">
{t("fingerprint.charging")}
@@ -1037,13 +1041,13 @@ export function SharedCamoufoxConfigForm({
id="charging-time"
type="number"
step="any"
value={fingerprintConfig["battery:chargingTime"] || ""}
onChange={(e) =>
value={fingerprintConfig["battery:chargingTime"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"battery:chargingTime",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
);
}}
placeholder="e.g., 0"
/>
</div>
@@ -1055,13 +1059,13 @@ export function SharedCamoufoxConfigForm({
id="discharging-time"
type="number"
step="any"
value={fingerprintConfig["battery:dischargingTime"] || ""}
onChange={(e) =>
value={fingerprintConfig["battery:dischargingTime"] ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"battery:dischargingTime",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
);
}}
placeholder="e.g., 0"
/>
</div>
@@ -1132,9 +1136,9 @@ export function SharedCamoufoxConfigForm({
<Label>{t("fingerprint.osLabel")}</Label>
<Select
value={selectedOS}
onValueChange={(value: CamoufoxOS) =>
onConfigChange("os", value)
}
onValueChange={(value: CamoufoxOS) => {
onConfigChange("os", value);
}}
disabled={readOnly}
>
<SelectTrigger>
@@ -1170,10 +1174,10 @@ export function SharedCamoufoxConfigForm({
<div className="flex items-center space-x-2">
<Checkbox
id="randomize-fingerprint-auto"
checked={config.randomize_fingerprint_on_launch || false}
onCheckedChange={(checked) =>
onConfigChange("randomize_fingerprint_on_launch", checked)
}
checked={config.randomize_fingerprint_on_launch ?? false}
onCheckedChange={(checked) => {
onConfigChange("randomize_fingerprint_on_launch", checked);
}}
disabled={readOnly}
/>
<Label
@@ -1222,15 +1226,15 @@ export function SharedCamoufoxConfigForm({
<Input
id="screen-max-width"
type="number"
value={config.screen_max_width || ""}
onChange={(e) =>
value={config.screen_max_width ?? ""}
onChange={(e) => {
onConfigChange(
"screen_max_width",
e.target.value
? parseInt(e.target.value, 10)
: undefined,
)
}
);
}}
placeholder="e.g., 1920"
/>
</div>
@@ -1241,15 +1245,15 @@ export function SharedCamoufoxConfigForm({
<Input
id="screen-max-height"
type="number"
value={config.screen_max_height || ""}
onChange={(e) =>
value={config.screen_max_height ?? ""}
onChange={(e) => {
onConfigChange(
"screen_max_height",
e.target.value
? parseInt(e.target.value, 10)
: undefined,
)
}
);
}}
placeholder="e.g., 1080"
/>
</div>
@@ -1260,15 +1264,15 @@ export function SharedCamoufoxConfigForm({
<Input
id="screen-min-width"
type="number"
value={config.screen_min_width || ""}
onChange={(e) =>
value={config.screen_min_width ?? ""}
onChange={(e) => {
onConfigChange(
"screen_min_width",
e.target.value
? parseInt(e.target.value, 10)
: undefined,
)
}
);
}}
placeholder="e.g., 800"
/>
</div>
@@ -1279,15 +1283,15 @@ export function SharedCamoufoxConfigForm({
<Input
id="screen-min-height"
type="number"
value={config.screen_min_height || ""}
onChange={(e) =>
value={config.screen_min_height ?? ""}
onChange={(e) => {
onConfigChange(
"screen_min_height",
e.target.value
? parseInt(e.target.value, 10)
: undefined,
)
}
);
}}
placeholder="e.g., 600"
/>
</div>
+28 -18
View File
@@ -67,9 +67,7 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
const [isVerifying, setIsVerifying] = useState(false);
const [activeTab, setActiveTab] = useState<string>("cloud");
const [_liveProxyUsage, setLiveProxyUsage] = useState<ProxyUsage | null>(
null,
);
const [, setLiveProxyUsage] = useState<ProxyUsage | null>(null);
const [connectionStatus, setConnectionStatus] = useState<
"unknown" | "testing" | "connected" | "error"
@@ -91,8 +89,8 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
setIsLoading(true);
try {
const settings = await invoke<SyncSettings>("get_sync_settings");
setServerUrl(settings.sync_server_url || "");
setToken(settings.sync_token || "");
setServerUrl(settings.sync_server_url ?? "");
setToken(settings.sync_token ?? "");
if (settings.sync_server_url && settings.sync_token) {
void testConnection(settings.sync_server_url);
}
@@ -110,9 +108,11 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
setCodeSent(false);
setOtpCode("");
setEmail("");
invoke<ProxyUsage | null>("cloud_get_proxy_usage")
void invoke<ProxyUsage | null>("cloud_get_proxy_usage")
.then(setLiveProxyUsage)
.catch(() => setLiveProxyUsage(null));
.catch(() => {
setLiveProxyUsage(null);
});
}
}, [isOpen, loadSettings]);
@@ -342,7 +342,7 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
<Button
variant="outline"
className="flex-1"
onClick={handleCloudLogout}
onClick={() => void handleCloudLogout()}
>
{t("sync.cloud.logout")}
</Button>
@@ -388,7 +388,9 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
type="email"
placeholder={t("sync.cloud.emailPlaceholder")}
value={email}
onChange={(e) => setEmail(e.target.value)}
onChange={(e) => {
setEmail(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter" && !codeSent) {
void handleSendCode();
@@ -396,7 +398,7 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
}}
/>
<LoadingButton
onClick={handleSendCode}
onClick={() => void handleSendCode()}
isLoading={isSendingCode}
disabled={!email || codeSent}
variant="outline"
@@ -415,7 +417,9 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
id="cloud-otp"
placeholder={t("sync.cloud.codePlaceholder")}
value={otpCode}
onChange={(e) => setOtpCode(e.target.value)}
onChange={(e) => {
setOtpCode(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
void handleVerifyOtp();
@@ -423,7 +427,7 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
}}
/>
<LoadingButton
onClick={handleVerifyOtp}
onClick={() => void handleVerifyOtp()}
isLoading={isVerifying}
disabled={!otpCode}
className="w-full"
@@ -453,7 +457,9 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
id="sync-server-url"
placeholder={t("sync.serverUrlPlaceholder")}
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
onChange={(e) => {
setServerUrl(e.target.value);
}}
/>
</div>
@@ -465,14 +471,18 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
type={showToken ? "text" : "password"}
placeholder={t("sync.tokenPlaceholder")}
value={token}
onChange={(e) => setToken(e.target.value)}
onChange={(e) => {
setToken(e.target.value);
}}
className="pr-10"
/>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setShowToken(!showToken)}
onClick={() => {
setShowToken(!showToken);
}}
className="absolute right-3 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
aria-label={showToken ? "Hide token" : "Show token"}
>
@@ -515,7 +525,7 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
{hasConfig && (
<Button
variant="outline"
onClick={handleDisconnect}
onClick={() => void handleDisconnect()}
disabled={isSaving}
>
Disconnect
@@ -523,13 +533,13 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
)}
<Button
variant="outline"
onClick={handleTestConnection}
onClick={() => void handleTestConnection()}
disabled={isTesting || !serverUrl}
>
{isTesting ? "Testing..." : "Test Connection"}
</Button>
<LoadingButton
onClick={handleSave}
onClick={() => void handleSave()}
isLoading={isSaving}
disabled={!serverUrl || !token}
>
+12 -8
View File
@@ -156,21 +156,23 @@ export function SyncFollowerDialog({
<div
key={profile.id}
className="flex items-center gap-3 p-2 rounded-md hover:bg-accent cursor-pointer"
onClick={() =>
onClick={() => {
handleToggle(
profile.id,
!selectedIds.has(profile.id),
)
}
onKeyDown={() => {}}
);
}}
onKeyDown={() => {
/* empty */
}}
role="button"
tabIndex={0}
>
<Checkbox
checked={selectedIds.has(profile.id)}
onCheckedChange={(checked) =>
handleToggle(profile.id, checked === true)
}
onCheckedChange={(checked) => {
handleToggle(profile.id, checked === true);
}}
/>
<span className="text-sm truncate flex-1">
{profile.name}
@@ -203,7 +205,9 @@ export function SyncFollowerDialog({
<DialogFooter>
<RippleButton
variant="outline"
onClick={() => handleOpenChange(false)}
onClick={() => {
handleOpenChange(false);
}}
>
{t("common.buttons.cancel")}
</RippleButton>
+3 -1
View File
@@ -94,7 +94,9 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
};
// Apply after a short delay to ensure CSS has loaded
setTimeout(reapplyCustomTheme, 100);
setTimeout(() => {
void reapplyCustomTheme();
}, 100);
}
}, [isLoading, _mounted]);
+18 -9
View File
@@ -173,7 +173,9 @@ export function TrafficDetailsDialog({
};
void fetchStats();
const interval = setInterval(fetchStats, 2000);
const interval = setInterval(() => {
void fetchStats();
}, 2000);
return () => {
clearInterval(interval);
@@ -244,7 +246,12 @@ export function TrafficDetailsDialog({
}, [stats]);
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) onClose();
}}
>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>
@@ -265,7 +272,9 @@ export function TrafficDetailsDialog({
<h3 className="text-sm font-medium">Bandwidth Over Time</h3>
<Select
value={timePeriod}
onValueChange={(v) => setTimePeriod(v as TimePeriod)}
onValueChange={(v) => {
setTimePeriod(v as TimePeriod);
}}
>
<SelectTrigger className="w-[120px] h-8">
<SelectValue placeholder="Time period" />
@@ -400,7 +409,7 @@ export function TrafficDetailsDialog({
Sent ({timePeriod === "all" ? "total" : timePeriod})
</p>
<p className="text-lg font-semibold text-chart-1">
{formatBytes(stats?.period_bytes_sent || 0)}
{formatBytes(stats?.period_bytes_sent ?? 0)}
</p>
</div>
<div className="bg-muted/50 rounded-lg p-3">
@@ -408,7 +417,7 @@ export function TrafficDetailsDialog({
Received ({timePeriod === "all" ? "total" : timePeriod})
</p>
<p className="text-lg font-semibold text-chart-2">
{formatBytes(stats?.period_bytes_received || 0)}
{formatBytes(stats?.period_bytes_received ?? 0)}
</p>
</div>
<div className="bg-muted/50 rounded-lg p-3">
@@ -416,7 +425,7 @@ export function TrafficDetailsDialog({
Requests ({timePeriod === "all" ? "total" : timePeriod})
</p>
<p className="text-lg font-semibold">
{(stats?.period_requests || 0).toLocaleString()}
{(stats?.period_requests ?? 0).toLocaleString()}
</p>
</div>
</div>
@@ -426,13 +435,13 @@ export function TrafficDetailsDialog({
<div>
<span className="font-medium">All-time traffic:</span>{" "}
{formatBytes(
(stats?.total_bytes_sent || 0) +
(stats?.total_bytes_received || 0),
(stats?.total_bytes_sent ?? 0) +
(stats?.total_bytes_received ?? 0),
)}
</div>
<div>
<span className="font-medium">All-time requests:</span>{" "}
{stats?.total_requests?.toLocaleString() || 0}
{stats?.total_requests?.toLocaleString() ?? 0}
</div>
</div>
+10 -4
View File
@@ -214,7 +214,9 @@ export const ColorPickerSelection = memo(
);
useEffect(() => {
const handlePointerUp = () => setIsDragging(false);
const handlePointerUp = () => {
setIsDragging(false);
};
if (isDragging) {
window.addEventListener("pointermove", handlePointerMove);
@@ -268,7 +270,9 @@ export const ColorPickerHue = ({
<Slider.Root
className={cn("flex relative w-full h-4 touch-none", className)}
max={360}
onValueChange={([hue]) => setHue(hue)}
onValueChange={([hue]) => {
setHue(hue);
}}
step={1}
value={[hue]}
{...props}
@@ -293,7 +297,9 @@ export const ColorPickerAlpha = ({
<Slider.Root
className={cn("flex relative w-full h-4 touch-none", className)}
max={100}
onValueChange={([alpha]) => setAlpha(alpha)}
onValueChange={([alpha]) => {
setAlpha(alpha);
}}
step={1}
value={[alpha]}
{...props}
@@ -357,7 +363,7 @@ export type ColorPickerOutputProps = ComponentProps<typeof SelectTrigger>;
const formats = ["hex", "rgb", "css", "hsl"];
export const ColorPickerOutput = ({
className,
className: _className,
...props
}: ColorPickerOutputProps) => {
const { mode, setMode } = useColorPicker();
+1 -1
View File
@@ -45,7 +45,7 @@ export function CopyToClipboard({
<Button
variant={variant}
size={size}
className={`relative ${className || ""}`}
className={`relative ${className ?? ""}`}
onClick={copyToClipboard}
aria-label={copied ? "Copied" : "Copy to clipboard"}
>
+21 -17
View File
@@ -7,12 +7,12 @@ import { cn } from "@/lib/utils";
type HighlightMode = "children" | "parent";
type Bounds = {
interface Bounds {
top: number;
left: number;
width: number;
height: number;
};
}
const DEFAULT_BOUNDS_OFFSET: Bounds = {
top: 0,
@@ -21,7 +21,7 @@ const DEFAULT_BOUNDS_OFFSET: Bounds = {
height: 0,
};
type HighlightContextType<T extends string> = {
interface HighlightContextType<T extends string> {
as?: keyof HTMLElementTagNameMap;
mode: HighlightMode;
activeValue: T | null;
@@ -40,11 +40,10 @@ type HighlightContextType<T extends string> = {
enabled?: boolean;
exitDelay?: number;
forceUpdateBounds?: boolean;
};
}
const HighlightContext = React.createContext<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
HighlightContextType<any> | undefined
HighlightContextType<string> | undefined
>(undefined);
function useHighlight<T extends string>(): HighlightContextType<T> {
@@ -55,7 +54,7 @@ function useHighlight<T extends string>(): HighlightContextType<T> {
return context as unknown as HighlightContextType<T>;
}
type BaseHighlightProps<T extends React.ElementType = "div"> = {
interface BaseHighlightProps<T extends React.ElementType = "div"> {
as?: T;
ref?: React.Ref<HTMLDivElement>;
mode?: HighlightMode;
@@ -70,13 +69,13 @@ type BaseHighlightProps<T extends React.ElementType = "div"> = {
disabled?: boolean;
enabled?: boolean;
exitDelay?: number;
};
}
type ParentModeHighlightProps = {
interface ParentModeHighlightProps {
boundsOffset?: Partial<Bounds>;
containerClassName?: string;
forceUpdateBounds?: boolean;
};
}
type ControlledParentModeHighlightProps<T extends React.ElementType = "div"> =
BaseHighlightProps<T> &
@@ -142,7 +141,7 @@ function Highlight<T extends React.ElementType = "div">({
const localRef = React.useRef<HTMLDivElement>(null);
React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement);
const propsBoundsOffset = (props as ParentModeHighlightProps)?.boundsOffset;
const propsBoundsOffset = (props as ParentModeHighlightProps).boundsOffset;
const boundsOffset = propsBoundsOffset ?? DEFAULT_BOUNDS_OFFSET;
const boundsOffsetTop = boundsOffset.top ?? 0;
const boundsOffsetLeft = boundsOffset.left ?? 0;
@@ -249,7 +248,9 @@ function Highlight<T extends React.ElementType = "div">({
};
container.addEventListener("scroll", onScroll, { passive: true });
return () => container.removeEventListener("scroll", onScroll);
return () => {
container.removeEventListener("scroll", onScroll);
};
}, [mode, activeValue]);
const render = (children: React.ReactNode) => {
@@ -259,7 +260,7 @@ function Highlight<T extends React.ElementType = "div">({
ref={localRef}
data-slot="motion-highlight-container"
style={{ position: "relative", zIndex: 1 }}
className={(props as ParentModeHighlightProps)?.containerClassName}
className={(props as ParentModeHighlightProps).containerClassName}
>
<AnimatePresence initial={false} mode="wait">
{boundsState && (
@@ -320,7 +321,7 @@ function Highlight<T extends React.ElementType = "div">({
activeClassName: activeClassNameState,
setActiveClassName: setActiveClassNameState,
forceUpdateBounds: (props as ParentModeHighlightProps)
?.forceUpdateBounds,
.forceUpdateBounds,
}}
>
{enabled
@@ -328,7 +329,7 @@ function Highlight<T extends React.ElementType = "div">({
? render(children)
: render(
React.Children.map(children, (child, index) => (
<HighlightItem key={index} className={props?.itemsClassName}>
<HighlightItem key={index} className={props.itemsClassName}>
{child}
</HighlightItem>
)),
@@ -418,7 +419,7 @@ function HighlightItem<T extends React.ElementType>({
const Component = as ?? "div";
const element = children as React.ReactElement<ExtendedChildProps>;
const childValue =
id ?? value ?? element.props?.["data-value"] ?? element.props?.id ?? itemId;
id ?? value ?? element.props["data-value"] ?? element.props.id ?? itemId;
const isActive = activeValue === childValue;
const isDisabled = disabled === undefined ? contextDisabled : disabled;
const itemTransition = transition ?? contextTransition;
@@ -466,7 +467,10 @@ function HighlightItem<T extends React.ElementType>({
setActiveClassName(activeClassName ?? "");
} else if (!activeValue) clearBounds();
if (shouldUpdateBounds) return () => cancelAnimationFrame(rafId);
if (shouldUpdateBounds)
return () => {
cancelAnimationFrame(rafId);
};
}, [
mode,
isActive,
+2 -2
View File
@@ -50,11 +50,11 @@ const rippleVariants = cva("absolute rounded-full size-5 pointer-events-none", {
},
});
type Ripple = {
interface Ripple {
id: number;
x: number;
y: number;
};
}
type RippleButtonProps = HTMLMotionProps<"button"> & {
children: React.ReactNode;
+2 -2
View File
@@ -20,10 +20,10 @@ import { useControlledState } from "@/hooks/use-controlled-state";
import { getStrictContext } from "@/lib/get-strict-context";
import { cn } from "@/lib/utils";
type TabsContextType = {
interface TabsContextType {
value: string | undefined;
setValue: TabsProps["onValueChange"];
};
}
const [TabsProvider, useTabs] =
getStrictContext<TabsContextType>("TabsContext");
+1 -2
View File
@@ -2,8 +2,7 @@ import * as React from "react";
import { cn } from "@/lib/utils";
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
+39 -33
View File
@@ -273,7 +273,9 @@ export function VpnFormDialog({
<Label>VPN Type</Label>
<Select
value={vpnType}
onValueChange={(value) => setVpnType(value as VpnType)}
onValueChange={(value) => {
setVpnType(value as VpnType);
}}
disabled={isSubmitting}
>
<SelectTrigger className="w-full">
@@ -294,7 +296,9 @@ export function VpnFormDialog({
<Input
id="wg-name"
value={wireGuardForm.name}
onChange={(e) => updateWireGuard("name", e.target.value)}
onChange={(e) => {
updateWireGuard("name", e.target.value);
}}
placeholder="e.g. Home WireGuard"
disabled={isSubmitting}
/>
@@ -307,9 +311,9 @@ export function VpnFormDialog({
<Input
id="wg-private-key"
value={wireGuardForm.privateKey}
onChange={(e) =>
updateWireGuard("privateKey", e.target.value)
}
onChange={(e) => {
updateWireGuard("privateKey", e.target.value);
}}
placeholder="Base64-encoded private key"
disabled={isSubmitting}
/>
@@ -320,9 +324,9 @@ export function VpnFormDialog({
<Input
id="wg-address"
value={wireGuardForm.address}
onChange={(e) =>
updateWireGuard("address", e.target.value)
}
onChange={(e) => {
updateWireGuard("address", e.target.value);
}}
placeholder="e.g. 10.0.0.2/24"
disabled={isSubmitting}
/>
@@ -334,9 +338,9 @@ export function VpnFormDialog({
<Input
id="wg-dns"
value={wireGuardForm.dns}
onChange={(e) =>
updateWireGuard("dns", e.target.value)
}
onChange={(e) => {
updateWireGuard("dns", e.target.value);
}}
placeholder="e.g. 1.1.1.1"
disabled={isSubmitting}
/>
@@ -348,9 +352,9 @@ export function VpnFormDialog({
id="wg-mtu"
type="number"
value={wireGuardForm.mtu}
onChange={(e) =>
updateWireGuard("mtu", e.target.value)
}
onChange={(e) => {
updateWireGuard("mtu", e.target.value);
}}
placeholder="e.g. 1420"
disabled={isSubmitting}
/>
@@ -364,9 +368,9 @@ export function VpnFormDialog({
<Input
id="wg-peer-public-key"
value={wireGuardForm.peerPublicKey}
onChange={(e) =>
updateWireGuard("peerPublicKey", e.target.value)
}
onChange={(e) => {
updateWireGuard("peerPublicKey", e.target.value);
}}
placeholder="Base64-encoded peer public key"
disabled={isSubmitting}
/>
@@ -377,9 +381,9 @@ export function VpnFormDialog({
<Input
id="wg-peer-endpoint"
value={wireGuardForm.peerEndpoint}
onChange={(e) =>
updateWireGuard("peerEndpoint", e.target.value)
}
onChange={(e) => {
updateWireGuard("peerEndpoint", e.target.value);
}}
placeholder="e.g. vpn.example.com:51820"
disabled={isSubmitting}
/>
@@ -390,9 +394,9 @@ export function VpnFormDialog({
<Input
id="wg-allowed-ips"
value={wireGuardForm.allowedIps}
onChange={(e) =>
updateWireGuard("allowedIps", e.target.value)
}
onChange={(e) => {
updateWireGuard("allowedIps", e.target.value);
}}
placeholder="e.g. 0.0.0.0/0, ::/0"
disabled={isSubmitting}
/>
@@ -407,12 +411,12 @@ export function VpnFormDialog({
id="wg-keepalive"
type="number"
value={wireGuardForm.persistentKeepalive}
onChange={(e) =>
onChange={(e) => {
updateWireGuard(
"persistentKeepalive",
e.target.value,
)
}
);
}}
placeholder="e.g. 25"
disabled={isSubmitting}
/>
@@ -425,9 +429,9 @@ export function VpnFormDialog({
<Input
id="wg-preshared-key"
value={wireGuardForm.presharedKey}
onChange={(e) =>
updateWireGuard("presharedKey", e.target.value)
}
onChange={(e) => {
updateWireGuard("presharedKey", e.target.value);
}}
placeholder="Base64-encoded preshared key"
disabled={isSubmitting}
/>
@@ -445,7 +449,9 @@ export function VpnFormDialog({
<Input
id="ovpn-name"
value={openVpnForm.name}
onChange={(e) => updateOpenVpn("name", e.target.value)}
onChange={(e) => {
updateOpenVpn("name", e.target.value);
}}
placeholder="e.g. Work OpenVPN"
disabled={isSubmitting}
/>
@@ -457,9 +463,9 @@ export function VpnFormDialog({
<Textarea
id="ovpn-config"
value={openVpnForm.rawConfig}
onChange={(e) =>
updateOpenVpn("rawConfig", e.target.value)
}
onChange={(e) => {
updateOpenVpn("rawConfig", e.target.value);
}}
placeholder="Paste your .ovpn file content here..."
className="min-h-[200px] font-mono text-xs"
disabled={isSubmitting}
+3 -1
View File
@@ -276,7 +276,9 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
id="vpn-name"
placeholder="My VPN"
value={vpnName}
onChange={(e) => setVpnName(e.target.value)}
onChange={(e) => {
setVpnName(e.target.value);
}}
/>
</div>
+186 -184
View File
@@ -84,7 +84,7 @@ export function WayfernConfigForm({
try {
const configJson = JSON.stringify(config);
const result = await invoke<string>("generate_sample_fingerprint", {
browser: profileBrowser || "wayfern",
browser: profileBrowser ?? "wayfern",
version: profileVersion,
configJson,
});
@@ -193,7 +193,9 @@ export function WayfernConfigForm({
</div>
<Select
value={selectedOS}
onValueChange={(value: WayfernOS) => onConfigChange("os", value)}
onValueChange={(value: WayfernOS) => {
onConfigChange("os", value);
}}
disabled={readOnly}
>
<SelectTrigger>
@@ -229,10 +231,10 @@ export function WayfernConfigForm({
<div className="flex items-center space-x-2">
<Checkbox
id="randomize-fingerprint"
checked={config.randomize_fingerprint_on_launch || false}
onCheckedChange={(checked) =>
onConfigChange("randomize_fingerprint_on_launch", checked)
}
checked={config.randomize_fingerprint_on_launch ?? false}
onCheckedChange={(checked) => {
onConfigChange("randomize_fingerprint_on_launch", checked);
}}
disabled={readOnly}
/>
<Label htmlFor="randomize-fingerprint" className="font-medium">
@@ -293,13 +295,13 @@ export function WayfernConfigForm({
<Label htmlFor="user-agent">{t("fingerprint.userAgent")}</Label>
<Input
id="user-agent"
value={fingerprintConfig.userAgent || ""}
onChange={(e) =>
value={fingerprintConfig.userAgent ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"userAgent",
e.target.value || undefined,
)
}
);
}}
placeholder="Mozilla/5.0..."
/>
</div>
@@ -307,13 +309,13 @@ export function WayfernConfigForm({
<Label htmlFor="platform">{t("fingerprint.platform")}</Label>
<Input
id="platform"
value={fingerprintConfig.platform || ""}
onChange={(e) =>
value={fingerprintConfig.platform ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"platform",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., Win32, MacIntel, Linux x86_64"
/>
</div>
@@ -323,13 +325,13 @@ export function WayfernConfigForm({
</Label>
<Input
id="platform-version"
value={fingerprintConfig.platformVersion || ""}
onChange={(e) =>
value={fingerprintConfig.platformVersion ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"platformVersion",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., 10.0.0"
/>
</div>
@@ -337,13 +339,13 @@ export function WayfernConfigForm({
<Label htmlFor="brand">{t("fingerprint.brand")}</Label>
<Input
id="brand"
value={fingerprintConfig.brand || ""}
onChange={(e) =>
value={fingerprintConfig.brand ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"brand",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., Google Chrome"
/>
</div>
@@ -353,13 +355,13 @@ export function WayfernConfigForm({
</Label>
<Input
id="brand-version"
value={fingerprintConfig.brandVersion || ""}
onChange={(e) =>
value={fingerprintConfig.brandVersion ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"brandVersion",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., 143"
/>
</div>
@@ -377,13 +379,13 @@ export function WayfernConfigForm({
<Input
id="hardware-concurrency"
type="number"
value={fingerprintConfig.hardwareConcurrency || ""}
onChange={(e) =>
value={fingerprintConfig.hardwareConcurrency ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"hardwareConcurrency",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 8"
/>
</div>
@@ -394,13 +396,13 @@ export function WayfernConfigForm({
<Input
id="max-touch-points"
type="number"
value={fingerprintConfig.maxTouchPoints || ""}
onChange={(e) =>
value={fingerprintConfig.maxTouchPoints ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"maxTouchPoints",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 0"
/>
</div>
@@ -411,13 +413,13 @@ export function WayfernConfigForm({
<Input
id="device-memory"
type="number"
value={fingerprintConfig.deviceMemory || ""}
onChange={(e) =>
value={fingerprintConfig.deviceMemory ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"deviceMemory",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 8"
/>
</div>
@@ -435,13 +437,13 @@ export function WayfernConfigForm({
<Input
id="screen-width"
type="number"
value={fingerprintConfig.screenWidth || ""}
onChange={(e) =>
value={fingerprintConfig.screenWidth ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screenWidth",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1920"
/>
</div>
@@ -452,13 +454,13 @@ export function WayfernConfigForm({
<Input
id="screen-height"
type="number"
value={fingerprintConfig.screenHeight || ""}
onChange={(e) =>
value={fingerprintConfig.screenHeight ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screenHeight",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1080"
/>
</div>
@@ -470,13 +472,13 @@ export function WayfernConfigForm({
id="device-pixel-ratio"
type="number"
step="0.1"
value={fingerprintConfig.devicePixelRatio || ""}
onChange={(e) =>
value={fingerprintConfig.devicePixelRatio ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"devicePixelRatio",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
);
}}
placeholder="e.g., 1.0"
/>
</div>
@@ -487,13 +489,13 @@ export function WayfernConfigForm({
<Input
id="screen-avail-width"
type="number"
value={fingerprintConfig.screenAvailWidth || ""}
onChange={(e) =>
value={fingerprintConfig.screenAvailWidth ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screenAvailWidth",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1920"
/>
</div>
@@ -504,13 +506,13 @@ export function WayfernConfigForm({
<Input
id="screen-avail-height"
type="number"
value={fingerprintConfig.screenAvailHeight || ""}
onChange={(e) =>
value={fingerprintConfig.screenAvailHeight ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screenAvailHeight",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1040"
/>
</div>
@@ -521,13 +523,13 @@ export function WayfernConfigForm({
<Input
id="screen-color-depth"
type="number"
value={fingerprintConfig.screenColorDepth || ""}
onChange={(e) =>
value={fingerprintConfig.screenColorDepth ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screenColorDepth",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 24"
/>
</div>
@@ -545,13 +547,13 @@ export function WayfernConfigForm({
<Input
id="window-outer-width"
type="number"
value={fingerprintConfig.windowOuterWidth || ""}
onChange={(e) =>
value={fingerprintConfig.windowOuterWidth ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"windowOuterWidth",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1920"
/>
</div>
@@ -562,13 +564,13 @@ export function WayfernConfigForm({
<Input
id="window-outer-height"
type="number"
value={fingerprintConfig.windowOuterHeight || ""}
onChange={(e) =>
value={fingerprintConfig.windowOuterHeight ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"windowOuterHeight",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1040"
/>
</div>
@@ -579,13 +581,13 @@ export function WayfernConfigForm({
<Input
id="window-inner-width"
type="number"
value={fingerprintConfig.windowInnerWidth || ""}
onChange={(e) =>
value={fingerprintConfig.windowInnerWidth ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"windowInnerWidth",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 1920"
/>
</div>
@@ -596,13 +598,13 @@ export function WayfernConfigForm({
<Input
id="window-inner-height"
type="number"
value={fingerprintConfig.windowInnerHeight || ""}
onChange={(e) =>
value={fingerprintConfig.windowInnerHeight ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"windowInnerHeight",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 940"
/>
</div>
@@ -611,13 +613,13 @@ export function WayfernConfigForm({
<Input
id="screen-x"
type="number"
value={fingerprintConfig.screenX || ""}
onChange={(e) =>
value={fingerprintConfig.screenX ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screenX",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 0"
/>
</div>
@@ -626,13 +628,13 @@ export function WayfernConfigForm({
<Input
id="screen-y"
type="number"
value={fingerprintConfig.screenY || ""}
onChange={(e) =>
value={fingerprintConfig.screenY ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"screenY",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 0"
/>
</div>
@@ -649,13 +651,13 @@ export function WayfernConfigForm({
</Label>
<Input
id="language"
value={fingerprintConfig.language || ""}
onChange={(e) =>
value={fingerprintConfig.language ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"language",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., en-US"
/>
</div>
@@ -690,10 +692,10 @@ export function WayfernConfigForm({
{t("fingerprint.doNotTrack")}
</Label>
<Select
value={fingerprintConfig.doNotTrack || ""}
onValueChange={(value) =>
updateFingerprintConfig("doNotTrack", value || undefined)
}
value={fingerprintConfig.doNotTrack ?? ""}
onValueChange={(value) => {
updateFingerprintConfig("doNotTrack", value || undefined);
}}
>
<SelectTrigger>
<SelectValue
@@ -729,13 +731,13 @@ export function WayfernConfigForm({
</Label>
<Input
id="timezone"
value={fingerprintConfig.timezone || ""}
onChange={(e) =>
value={fingerprintConfig.timezone ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"timezone",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., America/New_York"
/>
</div>
@@ -747,12 +749,12 @@ export function WayfernConfigForm({
id="timezone-offset"
type="number"
value={fingerprintConfig.timezoneOffset ?? ""}
onChange={(e) =>
onChange={(e) => {
updateFingerprintConfig(
"timezoneOffset",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 300 for EST (UTC-5)"
/>
</div>
@@ -762,13 +764,13 @@ export function WayfernConfigForm({
id="latitude"
type="number"
step="any"
value={fingerprintConfig.latitude || ""}
onChange={(e) =>
value={fingerprintConfig.latitude ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"latitude",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
);
}}
placeholder="e.g., 40.7128"
/>
</div>
@@ -778,13 +780,13 @@ export function WayfernConfigForm({
id="longitude"
type="number"
step="any"
value={fingerprintConfig.longitude || ""}
onChange={(e) =>
value={fingerprintConfig.longitude ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"longitude",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
);
}}
placeholder="e.g., -74.0060"
/>
</div>
@@ -793,13 +795,13 @@ export function WayfernConfigForm({
<Input
id="accuracy"
type="number"
value={fingerprintConfig.accuracy || ""}
onChange={(e) =>
value={fingerprintConfig.accuracy ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"accuracy",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
);
}}
placeholder="e.g., 100"
/>
</div>
@@ -816,13 +818,13 @@ export function WayfernConfigForm({
</Label>
<Input
id="webgl-vendor"
value={fingerprintConfig.webglVendor || ""}
onChange={(e) =>
value={fingerprintConfig.webglVendor ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"webglVendor",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., Intel"
/>
</div>
@@ -832,13 +834,13 @@ export function WayfernConfigForm({
</Label>
<Input
id="webgl-renderer"
value={fingerprintConfig.webglRenderer || ""}
onChange={(e) =>
value={fingerprintConfig.webglRenderer ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"webglRenderer",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., Intel(R) HD Graphics"
/>
</div>
@@ -849,13 +851,13 @@ export function WayfernConfigForm({
<div className="space-y-3">
<Label>{t("fingerprint.webglParametersJson")}</Label>
<Textarea
value={fingerprintConfig.webglParameters || ""}
onChange={(e) =>
value={fingerprintConfig.webglParameters ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"webglParameters",
e.target.value || undefined,
)
}
);
}}
placeholder='{"7936": "Intel", "7937": "Intel(R) HD Graphics"}'
className="font-mono text-sm"
rows={4}
@@ -871,13 +873,13 @@ export function WayfernConfigForm({
</Label>
<Input
id="canvas-noise-seed"
value={fingerprintConfig.canvasNoiseSeed || ""}
onChange={(e) =>
value={fingerprintConfig.canvasNoiseSeed ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"canvasNoiseSeed",
e.target.value || undefined,
)
}
);
}}
placeholder="Enter a seed string for canvas fingerprint"
/>
<p className="text-sm text-muted-foreground">
@@ -890,10 +892,10 @@ export function WayfernConfigForm({
<div className="space-y-3">
<Label>{t("fingerprint.fontsJson")}</Label>
<Textarea
value={fingerprintConfig.fonts || ""}
onChange={(e) =>
updateFingerprintConfig("fonts", e.target.value || undefined)
}
value={fingerprintConfig.fonts ?? ""}
onChange={(e) => {
updateFingerprintConfig("fonts", e.target.value || undefined);
}}
placeholder='["Arial", "Verdana", "Times New Roman"]'
className="font-mono text-sm"
rows={3}
@@ -911,13 +913,13 @@ export function WayfernConfigForm({
<Input
id="audio-sample-rate"
type="number"
value={fingerprintConfig.audioSampleRate || ""}
onChange={(e) =>
value={fingerprintConfig.audioSampleRate ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"audioSampleRate",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 48000"
/>
</div>
@@ -928,13 +930,13 @@ export function WayfernConfigForm({
<Input
id="audio-max-channel-count"
type="number"
value={fingerprintConfig.audioMaxChannelCount || ""}
onChange={(e) =>
value={fingerprintConfig.audioMaxChannelCount ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"audioMaxChannelCount",
e.target.value ? parseInt(e.target.value, 10) : undefined,
)
}
);
}}
placeholder="e.g., 2"
/>
</div>
@@ -949,13 +951,13 @@ export function WayfernConfigForm({
<div className="flex items-center space-x-2">
<Checkbox
id="battery-charging"
checked={fingerprintConfig.batteryCharging || false}
onCheckedChange={(checked) =>
checked={fingerprintConfig.batteryCharging ?? false}
onCheckedChange={(checked) => {
updateFingerprintConfig(
"batteryCharging",
checked || undefined,
)
}
);
}}
/>
<Label htmlFor="battery-charging">
{t("fingerprint.charging")}
@@ -972,13 +974,13 @@ export function WayfernConfigForm({
step="0.01"
min="0"
max="1"
value={fingerprintConfig.batteryLevel || ""}
onChange={(e) =>
value={fingerprintConfig.batteryLevel ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"batteryLevel",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
);
}}
placeholder="e.g., 0.85"
/>
</div>
@@ -993,13 +995,13 @@ export function WayfernConfigForm({
<Label htmlFor="vendor">{t("fingerprint.vendor")}</Label>
<Input
id="vendor"
value={fingerprintConfig.vendor || ""}
onChange={(e) =>
value={fingerprintConfig.vendor ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"vendor",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., Google Inc."
/>
</div>
@@ -1007,13 +1009,13 @@ export function WayfernConfigForm({
<Label htmlFor="vendor-sub">{t("fingerprint.vendorSub")}</Label>
<Input
id="vendor-sub"
value={fingerprintConfig.vendorSub || ""}
onChange={(e) =>
value={fingerprintConfig.vendorSub ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"vendorSub",
e.target.value || undefined,
)
}
);
}}
placeholder=""
/>
</div>
@@ -1023,13 +1025,13 @@ export function WayfernConfigForm({
</Label>
<Input
id="product-sub"
value={fingerprintConfig.productSub || ""}
onChange={(e) =>
value={fingerprintConfig.productSub ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"productSub",
e.target.value || undefined,
)
}
);
}}
placeholder="e.g., 20030107"
/>
</div>
@@ -1082,9 +1084,9 @@ export function WayfernConfigForm({
<Label>{t("fingerprint.osLabel")}</Label>
<Select
value={selectedOS}
onValueChange={(value: WayfernOS) =>
onConfigChange("os", value)
}
onValueChange={(value: WayfernOS) => {
onConfigChange("os", value);
}}
disabled={readOnly}
>
<SelectTrigger>
@@ -1128,10 +1130,10 @@ export function WayfernConfigForm({
<div className="flex items-center space-x-2">
<Checkbox
id="randomize-fingerprint-auto"
checked={config.randomize_fingerprint_on_launch || false}
onCheckedChange={(checked) =>
onConfigChange("randomize_fingerprint_on_launch", checked)
}
checked={config.randomize_fingerprint_on_launch ?? false}
onCheckedChange={(checked) => {
onConfigChange("randomize_fingerprint_on_launch", checked);
}}
disabled={readOnly}
/>
<Label
@@ -1180,15 +1182,15 @@ export function WayfernConfigForm({
<Input
id="screen-max-width"
type="number"
value={config.screen_max_width || ""}
onChange={(e) =>
value={config.screen_max_width ?? ""}
onChange={(e) => {
onConfigChange(
"screen_max_width",
e.target.value
? parseInt(e.target.value, 10)
: undefined,
)
}
);
}}
placeholder="e.g., 1920"
/>
</div>
@@ -1199,15 +1201,15 @@ export function WayfernConfigForm({
<Input
id="screen-max-height"
type="number"
value={config.screen_max_height || ""}
onChange={(e) =>
value={config.screen_max_height ?? ""}
onChange={(e) => {
onConfigChange(
"screen_max_height",
e.target.value
? parseInt(e.target.value, 10)
: undefined,
)
}
);
}}
placeholder="e.g., 1080"
/>
</div>
@@ -1218,15 +1220,15 @@ export function WayfernConfigForm({
<Input
id="screen-min-width"
type="number"
value={config.screen_min_width || ""}
onChange={(e) =>
value={config.screen_min_width ?? ""}
onChange={(e) => {
onConfigChange(
"screen_min_width",
e.target.value
? parseInt(e.target.value, 10)
: undefined,
)
}
);
}}
placeholder="e.g., 800"
/>
</div>
@@ -1237,15 +1239,15 @@ export function WayfernConfigForm({
<Input
id="screen-min-height"
type="number"
value={config.screen_min_height || ""}
onChange={(e) =>
value={config.screen_min_height ?? ""}
onChange={(e) => {
onConfigChange(
"screen_min_height",
e.target.value
? parseInt(e.target.value, 10)
: undefined,
)
}
);
}}
placeholder="e.g., 600"
/>
</div>
+9 -3
View File
@@ -45,9 +45,15 @@ export function WayfernTermsDialog({
<Dialog open={isOpen}>
<DialogContent
className="sm:max-w-lg"
onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => {
e.preventDefault();
}}
onPointerDownOutside={(e) => {
e.preventDefault();
}}
onInteractOutside={(e) => {
e.preventDefault();
}}
>
<DialogHeader>
<DialogTitle>Wayfern Terms and Conditions</DialogTitle>
+6 -2
View File
@@ -93,7 +93,9 @@ export function WindowDragArea() {
<div className="flex items-center h-full">
<button
type="button"
onClick={handleMinimize}
onClick={() => {
void handleMinimize();
}}
className="flex items-center justify-center w-12 h-full hover:bg-muted/50 transition-colors text-muted-foreground hover:text-foreground"
>
<svg
@@ -109,7 +111,9 @@ export function WindowDragArea() {
</button>
<button
type="button"
onClick={handleClose}
onClick={() => {
void handleClose();
}}
className="flex items-center justify-center w-12 h-full hover:bg-destructive/90 transition-colors text-muted-foreground hover:text-destructive-foreground"
>
<svg
@@ -63,9 +63,15 @@ export function WindowResizeWarningDialog({
<Dialog open={isOpen}>
<DialogContent
className="sm:max-w-sm"
onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => {
e.preventDefault();
}}
onPointerDownOutside={(e) => {
e.preventDefault();
}}
onInteractOutside={(e) => {
e.preventDefault();
}}
>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
@@ -77,7 +83,9 @@ export function WindowResizeWarningDialog({
<Checkbox
id="dont-show-again"
checked={dontShowAgain}
onCheckedChange={(checked) => setDontShowAgain(checked === true)}
onCheckedChange={(checked) => {
setDontShowAgain(checked === true);
}}
/>
<Label htmlFor="dont-show-again" className="text-sm">
{t("warnings.dontShowAgain")}
+11 -16
View File
@@ -2,10 +2,10 @@
import * as React from "react";
type AutoHeightOptions = {
interface AutoHeightOptions {
includeParentBox?: boolean;
includeSelfBox?: boolean;
};
}
export function useAutoHeight<T extends HTMLElement = HTMLDivElement>(
deps: React.DependencyList = [],
@@ -22,18 +22,15 @@ export function useAutoHeight<T extends HTMLElement = HTMLDivElement>(
const el = ref.current;
if (!el) return 0;
const base = el.getBoundingClientRect().height || 0;
const base = el.getBoundingClientRect().height;
let extra = 0;
if (options.includeParentBox && el.parentElement) {
const cs = getComputedStyle(el.parentElement);
const paddingY =
(parseFloat(cs.paddingTop || "0") || 0) +
(parseFloat(cs.paddingBottom || "0") || 0);
const paddingY = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom);
const borderY =
(parseFloat(cs.borderTopWidth || "0") || 0) +
(parseFloat(cs.borderBottomWidth || "0") || 0);
parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth);
const isBorderBox = cs.boxSizing === "border-box";
if (isBorderBox) {
extra += paddingY + borderY;
@@ -42,20 +39,16 @@ export function useAutoHeight<T extends HTMLElement = HTMLDivElement>(
if (options.includeSelfBox) {
const cs = getComputedStyle(el);
const paddingY =
(parseFloat(cs.paddingTop || "0") || 0) +
(parseFloat(cs.paddingBottom || "0") || 0);
const paddingY = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom);
const borderY =
(parseFloat(cs.borderTopWidth || "0") || 0) +
(parseFloat(cs.borderBottomWidth || "0") || 0);
parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth);
const isBorderBox = cs.boxSizing === "border-box";
if (isBorderBox) {
extra += paddingY + borderY;
}
}
const dpr =
typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
const total = Math.ceil((base + extra) * dpr) / dpr;
return total;
@@ -74,7 +67,9 @@ export function useAutoHeight<T extends HTMLElement = HTMLDivElement>(
const ro = new ResizeObserver(() => {
const next = measure();
requestAnimationFrame(() => setHeight(next));
requestAnimationFrame(() => {
setHeight(next);
});
});
ro.observe(el);
+6 -4
View File
@@ -314,9 +314,9 @@ export function useBrowserDownload() {
invoke("cancel_download", {
browserStr: progress.browser,
version: progress.version,
}).catch((err) =>
console.error("Failed to cancel download:", err),
);
}).catch((err) => {
console.error("Failed to cancel download:", err);
});
dismissToast(toastId);
},
},
@@ -367,7 +367,9 @@ export function useBrowserDownload() {
? loadDownloadedVersions("camoufox")
: Promise.resolve([]),
]);
} catch {}
} catch {
/* empty */
}
showDownloadToast(browserName, progress.version, "completed");
setDownloadProgress(null);
}
-1
View File
@@ -15,7 +15,6 @@ export function useBrowserState(
_isUpdating: (browser: string) => boolean,
launchingProfiles: Set<string>,
stoppingProfiles: Set<string>,
_crossOsUnlocked = false,
) {
const [isClient, setIsClient] = useState(false);
+3 -3
View File
@@ -30,14 +30,14 @@ export function useCloudAuth(): UseCloudAuthReturn {
}, []);
useEffect(() => {
loadUser();
void loadUser();
const unlistenExpired = listen("cloud-auth-expired", () => {
setAuthState(null);
});
const unlistenChanged = listen("cloud-auth-changed", () => {
loadUser();
void loadUser();
});
return () => {
@@ -50,7 +50,7 @@ export function useCloudAuth(): UseCloudAuthReturn {
};
}, [loadUser]);
const requestOtp = useCallback(async (email: string): Promise<string> => {
const requestOtp = useCallback((email: string): Promise<string> => {
return invoke<string>("cloud_request_otp", { email });
}, []);
+7 -3
View File
@@ -43,11 +43,15 @@ export function useCommercialTrial(): UseCommercialTrialReturn {
}, []);
useEffect(() => {
checkTrialStatus();
void checkTrialStatus();
// Check trial status every minute to update the countdown
const interval = setInterval(checkTrialStatus, 60000);
return () => clearInterval(interval);
const interval = setInterval(() => {
void checkTrialStatus();
}, 60000);
return () => {
clearInterval(interval);
};
}, [checkTrialStatus]);
return {
+2 -3
View File
@@ -5,8 +5,7 @@ interface CommonControlledStateProps<T> {
defaultValue?: T;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useControlledState<T, Rest extends any[] = []>(
export function useControlledState<T, Rest extends unknown[] = []>(
props: CommonControlledStateProps<T> & {
onChange?: (value: T, ...args: Rest) => void;
},
@@ -14,7 +13,7 @@ export function useControlledState<T, Rest extends any[] = []>(
const { value, defaultValue, onChange } = props;
const [state, setInternalState] = React.useState<T>(
value !== undefined ? value : (defaultValue as T),
value ?? (defaultValue as T),
);
React.useEffect(() => {
+2 -2
View File
@@ -120,7 +120,7 @@ export function usePermissions(): UsePermissionsReturn {
// Initialize platform detection and start interval checking
useEffect(() => {
const initializePlatform = async () => {
const initializePlatform = () => {
try {
// Detect platform - on macOS we need permissions, on others we don't
const userAgent = navigator.userAgent;
@@ -142,7 +142,7 @@ export function usePermissions(): UsePermissionsReturn {
}
};
initializePlatform().catch(console.error);
initializePlatform();
}, []);
// Set up interval checking when platform is determined

Some files were not shown because too many files have changed in this diff Show More