Compare commits

...

36 Commits

Author SHA1 Message Date
zhom 4fc2cb7730 Merge pull request #110 from zhom/dependabot/github_actions/github-actions-36c42c0093
ci(deps): bump the github-actions group with 2 updates
2025-10-13 06:30:41 +00:00
dependabot[bot] c41a5d84b2 ci(deps): bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [pnpm/action-setup](https://github.com/pnpm/action-setup) and [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `pnpm/action-setup` from 4.1.0 to 4.2.0
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/a7487c7e89a18df4991f7f222e4898a00d66ddda...41ff72655975bd51cab0327fa583b6e92b6d3061)

Updates `crate-ci/typos` from 1.37.2 to 1.38.1
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/7436548694def3314aacd93ed06c721b1f91ea04...80c8a4945eec0f6d464eaf9e65ed98ef085283d1)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: crate-ci/typos
  dependency-version: 1.38.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-11 09:06:58 +00:00
zhom fda2887aef Merge pull request #106 from zhom/dependabot/github_actions/github-actions-61623bb75b
ci(deps): bump the github-actions group with 5 updates
2025-10-04 09:30:48 +00:00
dependabot[bot] f58b790293 ci(deps): bump the github-actions group with 5 updates
Bumps the github-actions group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [google/osv-scanner-action](https://github.com/google/osv-scanner-action) | `2.2.2` | `2.2.3` |
| [ridedott/merge-me-action](https://github.com/ridedott/merge-me-action) | `2.10.130` | `2.10.131` |
| [actions/first-interaction](https://github.com/actions/first-interaction) | `3.0.0` | `3.1.0` |
| [crate-ci/typos](https://github.com/crate-ci/typos) | `1.36.3` | `1.37.2` |
| [actions/stale](https://github.com/actions/stale) | `10.0.0` | `10.1.0` |


Updates `google/osv-scanner-action` from 2.2.2 to 2.2.3
- [Release notes](https://github.com/google/osv-scanner-action/releases)
- [Commits](https://github.com/google/osv-scanner-action/compare/90b209d0ea55cea1da9fc0c4e65782cc6acb6e2e...e92b5d07338d4f0ba0981dffed17c48976ca4730)

Updates `ridedott/merge-me-action` from 2.10.130 to 2.10.131
- [Release notes](https://github.com/ridedott/merge-me-action/releases)
- [Changelog](https://github.com/ridedott/merge-me-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ridedott/merge-me-action/compare/a310eac074af628e0fd6c6d78858bba5bcf01179...a3b9ffd551d69f9f4375a87e9fa56235a0749518)

Updates `actions/first-interaction` from 3.0.0 to 3.1.0
- [Release notes](https://github.com/actions/first-interaction/releases)
- [Commits](https://github.com/actions/first-interaction/compare/753c925c8d1ac6fede23781875376600628d9b5d...1c4688942c71f71d4f5502a26ea67c331730fa4d)

Updates `crate-ci/typos` from 1.36.3 to 1.37.2
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/0c17dabcee8b8f1957fa917d17393a23e02e1583...7436548694def3314aacd93ed06c721b1f91ea04)

Updates `actions/stale` from 10.0.0 to 10.1.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/3a9db7e6a41a89f618792c92c0e97cc736e1b13f...5f858e3efba33a5ca4407a664cc011ad407f2008)

---
updated-dependencies:
- dependency-name: google/osv-scanner-action
  dependency-version: 2.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: ridedott/merge-me-action
  dependency-version: 2.10.131
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/first-interaction
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: crate-ci/typos
  dependency-version: 1.37.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/stale
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-04 09:05:48 +00:00
zhom 518a02f782 chore: version bump 2025-10-02 20:35:20 +04:00
zhom 0999a265dc chore: hide allow addon new tab 2025-10-02 20:33:09 +04:00
zhom 984f529505 chore: remove unused dependency 2025-10-02 19:50:22 +04:00
zhom 3b030df37f build: install dependencies in correct order 2025-10-02 19:49:10 +04:00
zhom 03b8cae825 feat: allow user configuring allowAddonNewTab 2025-10-02 19:43:18 +04:00
zhom 00e486cc85 build: set pnpm version only in package.json 2025-09-30 10:34:35 +04:00
zhom 640185ff2e build: set pnpm to 10 and install dependencies manually 2025-09-30 10:24:52 +04:00
zhom 22fa2cfef0 fix: don't create 2 .desktop files 2025-09-30 10:13:23 +04:00
zhom a1db587314 chore: pnpm update 2025-09-30 09:34:10 +04:00
zhom 8862630a09 chore: cleanup 2025-09-30 09:34:10 +04:00
zhom 5956daeb9a Merge pull request #99 from zhom/dependabot/github_actions/github-actions-7ebf98940a
ci(deps): bump crate-ci/typos from 1.36.2 to 1.36.3 in the github-actions group
2025-09-27 15:20:42 +04:00
dependabot[bot] dfde9df72e ci(deps): bump crate-ci/typos in the github-actions group
Bumps the github-actions group with 1 update: [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `crate-ci/typos` from 1.36.2 to 1.36.3
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/85f62a8a84f939ae994ab3763f01a0296d61a7ee...0c17dabcee8b8f1957fa917d17393a23e02e1583)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.36.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-27 09:04:52 +00:00
zhom 3cbbd75618 Merge pull request #92 from zhom/dependabot/github_actions/github-actions-8939090574
ci(deps): bump the github-actions group with 2 updates
2025-09-20 13:25:16 +04:00
dependabot[bot] 8a32d73a25 ci(deps): bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [swatinem/rust-cache](https://github.com/swatinem/rust-cache) and [ridedott/merge-me-action](https://github.com/ridedott/merge-me-action).


Updates `swatinem/rust-cache` from 2.8.0 to 2.8.1
- [Release notes](https://github.com/swatinem/rust-cache/releases)
- [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md)
- [Commits](https://github.com/swatinem/rust-cache/compare/98c8021b550208e191a6a3145459bfc9fb29c4c0...f13886b937689c021905a6b90929199931d60db1)

Updates `ridedott/merge-me-action` from 2.10.129 to 2.10.130
- [Release notes](https://github.com/ridedott/merge-me-action/releases)
- [Changelog](https://github.com/ridedott/merge-me-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ridedott/merge-me-action/compare/884aad0742ac6ee2eb4ff7c4496786d73df4ff69...a310eac074af628e0fd6c6d78858bba5bcf01179)

---
updated-dependencies:
- dependency-name: swatinem/rust-cache
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: ridedott/merge-me-action
  dependency-version: 2.10.130
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-20 09:05:28 +00:00
zhom 2007080d4b Merge pull request #87 from zhom/dependabot/github_actions/github-actions-3a7b1f4069
ci(deps): bump ridedott/merge-me-action from 2.10.128 to 2.10.129 in the github-actions group
2025-09-14 23:40:26 +04:00
dependabot[bot] feb604ffaa ci(deps): bump ridedott/merge-me-action in the github-actions group
Bumps the github-actions group with 1 update: [ridedott/merge-me-action](https://github.com/ridedott/merge-me-action).


Updates `ridedott/merge-me-action` from 2.10.128 to 2.10.129
- [Release notes](https://github.com/ridedott/merge-me-action/releases)
- [Changelog](https://github.com/ridedott/merge-me-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ridedott/merge-me-action/compare/60142b76c22362f5845c877672fd2822b4d07c13...884aad0742ac6ee2eb4ff7c4496786d73df4ff69)

---
updated-dependencies:
- dependency-name: ridedott/merge-me-action
  dependency-version: 2.10.129
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-13 09:04:59 +00:00
zhom 14659180d7 Merge pull request #83 from zhom/dependabot/github_actions/github-actions-2ae6d0a682
ci(deps): bump the github-actions group with 4 updates
2025-09-06 20:36:18 +04:00
zhom 82ebd7dc18 Merge pull request #84 from zhom/dependabot/npm_and_yarn/frontend-dependencies-fa3fda6213
deps(deps): bump the frontend-dependencies group with 76 updates
2025-09-06 20:36:08 +04:00
zhom 1c995e676c Merge pull request #85 from zhom/dependabot/cargo/src-tauri/rust-dependencies-ebac297506
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 19 updates
2025-09-06 20:35:58 +04:00
dependabot[bot] e5fd63d03d deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 19 updates:

| Package | From | To |
| --- | --- | --- |
| [tauri](https://github.com/tauri-apps/tauri) | `2.8.4` | `2.8.5` |
| [tauri-plugin-deep-link](https://github.com/tauri-apps/plugins-workspace) | `2.4.2` | `2.4.3` |
| [tauri-plugin-dialog](https://github.com/tauri-apps/plugins-workspace) | `2.3.3` | `2.4.0` |
| [zip](https://github.com/zip-rs/zip2) | `4.5.0` | `5.0.0` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.18.0` | `1.18.1` |
| [windows](https://github.com/microsoft/windows-rs) | `0.61.3` | `0.62.0` |
| [tauri-build](https://github.com/tauri-apps/tauri) | `2.4.0` | `2.4.1` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.34` | `1.2.36` |
| [deadpool](https://github.com/bikeshedder/deadpool) | `0.12.2` | `0.12.3` |
| [libz-rs-sys](https://github.com/trifectatechfoundation/zlib-rs) | `0.5.1` | `0.5.2` |
| [log](https://github.com/rust-lang/log) | `0.4.27` | `0.4.28` |
| [rust-ini](https://github.com/zonyitoo/rust-ini) | `0.21.1` | `0.21.3` |
| [tao](https://github.com/tauri-apps/tao) | `0.34.2` | `0.34.3` |
| [time](https://github.com/time-rs/time) | `0.3.41` | `0.3.43` |
| [time-core](https://github.com/time-rs/time) | `0.1.4` | `0.1.6` |
| [time-macros](https://github.com/time-rs/time) | `0.2.22` | `0.2.24` |
| [windows-version](https://github.com/microsoft/windows-rs) | `0.1.4` | `0.1.5` |
| [zlib-rs](https://github.com/trifectatechfoundation/zlib-rs) | `0.5.1` | `0.5.2` |
| [zstd-sys](https://github.com/gyscos/zstd-rs) | `2.0.15+zstd.1.5.7` | `2.0.16+zstd.1.5.7` |


Updates `tauri` from 2.8.4 to 2.8.5
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.8.4...tauri-v2.8.5)

Updates `tauri-plugin-deep-link` from 2.4.2 to 2.4.3
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/fs-v2.4.2...http-v2.4.3)

Updates `tauri-plugin-dialog` from 2.3.3 to 2.4.0
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/dialog-v2.3.3...fs-v2.4.0)

Updates `zip` from 4.5.0 to 5.0.0
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v4.5.0...v5.0.0)

Updates `uuid` from 1.18.0 to 1.18.1
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.18.0...v1.18.1)

Updates `windows` from 0.61.3 to 0.62.0
- [Release notes](https://github.com/microsoft/windows-rs/releases)
- [Commits](https://github.com/microsoft/windows-rs/commits/0.62.0)

Updates `tauri-build` from 2.4.0 to 2.4.1
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-build-v2.4.0...tauri-build-v2.4.1)

Updates `cc` from 1.2.34 to 1.2.36
- [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.34...cc-v1.2.36)

Updates `deadpool` from 0.12.2 to 0.12.3
- [Changelog](https://github.com/deadpool-rs/deadpool/blob/main/release.toml)
- [Commits](https://github.com/bikeshedder/deadpool/compare/deadpool-v0.12.2...deadpool-v0.12.3)

Updates `libz-rs-sys` from 0.5.1 to 0.5.2
- [Release notes](https://github.com/trifectatechfoundation/zlib-rs/releases)
- [Changelog](https://github.com/trifectatechfoundation/zlib-rs/blob/main/docs/release.md)
- [Commits](https://github.com/trifectatechfoundation/zlib-rs/compare/v0.5.1...v0.5.2)

Updates `log` from 0.4.27 to 0.4.28
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.27...0.4.28)

Updates `rust-ini` from 0.21.1 to 0.21.3
- [Release notes](https://github.com/zonyitoo/rust-ini/releases)
- [Commits](https://github.com/zonyitoo/rust-ini/compare/v0.21.1...v0.21.3)

Updates `tao` from 0.34.2 to 0.34.3
- [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.2...tao-v0.34.3)

Updates `time` from 0.3.41 to 0.3.43
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.41...v0.3.43)

Updates `time-core` from 0.1.4 to 0.1.6
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/commits)

Updates `time-macros` from 0.2.22 to 0.2.24
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.2.22...v0.2.24)

Updates `windows-version` from 0.1.4 to 0.1.5
- [Release notes](https://github.com/microsoft/windows-rs/releases)
- [Commits](https://github.com/microsoft/windows-rs/commits)

Updates `zlib-rs` from 0.5.1 to 0.5.2
- [Release notes](https://github.com/trifectatechfoundation/zlib-rs/releases)
- [Changelog](https://github.com/trifectatechfoundation/zlib-rs/blob/main/docs/release.md)
- [Commits](https://github.com/trifectatechfoundation/zlib-rs/compare/v0.5.1...v0.5.2)

Updates `zstd-sys` from 2.0.15+zstd.1.5.7 to 2.0.16+zstd.1.5.7
- [Release notes](https://github.com/gyscos/zstd-rs/releases)
- [Commits](https://github.com/gyscos/zstd-rs/commits)

---
updated-dependencies:
- dependency-name: tauri
  dependency-version: 2.8.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tauri-plugin-deep-link
  dependency-version: 2.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tauri-plugin-dialog
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: zip
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: rust-dependencies
- dependency-name: uuid
  dependency-version: 1.18.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: windows
  dependency-version: 0.62.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-build
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.36
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: deadpool
  dependency-version: 0.12.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libz-rs-sys
  dependency-version: 0.5.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: log
  dependency-version: 0.4.28
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rust-ini
  dependency-version: 0.21.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tao
  dependency-version: 0.34.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: time
  dependency-version: 0.3.43
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: time-core
  dependency-version: 0.1.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: time-macros
  dependency-version: 0.2.24
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: windows-version
  dependency-version: 0.1.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zlib-rs
  dependency-version: 0.5.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zstd-sys
  dependency-version: 2.0.16+zstd.1.5.7
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-06 09:38:49 +00:00
dependabot[bot] 11200dbe09 deps(deps): bump the frontend-dependencies group with 76 updates
Bumps the frontend-dependencies group with 76 updates:

| Package | From | To |
| --- | --- | --- |
| [@tauri-apps/plugin-deep-link](https://github.com/tauri-apps/plugins-workspace) | `2.4.2` | `2.4.3` |
| [@tauri-apps/plugin-dialog](https://github.com/tauri-apps/plugins-workspace) | `2.3.3` | `2.4.0` |
| [ahooks](https://github.com/alibaba/hooks) | `3.9.4` | `3.9.5` |
| [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.2.2` | `2.2.3` |
| [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.1.12` | `4.1.13` |
| [@tauri-apps/cli](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.8.4` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.3.0` | `24.3.1` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `16.1.5` | `16.1.6` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.1.12` | `4.1.13` |
| [tw-animate-css](https://github.com/Wombosvideo/tw-animate-css) | `1.3.7` | `1.3.8` |
| [dotenv](https://github.com/motdotla/dotenv) | `17.2.1` | `17.2.2` |
| [fingerprint-generator](https://github.com/apify/fingerprint-suite) | `2.1.70` | `2.1.72` |
| [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) | `7.28.3` | `7.28.4` |
| [@biomejs/cli-darwin-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.2.2` | `2.2.3` |
| [@biomejs/cli-darwin-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.2.2` | `2.2.3` |
| [@biomejs/cli-linux-arm64-musl](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.2.2` | `2.2.3` |
| [@biomejs/cli-linux-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.2.2` | `2.2.3` |
| [@biomejs/cli-linux-x64-musl](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.2.2` | `2.2.3` |
| [@biomejs/cli-linux-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.2.2` | `2.2.3` |
| [@biomejs/cli-win32-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.2.2` | `2.2.3` |
| [@biomejs/cli-win32-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.2.2` | `2.2.3` |
| [@rollup/rollup-android-arm-eabi](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-android-arm64](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-darwin-arm64](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-darwin-x64](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-freebsd-arm64](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-freebsd-x64](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-linux-arm-gnueabihf](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-linux-arm-musleabihf](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-linux-arm64-gnu](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-linux-arm64-musl](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-linux-loongarch64-gnu](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-linux-ppc64-gnu](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-linux-riscv64-gnu](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-linux-riscv64-musl](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-linux-s390x-gnu](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-linux-x64-musl](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-win32-arm64-msvc](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-win32-ia32-msvc](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@rollup/rollup-win32-x64-msvc](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |
| [@tailwindcss/node](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-node) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide-android-arm64](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/android-arm64) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide-darwin-arm64](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/darwin-arm64) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide-darwin-x64](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/darwin-x64) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide-freebsd-x64](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/freebsd-x64) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide-linux-arm-gnueabihf](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/linux-arm-gnueabihf) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide-linux-arm64-gnu](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/linux-arm64-gnu) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide-linux-arm64-musl](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/linux-arm64-musl) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide-linux-x64-gnu](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/linux-x64-gnu) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide-linux-x64-musl](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/linux-x64-musl) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide-wasm32-wasi](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide-win32-arm64-msvc](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/win32-arm64-msvc) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide-win32-x64-msvc](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/win32-x64-msvc) | `4.1.12` | `4.1.13` |
| [@tailwindcss/oxide](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node) | `4.1.12` | `4.1.13` |
| [@tauri-apps/cli-darwin-arm64](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.8.4` |
| [@tauri-apps/cli-darwin-x64](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.8.4` |
| [@tauri-apps/cli-linux-arm-gnueabihf](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.8.4` |
| [@tauri-apps/cli-linux-arm64-gnu](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.8.4` |
| [@tauri-apps/cli-linux-arm64-musl](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.8.4` |
| [@tauri-apps/cli-linux-riscv64-gnu](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.8.4` |
| [@tauri-apps/cli-linux-x64-gnu](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.8.4` |
| [@tauri-apps/cli-linux-x64-musl](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.8.4` |
| [@tauri-apps/cli-win32-arm64-msvc](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.8.4` |
| [@tauri-apps/cli-win32-ia32-msvc](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.8.4` |
| [@tauri-apps/cli-win32-x64-msvc](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.8.4` |
| [browserslist](https://github.com/browserslist/browserslist) | `4.25.3` | `4.25.4` |
| [dayjs](https://github.com/iamkun/dayjs) | `1.11.13` | `1.11.18` |
| [electron-to-chromium](https://github.com/kilian/electron-to-chromium) | `1.5.209` | `1.5.214` |
| [generative-bayesian-network](https://github.com/apify/fingerprint-suite) | `2.1.70` | `2.1.72` |
| [get-east-asian-width](https://github.com/sindresorhus/get-east-asian-width) | `1.3.0` | `1.3.1` |
| [header-generator](https://github.com/apify/fingerprint-suite) | `2.1.70` | `2.1.72` |
| [listr2](https://github.com/listr2/listr2) | `9.0.2` | `9.0.3` |
| [nano-spawn](https://github.com/sindresorhus/nano-spawn) | `1.0.2` | `1.0.3` |
| [node-releases](https://github.com/chicoxyzzy/node-releases) | `2.0.19` | `2.0.20` |
| [rollup](https://github.com/rollup/rollup) | `4.49.0` | `4.50.0` |


Updates `@tauri-apps/plugin-deep-link` from 2.4.2 to 2.4.3
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/fs-v2.4.2...http-v2.4.3)

Updates `@tauri-apps/plugin-dialog` from 2.3.3 to 2.4.0
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/dialog-v2.3.3...fs-v2.4.0)

Updates `ahooks` from 3.9.4 to 3.9.5
- [Release notes](https://github.com/alibaba/hooks/releases)
- [Commits](https://github.com/alibaba/hooks/compare/v3.9.4...v3.9.5)

Updates `@biomejs/biome` from 2.2.2 to 2.2.3
- [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.2.3/packages/@biomejs/biome)

Updates `@tailwindcss/postcss` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/packages/@tailwindcss-postcss)

Updates `@tauri-apps/cli` from 2.8.3 to 2.8.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/@tauri-apps/cli-v2.8.3...@tauri-apps/cli-v2.8.4)

Updates `@types/node` from 24.3.0 to 24.3.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `lint-staged` from 16.1.5 to 16.1.6
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v16.1.5...v16.1.6)

Updates `tailwindcss` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/packages/tailwindcss)

Updates `tw-animate-css` from 1.3.7 to 1.3.8
- [Release notes](https://github.com/Wombosvideo/tw-animate-css/releases)
- [Commits](https://github.com/Wombosvideo/tw-animate-css/compare/v1.3.7...v1.3.8)

Updates `dotenv` from 17.2.1 to 17.2.2
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v17.2.1...v17.2.2)

Updates `fingerprint-generator` from 2.1.70 to 2.1.72
- [Release notes](https://github.com/apify/fingerprint-suite/releases)
- [Commits](https://github.com/apify/fingerprint-suite/compare/v2.1.70...v2.1.72)

Updates `@babel/runtime` from 7.28.3 to 7.28.4
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.4/packages/babel-runtime)

Updates `@biomejs/cli-darwin-arm64` from 2.2.2 to 2.2.3
- [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.2.3/packages/@biomejs/biome)

Updates `@biomejs/cli-darwin-x64` from 2.2.2 to 2.2.3
- [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.2.3/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-arm64-musl` from 2.2.2 to 2.2.3
- [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.2.3/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-arm64` from 2.2.2 to 2.2.3
- [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.2.3/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-x64-musl` from 2.2.2 to 2.2.3
- [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.2.3/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-x64` from 2.2.2 to 2.2.3
- [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.2.3/packages/@biomejs/biome)

Updates `@biomejs/cli-win32-arm64` from 2.2.2 to 2.2.3
- [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.2.3/packages/@biomejs/biome)

Updates `@biomejs/cli-win32-x64` from 2.2.2 to 2.2.3
- [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.2.3/packages/@biomejs/biome)

Updates `@rollup/rollup-android-arm-eabi` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-android-arm64` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-darwin-arm64` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-darwin-x64` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-freebsd-arm64` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-freebsd-x64` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-linux-arm-gnueabihf` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-linux-arm-musleabihf` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-linux-arm64-gnu` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-linux-arm64-musl` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-linux-loongarch64-gnu` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-linux-ppc64-gnu` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-linux-riscv64-gnu` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-linux-riscv64-musl` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-linux-s390x-gnu` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-linux-x64-gnu` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-linux-x64-musl` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-win32-arm64-msvc` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-win32-ia32-msvc` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@rollup/rollup-win32-x64-msvc` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

Updates `@tailwindcss/node` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/packages/@tailwindcss-node)

Updates `@tailwindcss/oxide-android-arm64` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node/npm/android-arm64)

Updates `@tailwindcss/oxide-darwin-arm64` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node/npm/darwin-arm64)

Updates `@tailwindcss/oxide-darwin-x64` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node/npm/darwin-x64)

Updates `@tailwindcss/oxide-freebsd-x64` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node/npm/freebsd-x64)

Updates `@tailwindcss/oxide-linux-arm-gnueabihf` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node/npm/linux-arm-gnueabihf)

Updates `@tailwindcss/oxide-linux-arm64-gnu` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node/npm/linux-arm64-gnu)

Updates `@tailwindcss/oxide-linux-arm64-musl` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node/npm/linux-arm64-musl)

Updates `@tailwindcss/oxide-linux-x64-gnu` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node/npm/linux-x64-gnu)

Updates `@tailwindcss/oxide-linux-x64-musl` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node/npm/linux-x64-musl)

Updates `@tailwindcss/oxide-wasm32-wasi` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node)

Updates `@tailwindcss/oxide-win32-arm64-msvc` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node/npm/win32-arm64-msvc)

Updates `@tailwindcss/oxide-win32-x64-msvc` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node/npm/win32-x64-msvc)

Updates `@tailwindcss/oxide` from 4.1.12 to 4.1.13
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.13/crates/node)

Updates `@tauri-apps/cli-darwin-arm64` from 2.8.3 to 2.8.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.8.3...tauri-v2.8.4)

Updates `@tauri-apps/cli-darwin-x64` from 2.8.3 to 2.8.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.8.3...tauri-v2.8.4)

Updates `@tauri-apps/cli-linux-arm-gnueabihf` from 2.8.3 to 2.8.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.8.3...tauri-v2.8.4)

Updates `@tauri-apps/cli-linux-arm64-gnu` from 2.8.3 to 2.8.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.8.3...tauri-v2.8.4)

Updates `@tauri-apps/cli-linux-arm64-musl` from 2.8.3 to 2.8.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.8.3...tauri-v2.8.4)

Updates `@tauri-apps/cli-linux-riscv64-gnu` from 2.8.3 to 2.8.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.8.3...tauri-v2.8.4)

Updates `@tauri-apps/cli-linux-x64-gnu` from 2.8.3 to 2.8.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.8.3...tauri-v2.8.4)

Updates `@tauri-apps/cli-linux-x64-musl` from 2.8.3 to 2.8.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.8.3...tauri-v2.8.4)

Updates `@tauri-apps/cli-win32-arm64-msvc` from 2.8.3 to 2.8.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.8.3...tauri-v2.8.4)

Updates `@tauri-apps/cli-win32-ia32-msvc` from 2.8.3 to 2.8.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.8.3...tauri-v2.8.4)

Updates `@tauri-apps/cli-win32-x64-msvc` from 2.8.3 to 2.8.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.8.3...tauri-v2.8.4)

Updates `browserslist` from 4.25.3 to 4.25.4
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.25.3...4.25.4)

Updates `dayjs` from 1.11.13 to 1.11.18
- [Release notes](https://github.com/iamkun/dayjs/releases)
- [Changelog](https://github.com/iamkun/dayjs/blob/v1.11.18/CHANGELOG.md)
- [Commits](https://github.com/iamkun/dayjs/compare/v1.11.13...v1.11.18)

Updates `electron-to-chromium` from 1.5.209 to 1.5.214
- [Changelog](https://github.com/Kilian/electron-to-chromium/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kilian/electron-to-chromium/compare/v1.5.209...v1.5.214)

Updates `generative-bayesian-network` from 2.1.70 to 2.1.72
- [Release notes](https://github.com/apify/fingerprint-suite/releases)
- [Commits](https://github.com/apify/fingerprint-suite/compare/v2.1.70...v2.1.72)

Updates `get-east-asian-width` from 1.3.0 to 1.3.1
- [Release notes](https://github.com/sindresorhus/get-east-asian-width/releases)
- [Commits](https://github.com/sindresorhus/get-east-asian-width/compare/v1.3.0...v1.3.1)

Updates `header-generator` from 2.1.70 to 2.1.72
- [Release notes](https://github.com/apify/fingerprint-suite/releases)
- [Commits](https://github.com/apify/fingerprint-suite/compare/v2.1.70...v2.1.72)

Updates `listr2` from 9.0.2 to 9.0.3
- [Release notes](https://github.com/listr2/listr2/releases)
- [Changelog](https://github.com/listr2/listr2/blob/master/release.config.js)
- [Commits](https://github.com/listr2/listr2/compare/listr2@9.0.2...listr2@9.0.3)

Updates `nano-spawn` from 1.0.2 to 1.0.3
- [Release notes](https://github.com/sindresorhus/nano-spawn/releases)
- [Commits](https://github.com/sindresorhus/nano-spawn/compare/v1.0.2...v1.0.3)

Updates `node-releases` from 2.0.19 to 2.0.20
- [Commits](https://github.com/chicoxyzzy/node-releases/commits)

Updates `rollup` from 4.49.0 to 4.50.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

---
updated-dependencies:
- dependency-name: "@tauri-apps/plugin-deep-link"
  dependency-version: 2.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/plugin-dialog"
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: ahooks
  dependency-version: 3.9.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/biome"
  dependency-version: 2.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.1.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli"
  dependency-version: 2.8.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@types/node"
  dependency-version: 24.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: lint-staged
  dependency-version: 16.1.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.1.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: tw-animate-css
  dependency-version: 1.3.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: dotenv
  dependency-version: 17.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: fingerprint-generator
  dependency-version: 2.1.72
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@babel/runtime"
  dependency-version: 7.28.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-arm64"
  dependency-version: 2.2.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-x64"
  dependency-version: 2.2.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64-musl"
  dependency-version: 2.2.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64"
  dependency-version: 2.2.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64-musl"
  dependency-version: 2.2.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64"
  dependency-version: 2.2.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-arm64"
  dependency-version: 2.2.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-x64"
  dependency-version: 2.2.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm-eabi"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm64"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-x64"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-arm64"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-x64"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-gnueabihf"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-musleabihf"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-musl"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-loongarch64-gnu"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-ppc64-gnu"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-gnu"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-musl"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-s390x-gnu"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-musl"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-arm64-msvc"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-ia32-msvc"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-x64-msvc"
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/node"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-android-arm64"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-darwin-arm64"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-darwin-x64"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-freebsd-x64"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-linux-arm-gnueabihf"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-linux-arm64-gnu"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-linux-arm64-musl"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-linux-x64-gnu"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-linux-x64-musl"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-wasm32-wasi"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-win32-arm64-msvc"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-win32-x64-msvc"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide"
  dependency-version: 4.1.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-darwin-arm64"
  dependency-version: 2.8.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-darwin-x64"
  dependency-version: 2.8.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-arm-gnueabihf"
  dependency-version: 2.8.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-arm64-gnu"
  dependency-version: 2.8.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-arm64-musl"
  dependency-version: 2.8.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-riscv64-gnu"
  dependency-version: 2.8.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-x64-gnu"
  dependency-version: 2.8.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-x64-musl"
  dependency-version: 2.8.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-win32-arm64-msvc"
  dependency-version: 2.8.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-win32-ia32-msvc"
  dependency-version: 2.8.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-win32-x64-msvc"
  dependency-version: 2.8.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: browserslist
  dependency-version: 4.25.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: dayjs
  dependency-version: 1.11.18
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: electron-to-chromium
  dependency-version: 1.5.214
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: generative-bayesian-network
  dependency-version: 2.1.72
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: get-east-asian-width
  dependency-version: 1.3.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: header-generator
  dependency-version: 2.1.72
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: listr2
  dependency-version: 9.0.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: nano-spawn
  dependency-version: 1.0.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: node-releases
  dependency-version: 2.0.20
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: rollup
  dependency-version: 4.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-06 09:27:22 +00:00
dependabot[bot] 2bd01376db ci(deps): bump the github-actions group with 4 updates
Bumps the github-actions group with 4 updates: [actions/setup-node](https://github.com/actions/setup-node), [ridedott/merge-me-action](https://github.com/ridedott/merge-me-action), [crate-ci/typos](https://github.com/crate-ci/typos) and [actions/stale](https://github.com/actions/stale).


Updates `actions/setup-node` from 4.4.0 to 5.0.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/49933ea5288caeca8642d1e84afbd3f7d6820020...a0853c24544627f65ddf259abe73b1d18a591444)

Updates `ridedott/merge-me-action` from 2.10.126 to 2.10.128
- [Release notes](https://github.com/ridedott/merge-me-action/releases)
- [Changelog](https://github.com/ridedott/merge-me-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ridedott/merge-me-action/compare/ad649157c69da4d34e601ee360de7a74ce4e2090...60142b76c22362f5845c877672fd2822b4d07c13)

Updates `crate-ci/typos` from 1.35.7 to 1.36.2
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/65f69f021b736bdbe548ce72200500752d42b40e...85f62a8a84f939ae994ab3763f01a0296d61a7ee)

Updates `actions/stale` from 9.1.0 to 10.0.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/5bef64f19d7facfb25b37b414482c7164d639639...3a9db7e6a41a89f618792c92c0e97cc736e1b13f)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: ridedott/merge-me-action
  dependency-version: 2.10.128
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: crate-ci/typos
  dependency-version: 1.36.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/stale
  dependency-version: 10.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-06 09:05:57 +00:00
zhom ba36956158 chore: next-env autogen 2025-09-03 22:23:40 +04:00
zhom ce3e27ca64 chore: version bump 2025-09-03 22:05:42 +04:00
zhom fd0fb8c7ca refactor: mark all firefox developers edition releases as nightly 2025-09-03 22:02:48 +04:00
zhom 701c8aefd3 fix: properly update profile after downloading broweser 2025-09-03 20:49:26 +04:00
zhom d4a7c347b6 chore: next-env autogen 2025-09-03 19:57:51 +04:00
zhom 3c3e6df3b2 chore: ignore ts files inside tauri output 2025-09-03 19:44:42 +04:00
zhom cd4b23bd27 refactor: simplify browser runner 2025-09-03 19:40:17 +04:00
zhom 042a348971 refactor: better binary management 2025-09-02 23:41:17 +04:00
zhom b8f4e4adda build: switch back to ubuntu 22.04 runner 2025-09-02 19:26:54 +04:00
zhom e8852a3caf build: switch to ubuntu 18 for linux build 2025-09-02 18:44:21 +04:00
44 changed files with 3682 additions and 4288 deletions
+5 -3
View File
@@ -34,10 +34,12 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
- name: Set up pnpm package manager
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 #v5.0.0
with:
node-version-file: .node-version
cache: "pnpm"
@@ -55,7 +57,7 @@ jobs:
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev pkg-config xdg-utils
- name: Rust cache
uses: swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 #v2.8.0
uses: swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 #v2.8.1
with:
workdir: ./src-tauri
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
security-scan:
name: Security Vulnerability Scan
if: ${{ github.actor == 'dependabot[bot]' }}
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@90b209d0ea55cea1da9fc0c4e65782cc6acb6e2e" # v2.2.2
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@e92b5d07338d4f0ba0981dffed17c48976ca4730" # v2.2.3
with:
scan-args: |-
-r
@@ -73,7 +73,7 @@ jobs:
compat-lookup: true
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Auto-merge minor and patch updates
uses: ridedott/merge-me-action@ad649157c69da4d34e601ee360de7a74ce4e2090 #v2.10.126
uses: ridedott/merge-me-action@a3b9ffd551d69f9f4375a87e9fa56235a0749518 #v2.10.131
with:
GITHUB_TOKEN: ${{ secrets.SECRET_DEPENDABOT_GITHUB_TOKEN }}
MERGE_METHOD: SQUASH
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/first-interaction@753c925c8d1ac6fede23781875376600628d9b5d # v3.0.0
- uses: actions/first-interaction@1c4688942c71f71d4f5502a26ea67c331730fa4d # v3.1.0
with:
issue-message: "Thank you for your first issue ❤️ If it's a feature request, please make sure it's clear what you want, why you want it, and how important it is to you. If you posted a bug report, please make sure it includes as much detail as possible."
pr-message: "Welcome to the community and thank you for your first contribution ❤️ A human will review your PR shortly. Make sure that the pipelines are green, so that the PR is considered ready for a review and could be merged."
+4 -2
View File
@@ -37,10 +37,12 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
- name: Set up pnpm package manager
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 #v5.0.0
with:
node-version-file: .node-version
cache: "pnpm"
+8 -6
View File
@@ -31,7 +31,7 @@ jobs:
build:
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
os: [macos-latest, ubuntu-22.04]
runs-on: ${{ matrix.os }}
@@ -44,10 +44,12 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
- name: Set up pnpm package manager
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 #v5.0.0
with:
node-version-file: .node-version
cache: "pnpm"
@@ -65,7 +67,7 @@ jobs:
run: cargo install banderole
- name: Install dependencies (Ubuntu only)
if: matrix.os == 'ubuntu-latest'
if: matrix.os == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
@@ -77,7 +79,7 @@ jobs:
shell: bash
working-directory: ./nodecar
run: |
if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then
if [[ "${{ matrix.os }}" == "ubuntu-22.04" ]]; then
pnpm run build:linux-x64
elif [[ "${{ matrix.os }}" == "macos-latest" ]]; then
pnpm run build:mac-aarch64
@@ -94,7 +96,7 @@ jobs:
shell: bash
run: |
mkdir -p src-tauri/binaries
if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then
if [[ "${{ matrix.os }}" == "ubuntu-22.04" ]]; then
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-x86_64-unknown-linux-gnu
elif [[ "${{ matrix.os }}" == "macos-latest" ]]; then
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-aarch64-apple-darwin
+2 -2
View File
@@ -50,7 +50,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@90b209d0ea55cea1da9fc0c4e65782cc6acb6e2e" # v2.2.2
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@e92b5d07338d4f0ba0981dffed17c48976ca4730" # v2.2.3
with:
scan-args: |-
-r
@@ -63,7 +63,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@90b209d0ea55cea1da9fc0c4e65782cc6acb6e2e" # v2.2.2
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@e92b5d07338d4f0ba0981dffed17c48976ca4730" # v2.2.3
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@90b209d0ea55cea1da9fc0c4e65782cc6acb6e2e" # v2.2.2
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@e92b5d07338d4f0ba0981dffed17c48976ca4730" # v2.2.3
with:
scan-args: |-
-r
+9 -6
View File
@@ -13,7 +13,7 @@ env:
jobs:
security-scan:
name: Security Vulnerability Scan
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@90b209d0ea55cea1da9fc0c4e65782cc6acb6e2e" # v2.2.2
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@e92b5d07338d4f0ba0981dffed17c48976ca4730" # v2.2.3
with:
scan-args: |-
-r
@@ -107,13 +107,16 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 #v5.0.0
with:
node-version-file: .node-version
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
cache: "pnpm"
- name: Setup Rust
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 #master
@@ -128,7 +131,7 @@ jobs:
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev pkg-config xdg-utils
- name: Rust cache
uses: swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 #v2.8.0
uses: swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 #v2.8.1
with:
workdir: ./src-tauri
+9 -6
View File
@@ -12,7 +12,7 @@ env:
jobs:
security-scan:
name: Security Vulnerability Scan
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@90b209d0ea55cea1da9fc0c4e65782cc6acb6e2e" # v2.2.2
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@e92b5d07338d4f0ba0981dffed17c48976ca4730" # v2.2.3
with:
scan-args: |-
-r
@@ -106,13 +106,16 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 #v5.0.0
with:
node-version-file: .node-version
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
cache: "pnpm"
- name: Setup Rust
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 #master
@@ -127,7 +130,7 @@ jobs:
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev pkg-config xdg-utils
- name: Rust cache
uses: swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 #v2.8.0
uses: swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 #v2.8.1
with:
workdir: ./src-tauri
+1 -1
View File
@@ -23,4 +23,4 @@ jobs:
- name: Checkout Actions Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
- name: Spell Check Repo
uses: crate-ci/typos@65f69f021b736bdbe548ce72200500752d42b40e #v1.35.7
uses: crate-ci/typos@80c8a4945eec0f6d464eaf9e65ed98ef085283d1 #v1.38.1
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
pull-requests: write
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue has been inactive for 60 days. Please respond to keep it open."
+5
View File
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"ABORTIFHUNG",
"adwaita",
"ahooks",
"akhilmhdh",
@@ -83,6 +84,7 @@
"libwebkit",
"libxdo",
"localtime",
"lpdw",
"lxml",
"lzma",
"Matchalk",
@@ -114,6 +116,7 @@
"peerconnection",
"pids",
"pixbuf",
"pkexec",
"pkill",
"plasmohq",
"platformdirs",
@@ -136,6 +139,7 @@
"screeninfo",
"selectables",
"serde",
"SETTINGCHANGE",
"setuptools",
"shadcn",
"showcursor",
@@ -143,6 +147,7 @@
"signon",
"signum",
"sklearn",
"SMTO",
"sonner",
"splitn",
"sspi",
+6 -6
View File
@@ -21,14 +21,14 @@
"author": "",
"license": "AGPL-3.0",
"dependencies": {
"@types/node": "^24.3.0",
"commander": "^14.0.0",
"donutbrowser-camoufox-js": "^0.6.6",
"dotenv": "^17.2.1",
"fingerprint-generator": "^2.1.70",
"@types/node": "^24.6.0",
"commander": "^14.0.1",
"donutbrowser-camoufox-js": "^0.7.0",
"dotenv": "^17.2.3",
"fingerprint-generator": "^2.1.73",
"get-port": "^7.1.0",
"nodemon": "^3.1.10",
"playwright-core": "^1.55.0",
"playwright-core": "^1.55.1",
"proxy-chain": "^2.5.9",
"tmp": "^0.2.5",
"ts-node": "^10.9.2",
+2
View File
@@ -431,6 +431,8 @@ export async function generateCamoufoxConfig(
}
}
launchOpts.allowAddonNewTab = true;
// Generate the configuration using launchOptions
const generatedOptions = await launchOptions(launchOpts);
+124 -1
View File
@@ -1,18 +1,40 @@
import fs from "node:fs";
import path from "node:path";
import { launchOptions } from "donutbrowser-camoufox-js";
import type { LaunchOptions } from "donutbrowser-camoufox-js/dist/utils.js";
import { type Browser, type BrowserContext, firefox } from "playwright-core";
import tmp from "tmp";
import { getCamoufoxConfig, saveCamoufoxConfig } from "./camoufox-storage.js";
import { getEnvVars, parseProxyString } from "./utils.js";
// Set up debug logging to a file
const LOG_DIR = path.join(tmp.tmpdir, "donutbrowser", "camoufox-logs");
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
function debugLog(id: string, message: string, data?: any): void {
const logFile = path.join(LOG_DIR, `${id}.log`);
const timestamp = new Date().toISOString();
const logMessage = data
? `[${timestamp}] ${message}: ${JSON.stringify(data, null, 2)}\n`
: `[${timestamp}] ${message}\n`;
fs.appendFileSync(logFile, logMessage);
}
/**
* Run a Camoufox browser server as a worker process
* @param id The Camoufox configuration ID
*/
export async function runCamoufoxWorker(id: string): Promise<void> {
debugLog(id, "Worker starting", { pid: process.pid });
// Get the Camoufox configuration
debugLog(id, "Loading Camoufox configuration");
const config = getCamoufoxConfig(id);
if (!config) {
debugLog(id, "Configuration not found");
console.error(
JSON.stringify({
error: "Configuration not found",
@@ -22,6 +44,13 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
process.exit(1);
}
debugLog(id, "Configuration loaded successfully", {
profilePath: config.profilePath,
hasOptions: !!config.options,
hasCustomConfig: !!config.customConfig,
hasUrl: !!config.url,
});
config.processId = process.pid;
saveCamoufoxConfig(config);
@@ -37,12 +66,14 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
// Launch browser in background - this can take time and may fail
setImmediate(async () => {
debugLog(id, "Starting browser launch in background");
let browser: Browser | null = null;
let context: BrowserContext | null = null;
let windowCheckInterval: NodeJS.Timeout | null = null;
// Graceful shutdown handler with access to browser and server
const gracefulShutdown = async () => {
debugLog(id, "Graceful shutdown initiated");
try {
// Clear any intervals first
if (windowCheckInterval) {
@@ -76,14 +107,19 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
process.on("unhandledRejection", () => void gracefulShutdown());
try {
debugLog(id, "Preparing launch options");
// Deep clone to avoid reference sharing and ensure fresh configuration for each instance
const camoufoxOptions: LaunchOptions = JSON.parse(
JSON.stringify(config.options || {}),
);
debugLog(id, "Base options cloned", {
hasOptions: Object.keys(camoufoxOptions).length,
});
// Add profile path if provided
if (config.profilePath) {
camoufoxOptions.user_data_dir = config.profilePath;
debugLog(id, "Set user_data_dir", { profilePath: config.profilePath });
}
// Ensure block options are properly set
@@ -111,52 +147,94 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
showcursor: false,
...(camoufoxOptions.config || {}),
};
debugLog(id, "Set default options", {
i_know_what_im_doing: true,
disableTheming: true,
showcursor: false,
});
// Generate fresh options for this specific instance
debugLog(id, "Generating launch options via launchOptions function");
const generatedOptions = await launchOptions(camoufoxOptions);
debugLog(id, "Launch options generated successfully", {
hasEnv: !!generatedOptions.env,
argsLength: generatedOptions.args?.length || 0,
});
// Start with process environment to ensure proper inheritance
let finalEnv = { ...process.env };
debugLog(id, "Base environment variables set", {
envVarCount: Object.keys(finalEnv).length,
});
// Add generated options environment variables
if (generatedOptions.env) {
finalEnv = { ...finalEnv, ...generatedOptions.env };
debugLog(id, "Added generated environment variables", {
generatedEnvCount: Object.keys(generatedOptions.env).length,
totalEnvCount: Object.keys(finalEnv).length,
});
}
// If we have a custom config from Rust, use it directly as environment variables
if (config.customConfig) {
debugLog(id, "Processing custom config", {
customConfigLength: config.customConfig.length,
});
try {
// Parse the custom config JSON string
const customConfigObj = JSON.parse(config.customConfig);
debugLog(id, "Custom config parsed successfully", {
customConfigKeys: Object.keys(customConfigObj),
});
// Ensure default config values are preserved even with custom config
const mergedConfig = {
...customConfigObj,
disableTheming: true,
showcursor: false,
// allowAddonNewTab will be handled from the fingerprint config if present
};
// Convert merged config to environment variables using getEnvVars
const customEnvVars = getEnvVars(mergedConfig);
debugLog(id, "Custom config converted to environment variables", {
customEnvVarCount: Object.keys(customEnvVars).length,
});
// Merge custom config with generated config (custom takes precedence)
finalEnv = { ...finalEnv, ...customEnvVars };
debugLog(id, "Custom config merged with final environment", {
finalEnvCount: Object.keys(finalEnv).length,
});
} catch (error) {
debugLog(id, "Failed to parse custom config", {
error: error instanceof Error ? error.message : String(error),
});
console.error(
`Camoufox worker ${id}: Failed to parse custom config, using generated config:`,
error,
);
await gracefulShutdown();
return;
}
} else {
debugLog(id, "No custom config provided");
}
// Prepare profile path for persistent context
const profilePath = config.profilePath || "";
debugLog(id, "Profile path prepared", { profilePath });
// Launch persistent context with the final configuration
const finalOptions: any = {
...generatedOptions,
env: finalEnv,
};
debugLog(id, "Final launch options prepared", {
hasExecutablePath: !!finalOptions.executablePath,
hasProxy: !!camoufoxOptions.proxy,
profilePath,
});
// If a custom executable path was provided, ensure Playwright uses it
if (
@@ -165,46 +243,66 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
) {
finalOptions.executablePath = (camoufoxOptions as any)
.executable_path as string;
debugLog(id, "Custom executable path set", {
executablePath: finalOptions.executablePath,
});
}
// Only add proxy if it exists and is valid
if (camoufoxOptions.proxy) {
debugLog(id, "Processing proxy configuration", {
proxyString: camoufoxOptions.proxy,
});
try {
finalOptions.proxy = parseProxyString(camoufoxOptions.proxy);
debugLog(id, "Proxy parsed successfully");
} catch (error) {
debugLog(id, "Failed to parse proxy", {
error: error instanceof Error ? error.message : String(error),
});
console.error({
message: "Failed to parse proxy, launching without proxy",
error,
});
await gracefulShutdown();
return;
}
}
// Use launchPersistentContext instead of launchServer
debugLog(id, "Launching persistent context", { profilePath });
context = await firefox.launchPersistentContext(
profilePath,
finalOptions,
);
debugLog(id, "Persistent context launched successfully");
// Get the browser instance from context
browser = context.browser();
debugLog(id, "Browser instance obtained from context", {
browserConnected: browser?.isConnected(),
});
// Handle browser disconnection for proper cleanup
if (browser) {
browser.on("disconnected", () => void gracefulShutdown());
debugLog(id, "Browser disconnect handler registered");
}
// Handle context close for proper cleanup
context.on("close", () => void gracefulShutdown());
debugLog(id, "Context close handler registered");
saveCamoufoxConfig(config);
// Monitor for window closure
const startWindowMonitoring = () => {
debugLog(id, "Starting window monitoring");
windowCheckInterval = setInterval(async () => {
try {
// Check if context is still active
if (!context?.pages || context.pages().length === 0) {
debugLog(id, "No pages found in context, shutting down");
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
@@ -214,6 +312,7 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
// Check if browser is still connected (if available)
if (browser && !browser.isConnected()) {
debugLog(id, "Browser disconnected, shutting down");
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
@@ -224,12 +323,16 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
// Check pages in the persistent context
const pages = context.pages();
if (pages.length === 0) {
debugLog(id, "No pages in context, shutting down");
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
await gracefulShutdown();
}
} catch {
} catch (error) {
debugLog(id, "Error in window monitoring", {
error: error instanceof Error ? error.message : String(error),
});
// If we can't check windows, assume browser is closing
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
@@ -241,19 +344,29 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
// Handle URL opening if provided
if (config.url) {
debugLog(id, "Opening URL in browser", { url: config.url });
try {
const pages = await context.pages();
if (pages.length) {
const page = pages[0];
debugLog(id, "Navigating to URL");
await page.goto(config.url, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
debugLog(id, "URL opened successfully");
// Start monitoring after page is created
startWindowMonitoring();
} else {
debugLog(id, "No pages available to open URL");
startWindowMonitoring();
}
} catch (urlError) {
debugLog(id, "Failed to open URL", {
error:
urlError instanceof Error ? urlError.message : String(urlError),
});
console.error({
message: "Failed to open URL",
error: urlError,
@@ -263,15 +376,18 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
startWindowMonitoring();
}
} else {
debugLog(id, "No URL provided, starting monitoring");
// Start monitoring after page is created
startWindowMonitoring();
}
// Monitor browser/context connection
debugLog(id, "Starting keep-alive monitoring");
const keepAlive = setInterval(async () => {
try {
// Check if context is still active
if (!context?.pages) {
debugLog(id, "Context not active in keep-alive, shutting down");
clearInterval(keepAlive);
await gracefulShutdown();
return;
@@ -279,11 +395,15 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
// Check browser connection if available
if (browser && !browser.isConnected()) {
debugLog(id, "Browser not connected in keep-alive, shutting down");
clearInterval(keepAlive);
await gracefulShutdown();
return;
}
} catch (error) {
debugLog(id, "Error in keep-alive check", {
error: error instanceof Error ? error.message : String(error),
});
console.error({
message: "Error in keepAlive check",
error,
@@ -293,6 +413,9 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
}
}, 2000);
} catch (error) {
debugLog(id, "Failed to launch Camoufox", {
error: error instanceof Error ? error.message : String(error),
});
console.error({
message: "Failed to launch Camoufox",
error,
+16 -17
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.12.1",
"version": "0.12.3",
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
@@ -38,18 +38,17 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3",
"@tauri-apps/api": "^2.8.0",
"@tauri-apps/plugin-deep-link": "^2.4.2",
"@tauri-apps/plugin-dialog": "^2.3.3",
"@tauri-apps/plugin-deep-link": "^2.4.3",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "~2.4.2",
"@tauri-apps/plugin-opener": "^2.5.0",
"ahooks": "^3.9.4",
"ahooks": "^3.9.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"color": "^5.0.0",
"lucide-react": "^0.542.0",
"motion": "^12.23.12",
"next": "^15.5.2",
"color": "^5.0.2",
"motion": "^12.23.22",
"next": "^15.5.4",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "^19.1.1",
@@ -60,19 +59,19 @@
"tauri-plugin-macos-permissions-api": "^2.3.0"
},
"devDependencies": {
"@biomejs/biome": "2.2.2",
"@tailwindcss/postcss": "^4.1.12",
"@tauri-apps/cli": "^2.8.3",
"@biomejs/biome": "2.2.3",
"@tailwindcss/postcss": "^4.1.13",
"@tauri-apps/cli": "^2.8.4",
"@types/color": "^4.2.0",
"@types/node": "^24.3.0",
"@types/react": "^19.1.12",
"@types/node": "^24.6.0",
"@types/react": "^19.1.15",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.2",
"@vitejs/plugin-react": "^5.0.4",
"husky": "^9.1.7",
"lint-staged": "^16.1.5",
"tailwindcss": "^4.1.12",
"lint-staged": "^16.2.3",
"tailwindcss": "^4.1.13",
"ts-unused-exports": "^11.0.1",
"tw-animate-css": "^1.3.7",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.2"
},
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
+1403 -2348
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -1,8 +1,10 @@
packages:
- nodecar
onlyBuiltDependencies:
- '@biomejs/biome'
- '@tailwindcss/oxide'
- better-sqlite3
- esbuild
- sharp
- sqlite3
+524 -597
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.12.1"
version = "0.12.3"
description = "Simple Yet Powerful Anti-Detect Browser"
authors = ["zhom@github"]
edition = "2021"
@@ -39,14 +39,14 @@ lazy_static = "1.4"
base64 = "0.22"
async-trait = "0.1"
futures-util = "0.3"
zip = "4"
zip = "5"
tar = "0"
bzip2 = "0"
flate2 = "1"
lzma-rs = "0"
msi-extract = "0"
uuid = { version = "1.0", features = ["v4", "serde"] }
uuid = { version = "1.18", features = ["v4", "serde"] }
url = "2.5"
chrono = { version = "0.4", features = ["serde"] }
axum = "0.8.4"
@@ -66,7 +66,7 @@ objc2-app-kit = { version = "0.3.1", features = ["NSWindow"] }
[target.'cfg(target_os = "windows")'.dependencies]
winreg = "0.55"
windows = { version = "0.61", features = [
windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_System_ProcessStatus",
"Win32_System_Threading",
+3
View File
@@ -2,6 +2,9 @@
Version=1.0
Type=Application
Name=Donut Browser
Name[en]=Donut Browser
GenericName=Web Browser
X-GNOME-FullName=Donut Browser
Comment=Simple Yet Powerful Anti-Detect Browser
Exec=donutbrowser %u
Icon=donutbrowser
+20 -1
View File
@@ -221,6 +221,20 @@ pub fn sort_versions(versions: &mut [String]) {
});
}
// Helper function to compare two versions
pub fn compare_versions(version1: &str, version2: &str) -> std::cmp::Ordering {
let version_a = VersionComponent::parse(version1);
let version_b = VersionComponent::parse(version2);
version_a.cmp(&version_b)
}
pub fn is_version_newer(version1: &str, version2: &str) -> bool {
// Use the proper VersionComponent comparison from api_client.rs
let version_a = VersionComponent::parse(version1);
let version_b = VersionComponent::parse(version2);
version_a > version_b
}
// Helper function to sort GitHub releases
pub fn sort_github_releases(releases: &mut [GithubRelease]) {
releases.sort_by(|a, b| {
@@ -268,7 +282,12 @@ pub fn is_browser_version_nightly(
// Last resort: when no name available, treat as nightly (non-Release)
true
}
"firefox" | "firefox-developer" => {
"firefox-developer" => {
// For Firefox Developer Edition, always treat as nightly/prerelease
// This ensures consistent behavior regardless of cache state or API response parsing
true
}
"firefox" => {
// For Firefox, use the category from the API response to determine stability
// This will be handled in the API parsing, so this fallback is for cached versions
is_nightly_version(version)
+9 -13
View File
@@ -1,3 +1,4 @@
use crate::camoufox_manager::CamoufoxConfig;
use crate::group_manager::GROUP_MANAGER;
use crate::profile::manager::ProfileManager;
use crate::proxy_manager::PROXY_MANAGER;
@@ -509,8 +510,7 @@ async fn update_profile(
}
if let Some(camoufox_config) = request.camoufox_config {
let config: Result<crate::camoufox::CamoufoxConfig, _> =
serde_json::from_value(camoufox_config);
let config: Result<CamoufoxConfig, _> = serde_json::from_value(camoufox_config);
match config {
Ok(config) => {
if profile_manager
@@ -830,15 +830,12 @@ async fn download_browser_api(
State(state): State<ApiServerState>,
Json(request): Json<DownloadBrowserRequest>,
) -> Result<Json<DownloadBrowserResponse>, StatusCode> {
let browser_runner = crate::browser_runner::BrowserRunner::instance();
match browser_runner
.download_browser_impl(
state.app_handle.clone(),
request.browser.clone(),
request.version.clone(),
)
.await
match crate::downloader::download_browser(
state.app_handle.clone(),
request.browser.clone(),
request.version.clone(),
)
.await
{
Ok(_) => Ok(Json(DownloadBrowserResponse {
browser: request.browser,
@@ -870,7 +867,6 @@ async fn check_browser_downloaded(
Path((browser, version)): Path<(String, String)>,
State(_state): State<ApiServerState>,
) -> Result<Json<bool>, StatusCode> {
let browser_runner = crate::browser_runner::BrowserRunner::instance();
let is_downloaded = browser_runner.is_browser_downloaded(&browser, &version);
let is_downloaded = crate::downloaded_browsers_registry::is_browser_downloaded(browser, version);
Ok(Json(is_downloaded))
}
+9 -17
View File
@@ -120,12 +120,14 @@ pub struct AppUpdateProgress {
pub struct AppAutoUpdater {
client: Client,
extractor: &'static crate::extraction::Extractor,
}
impl AppAutoUpdater {
fn new() -> Self {
Self {
client: Client::new(),
extractor: crate::extraction::Extractor::instance(),
}
}
@@ -829,8 +831,6 @@ impl AppAutoUpdater {
archive_path: &Path,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let extractor = crate::extraction::Extractor::instance();
let file_name = archive_path
.file_name()
.and_then(|name| name.to_str())
@@ -838,7 +838,7 @@ impl AppAutoUpdater {
// Handle compound extensions like .tar.gz
if file_name.ends_with(".tar.gz") {
return extractor.extract_tar_gz(archive_path, dest_dir).await;
return self.extractor.extract_tar_gz(archive_path, dest_dir).await;
}
let extension = archive_path
@@ -850,7 +850,7 @@ impl AppAutoUpdater {
"dmg" => {
#[cfg(target_os = "macos")]
{
extractor.extract_dmg(archive_path, dest_dir).await
self.extractor.extract_dmg(archive_path, dest_dir).await
}
#[cfg(not(target_os = "macos"))]
{
@@ -914,7 +914,7 @@ impl AppAutoUpdater {
Err("AppImage installation is only supported on Linux".into())
}
}
"zip" => extractor.extract_zip(archive_path, dest_dir).await,
"zip" => self.extractor.extract_zip(archive_path, dest_dir).await,
_ => Err(format!("Unsupported archive format: {extension}").into()),
}
}
@@ -1083,8 +1083,8 @@ impl AppAutoUpdater {
fs::create_dir_all(&temp_extract_dir)?;
// Extract ZIP file
let extractor = crate::extraction::Extractor::instance();
let extracted_path = extractor
let extracted_path = self
.extractor
.extract_zip(installer_path, &temp_extract_dir)
.await?;
@@ -1314,8 +1314,8 @@ impl AppAutoUpdater {
fs::create_dir_all(&temp_extract_dir)?;
// Extract tarball
let extractor = crate::extraction::Extractor::instance();
let extracted_path = extractor
let extracted_path = self
.extractor
.extract_tar_gz(tarball_path, &temp_extract_dir)
.await?;
@@ -1614,14 +1614,6 @@ pub async fn check_for_app_updates_manual() -> Result<Option<AppUpdateInfo>, Str
.map_err(|e| format!("Failed to check for app updates: {e}"))
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PlatformInfo {
pub os: String,
pub arch: String,
pub installation_method: String,
pub supported_formats: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
+33 -67
View File
@@ -1,6 +1,5 @@
use crate::api_client::is_browser_version_nightly;
use crate::browser_version_manager::{BrowserVersionInfo, BrowserVersionManager};
use crate::profile::BrowserProfile;
use crate::profile::{BrowserProfile, ProfileManager};
use crate::settings_manager::SettingsManager;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
@@ -29,15 +28,17 @@ pub struct AutoUpdateState {
}
pub struct AutoUpdater {
version_service: &'static BrowserVersionManager,
browser_version_manager: &'static BrowserVersionManager,
settings_manager: &'static SettingsManager,
profile_manager: &'static ProfileManager,
}
impl AutoUpdater {
fn new() -> Self {
Self {
version_service: BrowserVersionManager::instance(),
browser_version_manager: BrowserVersionManager::instance(),
settings_manager: SettingsManager::instance(),
profile_manager: ProfileManager::instance(),
}
}
@@ -53,8 +54,8 @@ impl AutoUpdater {
let mut browser_versions: HashMap<String, Vec<BrowserVersionInfo>> = HashMap::new();
// Group profiles by browser
let profile_manager = crate::profile::ProfileManager::instance();
let profiles = profile_manager
let profiles = self
.profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let mut browser_profiles: HashMap<String, Vec<BrowserProfile>> = HashMap::new();
@@ -62,7 +63,7 @@ impl AutoUpdater {
for profile in profiles {
// Only check supported browsers
if !self
.version_service
.browser_version_manager
.is_browser_supported(&profile.browser)
.unwrap_or(false)
{
@@ -78,14 +79,14 @@ impl AutoUpdater {
for (browser, profiles) in browser_profiles {
// Get cached versions first, then try to fetch if needed
let versions = if let Some(cached) = self
.version_service
.browser_version_manager
.get_cached_browser_versions_detailed(&browser)
{
cached
} else if self.version_service.should_update_cache(&browser) {
} else if self.browser_version_manager.should_update_cache(&browser) {
// Try to fetch fresh versions
match self
.version_service
.browser_version_manager
.fetch_browser_versions_detailed(&browser, false)
.await
{
@@ -156,8 +157,9 @@ impl AutoUpdater {
// Spawn async task to handle the download and auto-update
tokio::spawn(async move {
// TODO: update the logic to use the downloaded browsers registry instance instead of the static method
// First, check if browser already exists
match crate::browser_runner::is_browser_downloaded(
match crate::downloaded_browsers_registry::is_browser_downloaded(
browser.clone(),
new_version.clone(),
) {
@@ -165,12 +167,13 @@ impl AutoUpdater {
println!("Browser {browser} {new_version} already downloaded, proceeding to auto-update profiles");
// Browser already exists, go straight to profile update
match crate::auto_updater::complete_browser_update_with_auto_update(
app_handle_clone,
browser.clone(),
new_version.clone(),
)
.await
match AutoUpdater::instance()
.complete_browser_update_with_auto_update(
&app_handle_clone,
&browser.clone(),
&new_version.clone(),
)
.await
{
Ok(updated_profiles) => {
println!(
@@ -223,7 +226,8 @@ impl AutoUpdater {
available_versions: &[BrowserVersionInfo],
) -> Result<Option<UpdateNotification>, Box<dyn std::error::Error + Send + Sync>> {
let current_version = &profile.version;
let is_current_nightly = is_browser_version_nightly(&profile.browser, current_version, None);
let is_current_nightly =
crate::api_client::is_browser_version_nightly(&profile.browser, current_version, None);
// Find the best available update
let best_update = available_versions
@@ -231,7 +235,8 @@ impl AutoUpdater {
.filter(|v| {
// Only consider versions newer than current
self.is_version_newer(&v.version, current_version)
&& is_browser_version_nightly(&profile.browser, &v.version, None) == is_current_nightly
&& crate::api_client::is_browser_version_nightly(&profile.browser, &v.version, None)
== is_current_nightly
})
.max_by(|a, b| self.compare_versions(&a.version, &b.version));
@@ -298,8 +303,8 @@ impl AutoUpdater {
browser: &str,
new_version: &str,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let profile_manager = crate::profile::ProfileManager::instance();
let profiles = profile_manager
let profiles = self
.profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
@@ -316,7 +321,11 @@ impl AutoUpdater {
// Check if this is an update (newer version)
if self.is_version_newer(new_version, &profile.version) {
// Update the profile version
match profile_manager.update_profile_version(app_handle, &profile.name, new_version) {
match self.profile_manager.update_profile_version(
app_handle,
&profile.id.to_string(),
new_version,
) {
Ok(_) => {
updated_profiles.push(profile.name);
}
@@ -350,46 +359,9 @@ impl AutoUpdater {
state.auto_update_downloads.remove(&download_key);
self.save_auto_update_state(&state)?;
// Always perform cleanup after auto-update - don't fail the update if cleanup fails
if let Err(e) = self.cleanup_unused_binaries_internal() {
eprintln!("Warning: Failed to cleanup unused binaries after auto-update: {e}");
}
Ok(updated_profiles)
}
/// Internal method to cleanup unused binaries (used by auto-cleanup)
fn cleanup_unused_binaries_internal(
&self,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
// Load current profiles
let profile_manager = crate::profile::ProfileManager::instance();
let profiles = profile_manager
.list_profiles()
.map_err(|e| format!("Failed to load profiles: {e}"))?;
// Get registry instance
let registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::instance();
// Get active browser versions (all profiles)
let active_versions = registry.get_active_browser_versions(&profiles);
// Get running browser versions (only running profiles)
let running_versions = registry.get_running_browser_versions(&profiles);
// Cleanup unused binaries (but keep running ones)
let cleaned_up = registry
.cleanup_unused_binaries(&active_versions, &running_versions)
.map_err(|e| format!("Failed to cleanup unused binaries: {e}"))?;
// Save updated registry
registry
.save()
.map_err(|e| format!("Failed to save registry: {e}"))?;
Ok(cleaned_up)
}
/// Check if browser is disabled due to ongoing update
pub fn is_browser_disabled(
&self,
@@ -411,17 +383,11 @@ impl AutoUpdater {
}
fn is_version_newer(&self, version1: &str, version2: &str) -> bool {
// Use the proper VersionComponent comparison from api_client.rs
let version_a = crate::api_client::VersionComponent::parse(version1);
let version_b = crate::api_client::VersionComponent::parse(version2);
version_a > version_b
crate::api_client::is_version_newer(version1, version2)
}
fn compare_versions(&self, version1: &str, version2: &str) -> std::cmp::Ordering {
// Use the proper VersionComponent comparison from api_client.rs
let version_a = crate::api_client::VersionComponent::parse(version1);
let version_b = crate::api_client::VersionComponent::parse(version2);
version_a.cmp(&version_b)
crate::api_client::compare_versions(version1, version2)
}
fn get_auto_update_state_file(&self) -> PathBuf {
File diff suppressed because it is too large Load Diff
+98 -2
View File
@@ -117,7 +117,8 @@ impl BrowserVersionManager {
/// Get cached browser versions immediately (returns None if no cache exists)
pub fn get_cached_browser_versions(&self, browser: &str) -> Option<Vec<String>> {
if browser == "brave" {
return ApiClient::instance()
return self
.api_client
.get_cached_github_releases("brave")
.map(|releases| releases.into_iter().map(|r| r.tag_name).collect());
}
@@ -134,7 +135,7 @@ impl BrowserVersionManager {
browser: &str,
) -> Option<Vec<BrowserVersionInfo>> {
if browser == "brave" {
if let Some(releases) = ApiClient::instance().get_cached_github_releases("brave") {
if let Some(releases) = self.api_client.get_cached_github_releases("brave") {
let detailed_info: Vec<BrowserVersionInfo> = releases
.into_iter()
.map(|r| BrowserVersionInfo {
@@ -1274,6 +1275,101 @@ mod tests {
}
}
#[tauri::command]
pub fn get_supported_browsers() -> Result<Vec<String>, String> {
let service = BrowserVersionManager::instance();
Ok(service.get_supported_browsers())
}
#[tauri::command]
pub fn is_browser_supported_on_platform(browser_str: String) -> Result<bool, String> {
let service = BrowserVersionManager::instance();
service
.is_browser_supported(&browser_str)
.map_err(|e| format!("Failed to check browser support: {e}"))
}
#[tauri::command]
pub async fn fetch_browser_versions_cached_first(
browser_str: String,
) -> Result<Vec<BrowserVersionInfo>, String> {
let service = BrowserVersionManager::instance();
// Get cached versions immediately if available
if let Some(cached_versions) = service.get_cached_browser_versions_detailed(&browser_str) {
// Check if we should update cache in background
if service.should_update_cache(&browser_str) {
// Start background update but return cached data immediately
let service_clone = BrowserVersionManager::instance();
let browser_str_clone = browser_str.clone();
tokio::spawn(async move {
if let Err(e) = service_clone
.fetch_browser_versions_detailed(&browser_str_clone, false)
.await
{
eprintln!("Background version update failed for {browser_str_clone}: {e}");
}
});
}
Ok(cached_versions)
} else {
// No cache available, fetch fresh
service
.fetch_browser_versions_detailed(&browser_str, false)
.await
.map_err(|e| format!("Failed to fetch detailed browser versions: {e}"))
}
}
#[tauri::command]
pub async fn fetch_browser_versions_with_count_cached_first(
browser_str: String,
) -> Result<BrowserVersionsResult, String> {
let service = BrowserVersionManager::instance();
// Get cached versions immediately if available
if let Some(cached_versions) = service.get_cached_browser_versions(&browser_str) {
// Check if we should update cache in background
if service.should_update_cache(&browser_str) {
// Start background update but return cached data immediately
let service_clone = BrowserVersionManager::instance();
let browser_str_clone = browser_str.clone();
tokio::spawn(async move {
if let Err(e) = service_clone
.fetch_browser_versions_with_count(&browser_str_clone, false)
.await
{
eprintln!("Background version update failed for {browser_str_clone}: {e}");
}
});
}
// Return cached data in the expected format
Ok(BrowserVersionsResult {
versions: cached_versions.clone(),
new_versions_count: None, // No new versions when returning cached data
total_versions_count: cached_versions.len(),
})
} else {
// No cache available, fetch fresh
service
.fetch_browser_versions_with_count(&browser_str, false)
.await
.map_err(|e| format!("Failed to fetch browser versions: {e}"))
}
}
#[tauri::command]
pub async fn fetch_browser_versions_with_count(
browser_str: String,
) -> Result<BrowserVersionsResult, String> {
let service = BrowserVersionManager::instance();
service
.fetch_browser_versions_with_count(&browser_str, false)
.await
.map_err(|e| format!("Failed to fetch browser versions: {e}"))
}
// Global singleton instance
lazy_static::lazy_static! {
static ref BROWSER_VERSION_SERVICE: BrowserVersionManager = BrowserVersionManager::new();
@@ -1,8 +1,10 @@
use crate::browser_runner::BrowserRunner;
use crate::profile::BrowserProfile;
use directories::BaseDirs;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tauri::AppHandle;
use tauri_plugin_shell::ShellExt;
use tokio::sync::Mutex as AsyncMutex;
@@ -60,27 +62,40 @@ struct CamoufoxInstance {
url: Option<String>,
}
struct CamoufoxNodecarLauncherInner {
struct CamoufoxManagerInner {
instances: HashMap<String, CamoufoxInstance>,
}
pub struct CamoufoxNodecarLauncher {
inner: Arc<AsyncMutex<CamoufoxNodecarLauncherInner>>,
pub struct CamoufoxManager {
inner: Arc<AsyncMutex<CamoufoxManagerInner>>,
base_dirs: BaseDirs,
}
impl CamoufoxNodecarLauncher {
impl CamoufoxManager {
fn new() -> Self {
Self {
inner: Arc::new(AsyncMutex::new(CamoufoxNodecarLauncherInner {
inner: Arc::new(AsyncMutex::new(CamoufoxManagerInner {
instances: HashMap::new(),
})),
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
}
}
pub fn instance() -> &'static CamoufoxNodecarLauncher {
pub fn instance() -> &'static CamoufoxManager {
&CAMOUFOX_NODECAR_LAUNCHER
}
pub fn get_profiles_dir(&self) -> PathBuf {
let mut path = self.base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
path.push("profiles");
path
}
/// Generate Camoufox fingerprint configuration during profile creation
pub async fn generate_fingerprint_config(
&self,
@@ -95,8 +110,8 @@ impl CamoufoxNodecarLauncher {
path.clone()
} else {
// Use the browser runner helper with the real profile
let browser_runner = crate::browser_runner::BrowserRunner::instance();
browser_runner
// Use self.browser_runner instead of instance()
BrowserRunner::instance()
.get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?
.to_string_lossy()
@@ -202,8 +217,8 @@ impl CamoufoxNodecarLauncher {
path.clone()
} else {
// Use the browser runner helper with the real profile
let browser_runner = crate::browser_runner::BrowserRunner::instance();
browser_runner
// Use self.browser_runner instead of instance()
BrowserRunner::instance()
.get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?
.to_string_lossy()
@@ -431,7 +446,7 @@ impl CamoufoxNodecarLauncher {
}
}
impl CamoufoxNodecarLauncher {
impl CamoufoxManager {
pub async fn launch_camoufox_profile_nodecar(
&self,
app_handle: AppHandle,
@@ -440,8 +455,7 @@ impl CamoufoxNodecarLauncher {
url: Option<String>,
) -> Result<CamoufoxLaunchResult, String> {
// Get profile path
let browser_runner = crate::browser_runner::BrowserRunner::instance();
let profiles_dir = browser_runner.get_profiles_dir();
let profiles_dir = self.get_profiles_dir();
let profile_path = profile.get_profile_data_path(&profiles_dir);
let profile_path_str = profile_path.to_string_lossy();
@@ -484,5 +498,5 @@ mod tests {
// Global singleton instance
lazy_static::lazy_static! {
static ref CAMOUFOX_NODECAR_LAUNCHER: CamoufoxNodecarLauncher = CamoufoxNodecarLauncher::new();
static ref CAMOUFOX_NODECAR_LAUNCHER: CamoufoxManager = CamoufoxManager::new();
}
+2 -46
View File
@@ -1,10 +1,10 @@
use tauri::command;
pub struct DefaultBrowser;
pub struct DefaultBrowser {}
impl DefaultBrowser {
fn new() -> Self {
Self
Self {}
}
pub fn instance() -> &'static DefaultBrowser {
@@ -38,38 +38,6 @@ impl DefaultBrowser {
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
Err("Unsupported platform".to_string())
}
pub async fn open_url_with_profile(
&self,
app_handle: tauri::AppHandle,
profile_id: String,
url: String,
) -> Result<(), String> {
let runner = crate::browser_runner::BrowserRunner::instance();
// Get the profile by name
let profiles = runner
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let profile = profiles
.into_iter()
.find(|p| p.id.to_string() == profile_id)
.ok_or_else(|| format!("Profile '{profile_id}' not found"))?;
println!("Opening URL '{url}' with profile '{profile_id}'");
// Use launch_or_open_url which handles both launching new instances and opening in existing ones
runner
.launch_or_open_url(app_handle, &profile, Some(url.clone()), None)
.await
.map_err(|e| {
println!("Failed to open URL with profile '{profile_id}': {e}");
format!("Failed to open URL with profile: {e}")
})?;
println!("Successfully opened URL '{url}' with profile '{profile_id}'");
Ok(())
}
}
#[cfg(target_os = "macos")]
@@ -570,15 +538,3 @@ pub async fn set_as_default_browser() -> Result<(), String> {
let default_browser = DefaultBrowser::instance();
default_browser.set_as_default_browser().await
}
#[tauri::command]
pub async fn open_url_with_profile(
app_handle: tauri::AppHandle,
profile_id: String,
url: String,
) -> Result<(), String> {
let default_browser = DefaultBrowser::instance();
default_browser
.open_url_with_profile(app_handle, profile_id, url)
.await
}
@@ -5,6 +5,9 @@ use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
use crate::geoip_downloader::GeoIPDownloader;
use crate::profile::{BrowserProfile, ProfileManager};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DownloadedBrowserInfo {
pub browser: String,
@@ -19,12 +22,18 @@ struct RegistryData {
pub struct DownloadedBrowsersRegistry {
data: Mutex<RegistryData>,
profile_manager: &'static ProfileManager,
auto_updater: &'static crate::auto_updater::AutoUpdater,
geoip_downloader: &'static GeoIPDownloader,
}
impl DownloadedBrowsersRegistry {
fn new() -> Self {
Self {
data: Mutex::new(RegistryData::default()),
profile_manager: ProfileManager::instance(),
auto_updater: crate::auto_updater::AutoUpdater::instance(),
geoip_downloader: GeoIPDownloader::instance(),
}
}
@@ -88,7 +97,9 @@ impl DownloadedBrowsersRegistry {
data.browsers.get_mut(browser)?.remove(version)
}
pub fn is_browser_downloaded(&self, browser: &str, version: &str) -> bool {
/// Check if browser is registered in the registry (without disk validation)
/// This method only checks the in-memory registry and does not validate file existence
pub fn is_browser_registered(&self, browser: &str, version: &str) -> bool {
let data = self.data.lock().unwrap();
data
.browsers
@@ -97,6 +108,52 @@ impl DownloadedBrowsersRegistry {
.is_some()
}
/// Check if browser is downloaded and files exist on disk
/// This method validates both registry entry and actual file existence
pub fn is_browser_downloaded(&self, browser: &str, version: &str) -> bool {
use crate::browser::{create_browser, BrowserType};
// First check if browser is registered
if !self.is_browser_registered(browser, version) {
return false;
}
// Always check if files actually exist on disk
let browser_type = match BrowserType::from_str(browser) {
Ok(bt) => bt,
Err(_) => {
println!("Invalid browser type: {browser}");
return false;
}
};
let browser_instance = create_browser(browser_type.clone());
// Get binaries directory
let binaries_dir = if let Some(base_dirs) = directories::BaseDirs::new() {
let mut path = base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
path.push("binaries");
path
} else {
return false;
};
let files_exist = browser_instance.is_version_downloaded(version, &binaries_dir);
// If files don't exist but registry thinks they do, clean up the registry
if !files_exist {
println!("Cleaning up stale registry entry for {browser} {version}");
self.remove_browser(browser, version);
let _ = self.save(); // Don't fail if save fails, just log
}
files_exist
}
pub fn get_downloaded_versions(&self, browser: &str) -> Vec<String> {
let data = self.data.lock().unwrap();
data
@@ -107,26 +164,31 @@ impl DownloadedBrowsersRegistry {
}
pub fn mark_download_started(&self, browser: &str, version: &str, file_path: PathBuf) {
// Only mark download started, don't add to registry yet
// The browser will be added to registry only after verification succeeds
println!(
"Marking download started for {}:{} at {}",
browser,
version,
file_path.display()
);
}
pub fn mark_download_completed(
&self,
browser: &str,
version: &str,
file_path: PathBuf,
) -> Result<(), String> {
// Only mark as completed after verification succeeds
let info = DownloadedBrowserInfo {
browser: browser.to_string(),
version: version.to_string(),
file_path,
};
self.add_browser(info);
}
pub fn mark_download_completed(&self, browser: &str, version: &str) -> Result<(), String> {
let data = self.data.lock().unwrap();
if data
.browsers
.get(browser)
.and_then(|versions| versions.get(version))
.is_some()
{
Ok(())
} else {
Err(format!("Browser {browser}:{version} not found in registry"))
}
println!("Browser {browser}:{version} successfully added to registry after verification");
Ok(())
}
pub fn cleanup_failed_download(
@@ -191,7 +253,7 @@ impl DownloadedBrowsersRegistry {
}
/// Find and remove unused browser binaries that are not referenced by any active profiles
pub fn cleanup_unused_binaries(
fn cleanup_unused_binaries_internal(
&self,
active_profiles: &[(String, String)], // (browser, version) pairs
running_profiles: &[(String, String)], // (browser, version) pairs for running profiles
@@ -203,14 +265,13 @@ impl DownloadedBrowsersRegistry {
let mut cleaned_up = Vec::new();
// Get pending update versions from auto updater
let pending_updates =
match crate::auto_updater::AutoUpdater::instance().get_pending_update_versions() {
Ok(updates) => updates,
Err(e) => {
eprintln!("Warning: Failed to get pending updates for cleanup: {e}");
std::collections::HashSet::new()
}
};
let pending_updates = match self.auto_updater.get_pending_update_versions() {
Ok(updates) => updates,
Err(e) => {
eprintln!("Warning: Failed to get pending updates for cleanup: {e}");
std::collections::HashSet::new()
}
};
// Collect all downloaded browsers that are not in active profiles
let mut to_remove = Vec::new();
@@ -288,11 +349,10 @@ impl DownloadedBrowsersRegistry {
/// Verify that all registered browsers actually exist on disk and clean up stale entries
pub fn verify_and_cleanup_stale_entries(
&self,
browser_runner: &crate::browser_runner::BrowserRunner,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
use crate::browser::{create_browser, BrowserType};
let mut cleaned_up = Vec::new();
let binaries_dir = browser_runner.get_binaries_dir();
let binaries_dir = self.profile_manager.get_binaries_dir();
let browsers_to_check: Vec<(String, String)> = {
let data = self.data.lock().unwrap();
@@ -413,7 +473,7 @@ impl DownloadedBrowsersRegistry {
}
/// Comprehensive cleanup that removes unused binaries and syncs registry
pub fn comprehensive_cleanup(
fn comprehensive_cleanup(
&self,
binaries_dir: &std::path::Path,
active_profiles: &[(String, String)],
@@ -426,11 +486,12 @@ impl DownloadedBrowsersRegistry {
cleanup_results.extend(sync_results);
// Then perform the regular cleanup
let regular_cleanup = self.cleanup_unused_binaries(active_profiles, running_profiles)?;
let regular_cleanup =
self.cleanup_unused_binaries_internal(active_profiles, running_profiles)?;
cleanup_results.extend(regular_cleanup);
// Verify and cleanup stale entries
let stale_cleanup = self.verify_and_cleanup_stale_entries_simple(binaries_dir)?;
let stale_cleanup = self.verify_and_cleanup_stale_entries()?;
cleanup_results.extend(stale_cleanup);
// Clean up any remaining empty folders
@@ -590,36 +651,362 @@ impl DownloadedBrowsersRegistry {
Ok(cleaned_up)
}
/// Simplified version of verify_and_cleanup_stale_entries that doesn't need BrowserRunner
pub fn verify_and_cleanup_stale_entries_simple(
/// Consolidate browser versions - keep only the latest version per browser
pub fn consolidate_browser_versions(
&self,
binaries_dir: &std::path::Path,
app_handle: &tauri::AppHandle,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let mut cleaned_up = Vec::new();
let mut browsers_to_remove = Vec::new();
println!("Starting browser version consolidation...");
{
let data = self.data.lock().unwrap();
for (browser_str, versions) in &data.browsers {
for version in versions.keys() {
// Check if the browser directory actually exists
let browser_dir = binaries_dir.join(browser_str).join(version);
if !browser_dir.exists() {
browsers_to_remove.push((browser_str.clone(), version.clone()));
let profiles = self
.profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let binaries_dir = self.profile_manager.get_binaries_dir();
let mut consolidated = Vec::new();
// Group profiles by browser
let mut browser_profiles: std::collections::HashMap<String, Vec<&BrowserProfile>> =
std::collections::HashMap::new();
for profile in &profiles {
browser_profiles
.entry(profile.browser.clone())
.or_default()
.push(profile);
}
for (browser_name, browser_profiles) in browser_profiles.iter() {
// Find the latest version among all profiles for this browser that actually exists on disk
let mut available_versions: Vec<String> = Vec::new();
for profile in browser_profiles {
// Only consider versions that actually exist on disk
let browser_type = match crate::browser::BrowserType::from_str(browser_name) {
Ok(bt) => bt,
Err(_) => continue,
};
let browser = crate::browser::create_browser(browser_type.clone());
if browser.is_version_downloaded(&profile.version, &binaries_dir) {
available_versions.push(profile.version.clone());
} else {
println!(
"Profile '{}' references version {} that doesn't exist on disk",
profile.name, profile.version
);
}
}
if available_versions.is_empty() {
println!("No available versions found for {browser_name}, skipping consolidation");
continue;
}
// Sort available versions to find the latest
available_versions.sort_by(|a, b| {
// Sort versions using semantic versioning logic
crate::api_client::compare_versions(b, a)
});
let latest_version = &available_versions[0];
println!("Latest available version for {browser_name}: {latest_version}");
// Check which profiles need to be updated to the latest version
let mut profiles_to_update = Vec::new();
let mut older_versions_to_remove = std::collections::HashSet::<String>::new();
for profile in browser_profiles {
if profile.version != *latest_version {
// Only update if profile is not currently running
if profile.process_id.is_none() {
profiles_to_update.push(profile);
older_versions_to_remove.insert(profile.version.clone());
} else {
println!(
"Skipping version update for running profile: {} ({})",
profile.name, profile.version
);
}
}
// Update profiles to latest version
for profile in &profiles_to_update {
match self.profile_manager.update_profile_version(
app_handle,
&profile.id.to_string(),
latest_version,
) {
Ok(_) => {
consolidated.push(format!(
"Updated profile '{}' from {} to {}",
profile.name, profile.version, latest_version
));
}
Err(e) => {
eprintln!("Failed to update profile '{}': {}", profile.name, e);
}
}
}
// Remove older version binaries that are no longer needed
for old_version in &older_versions_to_remove {
println!("Consolidating: removing old version {browser_name} {old_version}");
match self.cleanup_failed_download(browser_name, old_version) {
Ok(_) => {
consolidated.push(format!("Removed old version: {browser_name} {old_version}"));
println!("Successfully removed old version: {browser_name} {old_version}");
}
Err(e) => {
eprintln!("Failed to cleanup old version {browser_name} {old_version}: {e}");
}
}
}
}
}
// Remove stale entries
for (browser_str, version) in browsers_to_remove {
if let Some(_removed) = self.remove_browser(&browser_str, &version) {
cleaned_up.push(format!(
"Removed stale registry entry for {browser_str} {version}"
));
// Save registry after consolidation
self
.save()
.map_err(|e| format!("Failed to save registry after consolidation: {e}"))?;
println!(
"Browser version consolidation completed: {} actions taken",
consolidated.len()
);
Ok(consolidated)
}
/// Check if browser binaries exist for all profiles and return missing binaries
pub async fn check_missing_binaries(
&self,
) -> Result<Vec<(String, String, String)>, Box<dyn std::error::Error + Send + Sync>> {
use crate::browser::{create_browser, BrowserType};
// Get all profiles
let profiles = self
.profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let mut missing_binaries = Vec::new();
for profile in profiles {
let browser_type = match BrowserType::from_str(&profile.browser) {
Ok(bt) => bt,
Err(_) => {
println!(
"Warning: Invalid browser type '{}' for profile '{}'",
profile.browser, profile.name
);
continue;
}
};
let browser = create_browser(browser_type.clone());
// Get binaries directory
let binaries_dir = if let Some(base_dirs) = directories::BaseDirs::new() {
let mut path = base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
path.push("binaries");
path
} else {
return Err("Failed to get base directories".into());
};
println!(
"binaries_dir: {binaries_dir:?} for profile: {}",
profile.name
);
// Check if the version is downloaded
if !browser.is_version_downloaded(&profile.version, &binaries_dir) {
missing_binaries.push((profile.name, profile.browser, profile.version));
}
}
Ok(missing_binaries)
}
/// Automatically download missing binaries for all profiles
pub async fn ensure_all_binaries_exist(
&self,
app_handle: &tauri::AppHandle,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
// First, clean up any stale registry entries
if let Ok(cleaned_up) = self.verify_and_cleanup_stale_entries() {
if !cleaned_up.is_empty() {
println!(
"Cleaned up {} stale registry entries: {}",
cleaned_up.len(),
cleaned_up.join(", ")
);
}
}
// Consolidate browser versions - keep only latest version per browser
if let Ok(consolidated) = self.consolidate_browser_versions(app_handle) {
if !consolidated.is_empty() {
println!("Version consolidation results:");
for action in &consolidated {
println!(" {action}");
}
}
}
let missing_binaries = self.check_missing_binaries().await?;
let mut downloaded = Vec::new();
for (profile_name, browser, version) in missing_binaries {
println!("Downloading missing binary for profile '{profile_name}': {browser} {version}");
match crate::downloader::download_browser(
app_handle.clone(),
browser.clone(),
version.clone(),
)
.await
{
Ok(_) => {
downloaded.push(format!(
"{browser} {version} (for profile '{profile_name}')"
));
// After successful download, update profiles that use this browser to the new version
match self
.update_profiles_to_version(app_handle, &browser, &version)
.await
{
Ok(updated_profiles) => {
if !updated_profiles.is_empty() {
println!(
"Successfully updated {} profiles to version {}:",
updated_profiles.len(),
version
);
for update_msg in updated_profiles {
println!(" {update_msg}");
}
}
}
Err(e) => {
eprintln!("CRITICAL: Failed to update profiles to version {version}: {e}");
eprintln!("This may cause profile version inconsistencies and cleanup issues");
}
}
}
Err(e) => {
eprintln!("Failed to download {browser} {version} for profile '{profile_name}': {e}");
}
}
}
// Check if GeoIP database is missing for Camoufox profiles
if self.geoip_downloader.check_missing_geoip_database()? {
println!("GeoIP database is missing for Camoufox profiles, downloading...");
match self
.geoip_downloader
.download_geoip_database(app_handle)
.await
{
Ok(_) => {
downloaded.push("GeoIP database for Camoufox".to_string());
println!("GeoIP database downloaded successfully");
}
Err(e) => {
eprintln!("Failed to download GeoIP database: {e}");
// Don't fail the entire operation if GeoIP download fails
}
}
}
Ok(downloaded)
}
/// Update all profiles using a specific browser to a new version
async fn update_profiles_to_version(
&self,
app_handle: &tauri::AppHandle,
browser: &str,
version: &str,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let profiles = self
.profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let mut updated_profiles = Vec::new();
for profile in profiles {
if profile.browser == browser && profile.version != version {
// Check if profile is currently running
if profile.process_id.is_some() {
println!(
"Skipping version update for running profile: {} ({})",
profile.name, profile.version
);
continue;
}
// Update the profile version
match self.profile_manager.update_profile_version(
app_handle,
&profile.id.to_string(),
version,
) {
Ok(_) => {
updated_profiles.push(format!(
"Updated profile '{}' from {} to {}",
profile.name, profile.version, version
));
println!(
"Successfully updated profile '{}' to version {}",
profile.name, version
);
// Save registry after each profile update to ensure consistency
if let Err(e) = self.save() {
eprintln!("Warning: Failed to save registry after profile update: {e}");
}
}
Err(e) => {
eprintln!("Failed to update profile '{}': {}", profile.name, e);
}
}
}
}
Ok(updated_profiles)
}
/// Cleanup unused binaries based on active and running profiles
pub fn cleanup_unused_binaries(
&self,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
// Load current profiles using injected ProfileManager
let profiles = self
.profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
// Get active browser versions (all profiles)
let active_versions = self.get_active_browser_versions(&profiles);
// Get running browser versions (only running profiles)
let running_versions = self.get_running_browser_versions(&profiles);
// Get binaries directory from profile manager
let binaries_dir = self.profile_manager.get_binaries_dir();
// Use comprehensive cleanup that syncs registry with disk and removes unused binaries
let cleaned_up =
self.comprehensive_cleanup(&binaries_dir, &active_versions, &running_versions)?;
// Registry is already saved by comprehensive_cleanup
Ok(cleaned_up)
}
}
@@ -641,6 +1028,7 @@ mod tests {
#[test]
fn test_registry_creation() {
// Create a mock profile manager for testing
let registry = DownloadedBrowsersRegistry::new();
let data = registry.data.lock().unwrap();
assert!(data.browsers.is_empty());
@@ -657,9 +1045,9 @@ mod tests {
registry.add_browser(info.clone());
assert!(registry.is_browser_downloaded("firefox", "139.0"));
assert!(!registry.is_browser_downloaded("firefox", "140.0"));
assert!(!registry.is_browser_downloaded("chrome", "139.0"));
assert!(registry.is_browser_registered("firefox", "139.0"));
assert!(!registry.is_browser_registered("firefox", "140.0"));
assert!(!registry.is_browser_registered("chrome", "139.0"));
}
#[test]
@@ -702,21 +1090,21 @@ mod tests {
// Mark download started
registry.mark_download_started("firefox", "139.0", PathBuf::from("/test/path"));
// Should be considered downloaded immediately
// Should NOT be registered until verification completes
assert!(
registry.is_browser_downloaded("firefox", "139.0"),
"Browser should be considered downloaded after marking as started"
!registry.is_browser_registered("firefox", "139.0"),
"Browser should NOT be registered after marking as started (only after verification)"
);
// Mark as completed
// Mark as completed (after verification)
registry
.mark_download_completed("firefox", "139.0")
.mark_download_completed("firefox", "139.0", PathBuf::from("/test/path"))
.expect("Failed to mark download as completed");
// Should still be considered downloaded
// Should now be registered
assert!(
registry.is_browser_downloaded("firefox", "139.0"),
"Browser should still be considered downloaded after completion"
registry.is_browser_registered("firefox", "139.0"),
"Browser should be registered after verification completes"
);
}
@@ -731,8 +1119,8 @@ mod tests {
registry.add_browser(info);
assert!(
registry.is_browser_downloaded("firefox", "139.0"),
"Browser should be downloaded after adding"
registry.is_browser_registered("firefox", "139.0"),
"Browser should be registered after adding"
);
let removed = registry.remove_browser("firefox", "139.0");
@@ -741,8 +1129,8 @@ mod tests {
"Remove operation should return the removed browser info"
);
assert!(
!registry.is_browser_downloaded("firefox", "139.0"),
"Browser should not be downloaded after removal"
!registry.is_browser_registered("firefox", "139.0"),
"Browser should not be registered after removal"
);
}
@@ -753,10 +1141,79 @@ mod tests {
// Mark twilight download started
registry.mark_download_started("zen", "twilight", PathBuf::from("/test/zen-twilight"));
// Check that it's registered
// Should NOT be registered until verification completes
assert!(
registry.is_browser_downloaded("zen", "twilight"),
"Zen twilight version should be registered as downloaded"
!registry.is_browser_registered("zen", "twilight"),
"Zen twilight version should NOT be registered until verification completes"
);
// Mark as completed (after verification)
registry
.mark_download_completed("zen", "twilight", PathBuf::from("/test/zen-twilight"))
.expect("Failed to mark twilight download as completed");
// Now it should be registered
assert!(
registry.is_browser_registered("zen", "twilight"),
"Zen twilight version should be registered after verification completes"
);
}
#[test]
fn test_is_browser_registered_vs_downloaded() {
let registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
file_path: PathBuf::from("/test/path"),
};
// Add browser to registry
registry.add_browser(info);
// Should be registered (in-memory check)
assert!(
registry.is_browser_registered("firefox", "139.0"),
"Browser should be registered after adding to registry"
);
// is_browser_downloaded should return false in test environment because files don't exist
// This tests the difference between registered (in registry) vs downloaded (files exist)
assert!(
!registry.is_browser_downloaded("firefox", "139.0"),
"Browser should not be considered downloaded when files don't exist on disk"
);
}
}
#[tauri::command]
pub fn get_downloaded_browser_versions(browser_str: String) -> Result<Vec<String>, String> {
let registry = DownloadedBrowsersRegistry::instance();
Ok(registry.get_downloaded_versions(&browser_str))
}
#[tauri::command]
pub fn is_browser_downloaded(browser_str: String, version: String) -> bool {
let registry = DownloadedBrowsersRegistry::instance();
registry.is_browser_downloaded(&browser_str, &version)
}
#[tauri::command]
pub async fn check_missing_binaries() -> Result<Vec<(String, String, String)>, String> {
let registry = DownloadedBrowsersRegistry::instance();
registry
.check_missing_binaries()
.await
.map_err(|e| format!("Failed to check missing binaries: {e}"))
}
#[tauri::command]
pub async fn ensure_all_binaries_exist(
app_handle: tauri::AppHandle,
) -> Result<Vec<String>, String> {
let registry = DownloadedBrowsersRegistry::instance();
registry
.ensure_all_binaries_exist(&app_handle)
.await
.map_err(|e| format!("Failed to ensure all binaries exist: {e}"))
}
@@ -2,12 +2,19 @@ use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tauri::Emitter;
use crate::api_client::ApiClient;
use crate::browser::BrowserType;
use crate::browser::{create_browser, BrowserType};
use crate::browser_version_manager::DownloadInfo;
// Global state to track currently downloading browser-version pairs
lazy_static::lazy_static! {
static ref DOWNLOADING_BROWSERS: std::sync::Arc<Mutex<std::collections::HashSet<String>>> =
std::sync::Arc::new(Mutex::new(std::collections::HashSet::new()));
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DownloadProgress {
pub browser: String,
@@ -23,6 +30,10 @@ pub struct DownloadProgress {
pub struct Downloader {
client: Client,
api_client: &'static ApiClient,
registry: &'static crate::downloaded_browsers_registry::DownloadedBrowsersRegistry,
version_service: &'static crate::browser_version_manager::BrowserVersionManager,
extractor: &'static crate::extraction::Extractor,
geoip_downloader: &'static crate::geoip_downloader::GeoIPDownloader,
}
impl Downloader {
@@ -30,6 +41,10 @@ impl Downloader {
Self {
client: Client::new(),
api_client: ApiClient::instance(),
registry: crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(),
version_service: crate::browser_version_manager::BrowserVersionManager::instance(),
extractor: crate::extraction::Extractor::instance(),
geoip_downloader: crate::geoip_downloader::GeoIPDownloader::instance(),
}
}
@@ -42,6 +57,10 @@ impl Downloader {
Self {
client: Client::new(),
api_client: ApiClient::instance(),
registry: crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(),
version_service: crate::browser_version_manager::BrowserVersionManager::instance(),
extractor: crate::extraction::Extractor::instance(),
geoip_downloader: crate::geoip_downloader::GeoIPDownloader::instance(),
}
}
@@ -573,6 +592,327 @@ impl Downloader {
Ok(file_path)
}
/// Download a browser binary, verify it, and register it in the downloaded browsers registry
pub async fn download_browser_full(
&self,
app_handle: &tauri::AppHandle,
browser_str: String,
version: String,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// Check if this browser-version pair is already being downloaded
let download_key = format!("{browser_str}-{version}");
{
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
if downloading.contains(&download_key) {
return Err(format!("Browser '{browser_str}' version '{version}' is already being downloaded. Please wait for the current download to complete.").into());
}
// Mark this browser-version pair as being downloaded
downloading.insert(download_key.clone());
}
let browser_type =
BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?;
let browser = create_browser(browser_type.clone());
// Use injected registry instance
// Get binaries directory - we need to get it from somewhere
// This is a bit tricky since we don't have access to BrowserRunner's get_binaries_dir
// We'll need to replicate this logic
let binaries_dir = if let Some(base_dirs) = directories::BaseDirs::new() {
let mut path = base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
path.push("binaries");
path
} else {
return Err("Failed to get base directories".into());
};
// Check if registry thinks it's downloaded, but also verify files actually exist
if self.registry.is_browser_downloaded(&browser_str, &version) {
let actually_exists = browser.is_version_downloaded(&version, &binaries_dir);
if actually_exists {
// Remove from downloading set since it's already downloaded
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
downloading.remove(&download_key);
return Ok(version);
} else {
// Registry says it's downloaded but files don't exist - clean up registry
println!("Registry indicates {browser_str} {version} is downloaded, but files are missing. Cleaning up registry entry.");
self.registry.remove_browser(&browser_str, &version);
self
.registry
.save()
.map_err(|e| format!("Failed to save cleaned registry: {e}"))?;
}
}
// Check if browser is supported on current platform before attempting download
if !self
.version_service
.is_browser_supported(&browser_str)
.unwrap_or(false)
{
// Remove from downloading set on error
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
downloading.remove(&download_key);
return Err(
format!(
"Browser '{}' is not supported on your platform ({} {}). Supported browsers: {}",
browser_str,
std::env::consts::OS,
std::env::consts::ARCH,
self.version_service.get_supported_browsers().join(", ")
)
.into(),
);
}
let download_info = self
.version_service
.get_download_info(&browser_str, &version)
.map_err(|e| format!("Failed to get download info: {e}"))?;
// Create browser directory
let mut browser_dir = binaries_dir.clone();
browser_dir.push(&browser_str);
browser_dir.push(&version);
std::fs::create_dir_all(&browser_dir)
.map_err(|e| format!("Failed to create browser directory: {e}"))?;
// Mark download as started (but don't add to registry yet)
self
.registry
.mark_download_started(&browser_str, &version, browser_dir.clone());
// Attempt to download the archive. If the download fails but an archive with the
// expected filename already exists (manual download), continue using that file.
let download_path: PathBuf = match self
.download_browser(
app_handle,
browser_type.clone(),
&version,
&download_info,
&browser_dir,
)
.await
{
Ok(path) => path,
Err(e) => {
// Do NOT continue with extraction on failed downloads. Partial files may exist but are invalid.
// Clean registry entry and stop here so the UI can show a single, clear error.
let _ = self.registry.remove_browser(&browser_str, &version);
let _ = self.registry.save();
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
downloading.remove(&download_key);
return Err(format!("Failed to download browser: {e}").into());
}
};
// Use the extraction module
if download_info.is_archive {
match self
.extractor
.extract_browser(
app_handle,
browser_type.clone(),
&version,
&download_path,
&browser_dir,
)
.await
{
Ok(_) => {
// Do not remove the archive here. We keep it until verification succeeds.
}
Err(e) => {
// Do not remove the archive or extracted files. Just drop the registry entry
// so it won't be reported as downloaded.
let _ = self.registry.remove_browser(&browser_str, &version);
let _ = self.registry.save();
// Remove browser-version pair from downloading set on error
{
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
downloading.remove(&download_key);
}
return Err(format!("Failed to extract browser: {e}").into());
}
}
// Give filesystem a moment to settle after extraction
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
// Emit verification progress
let progress = DownloadProgress {
browser: browser_str.clone(),
version: version.clone(),
downloaded_bytes: 0,
total_bytes: None,
percentage: 100.0,
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: "verifying".to_string(),
};
let _ = app_handle.emit("download-progress", &progress);
// Verify the browser was downloaded correctly
println!("Verifying download for browser: {browser_str}, version: {version}");
// Use the browser's own verification method
if !browser.is_version_downloaded(&version, &binaries_dir) {
// Provide detailed error information for debugging
let browser_dir = binaries_dir.join(&browser_str).join(&version);
let mut error_details = format!(
"Browser download completed but verification failed for {} {}. Expected directory: {}",
browser_str,
version,
browser_dir.display()
);
// List what files actually exist
if browser_dir.exists() {
error_details.push_str("\nFiles found in directory:");
if let Ok(entries) = std::fs::read_dir(&browser_dir) {
for entry in entries.flatten() {
let path = entry.path();
let file_type = if path.is_dir() { "DIR" } else { "FILE" };
error_details.push_str(&format!("\n {} {}", file_type, path.display()));
}
} else {
error_details.push_str("\n (Could not read directory contents)");
}
} else {
error_details.push_str("\nDirectory does not exist!");
}
// For Camoufox on Linux, provide specific expected files
if browser_str == "camoufox" && cfg!(target_os = "linux") {
let camoufox_subdir = browser_dir.join("camoufox");
error_details.push_str("\nExpected Camoufox executable locations:");
error_details.push_str(&format!("\n {}/camoufox-bin", camoufox_subdir.display()));
error_details.push_str(&format!("\n {}/camoufox", camoufox_subdir.display()));
if camoufox_subdir.exists() {
error_details.push_str(&format!(
"\nCamoufox subdirectory exists: {}",
camoufox_subdir.display()
));
if let Ok(entries) = std::fs::read_dir(&camoufox_subdir) {
error_details.push_str("\nFiles in camoufox subdirectory:");
for entry in entries.flatten() {
let path = entry.path();
let file_type = if path.is_dir() { "DIR" } else { "FILE" };
error_details.push_str(&format!("\n {} {}", file_type, path.display()));
}
}
} else {
error_details.push_str(&format!(
"\nCamoufox subdirectory does not exist: {}",
camoufox_subdir.display()
));
}
}
// Do not delete files on verification failure; keep archive for manual retry.
let _ = self.registry.remove_browser(&browser_str, &version);
let _ = self.registry.save();
// Remove browser-version pair from downloading set on verification failure
{
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
downloading.remove(&download_key);
}
return Err(error_details.into());
}
// Mark completion in registry - only now add to registry after verification
if let Err(e) =
self
.registry
.mark_download_completed(&browser_str, &version, browser_dir.clone())
{
eprintln!("Warning: Could not mark {browser_str} {version} as completed in registry: {e}");
}
self
.registry
.save()
.map_err(|e| format!("Failed to save registry: {e}"))?;
// Now that verification succeeded, remove the archive file if it exists
if download_info.is_archive {
let archive_path = browser_dir.join(&download_info.filename);
if archive_path.exists() {
if let Err(e) = std::fs::remove_file(&archive_path) {
println!("Warning: Could not delete archive file after verification: {e}");
}
}
}
// If this is Camoufox, automatically download GeoIP database
if browser_str == "camoufox" {
// Check if GeoIP database is already available
if !crate::geoip_downloader::GeoIPDownloader::is_geoip_database_available() {
println!("Downloading GeoIP database for Camoufox...");
match self
.geoip_downloader
.download_geoip_database(app_handle)
.await
{
Ok(_) => {
println!("GeoIP database downloaded successfully");
}
Err(e) => {
eprintln!("Failed to download GeoIP database: {e}");
// Don't fail the browser download if GeoIP download fails
}
}
} else {
println!("GeoIP database already available");
}
}
// Emit completion
let progress = DownloadProgress {
browser: browser_str.clone(),
version: version.clone(),
downloaded_bytes: 0,
total_bytes: None,
percentage: 100.0,
speed_bytes_per_sec: 0.0,
eta_seconds: Some(0.0),
stage: "completed".to_string(),
};
let _ = app_handle.emit("download-progress", &progress);
// Remove browser-version pair from downloading set
{
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
downloading.remove(&download_key);
}
Ok(version)
}
}
#[tauri::command]
pub async fn download_browser(
app_handle: tauri::AppHandle,
browser_str: String,
version: String,
) -> Result<String, String> {
let downloader = Downloader::instance();
downloader
.download_browser_full(&app_handle, browser_str, version)
.await
.map_err(|e| format!("Failed to download browser: {e}"))
}
#[cfg(test)]
+1 -1
View File
@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
use tauri::Emitter;
use crate::browser::BrowserType;
use crate::download::DownloadProgress;
use crate::downloader::DownloadProgress;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use std::process::Command;
+33 -5
View File
@@ -1,4 +1,5 @@
use crate::browser::GithubRelease;
use crate::profile::manager::ProfileManager;
use directories::BaseDirs;
use reqwest::Client;
use serde::{Deserialize, Serialize};
@@ -75,6 +76,25 @@ impl GeoIPDownloader {
false
}
}
/// Check if GeoIP database is missing for Camoufox profiles
pub fn check_missing_geoip_database(
&self,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
// Get all profiles
let profiles = ProfileManager::instance()
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
// Check if there are any Camoufox profiles
let has_camoufox_profiles = profiles.iter().any(|profile| profile.browser == "camoufox");
if has_camoufox_profiles {
// Check if GeoIP database is available
return Ok(!Self::is_geoip_database_available());
}
Ok(false)
}
fn find_city_mmdb_asset(&self, release: &GithubRelease) -> Option<String> {
for asset in &release.assets {
@@ -218,6 +238,19 @@ impl GeoIPDownloader {
}
}
#[tauri::command]
pub fn check_missing_geoip_database() -> Result<bool, String> {
let geoip_downloader = GeoIPDownloader::instance();
geoip_downloader
.check_missing_geoip_database()
.map_err(|e| format!("Failed to check missing GeoIP database: {e}"))
}
// Global singleton instance
lazy_static::lazy_static! {
static ref GEOIP_DOWNLOADER: GeoIPDownloader = GeoIPDownloader::new();
}
#[cfg(test)]
mod tests {
use super::*;
@@ -353,8 +386,3 @@ mod tests {
);
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref GEOIP_DOWNLOADER: GeoIPDownloader = GeoIPDownloader::new();
}
+38 -24
View File
@@ -14,10 +14,10 @@ mod auto_updater;
mod browser;
mod browser_runner;
mod browser_version_manager;
mod camoufox;
mod camoufox_manager;
mod default_browser;
mod download;
mod downloaded_browsers;
mod downloaded_browsers_registry;
mod downloader;
mod extraction;
mod geoip_downloader;
mod group_manager;
@@ -31,24 +31,38 @@ mod tag_manager;
mod version_updater;
use browser_runner::{
check_browser_exists, check_browser_status, check_missing_binaries, check_missing_geoip_database,
create_browser_profile_new, delete_profile, download_browser, ensure_all_binaries_exist,
fetch_browser_versions_cached_first, fetch_browser_versions_with_count,
fetch_browser_versions_with_count_cached_first, get_all_tags, get_downloaded_browser_versions,
get_supported_browsers, is_browser_supported_on_platform, kill_browser_profile,
launch_browser_profile, list_browser_profiles, rename_profile, update_camoufox_config,
update_profile_proxy, update_profile_tags,
check_browser_exists, kill_browser_profile, launch_browser_profile, open_url_with_profile,
};
use profile::manager::{
check_browser_status, create_browser_profile_new, delete_profile, list_browser_profiles,
rename_profile, update_camoufox_config, update_profile_proxy, update_profile_tags,
};
use browser_version_manager::{
fetch_browser_versions_cached_first, fetch_browser_versions_with_count,
fetch_browser_versions_with_count_cached_first, get_supported_browsers,
is_browser_supported_on_platform,
};
use downloaded_browsers_registry::{
check_missing_binaries, ensure_all_binaries_exist, get_downloaded_browser_versions,
};
use downloader::download_browser;
use settings_manager::{
clear_all_version_cache_and_refetch, get_app_settings, get_table_sorting_settings,
save_app_settings, save_table_sorting_settings, should_show_settings_on_startup,
get_app_settings, get_table_sorting_settings, save_app_settings, save_table_sorting_settings,
should_show_settings_on_startup,
};
use default_browser::{is_default_browser, open_url_with_profile, set_as_default_browser};
use tag_manager::get_all_tags;
use default_browser::{is_default_browser, set_as_default_browser};
use version_updater::{
get_version_update_status, get_version_updater, trigger_manual_version_update,
clear_all_version_cache_and_refetch, get_version_update_status, get_version_updater,
trigger_manual_version_update,
};
use auto_updater::{
@@ -66,7 +80,7 @@ use group_manager::{
get_groups_with_profile_counts, get_profile_groups, update_profile_group,
};
use geoip_downloader::GeoIPDownloader;
use geoip_downloader::{check_missing_geoip_database, GeoIPDownloader};
use browser_version_manager::get_browser_release_types;
@@ -379,8 +393,9 @@ pub fn run() {
loop {
interval.tick().await;
let browser_runner = crate::browser_runner::BrowserRunner::instance();
if let Err(e) = browser_runner.cleanup_unused_binaries_internal() {
let registry =
crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance();
if let Err(e) = registry.cleanup_unused_binaries() {
eprintln!("Periodic cleanup failed: {e}");
} else {
println!("Periodic cleanup completed successfully");
@@ -417,14 +432,14 @@ pub fn run() {
// Start Camoufox cleanup task
let _app_handle_cleanup = app.handle().clone();
tauri::async_runtime::spawn(async move {
let launcher = crate::camoufox::CamoufoxNodecarLauncher::instance();
let camoufox_manager = crate::camoufox_manager::CamoufoxManager::instance();
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
loop {
interval.tick().await;
match launcher.cleanup_dead_instances().await {
Ok(_dead_instances) => {
match camoufox_manager.cleanup_dead_instances().await {
Ok(_) => {
// Cleanup completed silently
}
Err(e) => {
@@ -440,8 +455,8 @@ pub fn run() {
// Wait a bit for the app to fully initialize
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
let browser_runner = crate::browser_runner::BrowserRunner::instance();
match browser_runner.check_missing_geoip_database() {
let geoip_downloader = crate::geoip_downloader::GeoIPDownloader::instance();
match geoip_downloader.check_missing_geoip_database() {
Ok(true) => {
println!("GeoIP database is missing for Camoufox profiles, downloading at startup...");
let geoip_downloader = GeoIPDownloader::instance();
@@ -502,7 +517,7 @@ pub fn run() {
let runner = crate::browser_runner::BrowserRunner::instance();
// If listing profiles fails, skip this tick
let profiles = match runner.list_profiles() {
let profiles = match runner.profile_manager.list_profiles() {
Ok(p) => p,
Err(e) => {
println!("Warning: Failed to list profiles in status checker: {e}");
@@ -658,7 +673,6 @@ pub fn run() {
check_for_app_updates,
check_for_app_updates_manual,
download_and_install_app_update,
// get_system_theme, // removed
detect_existing_profiles,
import_browser_profile,
check_missing_binaries,
+168 -43
View File
@@ -1,5 +1,7 @@
use crate::api_client::is_browser_version_nightly;
use crate::browser::{create_browser, BrowserType, ProxySettings};
use crate::camoufox::CamoufoxConfig;
use crate::camoufox_manager::CamoufoxConfig;
use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry;
use crate::profile::types::BrowserProfile;
use crate::proxy_manager::PROXY_MANAGER;
use directories::BaseDirs;
@@ -10,12 +12,14 @@ use tauri::Emitter;
pub struct ProfileManager {
base_dirs: BaseDirs,
camoufox_manager: &'static crate::camoufox_manager::CamoufoxManager,
}
impl ProfileManager {
fn new() -> Self {
Self {
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
camoufox_manager: crate::camoufox_manager::CamoufoxManager::instance(),
}
}
@@ -34,6 +38,17 @@ impl ProfileManager {
path
}
pub fn get_binaries_dir(&self) -> PathBuf {
let mut path = self.base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
path.push("binaries");
path
}
#[allow(clippy::too_many_arguments)]
pub async fn create_profile_with_group(
&self,
@@ -72,13 +87,12 @@ impl ProfileManager {
let final_camoufox_config = if browser == "camoufox" {
let mut config = camoufox_config.unwrap_or_else(|| {
println!("Creating default Camoufox config for profile: {name}");
crate::camoufox::CamoufoxConfig::default()
crate::camoufox_manager::CamoufoxConfig::default()
});
// Always ensure executable_path is set to the user's binary location
if config.executable_path.is_none() {
let browser_runner = crate::browser_runner::BrowserRunner::instance();
let mut browser_dir = browser_runner.get_binaries_dir();
let mut browser_dir = self.get_binaries_dir();
browser_dir.push(browser);
browser_dir.push(version);
@@ -137,7 +151,6 @@ impl ProfileManager {
println!("Generating fingerprint for Camoufox profile: {name}");
// Use the camoufox launcher to generate the config
let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance();
// Create a temporary profile for fingerprint generation
let temp_profile = BrowserProfile {
@@ -154,7 +167,8 @@ impl ProfileManager {
tags: Vec::new(),
};
match camoufox_launcher
match self
.camoufox_manager
.generate_fingerprint_config(app_handle, &temp_profile, &config)
.await
{
@@ -237,6 +251,11 @@ impl ProfileManager {
let json = serde_json::to_string_pretty(profile)?;
fs::write(profile_file, json)?;
// Update tag suggestions after any save
let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| {
let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default());
});
Ok(())
}
@@ -355,6 +374,11 @@ impl ProfileManager {
let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default());
});
// Always perform cleanup after profile deletion to remove unused binaries
if let Err(e) = DownloadedBrowsersRegistry::instance().cleanup_unused_binaries() {
println!("Warning: Failed to cleanup unused binaries after profile deletion: {e}");
}
// Emit profile deletion event
if let Err(e) = app_handle.emit("profiles-changed", ()) {
println!("Warning: Failed to emit profiles-changed event: {e}");
@@ -399,12 +423,11 @@ impl ProfileManager {
profile.version = version.to_string();
// Update the release_type based on the version and browser
profile.release_type =
if crate::api_client::is_browser_version_nightly(&profile.browser, version, None) {
"nightly".to_string()
} else {
"stable".to_string()
};
profile.release_type = if is_browser_version_nightly(&profile.browser, version, None) {
"nightly".to_string()
} else {
"stable".to_string()
};
// Save the updated profile
self.save_profile(&profile)?;
@@ -866,9 +889,7 @@ impl ProfileManager {
app_handle: &tauri::AppHandle,
profile: &BrowserProfile,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
use crate::camoufox::CamoufoxNodecarLauncher;
let launcher = CamoufoxNodecarLauncher::instance();
let launcher = self.camoufox_manager;
let profiles_dir = self.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
let profile_path_str = profile_data_path.to_string_lossy();
@@ -1012,17 +1033,6 @@ impl ProfileManager {
}
}
fn get_binaries_dir(&self) -> PathBuf {
let mut path = self.base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
path.push("binaries");
path
}
fn get_common_firefox_preferences(&self) -> Vec<String> {
vec![
// Disable default browser updates
@@ -1202,23 +1212,6 @@ mod tests {
);
}
#[test]
fn test_list_profiles_empty() {
let (manager, _temp_dir) = create_test_profile_manager();
let result = manager.list_profiles();
assert!(
result.is_ok(),
"Should successfully list profiles even when empty"
);
let profiles = result.unwrap();
assert!(
profiles.is_empty(),
"Should return empty vector when no profiles exist"
);
}
#[test]
fn test_get_common_firefox_preferences() {
let (manager, _temp_dir) = create_test_profile_manager();
@@ -1324,7 +1317,139 @@ mod tests {
}
}
#[allow(clippy::too_many_arguments)]
#[tauri::command]
pub async fn create_browser_profile_with_group(
app_handle: tauri::AppHandle,
name: String,
browser: String,
version: String,
release_type: String,
proxy_id: Option<String>,
camoufox_config: Option<CamoufoxConfig>,
group_id: Option<String>,
) -> Result<BrowserProfile, String> {
let profile_manager = ProfileManager::instance();
profile_manager
.create_profile_with_group(
&app_handle,
&name,
&browser,
&version,
&release_type,
proxy_id,
camoufox_config,
group_id,
)
.await
.map_err(|e| format!("Failed to create profile: {e}"))
}
#[tauri::command]
pub fn list_browser_profiles() -> Result<Vec<BrowserProfile>, String> {
let profile_manager = ProfileManager::instance();
profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))
}
#[tauri::command]
pub async fn update_profile_proxy(
app_handle: tauri::AppHandle,
profile_id: String,
proxy_id: Option<String>,
) -> Result<BrowserProfile, String> {
let profile_manager = ProfileManager::instance();
profile_manager
.update_profile_proxy(app_handle, &profile_id, proxy_id)
.await
.map_err(|e| format!("Failed to update profile: {e}"))
}
#[tauri::command]
pub fn update_profile_tags(
app_handle: tauri::AppHandle,
profile_id: String,
tags: Vec<String>,
) -> Result<BrowserProfile, String> {
let profile_manager = ProfileManager::instance();
profile_manager
.update_profile_tags(&app_handle, &profile_id, tags)
.map_err(|e| format!("Failed to update profile tags: {e}"))
}
#[tauri::command]
pub async fn check_browser_status(
app_handle: tauri::AppHandle,
profile: BrowserProfile,
) -> Result<bool, String> {
let profile_manager = ProfileManager::instance();
profile_manager
.check_browser_status(app_handle, &profile)
.await
.map_err(|e| format!("Failed to check browser status: {e}"))
}
#[tauri::command]
pub fn rename_profile(
app_handle: tauri::AppHandle,
profile_id: String,
new_name: String,
) -> Result<BrowserProfile, String> {
let profile_manager = ProfileManager::instance();
profile_manager
.rename_profile(&app_handle, &profile_id, &new_name)
.map_err(|e| format!("Failed to rename profile: {e}"))
}
#[allow(clippy::too_many_arguments)]
#[tauri::command]
pub async fn create_browser_profile_new(
app_handle: tauri::AppHandle,
name: String,
browser_str: String,
version: String,
release_type: String,
proxy_id: Option<String>,
camoufox_config: Option<CamoufoxConfig>,
group_id: Option<String>,
) -> Result<BrowserProfile, String> {
let browser_type =
BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?;
create_browser_profile_with_group(
app_handle,
name,
browser_type.as_str().to_string(),
version,
release_type,
proxy_id,
camoufox_config,
group_id,
)
.await
}
#[tauri::command]
pub async fn update_camoufox_config(
app_handle: tauri::AppHandle,
profile_id: String,
config: CamoufoxConfig,
) -> Result<(), String> {
let profile_manager = ProfileManager::instance();
profile_manager
.update_camoufox_config(app_handle, &profile_id, config)
.await
.map_err(|e| format!("Failed to update Camoufox config: {e}"))
}
// Global singleton instance
#[tauri::command]
pub fn delete_profile(app_handle: tauri::AppHandle, profile_id: String) -> Result<(), String> {
ProfileManager::instance()
.delete_profile(&app_handle, &profile_id)
.map_err(|e| format!("Failed to delete profile: {e}"))
}
lazy_static::lazy_static! {
static ref PROFILE_MANAGER: ProfileManager = ProfileManager::new();
}
+1 -1
View File
@@ -1,4 +1,4 @@
use crate::camoufox::CamoufoxConfig;
use crate::camoufox_manager::CamoufoxConfig;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
+12 -6
View File
@@ -5,7 +5,8 @@ use std::fs::{self, create_dir_all};
use std::path::{Path, PathBuf};
use crate::browser::BrowserType;
use crate::browser_runner::BrowserRunner;
use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry;
use crate::profile::ProfileManager;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DetectedProfile {
@@ -17,12 +18,16 @@ pub struct DetectedProfile {
pub struct ProfileImporter {
base_dirs: BaseDirs,
downloaded_browsers_registry: &'static DownloadedBrowsersRegistry,
profile_manager: &'static ProfileManager,
}
impl ProfileImporter {
fn new() -> Self {
Self {
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
downloaded_browsers_registry: DownloadedBrowsersRegistry::instance(),
profile_manager: ProfileManager::instance(),
}
}
@@ -520,7 +525,7 @@ impl ProfileImporter {
.map_err(|_| format!("Invalid browser type: {browser_type}"))?;
// Check if a profile with this name already exists
let existing_profiles = BrowserRunner::instance().list_profiles()?;
let existing_profiles = self.profile_manager.list_profiles()?;
if existing_profiles
.iter()
.any(|p| p.name.to_lowercase() == new_profile_name.to_lowercase())
@@ -530,7 +535,7 @@ impl ProfileImporter {
// Generate UUID for new profile and create the directory structure
let profile_id = uuid::Uuid::new_v4();
let profiles_dir = BrowserRunner::instance().get_profiles_dir();
let profiles_dir = self.profile_manager.get_profiles_dir();
let new_profile_uuid_dir = profiles_dir.join(profile_id.to_string());
let new_profile_data_dir = new_profile_uuid_dir.join("profile");
@@ -559,7 +564,7 @@ impl ProfileImporter {
};
// Save the profile metadata
BrowserRunner::instance().save_profile(&profile)?;
self.profile_manager.save_profile(&profile)?;
println!(
"Successfully imported profile '{}' from '{}'",
@@ -576,8 +581,9 @@ impl ProfileImporter {
browser_type: &str,
) -> Result<String, Box<dyn std::error::Error>> {
// Check if any version of the browser is downloaded
let registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::instance();
let downloaded_versions = registry.get_downloaded_versions(browser_type);
let downloaded_versions = self
.downloaded_browsers_registry
.get_downloaded_versions(browser_type);
if let Some(version) = downloaded_versions.first() {
return Ok(version.clone());
-51
View File
@@ -3,9 +3,6 @@ use serde::{Deserialize, Serialize};
use std::fs::{self, create_dir_all};
use std::path::PathBuf;
use crate::api_client::ApiClient;
use crate::version_updater;
use aes_gcm::{
aead::{Aead, AeadCore, KeyInit, OsRng},
Aes256Gcm, Key, Nonce,
@@ -442,54 +439,6 @@ pub async fn save_table_sorting_settings(sorting: TableSortingSettings) -> Resul
.map_err(|e| format!("Failed to save table sorting settings: {e}"))
}
#[tauri::command]
pub async fn clear_all_version_cache_and_refetch(
app_handle: tauri::AppHandle,
) -> Result<(), String> {
let api_client = ApiClient::instance();
// Clear all cache first
api_client
.clear_all_cache()
.map_err(|e| format!("Failed to clear version cache: {e}"))?;
// Disable all browsers during the update process
let auto_updater = crate::auto_updater::AutoUpdater::instance();
let supported_browsers =
crate::browser_version_manager::BrowserVersionManager::instance().get_supported_browsers();
// Load current state and disable all browsers
let mut state = auto_updater
.load_auto_update_state()
.map_err(|e| format!("Failed to load auto update state: {e}"))?;
for browser in &supported_browsers {
state.disabled_browsers.insert(browser.clone());
}
auto_updater
.save_auto_update_state(&state)
.map_err(|e| format!("Failed to save auto update state: {e}"))?;
let updater = version_updater::get_version_updater();
let updater_guard = updater.lock().await;
let result = updater_guard
.trigger_manual_update(&app_handle)
.await
.map_err(|e| format!("Failed to trigger version update: {e}"));
// Re-enable all browsers after the update completes (regardless of success/failure)
let mut final_state = auto_updater.load_auto_update_state().unwrap_or_default();
for browser in &supported_browsers {
final_state.disabled_browsers.remove(browser);
}
if let Err(e) = auto_updater.save_auto_update_state(&final_state) {
eprintln!("Warning: Failed to re-enable browsers after cache clear: {e}");
}
result?;
Ok(())
}
// Global singleton instance
lazy_static::lazy_static! {
static ref SETTINGS_MANAGER: SettingsManager = SettingsManager::new();
+8
View File
@@ -101,6 +101,14 @@ impl TagManager {
}
}
#[tauri::command]
pub fn get_all_tags() -> Result<Vec<String>, String> {
let tag_manager = crate::tag_manager::TAG_MANAGER.lock().unwrap();
tag_manager
.get_all_tags()
.map_err(|e| format!("Failed to get tags: {e}"))
}
lazy_static::lazy_static! {
pub static ref TAG_MANAGER: std::sync::Mutex<TagManager> = std::sync::Mutex::new(TagManager::new());
}
+66 -5
View File
@@ -46,8 +46,9 @@ impl Default for BackgroundUpdateState {
}
}
/// Extension of auto_updater.rs for background updates
pub struct VersionUpdater {
version_service: &'static BrowserVersionManager,
browser_version_manager: &'static BrowserVersionManager,
auto_updater: &'static AutoUpdater,
app_handle: Option<tauri::AppHandle>,
}
@@ -55,7 +56,7 @@ pub struct VersionUpdater {
impl VersionUpdater {
pub fn new() -> Self {
Self {
version_service: BrowserVersionManager::instance(),
browser_version_manager: BrowserVersionManager::instance(),
auto_updater: AutoUpdater::instance(),
app_handle: None,
}
@@ -263,7 +264,7 @@ impl VersionUpdater {
&self,
app_handle: &tauri::AppHandle,
) -> Result<Vec<BackgroundUpdateResult>, Box<dyn std::error::Error + Send + Sync>> {
let supported_browsers = self.version_service.get_supported_browsers();
let supported_browsers = self.browser_version_manager.get_supported_browsers();
let total_browsers = supported_browsers.len();
let mut results = Vec::new();
let mut total_new_versions = 0;
@@ -374,7 +375,7 @@ impl VersionUpdater {
browser: &str,
) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
self
.version_service
.browser_version_manager
.update_browser_versions_incrementally(browser)
.await
}
@@ -455,6 +456,63 @@ pub async fn get_version_update_status() -> Result<(Option<u64>, u64), String> {
Ok((last_update, time_until_next))
}
#[tauri::command]
pub async fn clear_all_version_cache_and_refetch(
app_handle: tauri::AppHandle,
) -> Result<(), String> {
let api_client = crate::api_client::ApiClient::instance();
let version_updater = VersionUpdater::new();
// Clear all cache first
api_client
.clear_all_cache()
.map_err(|e| format!("Failed to clear version cache: {e}"))?;
// Disable all browsers during the update process
let supported_browsers = version_updater
.browser_version_manager
.get_supported_browsers();
// Load current state and disable all browsers
let mut state = version_updater
.auto_updater
.load_auto_update_state()
.map_err(|e| format!("Failed to load auto update state: {e}"))?;
for browser in &supported_browsers {
state.disabled_browsers.insert(browser.clone());
}
version_updater
.auto_updater
.save_auto_update_state(&state)
.map_err(|e| format!("Failed to save auto update state: {e}"))?;
let updater = get_version_updater();
let updater_guard = updater.lock().await;
let result = updater_guard
.trigger_manual_update(&app_handle)
.await
.map_err(|e| format!("Failed to trigger version update: {e}"));
// Re-enable all browsers after the update completes (regardless of success/failure)
let mut final_state = version_updater
.auto_updater
.load_auto_update_state()
.unwrap_or_default();
for browser in &supported_browsers {
final_state.disabled_browsers.remove(browser);
}
if let Err(e) = version_updater
.auto_updater
.save_auto_update_state(&final_state)
{
eprintln!("Warning: Failed to re-enable browsers after cache clear: {e}");
}
result?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -607,7 +665,10 @@ mod tests {
// Should have valid references to services
assert!(
!std::ptr::eq(updater.version_service as *const _, std::ptr::null()),
!std::ptr::eq(
updater.browser_version_manager as *const _,
std::ptr::null()
),
"Version service should not be null"
);
assert!(
+5 -9
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut Browser",
"version": "0.12.1",
"version": "0.12.3",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm dev",
@@ -41,16 +41,12 @@
},
"linux": {
"deb": {
"depends": ["xdg-utils"],
"files": {
"/usr/share/applications/donutbrowser.desktop": "donutbrowser.desktop"
}
"desktopTemplate": "donutbrowser.desktop",
"depends": ["xdg-utils"]
},
"rpm": {
"depends": ["xdg-utils"],
"files": {
"/usr/share/applications/donutbrowser.desktop": "donutbrowser.desktop"
}
"desktopTemplate": "donutbrowser.desktop",
"depends": ["xdg-utils"]
},
"appimage": {
"files": {
@@ -808,6 +808,23 @@ export function SharedCamoufoxConfigForm({
</div>
</div>
</div>
{/* Browser Behavior */}
{/* <div className="space-y-3">
<Label>Browser Behavior</Label>
<div className="flex items-center space-x-2">
<Checkbox
id="allow-addon-new-tab"
checked={fingerprintConfig.allowAddonNewTab}
onCheckedChange={(checked) =>
updateFingerprintConfig("allowAddonNewTab", checked)
}
/>
<Label htmlFor="allow-addon-new-tab">
Allow browser addons to open new tabs automatically
</Label>
</div>
</div> */}
</div>
);
+3
View File
@@ -87,6 +87,9 @@ export interface CamoufoxConfig {
// Extended interface for the advanced fingerprint configuration
export interface CamoufoxFingerprintConfig {
// Browser behavior
allowAddonNewTab?: boolean;
// Navigator properties
"navigator.userAgent"?: string;
"navigator.appVersion"?: string;
+1 -1
View File
@@ -31,5 +31,5 @@
"next-env.d.ts",
"dist/types/**/*.ts"
],
"exclude": ["node_modules", "nodecar"]
"exclude": ["node_modules", "nodecar", "src-tauri/target"]
}