Compare commits

...

523 Commits

Author SHA1 Message Date
zhom 1d71729c9e style: copy 2025-08-13 11:20:03 +04:00
zhom a14da3d2f0 refactor: better proxy handling 2025-08-13 11:19:31 +04:00
zhom 59c69c44a1 chore: version bump 2025-08-13 10:55:05 +04:00
zhom 025523d0d3 style: warmup ui 2025-08-13 10:49:53 +04:00
zhom 76d17df281 style: warmup 2025-08-13 10:22:04 +04:00
zhom 727fa51a64 refactor: increase warmup timeout 2025-08-13 10:21:15 +04:00
zhom 80305ef903 refactor: try to supress all firefox update and default browser dialogs 2025-08-13 10:13:15 +04:00
zhom 4d98606f28 chore: linting 2025-08-13 10:10:44 +04:00
zhom c2d083a10d fix: properly warmup nodecar 2025-08-13 10:03:53 +04:00
zhom 6d1d15d366 refactor: block the ui while nodecar is initializing 2025-08-13 09:54:30 +04:00
zhom 2b2c855679 refactor: always point to local proxy 2025-08-13 09:53:31 +04:00
zhom e80043167f test: group manager linting 2025-08-13 09:51:09 +04:00
zhom 2ee3a90e25 refactor: only delete browser archive on successful browser download 2025-08-13 09:44:38 +04:00
zhom 231ac3f26c chore: remove dead code 2025-08-13 09:43:59 +04:00
zhom 41c02c539f refactor allow overriding groups data dir 2025-08-13 09:38:09 +04:00
zhom ec78787079 chore: linting 2025-08-13 09:35:29 +04:00
zhom 7fc6f985dd chore: disable doctest 2025-08-13 09:34:20 +04:00
zhom 5814f00f3d style: toast colors 2025-08-13 09:33:56 +04:00
zhom 621a2dd0a1 refactor: update proxy ui 2025-08-13 09:32:45 +04:00
zhom 3564762872 chore: switch to pnpm 10.14.0 2025-08-13 09:26:39 +04:00
zhom b12d3af3bd style: icons have monotonic color 2025-08-13 09:24:05 +04:00
zhom 32e70a5943 test: prevent concurrency issues 2025-08-13 09:15:46 +04:00
zhom 8b8ba31cce chore: cleanup 2025-08-13 09:08:57 +04:00
zhom 201e0270c7 refactor: display group even if it has no items 2025-08-13 09:07:46 +04:00
zhom ceb2eec80e refactor: don't close dialog on drag 2025-08-13 09:03:24 +04:00
zhom f2b3b2cc69 refactor: better process handling on macos 2025-08-13 08:48:27 +04:00
zhom 8ac077d81b chore: formatting 2025-08-13 08:35:58 +04:00
zhom dab5ab5805 style: copy 2025-08-13 08:35:22 +04:00
zhom 83a7c0e394 refactor: better profile deletion handling 2025-08-13 08:26:24 +04:00
zhom f622c77a3e chore: update entitlements 2025-08-12 19:05:04 +04:00
zhom 9af33efb08 chore: version bump 2025-08-12 12:21:33 +04:00
zhom 90cdf34e2b chore: linting 2025-08-12 12:21:20 +04:00
zhom bc2cbffcf4 chore: linting 2025-08-12 12:20:07 +04:00
zhom 341a461abf refactor: show deprecated profile names 2025-08-12 12:18:38 +04:00
zhom 69b7963dd4 build: switch back to ubuntu 22 2025-08-12 07:46:13 +04:00
zhom c1079cf7b1 docs: readme 2025-08-11 13:34:36 +04:00
zhom 1e3f1d4668 chore: version bump 2025-08-11 13:22:26 +04:00
zhom bb62ca350c refactor: better file path handling 2025-08-11 13:22:04 +04:00
zhom 1281fb3955 refactor: render window dragger above toast 2025-08-11 12:03:14 +04:00
zhom ac878aed48 refactor: pass custom executable path 2025-08-11 12:02:52 +04:00
zhom 140621dcbe refactor: make the geoip download toast the same as regular download toast 2025-08-11 09:59:36 +04:00
zhom 55b8955a20 refactor: fallback to firefox directory 2025-08-11 08:35:02 +04:00
zhom 1611c8e536 chore: add margin to creation button 2025-08-11 08:30:07 +04:00
zhom c17bb56fec chore: pnpm update 2025-08-11 08:27:24 +04:00
zhom cea8030268 chore: formatting 2025-08-11 06:17:52 +04:00
zhom d9e3e1f3ef chore: linting 2025-08-11 06:06:10 +04:00
zhom d48e26c7eb refactor: cleanup and better brave release fetching 2025-08-11 05:42:17 +04:00
zhom b7b75ec3d8 chore: formatting 2025-08-10 15:31:56 +04:00
zhom 4a8b0bd407 refactor: better continued download handling 2025-08-10 14:53:35 +04:00
zhom 8f24410f11 refactor: better camoufox creationg handling 2025-08-10 14:49:01 +04:00
zhom ff4aa572fe chore: authenticate gh in ai summary flow 2025-08-10 14:30:24 +04:00
zhom 0abea50279 chore: remove depricated props from greetings workflow 2025-08-10 14:29:47 +04:00
zhom 1f90b12fe5 refactor: simplify system theme detection 2025-08-10 14:10:06 +04:00
zhom bc0c31f527 fix: put valid categories for linux x64 2025-08-10 13:36:56 +04:00
zhom 357499168f build: add --verbose flag to the build command 2025-08-10 12:50:56 +04:00
zhom 7b1311f2ca build: fully switch from ubuntu 22 2025-08-10 10:28:50 +04:00
zhom d755978b34 chore: pnpm update 2025-08-10 07:54:28 +04:00
zhom bb164ce743 build: switch to ubuntu 24 on linux x64 build 2025-08-10 07:28:43 +04:00
zhom daa36f008b refactor: disable mullvad and tor profile creation as well as nightly releases 2025-08-10 06:57:45 +04:00
zhom 92ef2798d2 feat: add min height and width for camoufox 2025-08-10 04:46:20 +04:00
zhom a9720676ae test: cleanup 2025-08-10 04:14:43 +04:00
zhom fbcec2cbc1 Merge pull request #65 from zhom/dependabot/cargo/src-tauri/rust-dependencies-9c91849198
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 6 updates
2025-08-09 14:35:38 +04:00
zhom 5d395f606e Merge pull request #64 from zhom/dependabot/github_actions/github-actions-8d3d32b5fa
ci(deps): bump the github-actions group with 3 updates
2025-08-09 14:35:28 +04:00
dependabot[bot] 6963e07be5 deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [bytemuck](https://github.com/Lokathor/bytemuck) | `1.23.1` | `1.23.2` |
| [camino](https://github.com/camino-rs/camino) | `1.1.10` | `1.1.11` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.31` | `1.2.32` |
| [rustversion](https://github.com/dtolnay/rustversion) | `1.0.21` | `1.0.22` |
| [slab](https://github.com/tokio-rs/slab) | `0.4.10` | `0.4.11` |
| [tauri-winres](https://github.com/tauri-apps/winres) | `0.3.2` | `0.3.3` |


Updates `bytemuck` from 1.23.1 to 1.23.2
- [Changelog](https://github.com/Lokathor/bytemuck/blob/main/changelog.md)
- [Commits](https://github.com/Lokathor/bytemuck/compare/v1.23.1...v1.23.2)

Updates `camino` from 1.1.10 to 1.1.11
- [Release notes](https://github.com/camino-rs/camino/releases)
- [Changelog](https://github.com/camino-rs/camino/blob/main/CHANGELOG.md)
- [Commits](https://github.com/camino-rs/camino/compare/camino-1.1.10...camino-1.1.11)

Updates `cc` from 1.2.31 to 1.2.32
- [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.31...cc-v1.2.32)

Updates `rustversion` from 1.0.21 to 1.0.22
- [Release notes](https://github.com/dtolnay/rustversion/releases)
- [Commits](https://github.com/dtolnay/rustversion/compare/1.0.21...1.0.22)

Updates `slab` from 0.4.10 to 0.4.11
- [Release notes](https://github.com/tokio-rs/slab/releases)
- [Changelog](https://github.com/tokio-rs/slab/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/slab/compare/v0.4.10...v0.4.11)

Updates `tauri-winres` from 0.3.2 to 0.3.3
- [Release notes](https://github.com/tauri-apps/winres/releases)
- [Changelog](https://github.com/tauri-apps/winres/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tauri-apps/winres/compare/winres-v0.3.2...winres-v0.3.3)

---
updated-dependencies:
- dependency-name: bytemuck
  dependency-version: 1.23.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: camino
  dependency-version: 1.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.32
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rustversion
  dependency-version: 1.0.22
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: slab
  dependency-version: 0.4.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tauri-winres
  dependency-version: 0.3.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-09 10:26:41 +00:00
dependabot[bot] c28537d304 ci(deps): bump the github-actions group with 3 updates
Bumps the github-actions group with 3 updates: [ridedott/merge-me-action](https://github.com/ridedott/merge-me-action), [actions/ai-inference](https://github.com/actions/ai-inference) and [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `ridedott/merge-me-action` from 2.10.123 to 2.10.124
- [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/d288b479e76cb993344ca8b5e0fcaa7d6e667eed...f96a67511b4be051e77760230e6a3fb9cb7b1903)

Updates `actions/ai-inference` from 1.2.3 to 1.2.8
- [Release notes](https://github.com/actions/ai-inference/releases)
- [Commits](https://github.com/actions/ai-inference/compare/9693b137b6566bb66055a713613bf4f0493701eb...b81b2afb8390ee6839b494a404766bef6493c7d9)

Updates `crate-ci/typos` from 1.34.0 to 1.35.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/392b78fe18a52790c53f42456e46124f77346842...52bd719c2c91f9d676e2aa359fc8e0db8925e6d8)

---
updated-dependencies:
- dependency-name: ridedott/merge-me-action
  dependency-version: 2.10.124
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/ai-inference
  dependency-version: 1.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: crate-ci/typos
  dependency-version: 1.35.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-09 09:52:33 +00:00
zhom c303de4a8a Merge pull request #63 from zhom/dependabot/npm_and_yarn/frontend-dependencies-9f5034d2db
deps(deps): bump the frontend-dependencies group with 14 updates
2025-08-09 13:50:04 +04:00
dependabot[bot] 21a13fb217 deps(deps): bump the frontend-dependencies group with 14 updates
Bumps the frontend-dependencies group with 14 updates:

| Package | From | To |
| --- | --- | --- |
| [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.3` | `2.1.4` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.2.0` | `24.2.1` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `4.7.0` | `5.0.0` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `16.1.4` | `16.1.5` |
| [tmp](https://github.com/raszi/node-tmp) | `0.2.4` | `0.2.5` |
| [@biomejs/cli-darwin-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.3` | `2.1.4` |
| [@biomejs/cli-darwin-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.3` | `2.1.4` |
| [@biomejs/cli-linux-arm64-musl](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.3` | `2.1.4` |
| [@biomejs/cli-linux-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.3` | `2.1.4` |
| [@biomejs/cli-linux-x64-musl](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.3` | `2.1.4` |
| [@biomejs/cli-linux-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.3` | `2.1.4` |
| [@biomejs/cli-win32-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.3` | `2.1.4` |
| [@biomejs/cli-win32-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.3` | `2.1.4` |
| [@rolldown/pluginutils](https://github.com/rolldown/rolldown/tree/HEAD/packages/pluginutils) | `1.0.0-beta.27` | `1.0.0-beta.30` |


Updates `@biomejs/biome` from 2.1.3 to 2.1.4
- [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.1.4/packages/@biomejs/biome)

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

Updates `@vitejs/plugin-react` from 4.7.0 to 5.0.0
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.0.0/packages/plugin-react)

Updates `lint-staged` from 16.1.4 to 16.1.5
- [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.4...v16.1.5)

Updates `tmp` from 0.2.4 to 0.2.5
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.4...v0.2.5)

Updates `@biomejs/cli-darwin-arm64` from 2.1.3 to 2.1.4
- [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.1.4/packages/@biomejs/biome)

Updates `@biomejs/cli-darwin-x64` from 2.1.3 to 2.1.4
- [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.1.4/packages/@biomejs/biome)

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

Updates `@biomejs/cli-linux-arm64` from 2.1.3 to 2.1.4
- [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.1.4/packages/@biomejs/biome)

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

Updates `@biomejs/cli-linux-x64` from 2.1.3 to 2.1.4
- [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.1.4/packages/@biomejs/biome)

Updates `@biomejs/cli-win32-arm64` from 2.1.3 to 2.1.4
- [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.1.4/packages/@biomejs/biome)

Updates `@biomejs/cli-win32-x64` from 2.1.3 to 2.1.4
- [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.1.4/packages/@biomejs/biome)

Updates `@rolldown/pluginutils` from 1.0.0-beta.27 to 1.0.0-beta.30
- [Release notes](https://github.com/rolldown/rolldown/releases)
- [Changelog](https://github.com/rolldown/rolldown/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rolldown/rolldown/commits/v1.0.0-beta.30/packages/pluginutils)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@types/node"
  dependency-version: 24.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: lint-staged
  dependency-version: 16.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: tmp
  dependency-version: 0.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-arm64"
  dependency-version: 2.1.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-x64"
  dependency-version: 2.1.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64-musl"
  dependency-version: 2.1.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64"
  dependency-version: 2.1.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64-musl"
  dependency-version: 2.1.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64"
  dependency-version: 2.1.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-arm64"
  dependency-version: 2.1.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-x64"
  dependency-version: 2.1.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rolldown/pluginutils"
  dependency-version: 1.0.0-beta.30
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-09 09:22:34 +00:00
zhom 90d8e782de refactor: handle missing mmdb 2025-08-09 12:42:46 +04:00
zhom ad2d9b73f2 build: skip stripping on linux x64 2025-08-09 10:47:17 +04:00
zhom e645e212f2 chore: formatting 2025-08-09 10:42:32 +04:00
zhom 7df92ae8ee refactor: allow for manual browser install 2025-08-09 10:42:22 +04:00
zhom 18a0254ca7 refactor: update nodecar ping 2025-08-09 10:22:51 +04:00
zhom b0a58c3131 test: add basic url discovery coverage 2025-08-09 08:58:27 +04:00
zhom 467e82ca93 refactor: check for camoufox inside install dir directly 2025-08-09 08:51:36 +04:00
zhom 8d9654044a refactor: better appimage handling 2025-08-09 08:45:53 +04:00
zhom 711d94c9c1 refactor: dismiss download toast on error 2025-08-09 08:45:20 +04:00
zhom 305051d03d style: display proxy usage 2025-08-09 08:44:34 +04:00
zhom 8695884535 refactor: handle downloads with poor connectivity 2025-08-08 23:52:12 +04:00
zhom bb96936550 refactor: cleanup zip extraction, fail instead of skipping files 2025-08-08 23:51:40 +04:00
zhom 730621e5a1 rename service to manager 2025-08-08 15:39:20 +04:00
zhom 714102eb25 style: make badge darker on light theme 2025-08-08 15:26:51 +04:00
zhom e5378c1bb7 style: update group badge colors 2025-08-08 15:15:17 +04:00
zhom b7c8d5672a chore: remove unused test 2025-08-08 15:10:53 +04:00
zhom 3fb81e2ca0 style: make main window more flat 2025-08-08 15:09:13 +04:00
zhom 510de96393 style: consistent flow for no proxies and no groups 2025-08-08 15:08:56 +04:00
zhom 3688e88d67 style: darker notifications 2025-08-08 15:07:30 +04:00
zhom 9646a41788 chore: linting 2025-08-08 14:57:14 +04:00
zhom f7679d25ca chore: linting 2025-08-08 14:38:49 +04:00
zhom 2d42772718 chore: linting 2025-08-08 14:23:51 +04:00
zhom 3980f835d6 chore: remove unused command 2025-08-08 14:09:20 +04:00
zhom d0185dd5ae fix: pass proper type to starts_with 2025-08-08 13:24:03 +04:00
zhom de39fa4555 chore: formatting 2025-08-08 11:06:48 +04:00
zhom f773eb6f1c refactor: handle auto-update on linux 2025-08-08 11:05:11 +04:00
zhom 458c30433d chore: linting 2025-08-08 10:50:44 +04:00
zhom 1cb9ffa249 refactor: switch to ripple button 2025-08-08 10:46:00 +04:00
zhom 5c58b5c644 chore: linting 2025-08-08 10:25:44 +04:00
zhom f41311a7bb refactor: disable profile actions when it is launching or stopping 2025-08-08 10:15:38 +04:00
zhom c8c09c296e style: show anti detect profile creation form by default 2025-08-08 10:12:41 +04:00
zhom ca0c2614f4 style: remove Actions dropdown title 2025-08-08 10:11:21 +04:00
zhom dca5a2970e style: copy 2025-08-08 10:10:24 +04:00
zhom e0a1dd5a8a refactor: more robust zip extraction and handle invalid characters 2025-08-08 10:09:43 +04:00
zhom e48b681215 refactor: better error handling for browser download 2025-08-08 09:50:26 +04:00
zhom 6796912606 test: properly mock api requests 2025-08-08 07:54:11 +04:00
zhom 7105f6544f test: cross-platform binary check 2025-08-08 07:10:27 +04:00
zhom 3003f868e7 chore: formatting 2025-08-08 06:42:36 +04:00
zhom b733d26f10 chore: linting 2025-08-08 06:33:49 +04:00
zhom 675c2417d7 chore: linting 2025-08-08 06:23:49 +04:00
zhom e10a7bf089 refactor: migrate from external commands to rust crates for extraction 2025-08-08 05:48:42 +04:00
zhom c8e3cd39ff build: don't fail fast 2025-08-08 05:16:40 +04:00
zhom 0103150dc7 build: run rust linting on ubuntu 2025-08-08 05:14:08 +04:00
zhom be57ac3219 chore: don't format md files 2025-08-08 02:51:22 +04:00
zhom b4067b5e34 build: remove file_pattern property from changelog creator 2025-08-07 23:17:15 +04:00
zhom 3fa8822139 build: disable camoufox installation 2025-08-07 23:10:06 +04:00
zhom 1e0ef0b497 Merge pull request #61 from zhom/contributors-readme-action-eImXtNP1X9
docs(contributor): contributors readme action update
2025-08-07 09:36:05 +04:00
github-actions[bot] 2da832f100 docs(contributor): contrib-readme-action has updated readme 2025-08-07 04:10:57 +00:00
zhom 284dbc5a3b chore: version bump 2025-08-07 08:10:41 +04:00
zhom f328ceeb4f chore: copy 2025-08-07 08:08:56 +04:00
zhom 1bfbb365c5 chore: version bump 2025-08-07 07:23:25 +04:00
zhom 1a15af1ded refactor: disable proxy is profile is running 2025-08-07 07:22:42 +04:00
zhom ca20ccb489 test: disable camoufox config tests while CI is rate limited 2025-08-07 07:17:58 +04:00
zhom 0daba2eb9b Merge pull request #60 from zhom/contributors-readme-action-8cfPH1JcS_
docs(contributor): contributors readme action update
2025-08-07 06:56:32 +04:00
github-actions[bot] 1dfdfc6a21 docs(contributor): contrib-readme-action has updated readme 2025-08-07 02:43:06 +00:00
zhom ee2f728194 docs: readme 2025-08-07 06:23:18 +04:00
zhom 702c1545bf chore: version bump 2025-08-07 06:09:06 +04:00
zhom 53f403b82c refactor: don't allow the user to create profile while browser is downloading 2025-08-07 06:06:57 +04:00
zhom 2cae2824d3 refactor: increase cleanup interval 2025-08-07 05:39:29 +04:00
zhom ca790c74ce chore: update dependencies 2025-08-07 05:21:26 +04:00
zhom 25d4b30975 build: run camoufox fetch in rust lint 2025-08-07 05:07:09 +04:00
zhom 687fc7817f chore: dependency update 2025-08-07 05:03:30 +04:00
zhom 28818bed77 refactor: simplify app update toast 2025-08-07 04:52:28 +04:00
zhom d1d953b3e2 chore: update dependencies 2025-08-07 04:25:11 +04:00
zhom c2153d463c style: remove alert colors 2025-08-07 04:21:37 +04:00
zhom fa142a8cb0 refactor: don\'t allow camoufox to open links inside running instances 2025-08-07 04:17:01 +04:00
zhom cf291fb0d1 refactor: ensure camoufox has persistent context and can handle link openings 2025-08-07 04:16:06 +04:00
zhom 5fed6b7c3f refactor: create profile in the currently selected group 2025-08-07 04:15:31 +04:00
zhom 9ba51cd4e3 refactor: show loading state only on initial load 2025-08-07 04:12:47 +04:00
zhom a507a3daed refactor: ensure that camoufox profile always launch with persistent state 2025-08-07 02:25:06 +04:00
zhom aabae8d3d4 docs: update preview 2025-08-07 02:11:37 +04:00
zhom b7e6c1eb84 style: show cursor pointer for dropdown item menu 2025-08-07 02:07:20 +04:00
zhom d05f2190e8 refactor: ensure that only profiles without group are shown in the default list 2025-08-07 01:25:26 +04:00
zhom 34a9418474 refactor: don't allow user to assign running profile to group 2025-08-07 01:16:47 +04:00
zhom 63e125738d refactor: force ui refreshe after group changes 2025-08-07 01:11:19 +04:00
zhom 58d82d12c4 test: cleanup 2025-08-07 01:09:12 +04:00
zhom b1c86709b0 test: remove timeout for nodecar 2025-08-07 01:04:30 +04:00
zhom 12651f9f85 refactor: don't show options for camoufox list 2025-08-07 00:48:55 +04:00
zhom c1815fdfdc refactor: don't show logs on camoufox process termination 2025-08-07 00:45:36 +04:00
zhom 1662c1efba refactor: display storage errors for nodecar in json 2025-08-07 00:45:04 +04:00
zhom 4a8e905a44 refactor: show progress toast for manual cache update 2025-08-07 00:40:45 +04:00
zhom e165e35f2c style: add cursor pointer to interactive elements 2025-08-06 23:40:23 +04:00
zhom cbeb099cc9 style: don't show proxy address into profile creation dialog 2025-08-06 23:29:24 +04:00
zhom fef0c963cb refactor: don't show logs on process termination 2025-08-06 23:28:03 +04:00
zhom cd28531588 refactor: don't show settings on startup 2025-08-06 23:27:01 +04:00
zhom ed913309dd style: copy 2025-08-06 22:18:42 +04:00
zhom cecb4579c7 test: only cleanup test related instances 2025-08-06 22:13:32 +04:00
zhom 9cfed6d73e fix: properly pass proxy to camoufox 2025-08-06 22:13:19 +04:00
zhom a461fd4798 checkpoint 2025-08-06 20:43:14 +04:00
zhom 5159f943df refactor: ensure that upstream proxy is always used for fingerprint generation during profile creation 2025-08-06 06:45:16 +04:00
zhom 72af5f682f style: add margin to checkbox 2025-08-06 06:44:33 +04:00
zhom 6d77c872f2 refactor: only perform geoip lookup if the required flag is passed 2025-08-06 06:33:21 +04:00
zhom 0baecbdb0c style: copy 2025-08-06 06:32:37 +04:00
zhom eb2af5c10b test: increase timeouts 2025-08-06 06:28:36 +04:00
zhom 76cef4757a chore: cleanup 2025-08-06 05:29:39 +04:00
zhom 00d74bddaf style: treat timezone as a text field 2025-08-06 05:29:32 +04:00
zhom b5b08a0196 feat: fully implement happy flow for persistant fingerprint generation 2025-08-06 04:33:01 +04:00
zhom ff35717cb5 style: disable pointer events for toasts 2025-08-05 06:57:18 +04:00
zhom 669611ec68 chore: remove legacy profile migration functionality 2025-08-05 06:08:27 +04:00
zhom 8f1b84f615 refactor: updating firefox default preferences 2025-08-05 06:03:48 +04:00
zhom 2bf6531767 build: remove secret inheretance for dependabot 2025-08-04 14:55:40 +04:00
zhom da0af075fc feat: automatically set max user screen height and width during profile creation 2025-08-04 07:14:23 +04:00
zhom 83f4c2c162 refactor: block selection if the profile is launching or stopping 2025-08-04 07:02:38 +04:00
zhom e675441171 refactor: hide download info for in create profile dialog 2025-08-04 06:43:28 +04:00
zhom cf77d96042 refactor: move useBrowserState to its own file 2025-08-04 06:31:13 +04:00
zhom a4706a7f9a refactor: better handle browser download state in the create profile dialog 2025-08-04 06:28:42 +04:00
zhom b088ae675b feat: finalize camoufox integration 2025-08-03 14:38:44 +04:00
zhom 54fd9b7282 chore: cleanup 2025-08-03 04:21:36 +04:00
zhom 77a50c60d1 refactor: launch all browsers via proxy 2025-08-03 01:08:59 +04:00
zhom 62b9768006 fix: don't block browser launch on version update 2025-08-03 00:55:34 +04:00
zhom 66d3420000 refactor: select first profile if no running profiles are available 2025-08-02 18:22:29 +04:00
zhom 8f05c48594 refactor: change the way updating state is displayed 2025-08-02 18:17:07 +04:00
zhom a5709d95c7 fix: properly track select item change for release change 2025-08-02 17:08:06 +04:00
zhom eef3e19d2f refactor: do not allow the user to select profile if it is running 2025-08-02 16:45:06 +04:00
zhom 2c57920d44 refactor: migrate to singleton pattern 2025-08-02 16:29:40 +04:00
zhom 57a36a5fc2 chore: pnpm format 2025-08-02 16:29:07 +04:00
zhom a2a980d203 style: show cursor pointer for the whole select item 2025-08-02 16:22:19 +04:00
zhom 2deacbacab style: don't show tooltip for profiles with disabled proxies 2025-08-02 16:19:23 +04:00
zhom 7cd7d077ae Merge pull request #58 from zhom/dependabot/cargo/src-tauri/rust-dependencies-654b661f8c
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 3 updates
2025-08-02 14:16:20 +04:00
zhom 8679d0ca62 Merge pull request #57 from zhom/dependabot/github_actions/github-actions-19db2dab2b
ci(deps): bump the github-actions group with 2 updates
2025-08-02 14:16:05 +04:00
zhom 20cf9de4fa Merge pull request #56 from zhom/dependabot/npm_and_yarn/frontend-dependencies-2bd6671855
deps(deps): bump the frontend-dependencies group with 11 updates
2025-08-02 14:15:46 +04:00
dependabot[bot] 4202d595f2 deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 3 updates: [serde_json](https://github.com/serde-rs/json), [tokio](https://github.com/tokio-rs/tokio) and [cc](https://github.com/rust-lang/cc-rs).


Updates `serde_json` from 1.0.141 to 1.0.142
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.141...v1.0.142)

Updates `tokio` from 1.47.0 to 1.47.1
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.47.0...tokio-1.47.1)

Updates `cc` from 1.2.30 to 1.2.31
- [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.30...cc-v1.2.31)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.142
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tokio
  dependency-version: 1.47.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.31
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 09:56:00 +00:00
dependabot[bot] 3086ea0085 ci(deps): bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [akhilmhdh/contributors-readme-action](https://github.com/akhilmhdh/contributors-readme-action) and [ridedott/merge-me-action](https://github.com/ridedott/merge-me-action).


Updates `akhilmhdh/contributors-readme-action` from 2.3.10 to 2.3.11
- [Release notes](https://github.com/akhilmhdh/contributors-readme-action/releases)
- [Commits](https://github.com/akhilmhdh/contributors-readme-action/compare/1ff4c56187458b34cd602aee93e897344ce34bfc...83ea0b4f1ac928fbfe88b9e8460a932a528eb79f)

Updates `ridedott/merge-me-action` from 2.10.122 to 2.10.123
- [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/338053c6f9b9311a6be80208f6f0723981e40627...d288b479e76cb993344ca8b5e0fcaa7d6e667eed)

---
updated-dependencies:
- dependency-name: akhilmhdh/contributors-readme-action
  dependency-version: 2.3.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: ridedott/merge-me-action
  dependency-version: 2.10.123
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 09:50:41 +00:00
dependabot[bot] 1ddbc5228c deps(deps): bump the frontend-dependencies group with 11 updates
Bumps the frontend-dependencies group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.1` | `2.1.3` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.8.3` | `5.9.2` |
| [playwright-core](https://github.com/microsoft/playwright) | `1.54.1` | `1.54.2` |
| [@biomejs/cli-darwin-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.1` | `2.1.3` |
| [@biomejs/cli-darwin-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.1` | `2.1.3` |
| [@biomejs/cli-linux-arm64-musl](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.1` | `2.1.3` |
| [@biomejs/cli-linux-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.1` | `2.1.3` |
| [@biomejs/cli-linux-x64-musl](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.1` | `2.1.3` |
| [@biomejs/cli-linux-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.1` | `2.1.3` |
| [@biomejs/cli-win32-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.1` | `2.1.3` |
| [@biomejs/cli-win32-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.1.1` | `2.1.3` |


Updates `@biomejs/biome` from 2.1.1 to 2.1.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.1.3/packages/@biomejs/biome)

Updates `typescript` from 5.8.3 to 5.9.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.8.3...v5.9.2)

Updates `playwright-core` from 1.54.1 to 1.54.2
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.54.1...v1.54.2)

Updates `@biomejs/cli-darwin-arm64` from 2.1.1 to 2.1.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.1.3/packages/@biomejs/biome)

Updates `@biomejs/cli-darwin-x64` from 2.1.1 to 2.1.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.1.3/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-arm64-musl` from 2.1.1 to 2.1.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.1.3/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-arm64` from 2.1.1 to 2.1.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.1.3/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-x64-musl` from 2.1.1 to 2.1.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.1.3/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-x64` from 2.1.1 to 2.1.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.1.3/packages/@biomejs/biome)

Updates `@biomejs/cli-win32-arm64` from 2.1.1 to 2.1.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.1.3/packages/@biomejs/biome)

Updates `@biomejs/cli-win32-x64` from 2.1.1 to 2.1.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.1.3/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: typescript
  dependency-version: 5.9.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: playwright-core
  dependency-version: 1.54.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-arm64"
  dependency-version: 2.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-x64"
  dependency-version: 2.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64-musl"
  dependency-version: 2.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64"
  dependency-version: 2.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64-musl"
  dependency-version: 2.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64"
  dependency-version: 2.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-arm64"
  dependency-version: 2.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-x64"
  dependency-version: 2.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 09:29:07 +00:00
zhom 2d02095d4d build: disable windows arm for rolling release 2025-08-01 03:52:02 +04:00
zhom 4e0d985996 chore: update dependencies 2025-08-01 03:10:34 +04:00
zhom 5af751a9b2 build: remove extension for nodecar 2025-08-01 02:49:00 +04:00
zhom da7f791274 refactor: nodecar cleanup 2025-08-01 00:58:15 +04:00
zhom f4c33ad96e fix: always disable browser while it is updating 2025-07-31 22:40:34 +04:00
zhom 5b31cfaf32 refactor: partially migrate to singleton pattern 2025-07-31 22:29:08 +04:00
zhom 4997854577 fix: don't add extension on windows when copying nodecar 2025-07-31 18:03:58 +04:00
zhom a43e41a020 docs: agents 2025-07-31 17:53:30 +04:00
zhom b22b4cacf9 fix: prevent multi-child error for selector dialog 2025-07-31 06:18:14 +04:00
zhom 7f0df6f943 build: run all tests in one step 2025-07-31 05:41:42 +04:00
zhom dccf843952 refactor: bump chromium version requirement 2025-07-31 05:31:01 +04:00
zhom fc6ddb7cbf refactor: slop cleanup 2025-07-31 05:28:45 +04:00
zhom 63000c72bd refactor: better camoufox instance tracking 2025-07-31 03:56:41 +04:00
zhom 2fd344b9bb build: enable rolling release 2025-07-28 15:52:30 +04:00
zhom 44bd34d8f0 refactor: extract profile functionality into its own module 2025-07-28 15:46:59 +04:00
zhom d3822bdd88 style: show cursor pointer on launch button hover 2025-07-28 15:39:24 +04:00
zhom ed1132bdc3 build: disable windows for rolling release 2025-07-28 06:10:20 +04:00
zhom fcae0623c0 refactor: partially migrate from launching camoufox directly to launching via playwright 2025-07-28 06:09:40 +04:00
zhom fe843e14f1 chore: require agents recompile nodecar 2025-07-28 03:43:51 +04:00
zhom b071e971b3 refactor: don't show useless tooltips 2025-07-28 03:27:38 +04:00
zhom 0b7cf547b3 feat: show profile names for bulk profile deletion 2025-07-28 02:30:52 +04:00
zhom f024ce19ae refactor: use shared trimming function 2025-07-28 02:29:45 +04:00
zhom e1d3ff9000 feat: make clicking the logo offer the user to open main page 2025-07-28 02:18:20 +04:00
zhom e2a168b188 refactor: use shared camoufox config form 2025-07-28 02:16:48 +04:00
zhom af767da32c chore: linting 2025-07-28 02:15:55 +04:00
zhom b5dfe1233e refactor: trim long browser and profile names 2025-07-28 02:15:27 +04:00
zhom dddf8e2e39 build: remove python installation step 2025-07-27 18:30:08 +04:00
zhom adcb20fab9 chore: linting 2025-07-27 03:09:22 +04:00
zhom ff9c633b07 docs: update contribution guidelines 2025-07-27 03:06:35 +04:00
zhom 7ca76b1f78 refactor: warm up nodecar 2025-07-27 02:54:11 +04:00
zhom 4887a3db4d refactor: better unused binary tracking 2025-07-27 02:53:51 +04:00
zhom e38cd2e560 test: remove time requirements for nodecar 2025-07-27 02:32:47 +04:00
zhom 7e7b47cae3 chore: warnings 2025-07-27 02:32:20 +04:00
zhom 13ae170166 test: unused command 2025-07-26 19:03:42 +04:00
zhom df78e22650 refactor: properly cleanup unused binaries and simplify downloaded browser registry 2025-07-26 19:03:42 +04:00
zhom 328e6f16ee feat: ask for confirmation before bulk delete 2025-07-26 19:03:42 +04:00
zhom 40ad32af6d feat: add profile groups 2025-07-26 19:03:41 +04:00
zhom f299eeaea5 Merge pull request #53 from zhom/dependabot/github_actions/github-actions-d4d3ad3fbf
ci(deps): bump the github-actions group with 4 updates
2025-07-26 18:55:54 +04:00
dependabot[bot] 84142caac9 ci(deps): bump the github-actions group with 4 updates
Bumps the github-actions group with 4 updates: [google/osv-scanner-action](https://github.com/google/osv-scanner-action), [actions/first-interaction](https://github.com/actions/first-interaction), [actions/ai-inference](https://github.com/actions/ai-inference) and [actions/setup-python](https://github.com/actions/setup-python).


Updates `google/osv-scanner-action` from 2.0.3 to 2.1.0
- [Release notes](https://github.com/google/osv-scanner-action/releases)
- [Commits](https://github.com/google/osv-scanner-action/compare/40a8940a65eab1544a6af759e43d936201a131a2...b00f71e051ddddc6e46a193c31c8c0bf283bf9e6)

Updates `actions/first-interaction` from 1.3.0 to 2.0.0
- [Release notes](https://github.com/actions/first-interaction/releases)
- [Commits](https://github.com/actions/first-interaction/compare/34f15e814fe48ac9312ccf29db4e74fa767cbab7...2d4393e6bc0e2efb2e48fba7e06819c3bf61ffc9)

Updates `actions/ai-inference` from 1.1.0 to 1.2.3
- [Release notes](https://github.com/actions/ai-inference/releases)
- [Commits](https://github.com/actions/ai-inference/compare/d645f067d89ee1d5d736a5990e327e504d1c5a4a...9693b137b6566bb66055a713613bf4f0493701eb)

Updates `actions/setup-python` from 5.3.0 to 5.6.0
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/0b93645e9fea7318ecaed2b359559ac225c90a2b...a26af69be951a213d495a4c3e4e4022e16d87065)

---
updated-dependencies:
- dependency-name: google/osv-scanner-action
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/first-interaction
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/ai-inference
  dependency-version: 1.2.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/setup-python
  dependency-version: 5.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-26 09:23:42 +00:00
zhom d06dbb6c70 chore: add banderole to ci 2025-07-25 11:59:17 +04:00
zhom cf5b498bd6 chore: reset pnpm lock 2025-07-25 11:36:24 +04:00
zhom 3c28a169bd test: increase timeout for nodecar initial launch 2025-07-25 11:26:41 +04:00
zhom 25653e166b refactor: share browser state logic across data table and selector dialog 2025-07-25 11:21:26 +04:00
zhom 0b4263140d refactor: switch to banderole from pkg 2025-07-25 11:18:32 +04:00
zhom b500c28b96 fix: allow user download browser if there is only nightly version available 2025-07-25 09:19:31 +04:00
zhom 7c2be81531 refactor: do not allow changing camoufox config if it is running 2025-07-25 09:18:49 +04:00
zhom b55ef469ed refactor: only check for startup urls using js tauri api 2025-07-25 09:18:02 +04:00
zhom 76a206093d chore: rename agents file 2025-07-25 09:04:30 +04:00
zhom 3e88dbc30e refactor: increase delay for ui update after profile deletion 2025-07-25 08:55:36 +04:00
zhom 031823587e refactor: camoufox rust implementation 2025-07-25 08:52:13 +04:00
zhom c7a1ac228c Merge pull request #45 from zhom/dependabot/cargo/src-tauri/rust-dependencies-1dd9e3ae47
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 16 updates
2025-07-12 10:08:23 +00:00
zhom 8ede335bed Merge pull request #44 from zhom/dependabot/npm_and_yarn/frontend-dependencies-dd443c2936
deps(deps): bump the frontend-dependencies group with 30 updates
2025-07-12 10:08:10 +00:00
dependabot[bot] b170b8846d deps(rust)(deps): bump the rust-dependencies group
---
updated-dependencies:
- dependency-name: sysinfo
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: zip
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: hyper-util
  dependency-version: 0.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: async-channel
  dependency-version: 2.5.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: blocking
  dependency-version: 1.6.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: bzip2
  dependency-version: 0.6.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.29
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: embed-resource
  dependency-version: 3.0.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: plist
  dependency-version: 1.7.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: quick-xml
  dependency-version: 0.38.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: rustls
  dependency-version: 0.23.29
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rustls-webpki
  dependency-version: 0.103.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zbus
  dependency-version: 5.8.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: zbus_macros
  dependency-version: 5.8.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: zvariant
  dependency-version: 5.6.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: zvariant_derive
  dependency-version: 5.6.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-12 10:01:32 +00:00
dependabot[bot] 632d90a022 deps(deps): bump the frontend-dependencies group with 30 updates
Bumps the frontend-dependencies group with 30 updates:

| Package | From | To |
| --- | --- | --- |
| [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.0.6` | `2.1.1` |
| [@biomejs/cli-darwin-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.0.6` | `2.1.1` |
| [@biomejs/cli-darwin-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.0.6` | `2.1.1` |
| [@biomejs/cli-linux-arm64-musl](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.0.6` | `2.1.1` |
| [@biomejs/cli-linux-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.0.6` | `2.1.1` |
| [@biomejs/cli-linux-x64-musl](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.0.6` | `2.1.1` |
| [@biomejs/cli-linux-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.0.6` | `2.1.1` |
| [@biomejs/cli-win32-arm64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.0.6` | `2.1.1` |
| [@biomejs/cli-win32-x64](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.0.6` | `2.1.1` |
| [@rollup/rollup-android-arm-eabi](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-android-arm64](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-darwin-arm64](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-darwin-x64](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-freebsd-arm64](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-freebsd-x64](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-linux-arm-gnueabihf](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-linux-arm-musleabihf](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-linux-arm64-gnu](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-linux-arm64-musl](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-linux-loongarch64-gnu](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-linux-powerpc64le-gnu](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-linux-riscv64-gnu](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-linux-riscv64-musl](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-linux-s390x-gnu](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-linux-x64-musl](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-win32-arm64-msvc](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-win32-ia32-msvc](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [@rollup/rollup-win32-x64-msvc](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |
| [rollup](https://github.com/rollup/rollup) | `4.44.2` | `4.45.0` |


Updates `@biomejs/biome` from 2.0.6 to 2.1.1
- [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.1.1/packages/@biomejs/biome)

Updates `@biomejs/cli-darwin-arm64` from 2.0.6 to 2.1.1
- [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.1.1/packages/@biomejs/biome)

Updates `@biomejs/cli-darwin-x64` from 2.0.6 to 2.1.1
- [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.1.1/packages/@biomejs/biome)

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

Updates `@biomejs/cli-linux-arm64` from 2.0.6 to 2.1.1
- [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.1.1/packages/@biomejs/biome)

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

Updates `@biomejs/cli-linux-x64` from 2.0.6 to 2.1.1
- [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.1.1/packages/@biomejs/biome)

Updates `@biomejs/cli-win32-arm64` from 2.0.6 to 2.1.1
- [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.1.1/packages/@biomejs/biome)

Updates `@biomejs/cli-win32-x64` from 2.0.6 to 2.1.1
- [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.1.1/packages/@biomejs/biome)

Updates `@rollup/rollup-android-arm-eabi` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-android-arm64` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-darwin-arm64` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-darwin-x64` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-freebsd-arm64` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-freebsd-x64` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-linux-arm-gnueabihf` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-linux-arm-musleabihf` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-linux-arm64-gnu` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-linux-arm64-musl` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-linux-loongarch64-gnu` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-linux-powerpc64le-gnu` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-linux-riscv64-gnu` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-linux-riscv64-musl` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-linux-s390x-gnu` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-linux-x64-gnu` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-linux-x64-musl` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-win32-arm64-msvc` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-win32-ia32-msvc` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `@rollup/rollup-win32-x64-msvc` from 4.44.2 to 4.45.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.44.2...v4.45.0)

Updates `rollup` from 4.44.2 to 4.45.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.44.2...v4.45.0)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-arm64"
  dependency-version: 2.1.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-x64"
  dependency-version: 2.1.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64-musl"
  dependency-version: 2.1.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64"
  dependency-version: 2.1.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64-musl"
  dependency-version: 2.1.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64"
  dependency-version: 2.1.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-arm64"
  dependency-version: 2.1.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-x64"
  dependency-version: 2.1.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm-eabi"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm64"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-x64"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-arm64"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-x64"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-gnueabihf"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-musleabihf"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-musl"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-loongarch64-gnu"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-powerpc64le-gnu"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-gnu"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-musl"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-s390x-gnu"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-musl"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-arm64-msvc"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-ia32-msvc"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-x64-msvc"
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: rollup
  dependency-version: 4.45.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-12 09:34:29 +00:00
zhom 3bec00a2cd chore: reset lock file 2025-07-11 03:43:52 +04:00
zhom 3b78971df8 chore: pnpm update 2025-07-11 03:31:05 +04:00
zhom 5f9a716f62 chore: version bump 2025-07-11 03:22:36 +04:00
zhom 4d07984d99 chore: hide camoufox 2025-07-11 03:22:11 +04:00
zhom 188e14e5b5 style: copy 2025-07-11 03:10:53 +04:00
zhom bc1b9e9757 style: copy 2025-07-11 03:10:00 +04:00
zhom e742e5fdfa style: copy 2025-07-11 03:09:38 +04:00
zhom 9ce7757cb2 chore: version bump 2025-07-08 06:26:39 +04:00
zhom 3ca454a2c5 style: adjust modal height 2025-07-08 04:57:25 +04:00
zhom 689ac8e3ca fix: windows build correct string literal 2025-07-07 07:34:55 +04:00
zhom 0e1c5dcfb6 docs: add feature description 2025-07-07 07:33:41 +04:00
zhom f22a9f3557 style: copy 2025-07-07 07:13:26 +04:00
zhom 5a76fe3221 tests: treat all camoufox versions as stable 2025-07-07 07:13:03 +04:00
zhom 5edad9b97c fix: prevent version downgrade for camoufox 2025-07-07 07:04:49 +04:00
zhom 38556fc504 style: copy and minor self-update modal logic change 2025-07-07 06:44:15 +04:00
zhom 703ca2c50b feat: add anti-detect functionality 2025-07-07 06:19:43 +04:00
zhom 198046fca9 Merge pull request #42 from zhom/dependabot/cargo/src-tauri/rust-dependencies-77d4c5ce85
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 10 updates
2025-07-05 11:33:20 +00:00
zhom fdcce5c86a Merge pull request #41 from zhom/dependabot/npm_and_yarn/frontend-dependencies-199434007a
deps(deps): bump the frontend-dependencies group with 35 updates
2025-07-05 11:33:00 +00:00
zhom 1cd1c7b59d Merge pull request #40 from zhom/dependabot/github_actions/github-actions-4aaa0eafdc
ci(deps): bump crate-ci/typos from 1.33.1 to 1.34.0 in the github-actions group
2025-07-05 11:32:43 +00:00
dependabot[bot] d803361fca deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [reqwest](https://github.com/seanmonstar/reqwest) | `0.12.20` | `0.12.22` |
| [tokio](https://github.com/tokio-rs/tokio) | `1.45.1` | `1.46.1` |
| [async-channel](https://github.com/smol-rs/async-channel) | `2.3.1` | `2.4.0` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.27` | `1.2.28` |
| [h2](https://github.com/hyperium/h2) | `0.4.10` | `0.4.11` |
| [rust-ini](https://github.com/zonyitoo/rust-ini) | `0.21.1` | `0.21.2` |
| [serde_with](https://github.com/jonasbb/serde_with) | `3.13.0` | `3.14.0` |
| [serde_with_macros](https://github.com/jonasbb/serde_with) | `3.13.0` | `3.14.0` |
| [shared_child](https://github.com/oconnor663/shared_child.rs) | `1.1.0` | `1.1.1` |
| [sigchld](https://github.com/oconnor663/sigchld.rs) | `0.2.3` | `0.2.4` |


Updates `reqwest` from 0.12.20 to 0.12.22
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.20...v0.12.22)

Updates `tokio` from 1.45.1 to 1.46.1
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.45.1...tokio-1.46.1)

Updates `async-channel` from 2.3.1 to 2.4.0
- [Release notes](https://github.com/smol-rs/async-channel/releases)
- [Changelog](https://github.com/smol-rs/async-channel/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/async-channel/compare/v2.3.1...v2.4.0)

Updates `cc` from 1.2.27 to 1.2.28
- [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.27...cc-v1.2.28)

Updates `h2` from 0.4.10 to 0.4.11
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.4.10...v0.4.11)

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

Updates `serde_with` from 3.13.0 to 3.14.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.13.0...v3.14.0)

Updates `serde_with_macros` from 3.13.0 to 3.14.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.13.0...v3.14.0)

Updates `shared_child` from 1.1.0 to 1.1.1
- [Commits](https://github.com/oconnor663/shared_child.rs/compare/1.1.0...1.1.1)

Updates `sigchld` from 0.2.3 to 0.2.4
- [Commits](https://github.com/oconnor663/sigchld.rs/compare/0.2.3...0.2.4)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-version: 0.12.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tokio
  dependency-version: 1.46.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: async-channel
  dependency-version: 2.4.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.28
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: h2
  dependency-version: 0.4.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rust-ini
  dependency-version: 0.21.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: serde_with
  dependency-version: 3.14.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_with_macros
  dependency-version: 3.14.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: shared_child
  dependency-version: 1.1.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: sigchld
  dependency-version: 0.2.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-05 09:32:16 +00:00
dependabot[bot] 2f6f20eb29 deps(deps): bump the frontend-dependencies group with 35 updates
---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: sonner
  dependency-version: 2.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@types/node"
  dependency-version: 24.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: tw-animate-css
  dependency-version: 1.3.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: dotenv
  dependency-version: 17.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/env"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-darwin-arm64"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-darwin-x64"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-linux-arm64-gnu"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-linux-arm64-musl"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-linux-x64-gnu"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-linux-x64-musl"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-win32-arm64-msvc"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-win32-x64-msvc"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm-eabi"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm64"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-x64"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-arm64"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-x64"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-gnueabihf"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-musleabihf"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-musl"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-loongarch64-gnu"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-powerpc64le-gnu"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-gnu"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-musl"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-s390x-gnu"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-musl"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-arm64-msvc"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-ia32-msvc"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-x64-msvc"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: rollup
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-05 09:18:32 +00:00
dependabot[bot] 59272e0cff 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.33.1 to 1.34.0
- [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/b1ae8d918b6e85bd611117d3d9a3be4f903ee5e4...392b78fe18a52790c53f42456e46124f77346842)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-05 09:12:21 +00:00
zhom cac2273ad3 chore: version bump 2025-07-05 02:40:15 +04:00
zhom 1691a7a06b refactor: make mullvad handle links the same way tor browser does 2025-07-05 02:38:17 +04:00
zhom 5a4718fba6 refactor: more robust profile import logic 2025-07-05 01:58:27 +04:00
zhom 336543d06e chore: version bump 2025-07-04 03:38:54 +04:00
zhom 73cc6c2ac5 build: use zip on windows 2025-07-04 03:23:34 +04:00
zhom f4c96ec0c6 fix: accept unused profile path on linux in arguments 2025-07-04 03:17:42 +04:00
zhom f84b3c2812 chore: disable rust codeql 2025-07-04 02:57:06 +04:00
zhom 29603076f7 chore: don't try to install nodecar dependencies 2025-07-04 02:44:46 +04:00
zhom 76bcb73b39 chore: instal system dependencies only for rust codeql check 2025-07-04 02:41:30 +04:00
zhom 51983bf3a5 style: scroll data table instead of page 2025-07-04 02:36:56 +04:00
zhom eda83cf439 chore: install dependencies on ubuntu-latest 2025-07-04 02:13:22 +04:00
zhom 7b6ea00838 feat: add proxy management 2025-07-04 01:56:41 +04:00
zhom d8f07ddb11 chore: install ubuntu dependencies after setting up rust 2025-07-03 23:17:42 +04:00
zhom 1b0ebbc666 chore: install build dependencies on ubuntu in codeql 2025-07-03 22:52:59 +04:00
zhom d377809c77 chore: remove dead code 2025-07-03 21:50:52 +04:00
zhom fbf36b49df chore: remove unused dependencies 2025-07-03 21:50:34 +04:00
zhom 341751c9b2 refactor: update profile storage structure 2025-07-03 21:34:56 +04:00
zhom eea227d853 chore: add codeql for rust code 2025-07-03 20:41:43 +04:00
zhom 29b6aed475 feat: show donwload bar for app self-update 2025-07-03 17:52:50 +04:00
zhom 050f8b5353 chore: pnpm update 2025-07-03 02:31:47 +04:00
zhom 8793de8c87 chore: update greetings message 2025-07-01 05:13:49 +04:00
zhom 7408ec876c chore: version bump 2025-07-01 05:11:50 +04:00
zhom fc8c358088 refactor: fetch chromium versions after 200+ new builds 2025-07-01 05:10:59 +04:00
zhom b11495e3b9 fix: dropdowns are not visible 2025-07-01 05:10:28 +04:00
zhom 11567ca50e chore: pnpm update 2025-06-29 19:01:02 +04:00
zhom 1c2d5b3774 Merge pull request #38 from zhom/dependabot/npm_and_yarn/frontend-dependencies-63052f5461
deps(deps): bump the frontend-dependencies group with 12 updates
2025-06-29 14:52:29 +00:00
dependabot[bot] 852066ef41 deps(deps): bump the frontend-dependencies group with 12 updates
Bumps the frontend-dependencies group with 12 updates:

| Package | From | To |
| --- | --- | --- |
| [@tauri-apps/cli](https://github.com/tauri-apps/tauri) | `2.6.1` | `2.6.2` |
| [@tauri-apps/cli-darwin-arm64](https://github.com/tauri-apps/tauri) | `2.6.1` | `2.6.2` |
| [@tauri-apps/cli-darwin-x64](https://github.com/tauri-apps/tauri) | `2.6.1` | `2.6.2` |
| [@tauri-apps/cli-linux-arm-gnueabihf](https://github.com/tauri-apps/tauri) | `2.6.1` | `2.6.2` |
| [@tauri-apps/cli-linux-arm64-gnu](https://github.com/tauri-apps/tauri) | `2.6.1` | `2.6.2` |
| [@tauri-apps/cli-linux-arm64-musl](https://github.com/tauri-apps/tauri) | `2.6.1` | `2.6.2` |
| [@tauri-apps/cli-linux-riscv64-gnu](https://github.com/tauri-apps/tauri) | `2.6.1` | `2.6.2` |
| [@tauri-apps/cli-linux-x64-gnu](https://github.com/tauri-apps/tauri) | `2.6.1` | `2.6.2` |
| [@tauri-apps/cli-linux-x64-musl](https://github.com/tauri-apps/tauri) | `2.6.1` | `2.6.2` |
| [@tauri-apps/cli-win32-arm64-msvc](https://github.com/tauri-apps/tauri) | `2.6.1` | `2.6.2` |
| [@tauri-apps/cli-win32-ia32-msvc](https://github.com/tauri-apps/tauri) | `2.6.1` | `2.6.2` |
| [@tauri-apps/cli-win32-x64-msvc](https://github.com/tauri-apps/tauri) | `2.6.1` | `2.6.2` |


Updates `@tauri-apps/cli` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/@tauri-apps/cli-v2.6.1...@tauri-apps/cli-v2.6.2)

Updates `@tauri-apps/cli-darwin-arm64` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.6.1...tauri-v2.6.2)

Updates `@tauri-apps/cli-darwin-x64` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.6.1...tauri-v2.6.2)

Updates `@tauri-apps/cli-linux-arm-gnueabihf` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.6.1...tauri-v2.6.2)

Updates `@tauri-apps/cli-linux-arm64-gnu` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.6.1...tauri-v2.6.2)

Updates `@tauri-apps/cli-linux-arm64-musl` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.6.1...tauri-v2.6.2)

Updates `@tauri-apps/cli-linux-riscv64-gnu` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.6.1...tauri-v2.6.2)

Updates `@tauri-apps/cli-linux-x64-gnu` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.6.1...tauri-v2.6.2)

Updates `@tauri-apps/cli-linux-x64-musl` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.6.1...tauri-v2.6.2)

Updates `@tauri-apps/cli-win32-arm64-msvc` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.6.1...tauri-v2.6.2)

Updates `@tauri-apps/cli-win32-ia32-msvc` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.6.1...tauri-v2.6.2)

Updates `@tauri-apps/cli-win32-x64-msvc` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.6.1...tauri-v2.6.2)

---
updated-dependencies:
- dependency-name: "@tauri-apps/cli"
  dependency-version: 2.6.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-darwin-arm64"
  dependency-version: 2.6.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-darwin-x64"
  dependency-version: 2.6.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-arm-gnueabihf"
  dependency-version: 2.6.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-arm64-gnu"
  dependency-version: 2.6.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-arm64-musl"
  dependency-version: 2.6.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-riscv64-gnu"
  dependency-version: 2.6.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-x64-gnu"
  dependency-version: 2.6.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-x64-musl"
  dependency-version: 2.6.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-win32-arm64-msvc"
  dependency-version: 2.6.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-win32-ia32-msvc"
  dependency-version: 2.6.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-win32-x64-msvc"
  dependency-version: 2.6.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-29 13:41:14 +00:00
zhom 9622d85e73 Merge pull request #37 from zhom/dependabot/github_actions/github-actions-b68af14af7
ci(deps): bump tauri-apps/tauri-action from 0.5.21 to 0.5.22 in the github-actions group
2025-06-29 13:38:48 +00:00
dependabot[bot] 4e2b87c5f1 ci(deps): bump tauri-apps/tauri-action in the github-actions group
Bumps the github-actions group with 1 update: [tauri-apps/tauri-action](https://github.com/tauri-apps/tauri-action).


Updates `tauri-apps/tauri-action` from 0.5.21 to 0.5.22
- [Release notes](https://github.com/tauri-apps/tauri-action/releases)
- [Changelog](https://github.com/tauri-apps/tauri-action/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/tauri-apps/tauri-action/compare/8c94c894075e92c8a2b668b2d35c57e1e38cfdfb...564aea5a8075c7a54c167bb0cf5b3255314a7f9d)

---
updated-dependencies:
- dependency-name: tauri-apps/tauri-action
  dependency-version: 0.5.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-29 13:30:55 +00:00
zhom 2099dadbc0 chore: fully migrate to biome 2025-06-29 17:28:42 +04:00
zhom 00e4eb2715 chore: update dependabot automerge config 2025-06-29 17:07:23 +04:00
zhom 33bc4476a4 chore: update biome config schema version 2025-06-29 17:06:56 +04:00
zhom 0ad8988f7e Merge pull request #36 from zhom/dependabot/github_actions/github-actions-0bae03cf66
ci(deps): bump the github-actions group across 1 directory with 2 updates
2025-06-28 16:17:35 +00:00
zhom 2b3aaf1e92 Merge pull request #35 from zhom/dependabot/npm_and_yarn/frontend-dependencies-a36879a10f
deps(deps): bump the frontend-dependencies group across 1 directory with 86 updates
2025-06-28 16:17:22 +00:00
zhom 5a10e0b696 Merge pull request #34 from zhom/dependabot/cargo/src-tauri/rust-dependencies-c98a71ca2f
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 36 updates
2025-06-28 16:17:09 +00:00
dependabot[bot] 9e48ddbf3e deps(rust)(deps): bump the rust-dependencies group
---
updated-dependencies:
- dependency-name: tauri-plugin-opener
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-plugin-fs
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-plugin-shell
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-plugin-deep-link
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-plugin-dialog
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: zip
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-plugin-single-instance
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-build
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: brotli
  dependency-version: 8.0.1
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: rust-dependencies
- dependency-name: brotli-decompressor
  dependency-version: 5.0.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: rust-dependencies
- dependency-name: bumpalo
  dependency-version: 3.19.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: crunchy
  dependency-version: 0.2.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cssparser
  dependency-version: 0.29.6
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: html5ever
  dependency-version: 0.29.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: itoa
  dependency-version: 1.0.15
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: rust-dependencies
- dependency-name: kuchikiki
  dependency-version: 0.8.8-speedreader
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libredox
  dependency-version: 0.1.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: markup5ever
  dependency-version: 0.14.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: muda
  dependency-version: 0.17.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: num_enum
  dependency-version: 0.7.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: num_enum_derive
  dependency-version: 0.7.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: phf_macros
  dependency-version: 0.10.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: selectors
  dependency-version: 0.24.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: servo_arc
  dependency-version: 0.2.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tao
  dependency-version: 0.34.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-codegen
  dependency-version: 2.3.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-macros
  dependency-version: 2.3.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-plugin
  dependency-version: 2.3.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-runtime
  dependency-version: 2.7.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-runtime-wry
  dependency-version: 2.7.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-utils
  dependency-version: 2.5.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tray-icon
  dependency-version: 0.21.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: webview2-com
  dependency-version: 0.38.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: webview2-com-sys
  dependency-version: 0.38.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: windows-registry
  dependency-version: 0.5.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: wry
  dependency-version: 0.52.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-28 16:09:43 +00:00
dependabot[bot] bcbb2c1d42 ci(deps): bump the github-actions group across 1 directory with 2 updates
Bumps the github-actions group with 2 updates in the / directory: [swatinem/rust-cache](https://github.com/swatinem/rust-cache) and [tauri-apps/tauri-action](https://github.com/tauri-apps/tauri-action).


Updates `swatinem/rust-cache` from 2.7.8 to 2.8.0
- [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/9d47c6ad4b02e050fd481d890b2ea34778fd09d6...98c8021b550208e191a6a3145459bfc9fb29c4c0)

Updates `tauri-apps/tauri-action` from 0.5.20 to 0.5.21
- [Release notes](https://github.com/tauri-apps/tauri-action/releases)
- [Changelog](https://github.com/tauri-apps/tauri-action/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/tauri-apps/tauri-action/compare/42e9df6c59070d114bf90dcd3943a1b8f138b113...8c94c894075e92c8a2b668b2d35c57e1e38cfdfb)

---
updated-dependencies:
- dependency-name: swatinem/rust-cache
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: tauri-apps/tauri-action
  dependency-version: 0.5.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-28 16:06:05 +00:00
zhom 391bfdabdc build: inherit secrets in steps 2025-06-28 19:59:51 +04:00
dependabot[bot] 7b2dc84b5b deps(deps): bump the frontend-dependencies group across 1 directory with 86 updates
---
updated-dependencies:
- dependency-name: "@tauri-apps/api"
  dependency-version: 2.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/plugin-deep-link"
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/plugin-dialog"
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/plugin-fs"
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/plugin-opener"
  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.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/biome"
  dependency-version: 2.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@eslint/js"
  dependency-version: 9.30.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.1.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli"
  dependency-version: 2.6.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@types/node"
  dependency-version: 24.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.35.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.35.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 4.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.1.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: dotenv
  dependency-version: 17.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: "@babel/compat-data"
  dependency-version: 7.27.7
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@babel/core"
  dependency-version: 7.27.7
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@babel/traverse"
  dependency-version: 7.27.7
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-arm64"
  dependency-version: 2.0.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-x64"
  dependency-version: 2.0.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64-musl"
  dependency-version: 2.0.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64"
  dependency-version: 2.0.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64-musl"
  dependency-version: 2.0.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64"
  dependency-version: 2.0.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-arm64"
  dependency-version: 2.0.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-x64"
  dependency-version: 2.0.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rolldown/pluginutils"
  dependency-version: 1.0.0-beta.19
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm-eabi"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm64"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-x64"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-arm64"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-x64"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-gnueabihf"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-musleabihf"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-musl"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-loongarch64-gnu"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-powerpc64le-gnu"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-gnu"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-musl"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-s390x-gnu"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-musl"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-arm64-msvc"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-ia32-msvc"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-x64-msvc"
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/node"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-android-arm64"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-darwin-arm64"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-darwin-x64"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-freebsd-x64"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-linux-arm-gnueabihf"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-linux-arm64-gnu"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-linux-arm64-musl"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-linux-x64-gnu"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-linux-x64-musl"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-wasm32-wasi"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-win32-arm64-msvc"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide-win32-x64-msvc"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tailwindcss/oxide"
  dependency-version: 4.1.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-darwin-arm64"
  dependency-version: 2.6.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-darwin-x64"
  dependency-version: 2.6.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-arm-gnueabihf"
  dependency-version: 2.6.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-arm64-gnu"
  dependency-version: 2.6.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-arm64-musl"
  dependency-version: 2.6.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-riscv64-gnu"
  dependency-version: 2.6.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-x64-gnu"
  dependency-version: 2.6.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-linux-x64-musl"
  dependency-version: 2.6.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-win32-arm64-msvc"
  dependency-version: 2.6.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-win32-ia32-msvc"
  dependency-version: 2.6.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@tauri-apps/cli-win32-x64-msvc"
  dependency-version: 2.6.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/project-service"
  dependency-version: 8.35.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/scope-manager"
  dependency-version: 8.35.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/tsconfig-utils"
  dependency-version: 8.35.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/type-utils"
  dependency-version: 8.35.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/types"
  dependency-version: 8.35.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/typescript-estree"
  dependency-version: 8.35.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/utils"
  dependency-version: 8.35.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/visitor-keys"
  dependency-version: 8.35.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: browserslist
  dependency-version: 4.25.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: electron-to-chromium
  dependency-version: 1.5.177
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: enhanced-resolve
  dependency-version: 5.18.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: rollup
  dependency-version: 4.44.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-28 15:39:09 +00:00
zhom ddc09726f4 build: inherit secrets for automerge 2025-06-28 19:12:16 +04:00
zhom e1451d3fbb build: use updated dependabot token 2025-06-28 16:28:42 +04:00
zhom b18df6499f build: use default token for dependabot automerge workflow 2025-06-28 15:24:42 +04:00
zhom c5c2563a4e chore: version bump 2025-06-26 19:19:09 +04:00
zhom 8475f42821 refactor: improve titlebar interactions on macos 2025-06-26 19:17:38 +04:00
zhom f51aa9ed85 refactor: better state control for browser download 2025-06-22 06:23:27 +04:00
zhom 3d3a3b3816 chore: linting 2025-06-22 06:04:02 +04:00
zhom e090881917 Merge pull request #31 from zhom/dependabot/cargo/src-tauri/rust-dependencies-679f27469d
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 16 updates
2025-06-22 00:53:38 +00:00
zhom b46976f47d Merge pull request #29 from zhom/dependabot/github_actions/github-actions-97a53f9a15
ci(deps): bump google/osv-scanner-action from 2.0.2 to 2.0.3 in the github-actions group
2025-06-22 00:53:27 +00:00
dependabot[bot] 39a978682c ci(deps): bump google/osv-scanner-action in the github-actions group
Bumps the github-actions group with 1 update: [google/osv-scanner-action](https://github.com/google/osv-scanner-action).


Updates `google/osv-scanner-action` from 2.0.2 to 2.0.3
- [Release notes](https://github.com/google/osv-scanner-action/releases)
- [Commits](https://github.com/google/osv-scanner-action/compare/e69cc6c86b31f1e7e23935bbe7031b50e51082de...40a8940a65eab1544a6af759e43d936201a131a2)

---
updated-dependencies:
- dependency-name: google/osv-scanner-action
  dependency-version: 2.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-22 00:52:23 +00:00
zhom 38e58e604b chore: add token var to automerge 2025-06-22 04:24:51 +04:00
dependabot[bot] ffcff2ce7c deps(rust)(deps): bump the rust-dependencies group
---
updated-dependencies:
- dependency-name: tauri-plugin-opener
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-plugin-shell
  dependency-version: 2.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zip
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: wiremock
  dependency-version: 0.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: autocfg
  dependency-version: 1.5.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: embed-resource
  dependency-version: 3.0.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: errno
  dependency-version: 0.3.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libc
  dependency-version: 0.2.174
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: liblzma
  dependency-version: 0.4.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: r-efi
  dependency-version: 5.3.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: rustls
  dependency-version: 0.23.28
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: shared_child
  dependency-version: 1.1.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: slab
  dependency-version: 0.4.10
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tracing-attributes
  dependency-version: 0.1.30
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zerocopy
  dependency-version: 0.8.26
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zerocopy-derive
  dependency-version: 0.8.26
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-21 23:11:07 +00:00
zhom c8ea31f85d Merge pull request #30 from zhom/dependabot/npm_and_yarn/frontend-dependencies-424214cd75
deps(deps): bump the frontend-dependencies group with 80 updates
2025-06-21 18:06:22 +00:00
zhom 7ac6e21dbc chore: pass default token 2025-06-21 21:11:58 +04:00
dependabot[bot] 7533993909 deps(deps): bump the frontend-dependencies group with 80 updates
---
updated-dependencies:
- dependency-name: "@tauri-apps/plugin-opener"
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: next
  dependency-version: 15.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/biome"
  dependency-version: 2.0.4
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: "@next/eslint-plugin-next"
  dependency-version: 15.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@types/node"
  dependency-version: 24.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.34.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.34.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: eslint-config-next
  dependency-version: 15.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: lint-staged
  dependency-version: 16.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.34.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-arm64"
  dependency-version: 2.0.4
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-x64"
  dependency-version: 2.0.4
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64-musl"
  dependency-version: 2.0.4
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64"
  dependency-version: 2.0.4
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64-musl"
  dependency-version: 2.0.4
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64"
  dependency-version: 2.0.4
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-arm64"
  dependency-version: 2.0.4
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-x64"
  dependency-version: 2.0.4
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: "@next/env"
  dependency-version: 15.3.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-darwin-arm64"
  dependency-version: 15.3.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-darwin-x64"
  dependency-version: 15.3.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-linux-arm64-gnu"
  dependency-version: 15.3.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-linux-arm64-musl"
  dependency-version: 15.3.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-linux-x64-gnu"
  dependency-version: 15.3.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-linux-x64-musl"
  dependency-version: 15.3.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-win32-arm64-msvc"
  dependency-version: 15.3.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-win32-x64-msvc"
  dependency-version: 15.3.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm-eabi"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm64"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-x64"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-arm64"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-x64"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-gnueabihf"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-musleabihf"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-musl"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-loongarch64-gnu"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-powerpc64le-gnu"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-gnu"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-musl"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-s390x-gnu"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-musl"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-arm64-msvc"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-ia32-msvc"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-x64-msvc"
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@types/estree"
  dependency-version: 1.0.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/project-service"
  dependency-version: 8.34.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/scope-manager"
  dependency-version: 8.34.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/tsconfig-utils"
  dependency-version: 8.34.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/type-utils"
  dependency-version: 8.34.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/types"
  dependency-version: 8.34.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/typescript-estree"
  dependency-version: 8.34.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/utils"
  dependency-version: 8.34.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@typescript-eslint/visitor-keys"
  dependency-version: 8.34.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-android-arm-eabi"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-android-arm64"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-darwin-arm64"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-darwin-x64"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-freebsd-x64"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-linux-arm-gnueabihf"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-linux-arm-musleabihf"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-linux-arm64-gnu"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-linux-arm64-musl"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-linux-ppc64-gnu"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-linux-riscv64-gnu"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-linux-riscv64-musl"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-linux-s390x-gnu"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-linux-x64-gnu"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-linux-x64-musl"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-wasm32-wasi"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-win32-arm64-msvc"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-win32-ia32-msvc"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@unrs/resolver-binding-win32-x64-msvc"
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: caniuse-lite
  dependency-version: 1.0.30001724
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: eslint-module-utils
  dependency-version: 2.12.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: eslint-plugin-import
  dependency-version: 2.32.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: rollup
  dependency-version: 4.44.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: unrs-resolver
  dependency-version: 1.9.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-21 17:03:46 +00:00
zhom 8176f45e41 chore: use default github token in dependabot 2025-06-21 20:41:42 +04:00
zhom f55a3f7155 chore: add github token to dependabot 2025-06-21 19:45:01 +04:00
zhom 7d74ac09d9 refactor: update chromium after 100+ versions 2025-06-19 08:57:48 +04:00
zhom d314fa1f71 chore: store user input in variables 2025-06-19 07:24:33 +04:00
zhom 968969cf1e docs: clean up 2025-06-19 06:37:52 +04:00
zhom a7a3d99881 chore: ask for more info on issue 2025-06-19 06:36:15 +04:00
zhom 80cd2e4e7f build: generate release notes 2025-06-19 06:35:34 +04:00
zhom 6361a039bc chore: version bump 2025-06-19 03:52:26 +04:00
zhom 8005ec90b6 refactor: improve auto-delete and auto-install browser logic 2025-06-19 03:36:06 +04:00
zhom cdf30b7baa Merge pull request #28 from zhom/contributors-readme-action-OBsPbmEa9K
docs(contributor): contributors readme action update
2025-06-18 02:20:40 +00:00
github-actions[bot] fadef414fe docs(contributor): contrib-readme-action has updated readme 2025-06-18 02:17:09 +00:00
zhom e1c55233f7 chore: fix permissions for contributors workflow 2025-06-18 06:14:36 +04:00
zhom 801a2b5732 docs: rename agent instructions doc 2025-06-18 06:11:38 +04:00
zhom abe5c691ce docs: stale issue workflow 2025-06-18 06:08:15 +04:00
zhom 2f9a17c6e0 docs: automatically add contributors to readme 2025-06-18 05:38:38 +04:00
zhom fcdb80f75a docs: github newcomer greetings 2025-06-18 05:34:53 +04:00
zhom 7568e7998d chore : version bump 2025-06-18 03:00:45 +04:00
zhom e0f4f93c30 fix: don't create unique temp dir for every cli call 2025-06-18 02:59:11 +04:00
zhom d142b7f79b style: don't show release notes 2025-06-18 02:39:40 +04:00
zhom dc5553a5d3 chore: version bump 2025-06-18 01:30:02 +04:00
zhom 07445ff95b build: add content read permissions for linting workflows 2025-06-18 01:26:34 +04:00
zhom 6ecbc39e46 build: pin action versions 2025-06-18 01:25:21 +04:00
zhom 67849c00d5 refactor: use tmp for temp dirs and add more robust error handling for updateProxyConfig 2025-06-18 01:19:10 +04:00
zhom bdf71e4ef8 build: revert dependabot automerge workflow 2025-06-18 00:57:40 +04:00
zhom 2d2ebba40e build: assign read permission to all actions without one 2025-06-18 00:17:58 +04:00
zhom 2caac5bf4c build: pin action versions 2025-06-18 00:12:26 +04:00
zhom a816fbb140 chore: version bump 2025-06-17 19:24:17 +04:00
zhom c954668ed1 fix: launch chromium proxy directly instead of pac file 2025-06-17 19:22:18 +04:00
zhom 2db27b5ffd refactor: only use is_browser_version_nightly for release checks 2025-06-17 17:52:28 +04:00
zhom 845e9f28ad fix: don't let the user create profile or change version if latest version not downloaded 2025-06-17 16:29:42 +04:00
zhom ee8c6dcc85 chore: add checks for unused ts exports 2025-06-17 16:13:17 +04:00
zhom 08453fe9a6 build: update codeql permissions 2025-06-17 07:05:20 +04:00
zhom b486f00875 chore: version bump 2025-06-17 07:00:27 +04:00
zhom 703154b30f chore: linting 2025-06-17 07:00:18 +04:00
zhom 130f8b86d1 refactor: browser auto-update 2025-06-17 06:55:52 +04:00
zhom 607ed66e29 style: copy 2025-06-17 06:32:47 +04:00
zhom 9570b6d605 build: fix codeql permissions 2025-06-17 06:28:34 +04:00
zhom 2d92cbb0e5 refactor: fetch release information the same way for manual and automatic checks 2025-06-17 06:17:57 +04:00
zhom 251016609f refactor: change event emitting and remove sleep 2025-06-17 04:05:17 +04:00
zhom bddf796946 refactor: don't mark updates as automatic and fetch versions via version_updater only 2025-06-17 03:44:59 +04:00
zhom 8d793a6868 style: make release type selector behave the same in both creation and change modals 2025-06-17 03:42:47 +04:00
zhom 469f161293 refactor: add extra settings to not show update prompt 2025-06-17 03:24:10 +04:00
zhom 9756e64319 refactor: increase default update interval for firefox 2025-06-17 03:22:30 +04:00
zhom 800544ede9 refactor: supress all update prompts in the ui for firefox 2025-06-17 03:21:21 +04:00
zhom aa2228a8aa fix: show switch release option for correct browsers 2025-06-17 03:10:16 +04:00
zhom 432e5bff90 refactor: switch download mode from 0 to 2 on firefox 2025-06-17 03:06:50 +04:00
zhom f4b60eb6c7 chore: linting 2025-06-17 03:04:06 +04:00
zhom 30122c5781 reafctor: get cache releases first and don't return twilight in results 2025-06-17 03:00:09 +04:00
zhom b71d84fda4 refactor: improve signatures for public functions 2025-06-17 02:58:46 +04:00
zhom 859af72724 refactor: improve auto-update inside browser prevention 2025-06-17 02:57:30 +04:00
zhom 0360a89ceb style: remove 'enable automatic browser updates' setting 2025-06-17 02:46:33 +04:00
zhom cb6f744d6b style: only show the select menu if both stable and nightly releases are available 2025-06-17 02:42:03 +04:00
zhom 575d7f80b1 style: remove switch release option for zen and chromium 2025-06-17 02:40:48 +04:00
zhom d05b69ff3d build: require codeql and spellcheck to pass successfully before build starts 2025-06-16 23:28:53 +04:00
zhom 54abb11129 build: run spellcheck on build 2025-06-16 03:41:38 +04:00
zhom 04c690c750 build: run codeql before build 2025-06-16 03:30:13 +04:00
zhom 9a4be86e95 style: copy 2025-06-15 05:40:18 +04:00
zhom 6d013d86aa Merge pull request #26 from zhom/dependabot/cargo/src-tauri/rust-dependencies-f6a8cef228
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 2 updates
2025-06-14 18:26:30 +00:00
zhom 769fbf9d75 Merge pull request #25 from zhom/dependabot/github_actions/github-actions-eba975d771
ci(deps): bump stefanzweifel/git-auto-commit-action from 4 to 6 in the github-actions group
2025-06-14 18:26:09 +00:00
zhom 6e62abc601 chore: version bump 2025-06-14 22:22:17 +04:00
dependabot[bot] 8848fa8130 deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 2 updates: [serde_with](https://github.com/jonasbb/serde_with) and [serde_with_macros](https://github.com/jonasbb/serde_with).


Updates `serde_with` from 3.12.0 to 3.13.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.12.0...v3.13.0)

Updates `serde_with_macros` from 3.12.0 to 3.13.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.12.0...v3.13.0)

---
updated-dependencies:
- dependency-name: serde_with
  dependency-version: 3.13.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_with_macros
  dependency-version: 3.13.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-14 18:21:50 +00:00
zhom 1f0ecbe36e docs: use dark-themed preview for users with dark mode on 2025-06-14 22:21:26 +04:00
dependabot[bot] f83f2033fe ci(deps): bump stefanzweifel/git-auto-commit-action
Bumps the github-actions group with 1 update: [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action).


Updates `stefanzweifel/git-auto-commit-action` from 4 to 6
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v4...v6)

---
updated-dependencies:
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-14 18:02:10 +00:00
zhom 821cd4ea82 style: only allow user to switch between releases 2025-06-14 21:55:18 +04:00
zhom d3a63c37bf test: update mock 2025-06-14 19:20:51 +04:00
zhom 95cd2426c3 feat: automatically update browsers on new versions 2025-06-14 19:03:02 +04:00
zhom 5a3fb7b2b0 refactor: install chromium update after 50 new builds 2025-06-14 18:44:03 +04:00
zhom 767a0701ce refactor: don't check for nodecar dependency updates 2025-06-14 18:33:33 +04:00
zhom ec61d51c07 build: update changelog generation workflow 2025-06-14 17:33:38 +04:00
zhom 545c518a55 feat: don't spam update notification, show more concise toasts, don't fetch unsupported browser updates 2025-06-14 16:58:55 +04:00
zhom c99eee2c21 style: use proper zen icon 2025-06-14 16:08:26 +04:00
zhom 7f3683cc2e style: show 'not supported' for tor browser proxies 2025-06-14 15:57:33 +04:00
zhom baac3a533a refactor: fetch 100 latest app updates from github releases 2025-06-14 15:43:50 +04:00
zhom 5cd1774ffc chore: remove dependabot automerge workflow 2025-06-14 15:42:02 +04:00
zhom cb87641890 Merge pull request #23 from zhom/dependabot/cargo/src-tauri/rust-dependencies-af3af11ff5
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 17 updates
2025-06-14 11:34:34 +00:00
zhom 3df5ac671b Merge pull request #24 from zhom/dependabot/npm_and_yarn/frontend-dependencies-f9895d0c1f
deps(deps): bump the frontend-dependencies group with 3 updates
2025-06-14 11:34:05 +00:00
dependabot[bot] 390f79f97b deps(deps): bump the frontend-dependencies group with 3 updates
Bumps the frontend-dependencies group with 3 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [lint-staged](https://github.com/lint-staged/lint-staged) and [undici-types](https://github.com/nodejs/undici).


Updates `@types/node` from 22.15.31 to 24.0.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.0 to 16.1.1
- [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.0...v16.1.1)

Updates `undici-types` from 6.21.0 to 7.8.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.21.0...v7.8.0)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: lint-staged
  dependency-version: 16.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: undici-types
  dependency-version: 7.8.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-14 10:18:35 +00:00
dependabot[bot] c4dc2ed50c deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 17 updates:

| Package | From | To |
| --- | --- | --- |
| [reqwest](https://github.com/seanmonstar/reqwest) | `0.12.19` | `0.12.20` |
| [windows](https://github.com/microsoft/windows-rs) | `0.61.1` | `0.61.3` |
| [adler2](https://github.com/oyvindln/adler2) | `2.0.0` | `2.0.1` |
| [bytemuck](https://github.com/Lokathor/bytemuck) | `1.23.0` | `1.23.1` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.26` | `1.2.27` |
| [cfg-if](https://github.com/rust-lang/cfg-if) | `1.0.0` | `1.0.1` |
| [enumflags2](https://github.com/meithecatte/enumflags2) | `0.7.11` | `0.7.12` |
| [enumflags2_derive](https://github.com/meithecatte/enumflags2) | `0.7.11` | `0.7.12` |
| [hermit-abi](https://github.com/hermit-os/hermit-rs) | `0.5.1` | `0.5.2` |
| [libc](https://github.com/rust-lang/libc) | `0.2.172` | `0.2.173` |
| [memchr](https://github.com/BurntSushi/memchr) | `2.7.4` | `2.7.5` |
| [miniz_oxide](https://github.com/Frommi/miniz_oxide) | `0.8.8` | `0.8.9` |
| [plist](https://github.com/ebarnard/rust-plist) | `1.7.1` | `1.7.2` |
| [quick-xml](https://github.com/tafia/quick-xml) | `0.32.0` | `0.37.5` |
| redox_syscall | `0.5.12` | `0.5.13` |
| [rustc-demangle](https://github.com/rust-lang/rustc-demangle) | `0.1.24` | `0.1.25` |
| [windows-link](https://github.com/microsoft/windows-rs) | `0.1.1` | `0.1.3` |


Updates `reqwest` from 0.12.19 to 0.12.20
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.19...v0.12.20)

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

Updates `adler2` from 2.0.0 to 2.0.1
- [Changelog](https://github.com/oyvindln/adler2/blob/main/CHANGELOG.md)
- [Commits](https://github.com/oyvindln/adler2/commits)

Updates `bytemuck` from 1.23.0 to 1.23.1
- [Changelog](https://github.com/Lokathor/bytemuck/blob/main/changelog.md)
- [Commits](https://github.com/Lokathor/bytemuck/compare/v1.23.0...v1.23.1)

Updates `cc` from 1.2.26 to 1.2.27
- [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.26...cc-v1.2.27)

Updates `cfg-if` from 1.0.0 to 1.0.1
- [Release notes](https://github.com/rust-lang/cfg-if/releases)
- [Changelog](https://github.com/rust-lang/cfg-if/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cfg-if/compare/1.0.0...v1.0.1)

Updates `enumflags2` from 0.7.11 to 0.7.12
- [Release notes](https://github.com/meithecatte/enumflags2/releases)
- [Commits](https://github.com/meithecatte/enumflags2/compare/v0.7.11...v0.7.12)

Updates `enumflags2_derive` from 0.7.11 to 0.7.12
- [Release notes](https://github.com/meithecatte/enumflags2/releases)
- [Commits](https://github.com/meithecatte/enumflags2/compare/v0.7.11...v0.7.12)

Updates `hermit-abi` from 0.5.1 to 0.5.2
- [Release notes](https://github.com/hermit-os/hermit-rs/releases)
- [Commits](https://github.com/hermit-os/hermit-rs/compare/hermit-abi-0.5.1...hermit-abi-0.5.2)

Updates `libc` from 0.2.172 to 0.2.173
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.173/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.172...0.2.173)

Updates `memchr` from 2.7.4 to 2.7.5
- [Commits](https://github.com/BurntSushi/memchr/compare/2.7.4...2.7.5)

Updates `miniz_oxide` from 0.8.8 to 0.8.9
- [Changelog](https://github.com/Frommi/miniz_oxide/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Frommi/miniz_oxide/commits)

Updates `plist` from 1.7.1 to 1.7.2
- [Release notes](https://github.com/ebarnard/rust-plist/releases)
- [Commits](https://github.com/ebarnard/rust-plist/compare/v1.7.1...v1.7.2)

Updates `quick-xml` from 0.32.0 to 0.37.5
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.32.0...v0.37.5)

Updates `redox_syscall` from 0.5.12 to 0.5.13

Updates `rustc-demangle` from 0.1.24 to 0.1.25
- [Commits](https://github.com/rust-lang/rustc-demangle/commits)

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

---
updated-dependencies:
- dependency-name: reqwest
  dependency-version: 0.12.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: windows
  dependency-version: 0.61.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: adler2
  dependency-version: 2.0.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: bytemuck
  dependency-version: 1.23.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.27
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cfg-if
  dependency-version: 1.0.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: enumflags2
  dependency-version: 0.7.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: enumflags2_derive
  dependency-version: 0.7.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: hermit-abi
  dependency-version: 0.5.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libc
  dependency-version: 0.2.173
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: memchr
  dependency-version: 2.7.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: miniz_oxide
  dependency-version: 0.8.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: plist
  dependency-version: 1.7.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: quick-xml
  dependency-version: 0.37.5
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: redox_syscall
  dependency-version: 0.5.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rustc-demangle
  dependency-version: 0.1.25
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: windows-link
  dependency-version: 0.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-14 09:44:26 +00:00
zhom 3b7315cc0d docs: add fossa badge 2025-06-14 03:42:52 +04:00
zhom bbd0f5df0c chore: version bump 2025-06-14 02:51:24 +04:00
zhom 8e7982bdf8 fix: properly handle urls added by system events 2025-06-14 02:11:40 +04:00
zhom 9ac662aee8 chore: use single-instance plugin as described by the docs 2025-06-13 21:40:50 +04:00
zhom 2394716ea3 chore: version bump 2025-06-11 04:43:18 +04:00
zhom 06992f8b9a build: use nightly-date-hash naming for rolling releases 2025-06-11 04:23:03 +04:00
zhom 5a454e3647 chore: linting 2025-06-11 04:17:07 +04:00
zhom 3f91d92d8b refactor: add missing linting dependency 2025-06-11 04:00:12 +04:00
zhom c586046542 refactor: codacy autofix 2025-06-11 03:56:14 +04:00
zhom 42f63172fb docs: add codacy badge 2025-06-11 03:55:36 +04:00
zhom f717600fcb style: trucate profile name in table after over 15 characters 2025-06-11 03:40:01 +04:00
zhom c807ea5596 build: add changelog generation on release 2025-06-11 03:23:16 +04:00
zhom df2c1316d4 build: disable windows build for regular releases 2025-06-11 03:04:34 +04:00
zhom 1fdc552dc7 fix: make sure proxy configuration is discovered properly in the production build 2025-06-11 03:02:40 +04:00
zhom ab563f81fa style: update proxy settings form to match proxy-settings-dialog 2025-06-11 02:40:03 +04:00
zhom d17545bd05 docs: add extra check for agents to run tests and linting before finishing 2025-06-11 02:33:23 +04:00
zhom 29c329b432 feat: update proxy ui to accept credential outside url too 2025-06-11 01:55:54 +04:00
zhom 4d7bbe719f chore: run biome on nodecar file changes 2025-06-11 01:52:07 +04:00
zhom 5b869a6115 test: add tests for proxy manager 2025-06-10 00:33:58 +04:00
zhom c4b1745a0f style: update ui to accept proxy separately 2025-06-09 23:19:34 +04:00
zhom ac293f6204 build: rename nightly release tags for better ordering 2025-06-09 23:18:55 +04:00
zhom 7f3a3287d6 build: switch win x64 build to windows-latest 2025-06-09 19:48:30 +04:00
zhom dd9347d429 build: set proper shell for the hash step 2025-06-09 19:19:57 +04:00
zhom 615d7b8c8a build: fix windows build warnings 2025-06-09 18:56:38 +04:00
zhom 5c26ab5c33 build: windows build invalid lifetime fix 2025-06-09 18:02:48 +04:00
zhom dccaf6c7de build: blind windows ci build fixes 2025-06-09 15:20:06 +04:00
zhom bf5b2886f5 build: blind windows ci build fixes 2025-06-09 15:01:10 +04:00
zhom bbc12bcc03 fix: blind windows ci build fixes 2025-06-09 06:25:22 +04:00
zhom 6d437f30e1 feat: add windows build to ci (broken) 2025-06-09 06:05:22 +04:00
zhom 4b0ab6b732 refactor: windows pipeline test 2025-06-09 06:04:26 +04:00
zhom a802895491 fix: same fix for discovery 2025-06-09 03:05:20 +04:00
zhom a57f90899b fix: don't try to enter into nested directory on linux for chromium binary 2025-06-09 02:59:57 +04:00
zhom 3ebc714b23 chore: version bump 2025-06-08 20:27:50 +04:00
zhom 1acd4781b5 fix: properly handle permissions on macos 2025-06-08 20:27:17 +04:00
zhom a5b9afafcb docs: update agent instructions 2025-06-08 17:06:10 +04:00
zhom 0c8dd5ace5 Merge pull request #19 from zhom/dependabot/npm_and_yarn/frontend-dependencies-6813ab5a43
deps(deps): bump the frontend-dependencies group with 21 updates
2025-06-07 14:29:46 +04:00
dependabot[bot] e8c3188657 deps(deps): bump the frontend-dependencies group with 21 updates
Bumps the frontend-dependencies group with 21 updates:

| Package | From | To |
| --- | --- | --- |
| [@rollup/rollup-android-arm-eabi](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-android-arm64](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-darwin-arm64](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-darwin-x64](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-freebsd-arm64](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-freebsd-x64](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-linux-arm-gnueabihf](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-linux-arm-musleabihf](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-linux-arm64-gnu](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-linux-arm64-musl](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-linux-loongarch64-gnu](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-linux-powerpc64le-gnu](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-linux-riscv64-gnu](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-linux-riscv64-musl](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-linux-s390x-gnu](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-linux-x64-musl](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-win32-arm64-msvc](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-win32-ia32-msvc](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [@rollup/rollup-win32-x64-msvc](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |
| [rollup](https://github.com/rollup/rollup) | `4.41.1` | `4.42.0` |


Updates `@rollup/rollup-android-arm-eabi` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-android-arm64` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-darwin-arm64` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-darwin-x64` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-freebsd-arm64` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-freebsd-x64` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-linux-arm-gnueabihf` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-linux-arm-musleabihf` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-linux-arm64-gnu` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-linux-arm64-musl` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-linux-loongarch64-gnu` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-linux-powerpc64le-gnu` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-linux-riscv64-gnu` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-linux-riscv64-musl` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-linux-s390x-gnu` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-linux-x64-gnu` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-linux-x64-musl` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-win32-arm64-msvc` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-win32-ia32-msvc` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `@rollup/rollup-win32-x64-msvc` from 4.41.1 to 4.42.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.41.1...v4.42.0)

Updates `rollup` from 4.41.1 to 4.42.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.41.1...v4.42.0)

---
updated-dependencies:
- dependency-name: "@rollup/rollup-android-arm-eabi"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm64"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-x64"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-arm64"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-x64"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-gnueabihf"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-musleabihf"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-musl"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-loongarch64-gnu"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-powerpc64le-gnu"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-gnu"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-musl"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-s390x-gnu"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-musl"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-arm64-msvc"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-ia32-msvc"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-x64-msvc"
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: rollup
  dependency-version: 4.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-07 10:21:57 +00:00
zhom e6f0b2b9e9 Merge pull request #18 from zhom/dependabot/cargo/src-tauri/rust-dependencies-b0de36e2fd
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 13 updates
2025-06-07 14:05:42 +04:00
dependabot[bot] 394406e134 deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 13 updates:

| Package | From | To |
| --- | --- | --- |
| [bumpalo](https://github.com/fitzgen/bumpalo) | `3.18.0` | `3.18.1` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.25` | `1.2.26` |
| [flate2](https://github.com/rust-lang/flate2-rs) | `1.1.1` | `1.1.2` |
| [hyper-rustls](https://github.com/rustls/hyper-rustls) | `0.27.6` | `0.27.7` |
| [liblzma-sys](https://github.com/portable-network-archive/liblzma-rs) | `0.4.3` | `0.4.4` |
| [libz-rs-sys](https://github.com/trifectatechfoundation/zlib-rs) | `0.5.0` | `0.5.1` |
| [serde_spanned](https://github.com/toml-rs/toml) | `0.6.8` | `0.6.9` |
| [smallvec](https://github.com/servo/rust-smallvec) | `1.15.0` | `1.15.1` |
| [toml_datetime](https://github.com/toml-rs/toml) | `0.6.9` | `0.6.11` |
| [toml_write](https://github.com/toml-rs/toml) | `0.1.1` | `0.1.2` |
| [tracing-attributes](https://github.com/tokio-rs/tracing) | `0.1.28` | `0.1.29` |
| [tracing-core](https://github.com/tokio-rs/tracing) | `0.1.33` | `0.1.34` |
| [zlib-rs](https://github.com/trifectatechfoundation/zlib-rs) | `0.5.0` | `0.5.1` |


Updates `bumpalo` from 3.18.0 to 3.18.1
- [Changelog](https://github.com/fitzgen/bumpalo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fitzgen/bumpalo/compare/v3.18.0...v3.18.1)

Updates `cc` from 1.2.25 to 1.2.26
- [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.25...cc-v1.2.26)

Updates `flate2` from 1.1.1 to 1.1.2
- [Release notes](https://github.com/rust-lang/flate2-rs/releases)
- [Commits](https://github.com/rust-lang/flate2-rs/compare/1.1.1...1.1.2)

Updates `hyper-rustls` from 0.27.6 to 0.27.7
- [Release notes](https://github.com/rustls/hyper-rustls/releases)
- [Commits](https://github.com/rustls/hyper-rustls/compare/v/0.27.6...v/0.27.7)

Updates `liblzma-sys` from 0.4.3 to 0.4.4
- [Release notes](https://github.com/portable-network-archive/liblzma-rs/releases)
- [Commits](https://github.com/portable-network-archive/liblzma-rs/compare/liblzma-sys-0.4.3...liblzma-sys-0.4.4)

Updates `libz-rs-sys` from 0.5.0 to 0.5.1
- [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.0...v0.5.1)

Updates `serde_spanned` from 0.6.8 to 0.6.9
- [Commits](https://github.com/toml-rs/toml/compare/serde_spanned-v0.6.8...serde_spanned-v0.6.9)

Updates `smallvec` from 1.15.0 to 1.15.1
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.15.0...v1.15.1)

Updates `toml_datetime` from 0.6.9 to 0.6.11
- [Commits](https://github.com/toml-rs/toml/compare/toml_datetime-v0.6.9...toml_datetime-v0.6.11)

Updates `toml_write` from 0.1.1 to 0.1.2
- [Commits](https://github.com/toml-rs/toml/compare/toml_write-v0.1.1...toml_write-v0.1.2)

Updates `tracing-attributes` from 0.1.28 to 0.1.29
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-attributes-0.1.28...tracing-attributes-0.1.29)

Updates `tracing-core` from 0.1.33 to 0.1.34
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-core-0.1.33...tracing-core-0.1.34)

Updates `zlib-rs` from 0.5.0 to 0.5.1
- [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.0...v0.5.1)

---
updated-dependencies:
- dependency-name: bumpalo
  dependency-version: 3.18.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.26
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: flate2
  dependency-version: 1.1.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: hyper-rustls
  dependency-version: 0.27.7
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: liblzma-sys
  dependency-version: 0.4.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libz-rs-sys
  dependency-version: 0.5.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: serde_spanned
  dependency-version: 0.6.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: smallvec
  dependency-version: 1.15.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml_datetime
  dependency-version: 0.6.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml_write
  dependency-version: 0.1.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tracing-attributes
  dependency-version: 0.1.29
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tracing-core
  dependency-version: 0.1.34
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zlib-rs
  dependency-version: 0.5.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-07 09:45:20 +00:00
zhom b0ca14c184 chore: version bump 2025-06-07 06:14:34 +04:00
zhom eea94ad360 chore: linter 2025-06-07 06:12:44 +04:00
zhom 2b678ed04d fix: hide brave releases without zip files for linux 2025-06-07 06:00:55 +04:00
zhom dff201ddec chore: add cursor rule 2025-06-07 05:46:32 +04:00
zhom 743ad59348 fix: consider all brave releases nightly if there is no release name 2025-06-07 05:42:57 +04:00
zhom d43e9ef21b chore: ui copy 2025-06-07 05:30:33 +04:00
zhom 7515cbacd6 refactor: check for similar-named brave binaries 2025-06-07 05:25:57 +04:00
zhom f41172e822 fix: update brave url for linux arm 2025-06-07 05:12:04 +04:00
zhom 25ce691bbc fix: use correct file extension on linux 2025-06-07 05:02:24 +04:00
zhom b945ee7088 chore: add cursor rule 2025-06-07 04:39:57 +04:00
zhom eb3589b4c0 chore: add cursor rule 2025-06-07 02:19:55 +04:00
zhom b71b9a00ca docs: update feature list in readme 2025-06-07 02:08:03 +04:00
zhom 6535b37c98 Merge pull request #17 from zhom/docs/readme-tauri-duplicate-link
docs: remove duplicate tauri link
2025-06-07 01:20:39 +04:00
zhom bc72a837e2 docs: remove duplicate tauri link 2025-06-07 01:18:27 +04:00
zhom fb84068d30 docs: add features section to readme 2025-06-07 00:30:06 +04:00
zhom 5024eab062 chore: version bump 2025-06-07 00:01:01 +04:00
zhom 8137f9bf8d fix: adjust download logic to work with latest firefox cdn 2025-06-06 23:40:51 +04:00
zhom e2547c6ec7 docs: update agent instructions 2025-06-06 23:29:10 +04:00
zhom d8d59d2bd5 fix: extraction and version detection 2025-06-06 23:22:45 +04:00
zhom b84350eb13 docs: add ai agents instruction file 2025-06-06 23:22:29 +04:00
zhom 383cef916c docs: remove duplicate star history graph 2025-06-06 14:35:13 +04:00
zhom 743bc059be docs: update readme 2025-06-06 14:33:52 +04:00
zhom c46f54536b chore: linting 2025-06-06 04:52:05 +04:00
zhom 6cbc8627a1 chore: version bump 2025-06-06 04:43:22 +04:00
zhom a4f4cc2f27 style: clean up settings dialog 2025-06-06 04:41:50 +04:00
zhom 21c4d0a8ab build: use custom dependabot token for dependency automerge 2025-06-06 04:29:18 +04:00
zhom 9335149153 Merge pull request #12 from zhom/dependabot/cargo/src-tauri/rust-dependencies-ac9a5aa5e0
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 6 updates
2025-06-06 04:08:04 +04:00
zhom 6711659231 Merge pull request #13 from zhom/dependabot/npm_and_yarn/frontend-dependencies-1eccf95907
deps(deps): bump @types/node from 22.15.29 to 22.15.30 in the frontend-dependencies group
2025-06-06 04:07:37 +04:00
zhom 8a592e3d7d Merge pull request #14 from zhom/dependabot/npm_and_yarn/nodecar/nodecar-dependencies-1eccf95907
deps(nodecar)(deps): bump @types/node from 22.15.29 to 22.15.30 in /nodecar in the nodecar-dependencies group
2025-06-06 04:07:11 +04:00
zhom beea23307b build: install xdg-utils on ubuntu arm 2025-06-06 03:34:46 +04:00
zhom 19b66d006d build: switch to ubuntu-22.04-arm for linux arm build 2025-06-06 03:12:24 +04:00
zhom c7a36f6cd0 build: try to update mirrors if security.ubuntu.com is down 2025-06-06 02:51:39 +04:00
zhom 7404cb3ff8 build: install arm64 libraries on linux arm build worker 2025-06-06 02:12:52 +04:00
zhom ee91445fe1 feat: add ability to import existing profiles 2025-06-06 02:12:21 +04:00
zhom 77d53c7f32 build: install pkg-config on ubuntu 2025-06-06 01:28:34 +04:00
zhom a21f22a916 build: remove invalid package installation 2025-06-06 01:09:34 +04:00
dependabot[bot] 4aaf2eecbc deps(nodecar)(deps): bump @types/node
Bumps the nodecar-dependencies group in /nodecar with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 22.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nodecar-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-05 21:02:39 +00:00
dependabot[bot] f750e64b81 deps(deps): bump @types/node in the frontend-dependencies group
Bumps the frontend-dependencies group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 22.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-05 21:00:09 +00:00
dependabot[bot] 16fd3e3c5e deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [sysinfo](https://github.com/GuillaumeGomez/sysinfo) | `0.35.1` | `0.35.2` |
| [bumpalo](https://github.com/fitzgen/bumpalo) | `3.17.0` | `3.18.0` |
| [hyper-util](https://github.com/hyperium/hyper-util) | `0.1.13` | `0.1.14` |
| [tower-http](https://github.com/tower-rs/tower-http) | `0.6.5` | `0.6.6` |
| [windows-registry](https://github.com/microsoft/windows-rs) | `0.4.0` | `0.5.2` |
| [windows-strings](https://github.com/microsoft/windows-rs) | `0.3.1` | `0.4.2` |


Updates `sysinfo` from 0.35.1 to 0.35.2
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/compare/v0.35.1...v0.35.2)

Updates `bumpalo` from 3.17.0 to 3.18.0
- [Changelog](https://github.com/fitzgen/bumpalo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fitzgen/bumpalo/compare/3.17.0...v3.18.0)

Updates `hyper-util` from 0.1.13 to 0.1.14
- [Release notes](https://github.com/hyperium/hyper-util/releases)
- [Changelog](https://github.com/hyperium/hyper-util/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper-util/compare/v0.1.13...v0.1.14)

Updates `tower-http` from 0.6.5 to 0.6.6
- [Release notes](https://github.com/tower-rs/tower-http/releases)
- [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.6.5...tower-http-0.6.6)

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

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

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-version: 0.35.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: bumpalo
  dependency-version: 3.18.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: hyper-util
  dependency-version: 0.1.14
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tower-http
  dependency-version: 0.6.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: windows-registry
  dependency-version: 0.5.2
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: windows-strings
  dependency-version: 0.4.2
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-05 21:00:03 +00:00
zhom 93eae1d77f build: set pkg-config variables for linux arm 2025-06-06 00:57:21 +04:00
zhom 82615c24bd build: set cross-compile build variables for linux arm 2025-06-06 00:39:05 +04:00
zhom 0769106a51 build: run dependabot on saturday 2025-06-06 00:32:56 +04:00
zhom 18aa3cb87b test: fix 2025-06-06 00:22:01 +04:00
zhom f066105c0f build: remove universal macos build until @yao-pkg/pkg has support for universal macos binaries 2025-06-06 00:10:20 +04:00
zhom 9d8b3629f6 docs: readme 2025-06-06 00:04:38 +04:00
zhom 353e149886 build: switch to ubuntu 22 for linux build 2025-06-05 23:59:44 +04:00
zhom 2258edbb18 test: fix 2025-06-05 23:45:03 +04:00
zhom 69fff99bbe docs: readme 2025-06-05 23:38:16 +04:00
zhom a277b7d8a2 build: fix script path 2025-06-05 23:35:38 +04:00
zhom 7af3f7e86a docs: update readme 2025-06-05 23:34:48 +04:00
zhom de9c47241a build: cargo audit && copy binary instead of moving 2025-06-05 23:25:43 +04:00
zhom 5747729e30 build: use cargo audit for rust linting 2025-06-05 23:12:08 +04:00
zhom f71bda0fbf build: don't set extra build variables on linux arm systems 2025-06-05 22:59:51 +04:00
zhom 67bfb17e5a fix: correctly mark tor browser alpha versions: 2025-06-05 22:11:57 +04:00
zhom bb2eb65c1e chore: linter 2025-06-05 21:26:47 +04:00
zhom f397568785 docs: update readme to include linux 2025-06-05 21:17:10 +04:00
zhom 0da34f04cb feat: linux support preview 2025-06-05 21:15:05 +04:00
zhom 6836d73ffa refactor: prepare the backend for cross-platform function 2025-06-03 23:37:31 +04:00
zhom 63b890d47f docs: add responsible desclosure guidelines 2025-06-03 17:01:47 +04:00
zhom aeb6a08fc8 build: automatically merge dependendabot changes if they pass ci 2025-06-03 16:54:14 +04:00
zhom c698fff101 build: manage nodecar dependencies via workspace 2025-06-03 16:53:27 +04:00
zhom ccfd1f81f6 build: fail build if security scan fails 2025-06-03 16:30:34 +04:00
zhom 4c42099661 fix: osv permissions 2025-06-03 15:33:45 +04:00
zhom 4c4aa10d8c fix: reset lock files after bad dependabot merge 2025-06-03 15:33:03 +04:00
zhom a1a6ef63e4 Merge pull request #11 from zhom/dependabot/npm_and_yarn/frontend-dependencies-852ce57221
deps(deps): bump the frontend-dependencies group across 1 directory with 12 updates
2025-06-03 15:26:21 +04:00
dependabot[bot] bd7b9f1d9f deps(deps): bump the frontend-dependencies group across 1 directory with 12 updates
Bumps the frontend-dependencies group with 1 update in the / directory: [@jridgewell/trace-mapping](https://github.com/jridgewell/trace-mapping).


Updates `@jridgewell/trace-mapping` from 0.3.9 to 0.3.25
- [Release notes](https://github.com/jridgewell/trace-mapping/releases)
- [Commits](https://github.com/jridgewell/trace-mapping/compare/v0.3.9...v0.3.25)

Updates `ansi-regex` from 5.0.1 to 6.1.0
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v5.0.1...v6.1.0)

Updates `chownr` from 1.1.4 to 3.0.0
- [Commits](https://github.com/isaacs/chownr/compare/v1.1.4...v3.0.0)

Updates `emoji-regex` from 8.0.0 to 9.2.2
- [Commits](https://github.com/mathiasbynens/emoji-regex/compare/v8.0.0...v9.2.2)

Updates `has-flag` from 3.0.0 to 4.0.0
- [Release notes](https://github.com/sindresorhus/has-flag/releases)
- [Commits](https://github.com/sindresorhus/has-flag/compare/v3.0.0...v4.0.0)

Updates `is-fullwidth-code-point` from 3.0.0 to 4.0.0
- [Release notes](https://github.com/sindresorhus/is-fullwidth-code-point/releases)
- [Commits](https://github.com/sindresorhus/is-fullwidth-code-point/compare/v3.0.0...v4.0.0)

Updates `isarray` from 1.0.0 to 2.0.5
- [Release notes](https://github.com/juliangruber/isarray/releases)
- [Commits](https://github.com/juliangruber/isarray/compare/v1.0.0...v2.0.5)

Updates `string-width` from 4.2.3 to 7.2.0
- [Release notes](https://github.com/sindresorhus/string-width/releases)
- [Commits](https://github.com/sindresorhus/string-width/compare/v4.2.3...v7.2.0)

Updates `strip-ansi` from 6.0.1 to 7.1.0
- [Release notes](https://github.com/chalk/strip-ansi/releases)
- [Commits](https://github.com/chalk/strip-ansi/compare/v6.0.1...v7.1.0)

Updates `strip-json-comments` from 2.0.1 to 3.1.1
- [Release notes](https://github.com/sindresorhus/strip-json-comments/releases)
- [Commits](https://github.com/sindresorhus/strip-json-comments/compare/v2.0.1...v3.1.1)

Updates `supports-color` from 5.5.0 to 7.2.0
- [Release notes](https://github.com/chalk/supports-color/releases)
- [Commits](https://github.com/chalk/supports-color/compare/v5.5.0...v7.2.0)

Updates `wrap-ansi` from 7.0.0 to 9.0.0
- [Release notes](https://github.com/chalk/wrap-ansi/releases)
- [Commits](https://github.com/chalk/wrap-ansi/compare/v7.0.0...v9.0.0)

---
updated-dependencies:
- dependency-name: "@jridgewell/trace-mapping"
  dependency-version: 0.3.25
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: ansi-regex
  dependency-version: 6.1.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: chownr
  dependency-version: 3.0.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: emoji-regex
  dependency-version: 9.2.2
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: has-flag
  dependency-version: 4.0.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: is-fullwidth-code-point
  dependency-version: 4.0.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: isarray
  dependency-version: 2.0.5
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: string-width
  dependency-version: 7.2.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: strip-ansi
  dependency-version: 7.1.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: strip-json-comments
  dependency-version: 3.1.1
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: supports-color
  dependency-version: 7.2.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: wrap-ansi
  dependency-version: 9.0.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 11:24:36 +00:00
zhom f93b5daa9b Merge pull request #8 from zhom/dependabot/npm_and_yarn/nodecar/nodecar-dependencies-a69e916d1e
deps(nodecar)(deps): bump the nodecar-dependencies group in /nodecar with 4 updates
2025-06-03 15:10:21 +04:00
zhom f7f45bdc90 Merge pull request #10 from zhom/dependabot/cargo/src-tauri/rust-dependencies-04ecd6b2d6
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 3 updates
2025-06-03 15:09:36 +04:00
dependabot[bot] 48067ee3a7 deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 3 updates: [reqwest](https://github.com/seanmonstar/reqwest), [camino](https://github.com/camino-rs/camino) and [tower-http](https://github.com/tower-rs/tower-http).


Updates `reqwest` from 0.12.18 to 0.12.19
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.18...v0.12.19)

Updates `camino` from 1.1.9 to 1.1.10
- [Release notes](https://github.com/camino-rs/camino/releases)
- [Changelog](https://github.com/camino-rs/camino/blob/main/CHANGELOG.md)
- [Commits](https://github.com/camino-rs/camino/compare/camino-1.1.9...camino-1.1.10)

Updates `tower-http` from 0.6.4 to 0.6.5
- [Release notes](https://github.com/tower-rs/tower-http/releases)
- [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.6.4...tower-http-0.6.5)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-version: 0.12.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: camino
  dependency-version: 1.1.10
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tower-http
  dependency-version: 0.6.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 11:07:37 +00:00
zhom 87bd75aa21 chore: pnpm update && pnpm install --ignore-workspace 2025-06-03 15:07:13 +04:00
zhom cf443061b6 chore: pnpm install 2025-06-03 15:02:46 +04:00
zhom ca662d91a1 Merge pull request #7 from zhom/dependabot/github_actions/github-actions-0c45e47a22
ci(deps): bump google/osv-scanner-action from 1.7.1 to 2.0.2 in the github-actions group
2025-06-03 14:58:38 +04:00
dependabot[bot] 2963dbc0f9 deps(nodecar)(deps): bump the nodecar-dependencies group
Bumps the nodecar-dependencies group in /nodecar with 4 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [@yao-pkg/pkg](https://github.com/yao-pkg/pkg), [commander](https://github.com/tj/commander.js) and [proxy-chain](https://github.com/apify/proxy-chain).


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

Updates `@yao-pkg/pkg` from 6.4.1 to 6.5.1
- [Release notes](https://github.com/yao-pkg/pkg/releases)
- [Changelog](https://github.com/yao-pkg/pkg/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yao-pkg/pkg/compare/v6.4.1...v6.5.1)

Updates `commander` from 13.1.0 to 14.0.0
- [Release notes](https://github.com/tj/commander.js/releases)
- [Changelog](https://github.com/tj/commander.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tj/commander.js/compare/v13.1.0...v14.0.0)

Updates `proxy-chain` from 2.5.8 to 2.5.9
- [Release notes](https://github.com/apify/proxy-chain/releases)
- [Changelog](https://github.com/apify/proxy-chain/blob/master/CHANGELOG.md)
- [Commits](https://github.com/apify/proxy-chain/compare/v2.5.8...v2.5.9)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 22.15.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nodecar-dependencies
- dependency-name: "@yao-pkg/pkg"
  dependency-version: 6.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nodecar-dependencies
- dependency-name: commander
  dependency-version: 14.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: nodecar-dependencies
- dependency-name: proxy-chain
  dependency-version: 2.5.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nodecar-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 10:51:44 +00:00
dependabot[bot] 225ed05d08 ci(deps): bump google/osv-scanner-action in the github-actions group
Bumps the github-actions group with 1 update: [google/osv-scanner-action](https://github.com/google/osv-scanner-action).


Updates `google/osv-scanner-action` from 1.7.1 to 2.0.2
- [Release notes](https://github.com/google/osv-scanner-action/releases)
- [Commits](https://github.com/google/osv-scanner-action/compare/1f1242919d8a60496dd1874b24b62b2370ed4c78...e69cc6c86b31f1e7e23935bbe7031b50e51082de)

---
updated-dependencies:
- dependency-name: google/osv-scanner-action
  dependency-version: 2.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 10:48:03 +00:00
zhom 97de246ac6 feat: use osv, add pr checks, extend dependabot 2025-06-03 14:46:58 +04:00
zhom b00f62ebec fix: improve toast and dialog interations 2025-06-03 13:56:58 +04:00
zhom 2025a2a690 feat: better integrate with macos titlebar 2025-06-03 13:11:17 +04:00
zhom 2f1faa02e4 chore: version bump 2025-06-02 18:30:50 +04:00
zhom 7a5b807828 feat: select running profile if one available for opened urls 2025-06-02 13:41:38 +04:00
zhom d0a5c16ce9 chore: don't try to auto-approve dependabot pr 2025-06-02 13:12:35 +04:00
zhom e2e1ad1582 Merge pull request #5 from zhom/dependabot/npm_and_yarn/all-f53a2d5633
build(deps): bump the all group with 109 updates
2025-06-02 13:09:29 +04:00
dependabot[bot] cb61861503 build(deps): bump the all group with 109 updates
Bumps the all group with 109 updates:

| Package | From | To |
| --- | --- | --- |
| [next](https://github.com/vercel/next.js) | `15.3.2` | `15.3.3` |
| [sonner](https://github.com/emilkowalski/sonner) | `2.0.3` | `2.0.4` |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.27.0` | `9.28.0` |
| [@next/eslint-plugin-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-plugin-next) | `15.3.2` | `15.3.3` |
| [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.1.7` | `4.1.8` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.15.21` | `22.15.29` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.1.5` | `19.1.6` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.32.1` | `8.33.0` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.32.1` | `8.33.0` |
| [eslint](https://github.com/eslint/eslint) | `9.27.0` | `9.28.0` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `15.3.2` | `15.3.3` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `15.5.2` | `16.1.0` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.1.7` | `4.1.8` |
| [tw-animate-css](https://github.com/Wombosvideo/tw-animate-css) | `1.3.0` | `1.3.3` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.32.1` | `8.33.0` |
| [@babel/compat-data](https://github.com/babel/babel/tree/HEAD/packages/babel-compat-data) | `7.27.2` | `7.27.3` |
| [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) | `7.27.1` | `7.27.4` |
| [@babel/generator](https://github.com/babel/babel/tree/HEAD/packages/babel-generator) | `7.27.1` | `7.27.3` |
| [@babel/helper-module-transforms](https://github.com/babel/babel/tree/HEAD/packages/babel-helper-module-transforms) | `7.27.1` | `7.27.3` |
| [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) | `7.27.1` | `7.27.4` |
| [@babel/parser](https://github.com/babel/babel/tree/HEAD/packages/babel-parser) | `7.27.2` | `7.27.4` |
| [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) | `7.27.1` | `7.27.4` |
| [@babel/types](https://github.com/babel/babel/tree/HEAD/packages/babel-types) | `7.27.1` | `7.27.3` |
| [@esbuild/aix-ppc64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/android-arm64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/android-arm](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/android-x64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/darwin-arm64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/darwin-x64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/freebsd-arm64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/freebsd-x64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/linux-arm64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/linux-arm](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/linux-ia32](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/linux-loong64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/linux-mips64el](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/linux-ppc64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/linux-riscv64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/linux-s390x](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/linux-x64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/netbsd-arm64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/netbsd-x64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/openbsd-arm64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/openbsd-x64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/sunos-x64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/win32-arm64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/win32-ia32](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@esbuild/win32-x64](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [@next/env](https://github.com/vercel/next.js/tree/HEAD/packages/next-env) | `15.3.2` | `15.3.3` |
| [@next/swc-darwin-arm64](https://github.com/vercel/next.js/tree/HEAD/crates/napi/npm/darwin-arm64) | `15.3.2` | `15.3.3` |
| [@next/swc-darwin-x64](https://github.com/vercel/next.js/tree/HEAD/crates/napi/npm/darwin-x64) | `15.3.2` | `15.3.3` |
| [@next/swc-linux-arm64-gnu](https://github.com/vercel/next.js/tree/HEAD/crates/napi/npm/linux-arm64-gnu) | `15.3.2` | `15.3.3` |
| [@next/swc-linux-arm64-musl](https://github.com/vercel/next.js/tree/HEAD/crates/napi/npm/linux-arm64-musl) | `15.3.2` | `15.3.3` |
| [@next/swc-linux-x64-gnu](https://github.com/vercel/next.js/tree/HEAD/crates/napi/npm/linux-x64-gnu) | `15.3.2` | `15.3.3` |
| [@next/swc-linux-x64-musl](https://github.com/vercel/next.js/tree/HEAD/crates/napi/npm/linux-x64-musl) | `15.3.2` | `15.3.3` |
| [@next/swc-win32-arm64-msvc](https://github.com/vercel/next.js/tree/HEAD/crates/napi/npm/win32-arm64-msvc) | `15.3.2` | `15.3.3` |
| [@next/swc-win32-x64-msvc](https://github.com/vercel/next.js/tree/HEAD/crates/napi/npm/win32-x64-msvc) | `15.3.2` | `15.3.3` |
| [@rollup/rollup-android-arm-eabi](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-android-arm64](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-darwin-arm64](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-darwin-x64](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-freebsd-arm64](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-freebsd-x64](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-linux-arm-gnueabihf](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-linux-arm-musleabihf](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-linux-arm64-gnu](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-linux-arm64-musl](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-linux-loongarch64-gnu](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-linux-powerpc64le-gnu](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-linux-riscv64-gnu](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-linux-riscv64-musl](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-linux-s390x-gnu](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-linux-x64-musl](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-win32-arm64-msvc](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-win32-ia32-msvc](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@rollup/rollup-win32-x64-msvc](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [@tailwindcss/node](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-node) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide-android-arm64](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/android-arm64) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide-darwin-arm64](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/darwin-arm64) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide-darwin-x64](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/darwin-x64) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide-freebsd-x64](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/freebsd-x64) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide-linux-arm-gnueabihf](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/linux-arm-gnueabihf) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide-linux-arm64-gnu](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/linux-arm64-gnu) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide-linux-arm64-musl](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/linux-arm64-musl) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide-linux-x64-gnu](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/linux-x64-gnu) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide-linux-x64-musl](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/linux-x64-musl) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide-wasm32-wasi](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide-win32-arm64-msvc](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/win32-arm64-msvc) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide-win32-x64-msvc](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node/npm/win32-x64-msvc) | `4.1.7` | `4.1.8` |
| [@tailwindcss/oxide](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/crates/node) | `4.1.7` | `4.1.8` |
| [@typescript-eslint/scope-manager](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/scope-manager) | `8.32.1` | `8.33.0` |
| [@typescript-eslint/type-utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/type-utils) | `8.32.1` | `8.33.0` |
| [@typescript-eslint/types](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/types) | `8.32.1` | `8.33.0` |
| [@typescript-eslint/typescript-estree](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-estree) | `8.32.1` | `8.33.0` |
| [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) | `8.32.1` | `8.33.0` |
| [@typescript-eslint/visitor-keys](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/visitor-keys) | `8.32.1` | `8.33.0` |
| [browserslist](https://github.com/browserslist/browserslist) | `4.24.5` | `4.25.0` |
| [caniuse-lite](https://github.com/browserslist/caniuse-lite) | `1.0.30001718` | `1.0.30001720` |
| [commander](https://github.com/tj/commander.js) | `13.1.0` | `14.0.0` |
| [electron-to-chromium](https://github.com/kilian/electron-to-chromium) | `1.5.157` | `1.5.161` |
| [es-abstract](https://github.com/ljharb/es-abstract) | `1.23.10` | `1.24.0` |
| [esbuild](https://github.com/evanw/esbuild) | `0.25.4` | `0.25.5` |
| [fdir](https://github.com/thecodrr/fdir) | `6.4.4` | `6.4.5` |
| [onetime](https://github.com/sindresorhus/onetime) | `6.0.0` | `7.0.0` |
| [react-remove-scroll](https://github.com/theKashey/react-remove-scroll) | `2.7.0` | `2.7.1` |
| [rollup](https://github.com/rollup/rollup) | `4.41.0` | `4.41.1` |
| [tinyglobby](https://github.com/SuperchupuDev/tinyglobby) | `0.2.13` | `0.2.14` |
| [unrs-resolver](https://github.com/unrs/unrs-resolver) | `1.7.2` | `1.7.8` |


Updates `next` from 15.3.2 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.3.2...v15.3.3)

Updates `sonner` from 2.0.3 to 2.0.4
- [Release notes](https://github.com/emilkowalski/sonner/releases)
- [Commits](https://github.com/emilkowalski/sonner/compare/v2.0.3...v2.0.4)

Updates `@eslint/js` from 9.27.0 to 9.28.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.28.0/packages/js)

Updates `@next/eslint-plugin-next` from 15.3.2 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/packages/eslint-plugin-next)

Updates `@tailwindcss/postcss` from 4.1.7 to 4.1.8
- [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.8/packages/@tailwindcss-postcss)

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

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

Updates `@typescript-eslint/eslint-plugin` from 8.32.1 to 8.33.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.33.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.32.1 to 8.33.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.33.0/packages/parser)

Updates `eslint` from 9.27.0 to 9.28.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.27.0...v9.28.0)

Updates `eslint-config-next` from 15.3.2 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/packages/eslint-config-next)

Updates `lint-staged` from 15.5.2 to 16.1.0
- [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/v15.5.2...v16.1.0)

Updates `tailwindcss` from 4.1.7 to 4.1.8
- [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.8/packages/tailwindcss)

Updates `tw-animate-css` from 1.3.0 to 1.3.3
- [Release notes](https://github.com/Wombosvideo/tw-animate-css/releases)
- [Commits](https://github.com/Wombosvideo/tw-animate-css/compare/v1.3.0...v1.3.3)

Updates `typescript-eslint` from 8.32.1 to 8.33.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.33.0/packages/typescript-eslint)

Updates `@babel/compat-data` from 7.27.2 to 7.27.3
- [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.27.3/packages/babel-compat-data)

Updates `@babel/core` from 7.27.1 to 7.27.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.27.4/packages/babel-core)

Updates `@babel/generator` from 7.27.1 to 7.27.3
- [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.27.3/packages/babel-generator)

Updates `@babel/helper-module-transforms` from 7.27.1 to 7.27.3
- [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.27.3/packages/babel-helper-module-transforms)

Updates `@babel/helpers` from 7.27.1 to 7.27.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.27.4/packages/babel-helpers)

Updates `@babel/parser` from 7.27.2 to 7.27.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.27.4/packages/babel-parser)

Updates `@babel/traverse` from 7.27.1 to 7.27.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.27.4/packages/babel-traverse)

Updates `@babel/types` from 7.27.1 to 7.27.3
- [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.27.3/packages/babel-types)

Updates `@esbuild/aix-ppc64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/android-arm64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/android-arm` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/android-x64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/darwin-arm64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/darwin-x64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/freebsd-arm64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/freebsd-x64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/linux-arm64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/linux-arm` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/linux-ia32` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/linux-loong64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/linux-mips64el` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/linux-ppc64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/linux-riscv64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/linux-s390x` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/linux-x64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/netbsd-arm64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/netbsd-x64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/openbsd-arm64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/openbsd-x64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/sunos-x64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/win32-arm64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/win32-ia32` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@esbuild/win32-x64` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `@next/env` from 15.3.2 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/packages/next-env)

Updates `@next/swc-darwin-arm64` from 15.3.2 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/crates/napi/npm/darwin-arm64)

Updates `@next/swc-darwin-x64` from 15.3.2 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/crates/napi/npm/darwin-x64)

Updates `@next/swc-linux-arm64-gnu` from 15.3.2 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/crates/napi/npm/linux-arm64-gnu)

Updates `@next/swc-linux-arm64-musl` from 15.3.2 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/crates/napi/npm/linux-arm64-musl)

Updates `@next/swc-linux-x64-gnu` from 15.3.2 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/crates/napi/npm/linux-x64-gnu)

Updates `@next/swc-linux-x64-musl` from 15.3.2 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/crates/napi/npm/linux-x64-musl)

Updates `@next/swc-win32-arm64-msvc` from 15.3.2 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/crates/napi/npm/win32-arm64-msvc)

Updates `@next/swc-win32-x64-msvc` from 15.3.2 to 15.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/crates/napi/npm/win32-x64-msvc)

Updates `@rollup/rollup-android-arm-eabi` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-android-arm64` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-darwin-arm64` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-darwin-x64` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-freebsd-arm64` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-freebsd-x64` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-linux-arm-gnueabihf` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-linux-arm-musleabihf` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-linux-arm64-gnu` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-linux-arm64-musl` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-linux-loongarch64-gnu` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-linux-powerpc64le-gnu` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-linux-riscv64-gnu` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-linux-riscv64-musl` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-linux-s390x-gnu` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-linux-x64-gnu` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-linux-x64-musl` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-win32-arm64-msvc` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-win32-ia32-msvc` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@rollup/rollup-win32-x64-msvc` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `@tailwindcss/node` from 4.1.7 to 4.1.8
- [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.8/packages/@tailwindcss-node)

Updates `@tailwindcss/oxide-android-arm64` from 4.1.7 to 4.1.8
- [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.8/crates/node/npm/android-arm64)

Updates `@tailwindcss/oxide-darwin-arm64` from 4.1.7 to 4.1.8
- [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.8/crates/node/npm/darwin-arm64)

Updates `@tailwindcss/oxide-darwin-x64` from 4.1.7 to 4.1.8
- [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.8/crates/node/npm/darwin-x64)

Updates `@tailwindcss/oxide-freebsd-x64` from 4.1.7 to 4.1.8
- [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.8/crates/node/npm/freebsd-x64)

Updates `@tailwindcss/oxide-linux-arm-gnueabihf` from 4.1.7 to 4.1.8
- [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.8/crates/node/npm/linux-arm-gnueabihf)

Updates `@tailwindcss/oxide-linux-arm64-gnu` from 4.1.7 to 4.1.8
- [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.8/crates/node/npm/linux-arm64-gnu)

Updates `@tailwindcss/oxide-linux-arm64-musl` from 4.1.7 to 4.1.8
- [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.8/crates/node/npm/linux-arm64-musl)

Updates `@tailwindcss/oxide-linux-x64-gnu` from 4.1.7 to 4.1.8
- [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.8/crates/node/npm/linux-x64-gnu)

Updates `@tailwindcss/oxide-linux-x64-musl` from 4.1.7 to 4.1.8
- [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.8/crates/node/npm/linux-x64-musl)

Updates `@tailwindcss/oxide-wasm32-wasi` from 4.1.7 to 4.1.8
- [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.8/crates/node)

Updates `@tailwindcss/oxide-win32-arm64-msvc` from 4.1.7 to 4.1.8
- [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.8/crates/node/npm/win32-arm64-msvc)

Updates `@tailwindcss/oxide-win32-x64-msvc` from 4.1.7 to 4.1.8
- [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.8/crates/node/npm/win32-x64-msvc)

Updates `@tailwindcss/oxide` from 4.1.7 to 4.1.8
- [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.8/crates/node)

Updates `@typescript-eslint/scope-manager` from 8.32.1 to 8.33.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/scope-manager/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.33.0/packages/scope-manager)

Updates `@typescript-eslint/type-utils` from 8.32.1 to 8.33.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/type-utils/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.33.0/packages/type-utils)

Updates `@typescript-eslint/types` from 8.32.1 to 8.33.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/types/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.33.0/packages/types)

Updates `@typescript-eslint/typescript-estree` from 8.32.1 to 8.33.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-estree/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.33.0/packages/typescript-estree)

Updates `@typescript-eslint/utils` from 8.32.1 to 8.33.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.33.0/packages/utils)

Updates `@typescript-eslint/visitor-keys` from 8.32.1 to 8.33.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/visitor-keys/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.33.0/packages/visitor-keys)

Updates `browserslist` from 4.24.5 to 4.25.0
- [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.24.5...4.25.0)

Updates `caniuse-lite` from 1.0.30001718 to 1.0.30001720
- [Commits](https://github.com/browserslist/caniuse-lite/compare/1.0.30001718...1.0.30001720)

Updates `commander` from 13.1.0 to 14.0.0
- [Release notes](https://github.com/tj/commander.js/releases)
- [Changelog](https://github.com/tj/commander.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tj/commander.js/compare/v13.1.0...v14.0.0)

Updates `electron-to-chromium` from 1.5.157 to 1.5.161
- [Changelog](https://github.com/Kilian/electron-to-chromium/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kilian/electron-to-chromium/compare/v1.5.157...v1.5.161)

Updates `es-abstract` from 1.23.10 to 1.24.0
- [Changelog](https://github.com/ljharb/es-abstract/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/es-abstract/compare/v1.23.10...v1.24.0)

Updates `esbuild` from 0.25.4 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

Updates `fdir` from 6.4.4 to 6.4.5
- [Release notes](https://github.com/thecodrr/fdir/releases)
- [Commits](https://github.com/thecodrr/fdir/compare/v6.4.4...v6.4.5)

Updates `onetime` from 6.0.0 to 7.0.0
- [Release notes](https://github.com/sindresorhus/onetime/releases)
- [Commits](https://github.com/sindresorhus/onetime/compare/v6.0.0...v7.0.0)

Updates `react-remove-scroll` from 2.7.0 to 2.7.1
- [Release notes](https://github.com/theKashey/react-remove-scroll/releases)
- [Changelog](https://github.com/theKashey/react-remove-scroll/blob/master/CHANGELOG.md)
- [Commits](https://github.com/theKashey/react-remove-scroll/compare/v2.7.0...v2.7.1)

Updates `rollup` from 4.41.0 to 4.41.1
- [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.41.0...v4.41.1)

Updates `tinyglobby` from 0.2.13 to 0.2.14
- [Release notes](https://github.com/SuperchupuDev/tinyglobby/releases)
- [Changelog](https://github.com/SuperchupuDev/tinyglobby/blob/main/CHANGELOG.md)
- [Commits](https://github.com/SuperchupuDev/tinyglobby/compare/0.2.13...0.2.14)

Updates `unrs-resolver` from 1.7.2 to 1.7.8
- [Release notes](https://github.com/unrs/unrs-resolver/releases)
- [Changelog](https://github.com/unrs/unrs-resolver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/unrs/unrs-resolver/compare/v1.7.2...v1.7.8)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: sonner
  dependency-version: 2.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@eslint/js"
  dependency-version: 9.28.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: "@next/eslint-plugin-next"
  dependency-version: 15.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@types/node"
  dependency-version: 22.15.29
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@types/react"
  dependency-version: 19.1.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.33.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.33.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: eslint
  dependency-version: 9.28.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: eslint-config-next
  dependency-version: 15.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: lint-staged
  dependency-version: 16.1.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: all
- dependency-name: tailwindcss
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: tw-animate-css
  dependency-version: 1.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: typescript-eslint
  dependency-version: 8.33.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: "@babel/compat-data"
  dependency-version: 7.27.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@babel/core"
  dependency-version: 7.27.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@babel/generator"
  dependency-version: 7.27.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@babel/helper-module-transforms"
  dependency-version: 7.27.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@babel/helpers"
  dependency-version: 7.27.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@babel/parser"
  dependency-version: 7.27.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@babel/traverse"
  dependency-version: 7.27.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@babel/types"
  dependency-version: 7.27.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/aix-ppc64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/android-arm64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/android-arm"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/android-x64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/darwin-arm64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/darwin-x64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/freebsd-arm64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/freebsd-x64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/linux-arm64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/linux-arm"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/linux-ia32"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/linux-loong64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/linux-mips64el"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/linux-ppc64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/linux-riscv64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/linux-s390x"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/linux-x64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/netbsd-arm64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/netbsd-x64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/openbsd-arm64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/openbsd-x64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/sunos-x64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/win32-arm64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/win32-ia32"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@esbuild/win32-x64"
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@next/env"
  dependency-version: 15.3.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@next/swc-darwin-arm64"
  dependency-version: 15.3.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@next/swc-darwin-x64"
  dependency-version: 15.3.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@next/swc-linux-arm64-gnu"
  dependency-version: 15.3.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@next/swc-linux-arm64-musl"
  dependency-version: 15.3.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@next/swc-linux-x64-gnu"
  dependency-version: 15.3.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@next/swc-linux-x64-musl"
  dependency-version: 15.3.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@next/swc-win32-arm64-msvc"
  dependency-version: 15.3.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@next/swc-win32-x64-msvc"
  dependency-version: 15.3.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-android-arm-eabi"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-android-arm64"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-darwin-x64"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-freebsd-arm64"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-freebsd-x64"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-linux-arm-gnueabihf"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-linux-arm-musleabihf"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-linux-arm64-musl"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-linux-loongarch64-gnu"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-linux-powerpc64le-gnu"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-linux-riscv64-gnu"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-linux-riscv64-musl"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-linux-s390x-gnu"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-linux-x64-musl"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-win32-arm64-msvc"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-win32-ia32-msvc"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@rollup/rollup-win32-x64-msvc"
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/node"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide-android-arm64"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide-darwin-arm64"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide-darwin-x64"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide-freebsd-x64"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide-linux-arm-gnueabihf"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide-linux-arm64-gnu"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide-linux-arm64-musl"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide-linux-x64-gnu"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide-linux-x64-musl"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide-wasm32-wasi"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide-win32-arm64-msvc"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide-win32-x64-msvc"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@tailwindcss/oxide"
  dependency-version: 4.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: "@typescript-eslint/scope-manager"
  dependency-version: 8.33.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: "@typescript-eslint/type-utils"
  dependency-version: 8.33.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: "@typescript-eslint/types"
  dependency-version: 8.33.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: "@typescript-eslint/typescript-estree"
  dependency-version: 8.33.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: "@typescript-eslint/utils"
  dependency-version: 8.33.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: "@typescript-eslint/visitor-keys"
  dependency-version: 8.33.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: browserslist
  dependency-version: 4.25.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: caniuse-lite
  dependency-version: 1.0.30001720
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: commander
  dependency-version: 14.0.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: all
- dependency-name: electron-to-chromium
  dependency-version: 1.5.161
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: es-abstract
  dependency-version: 1.24.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: esbuild
  dependency-version: 0.25.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: fdir
  dependency-version: 6.4.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: onetime
  dependency-version: 7.0.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: all
- dependency-name: react-remove-scroll
  dependency-version: 2.7.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: rollup
  dependency-version: 4.41.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: tinyglobby
  dependency-version: 0.2.14
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: unrs-resolver
  dependency-version: 1.7.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 02:12:41 +00:00
zhom 1950ef0098 docs: typo fix 2025-06-02 05:10:48 +04:00
zhom 814875c28e docs: add option for urgent contact 2025-06-02 05:10:10 +04:00
zhom b06ca4f11e chore: set the correct license in package.json 2025-06-01 05:32:27 +04:00
160 changed files with 32214 additions and 13681 deletions
@@ -0,0 +1,6 @@
---
description:
globs:
alwaysApply: true
---
Before finishing the task and showing summary, always run "pnpm format && pnpm lint && pnpm test" at the root of the project to ensure that you don't finish with broken application.
+6
View File
@@ -0,0 +1,6 @@
---
description:
globs:
alwaysApply: true
---
Don't leave comments that don't add value.
+6
View File
@@ -0,0 +1,6 @@
---
description:
globs:
alwaysApply: true
---
Do not duplicate code unless you have a very good reason to do so. It is important that the same logic is not duplicated multiple times.
+6
View File
@@ -0,0 +1,6 @@
---
description:
globs:
alwaysApply: true
---
Anytime you change nodecar's code and try to test, recompile it with "cd nodecar && pnpm build".
@@ -0,0 +1,6 @@
---
description:
globs:
alwaysApply: true
---
After your changes, instead of running specific tests or linting specific files, run "pnpm format && pnpm lint && pnpm test". It means that you first format the code, then lint it, then test it, so that no part is broken after your changes.
+6
View File
@@ -0,0 +1,6 @@
---
description:
globs:
alwaysApply: true
---
If there is a global singleton of a struct, only use it inside a method while properly initializing it, unless I have explicitly specified in the request otherwise.
+8 -8
View File
@@ -1,14 +1,14 @@
# ✨ Pull Request
### 📓 Referenced Issue
## 📓 Referenced Issue
<!-- Please link the related issue. Use # before the issue number and use the verbs 'fixes', 'resolves' to auto-link it, for eg, Fixes: #<issue-number> -->
### ️ About the PR
## ️ About the PR
<!-- Please provide a description of your solution if it is not clear in the related issue or if the PR has a breaking change. If there is an interesting topic to discuss or you have questions or there is an issue with Tauri, Rust, or another library that you have used. -->
### 🔄 Type of Change
## 🔄 Type of Change
<!-- Mark the relevant option with an "x". -->
@@ -19,11 +19,11 @@
- [ ] 🧹 Code cleanup/refactoring
- [ ] ⚡ Performance improvement
### 🖼️ Testing Scenarios / Screenshots
## 🖼️ Testing Scenarios / Screenshots
<!-- Please include screenshots or gif to showcase the final output. Also, try to explain the testing you did to validate your change. -->
### ✅ Checklist
## ✅ Checklist
<!-- Mark completed items with an "x". -->
@@ -36,11 +36,11 @@
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published
### 🧪 How Has This Been Tested?
## 🧪 How Has This Been Tested?
<!-- Please describe the tests that you ran to verify your changes. -->
### 📱 Platform Testing
## 📱 Platform Testing
<!-- Which platforms have you tested on? -->
@@ -49,6 +49,6 @@
- [ ] Windows (if applicable)
- [ ] Linux (if applicable)
### 📋 Additional Notes
## 📋 Additional Notes
<!-- Any additional information that reviewers should know about this PR. -->
+30 -7
View File
@@ -1,28 +1,51 @@
version: 2
updates:
# Enable version updates for Node.js dependencies
# Frontend dependencies (root package.json)
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "saturday"
time: "09:00"
allow:
- dependency-type: "all"
groups:
all:
frontend-dependencies:
patterns:
- "*"
ignore:
- dependency-name: "eslint"
versions: ">= 9"
commit-message:
prefix: "deps"
include: "scope"
# Enable version updates for rust
# Rust dependencies
- package-ecosystem: "cargo"
directory: "/src-tauri"
schedule:
interval: "weekly"
day: "saturday"
time: "09:00"
allow:
- dependency-type: "all"
groups:
all:
rust-dependencies:
patterns:
- "*"
commit-message:
prefix: "deps(rust)"
include: "scope"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "saturday"
time: "09:00"
groups:
github-actions:
patterns:
- "*"
commit-message:
prefix: "ci"
include: "scope"
+98
View File
@@ -0,0 +1,98 @@
name: "CodeQL"
on:
workflow_call:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
- cron: "16 13 * * 5"
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
security-events: write
packages: read
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
# - language: rust
# build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
node-version-file: .node-version
cache: "pnpm"
- name: Setup Rust
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b #master
with:
toolchain: stable
targets: x86_64-unknown-linux-gnu
- name: Install system dependencies (Rust only)
if: matrix.language == 'rust'
run: |
sudo apt-get update
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
with:
workdir: ./src-tauri
- name: Install banderole
run: cargo install banderole
- name: Install dependencies from lockfile
run: pnpm install --frozen-lockfile
- name: Install rust dependencies
if: matrix.language == 'rust'
working-directory: ./src-tauri
run: |
cargo build
- name: Build nodecar sidecar
if: matrix.language == 'rust'
shell: bash
working-directory: ./nodecar
run: |
pnpm run build:linux-x64
- name: Copy nodecar binary to Tauri binaries
if: matrix.language == 'rust'
shell: bash
run: |
mkdir -p src-tauri/binaries
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-x86_64-unknown-linux-gnu
- name: Initialize CodeQL
uses: github/codeql-action/init@b1e4dc3db58c9601794e22a9f6d28d45461b9dbf #v3.29.0
with:
queries: security-extended
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b1e4dc3db58c9601794e22a9f6d28d45461b9dbf #v3.29.0
with:
category: "/language:${{matrix.language}}"
+21
View File
@@ -0,0 +1,21 @@
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
contrib-readme-job:
runs-on: ubuntu-latest
name: Automatically update the contributors list in the README
permissions:
contents: write
pull-requests: write
steps:
- name: Contribute List
uses: akhilmhdh/contributors-readme-action@83ea0b4f1ac928fbfe88b9e8460a932a528eb79f #v2.3.11
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+71 -23
View File
@@ -1,34 +1,82 @@
# Automatically squashes and merges Dependabot dependency upgrades if tests pass
name: Dependabot Automerge
name: Dependabot Auto-merge
on: pull_request_target
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
pull-requests: write
contents: write
checks: read
jobs:
dependabot:
runs-on: ubuntu-latest
security-scan:
name: Security Vulnerability Scan
if: ${{ github.actor == 'dependabot[bot]' }}
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@b00f71e051ddddc6e46a193c31c8c0bf283bf9e6" # v2.1.0
with:
scan-args: |-
-r
--skip-git
--lockfile=pnpm-lock.yaml
--lockfile=src-tauri/Cargo.lock
--lockfile=nodecar/pnpm-lock.yaml
./
permissions:
security-events: write
contents: read
actions: read
lint-js:
name: Lint JavaScript/TypeScript
if: ${{ github.actor == 'dependabot[bot]' }}
uses: ./.github/workflows/lint-js.yml
secrets: inherit
permissions:
contents: read
lint-rust:
name: Lint Rust
if: ${{ github.actor == 'dependabot[bot]' }}
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
permissions:
contents: read
codeql:
name: CodeQL
uses: ./.github/workflows/codeql.yml
secrets: inherit
permissions:
security-events: write
contents: read
packages: read
actions: read
spellcheck:
name: Spell Check
uses: ./.github/workflows/spellcheck.yml
secrets: inherit
permissions:
contents: read
dependabot-automerge:
name: Dependabot Automerge
if: ${{ github.actor == 'dependabot[bot]' }}
needs: [security-scan, lint-js, lint-rust, codeql, spellcheck]
runs-on: ubuntu-latest
steps:
- name: Fetch Dependabot metadata
id: dependabot-metadata
uses: dependabot/fetch-metadata@v2
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b #v2.4.0
with:
compat-lookup: true
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve Dependabot PR
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Auto-merge (squash) Dependabot PR
if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }}
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Auto-merge minor and patch updates
uses: ridedott/merge-me-action@f96a67511b4be051e77760230e6a3fb9cb7b1903 #v2.10.124
with:
GITHUB_TOKEN: ${{ secrets.SECRET_DEPENDABOT_GITHUB_TOKEN }}
MERGE_METHOD: SQUASH
PRESET: DEPENDABOT_MINOR
MAXIMUM_RETRIES: 5
timeout-minutes: 10
+19
View File
@@ -0,0 +1,19 @@
name: Greetings
on:
pull_request:
types: [opened]
issues:
types: [opened]
jobs:
greeting:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/first-interaction@2d4393e6bc0e2efb2e48fba7e06819c3bf61ffc9 # v2.0.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."
+199
View File
@@ -0,0 +1,199 @@
name: Issue Validation
on:
issues:
types: [opened]
permissions:
contents: read
issues: write
models: read
jobs:
validate-issue:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Get issue templates
id: get-templates
run: |
# Read the issue templates
if [ -f ".github/ISSUE_TEMPLATE/01-bug-report.md" ]; then
echo "bug-template-exists=true" >> $GITHUB_OUTPUT
fi
if [ -f ".github/ISSUE_TEMPLATE/02-feature-request.md" ]; then
echo "feature-template-exists=true" >> $GITHUB_OUTPUT
fi
- name: Create issue analysis prompt
id: create-prompt
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_LABELS: ${{ join(github.event.issue.labels.*.name, ', ') }}
run: |
cat > issue_analysis.txt << EOF
## Issue Content to Analyze:
**Title:** $ISSUE_TITLE
**Body:**
$ISSUE_BODY
**Labels:** $ISSUE_LABELS
EOF
- name: Validate issue with AI
id: validate
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
with:
prompt-file: issue_analysis.txt
system-prompt: |
You are an issue validation assistant for Donut Browser, an anti-detect browser.
Analyze the provided issue content and determine if it contains sufficient information based on these requirements:
**For Bug Reports, the issue should include:**
1. Clear description of the problem
2. Steps to reproduce the issue (numbered list preferred)
3. Expected vs actual behavior
4. Environment information (OS, browser version, etc.)
5. Error messages, stack traces, or screenshots if applicable
**For Feature Requests, the issue should include:**
1. Clear description of the requested feature
2. Use case or problem it solves
3. Proposed solution or how it should work
4. Priority level or importance
**General Requirements for all issues:**
1. Descriptive title
2. Sufficient detail to understand and act upon
3. Professional tone and clear communication
Respond in JSON format with the following structure:
```json
{
"is_valid": true|false,
"issue_type": "bug_report"|"feature_request"|"other",
"missing_info": [
"List of missing required information"
],
"suggestions": [
"Specific suggestions for improvement"
],
"overall_assessment": "Brief assessment of the issue quality"
}
```
Be constructive and helpful in your feedback. If the issue is incomplete, provide specific guidance on what's needed.
model: openai/gpt-4o
- name: Parse validation result and take action
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Prefer reading from the response file to avoid output truncation
RESPONSE_FILE='${{ steps.validate.outputs.response-file }}'
if [ -n "$RESPONSE_FILE" ] && [ -f "$RESPONSE_FILE" ]; then
RAW_OUTPUT=$(cat "$RESPONSE_FILE")
else
RAW_OUTPUT='${{ steps.validate.outputs.response }}'
fi
# Extract JSON if wrapped in markdown code fences; otherwise use raw
JSON_RESULT=$(printf "%s" "$RAW_OUTPUT" | sed -n '/```json/,/```/p' | sed '1d;$d')
if [ -z "$JSON_RESULT" ]; then
JSON_RESULT="$RAW_OUTPUT"
fi
# Parse JSON fields
IS_VALID=$(echo "$JSON_RESULT" | jq -r '.is_valid // false')
ISSUE_TYPE=$(echo "$JSON_RESULT" | jq -r '.issue_type // "other"')
MISSING_INFO=$(echo "$JSON_RESULT" | jq -r '.missing_info[]? // empty' | sed 's/^/- /')
SUGGESTIONS=$(echo "$JSON_RESULT" | jq -r '.suggestions[]? // empty' | sed 's/^/- /')
ASSESSMENT=$(echo "$JSON_RESULT" | jq -r '.overall_assessment // "No assessment provided"')
echo "Issue validation result: $IS_VALID"
echo "Issue type: $ISSUE_TYPE"
if [ "$IS_VALID" = "false" ]; then
# Create a comment asking for more information
cat > comment.md << EOF
## 🤖 Issue Validation
Thank you for submitting this issue! However, it appears that some required information might be missing to help us better understand and address your concern.
**Issue Type Detected:** \`$ISSUE_TYPE\`
**Assessment:** $ASSESSMENT
### 📋 Missing Information:
$MISSING_INFO
### 💡 Suggestions for Improvement:
$SUGGESTIONS
### 📝 How to Provide Additional Information:
Please edit your original issue description to include the missing information. Here are our issue templates for reference:
- **Bug Report Template:** [View Template](.github/ISSUE_TEMPLATE/01-bug-report.md)
- **Feature Request Template:** [View Template](.github/ISSUE_TEMPLATE/02-feature-request.md)
### 🔧 Quick Tips:
- For **bug reports**: Include step-by-step reproduction instructions, your environment details, and any error messages
- For **feature requests**: Describe the use case, expected behavior, and why this feature would be valuable
- Add **screenshots** or **logs** when applicable
Once you've updated the issue with the missing information, feel free to remove this comment or reply to let us know you've made the updates.
---
*This validation was performed automatically to ensure we have all the information needed to help you effectively.*
EOF
# Post the comment
gh issue comment ${{ github.event.issue.number }} --body-file comment.md
# Add a label to indicate validation needed
gh issue edit ${{ github.event.issue.number }} --add-label "needs-info"
echo "✅ Validation comment posted and 'needs-info' label added"
else
echo "✅ Issue contains sufficient information"
# Prepare a summary comment even when valid
cat > comment.md << EOF
## 🤖 Issue Validation
**Issue Type Detected:** \`$ISSUE_TYPE\`
**Assessment:** $ASSESSMENT
$( [ -n "$SUGGESTIONS" ] && echo "### 💡 Suggestions:" && echo "$SUGGESTIONS" )
---
*This validation was performed automatically to help triage issues.*
EOF
# Post the summary comment
gh issue comment ${{ github.event.issue.number }} --body-file comment.md
# Add appropriate labels based on issue type
case "$ISSUE_TYPE" in
"bug_report")
gh issue edit ${{ github.event.issue.number }} --add-label "bug"
;;
"feature_request")
gh issue edit ${{ github.event.issue.number }} --add-label "enhancement"
;;
esac
fi
- name: Cleanup
run: |
rm -f issue_analysis.txt comment.md
+9 -9
View File
@@ -13,6 +13,11 @@ on:
paths-ignore:
- "src-tauri/**"
- "README.md"
- ".github/workflows/lint-rs.yml"
- ".github/workflows/osv.yml"
permissions:
contents: read
jobs:
build:
@@ -29,13 +34,13 @@ jobs:
run: git config --global core.autocrlf false
- name: Checkout repository code
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- name: Set up Node.js v22
uses: actions/setup-node@v4
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
node-version-file: .node-version
cache: "pnpm"
@@ -43,10 +48,5 @@ jobs:
- name: Install dependencies from lockfile
run: pnpm install --frozen-lockfile
- name: Install nodecar dependencies
working-directory: ./nodecar
run: |
pnpm install --frozen-lockfile
- name: Run lint step
run: pnpm run lint:js
+37 -18
View File
@@ -12,18 +12,26 @@ on:
pull_request:
paths-ignore:
- "src/**"
- "nodecar/**"
- "package.json"
- "package-lock.json"
- "yarn.lock"
- "pnpm-lock.yaml"
- "yarn.lock"
- "README.md"
- ".github/workflows/lint-js.yml"
- ".github/workflows/osv.yml"
- "next.config.js"
- "tailwind.config.js"
- "tsconfig.json"
- "biome.json"
permissions:
contents: read
jobs:
build:
strategy:
fail-fast: true
matrix:
os: [macos-latest]
os: [macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
@@ -33,22 +41,29 @@ jobs:
run: git config --global core.autocrlf false
- name: Checkout repository code
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
node-version-file: .node-version
cache: "pnpm"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b #master
with:
toolchain: stable
components: rustfmt, clippy
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Install banderole
run: cargo install banderole
- name: Install dependencies (Ubuntu only)
if: matrix.os == 'ubuntu-latest'
run: |
@@ -58,11 +73,6 @@ jobs:
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Install nodecar dependencies
working-directory: ./nodecar
run: |
pnpm install --ignore-workspace --frozen-lockfile
- name: Build nodecar binary
shell: bash
working-directory: ./nodecar
@@ -70,21 +80,26 @@ jobs:
if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then
pnpm run build:linux-x64
elif [[ "${{ matrix.os }}" == "macos-latest" ]]; then
pnpm run build:aarch64
pnpm run build:mac-aarch64
elif [[ "${{ matrix.os }}" == "windows-latest" ]]; then
pnpm run build:win-x64
fi
# TODO: replace with an integration test that fetches everything from rust
# - name: Download Camoufox for testing
# run: npx camoufox-js fetch
# continue-on-error: true
- name: Copy nodecar binary to Tauri binaries
shell: bash
run: |
mkdir -p src-tauri/binaries
if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then
cp nodecar/dist/nodecar src-tauri/binaries/nodecar-x86_64-unknown-linux-gnu
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-x86_64-unknown-linux-gnu
elif [[ "${{ matrix.os }}" == "macos-latest" ]]; then
cp nodecar/dist/nodecar src-tauri/binaries/nodecar-aarch64-apple-darwin
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-aarch64-apple-darwin
elif [[ "${{ matrix.os }}" == "windows-latest" ]]; then
cp nodecar/dist/nodecar.exe src-tauri/binaries/nodecar-x86_64-pc-windows-msvc.exe
cp nodecar/nodecar-bin.exe src-tauri/binaries/nodecar-x86_64-pc-windows-msvc.exe
fi
- name: Create empty 'dist' directory
@@ -98,6 +113,10 @@ jobs:
run: cargo clippy --all-targets --all-features -- -D warnings -D clippy::all
working-directory: src-tauri
- name: Run Rust unit tests
- name: Run Rust tests
run: cargo test
working-directory: src-tauri
- name: Run cargo audit security check
run: cargo audit
working-directory: src-tauri
+74
View File
@@ -0,0 +1,74 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# A sample workflow which sets up periodic OSV-Scanner scanning for vulnerabilities,
# in addition to a PR check which fails if new vulnerabilities are introduced.
#
# For more examples and options, including how to ignore specific vulnerabilities,
# see https://google.github.io/osv-scanner/github-action/
# Security vulnerability scanning for Donut Browser
# Scans dependencies in package managers (npm/pnpm, Cargo) for known vulnerabilities
# Runs on schedule and when dependencies change
name: Security Vulnerability Scan
on:
pull_request:
branches: ["main"]
paths:
- "package.json"
- "pnpm-lock.yaml"
- "src-tauri/Cargo.toml"
- "src-tauri/Cargo.lock"
- "nodecar/package.json"
- "nodecar/pnpm-lock.yaml"
- ".github/workflows/osv.yml"
merge_group:
branches: ["main"]
schedule:
# Run weekly on Tuesdays at 2:20 PM UTC
- cron: "20 14 * * 2"
push:
branches: ["main"]
paths:
- "package.json"
- "pnpm-lock.yaml"
- "src-tauri/Cargo.toml"
- "src-tauri/Cargo.lock"
- "nodecar/package.json"
- "nodecar/pnpm-lock.yaml"
permissions:
security-events: write
contents: read
actions: read
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@b00f71e051ddddc6e46a193c31c8c0bf283bf9e6" # v2.1.0
with:
scan-args: |-
-r
--skip-git
--lockfile=pnpm-lock.yaml
--lockfile=src-tauri/Cargo.lock
--lockfile=nodecar/pnpm-lock.yaml
./
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@b00f71e051ddddc6e46a193c31c8c0bf283bf9e6" # v2.1.0
with:
scan-args: |-
-r
--skip-git
--lockfile=pnpm-lock.yaml
--lockfile=src-tauri/Cargo.lock
--lockfile=nodecar/pnpm-lock.yaml
./
+54
View File
@@ -0,0 +1,54 @@
name: Pull Request Checks
on:
pull_request:
branches: ["main"]
merge_group:
branches: ["main"]
permissions:
security-events: write
contents: read
actions: read
jobs:
lint-js:
name: Lint JavaScript/TypeScript
uses: ./.github/workflows/lint-js.yml
secrets: inherit
permissions:
contents: read
lint-rust:
name: Lint Rust
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
permissions:
contents: read
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@b00f71e051ddddc6e46a193c31c8c0bf283bf9e6" # v2.1.0
with:
scan-args: |-
-r
--skip-git
--lockfile=pnpm-lock.yaml
--lockfile=nodecar/pnpm-lock.yaml
--lockfile=src-tauri/Cargo.lock
./
pr-status:
name: PR Status Check
runs-on: ubuntu-latest
needs: [lint-js, lint-rust, security-scan]
if: always()
steps:
- name: Check all jobs succeeded
run: |
if [[ "${{ needs.lint-js.result }}" != "success" || "${{ needs.lint-rust.result }}" != "success" || "${{ needs.security-scan.result }}" != "success" ]]; then
echo "One or more checks failed"
exit 1
fi
echo "All checks passed!"
@@ -0,0 +1,118 @@
name: Generate Release Notes
on:
release:
types: [published]
permissions:
contents: write
models: read
jobs:
generate-release-notes:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') && !github.event.release.prerelease
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
with:
fetch-depth: 0 # Fetch full history to compare with previous release
- name: Get previous release tag
id: get-previous-tag
run: |
# Get the previous release tag (excluding the current one)
CURRENT_TAG="${{ github.ref_name }}"
PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -v "$CURRENT_TAG" | head -n 1)
if [ -z "$PREVIOUS_TAG" ]; then
echo "No previous release found, using initial commit"
PREVIOUS_TAG=$(git rev-list --max-parents=0 HEAD)
fi
echo "current-tag=$CURRENT_TAG" >> $GITHUB_OUTPUT
echo "previous-tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT
echo "Previous release: $PREVIOUS_TAG"
echo "Current release: $CURRENT_TAG"
- name: Get commit messages between releases
id: get-commits
run: |
# Get commit messages between previous and current release
PREVIOUS_TAG="${{ steps.get-previous-tag.outputs.previous-tag }}"
CURRENT_TAG="${{ steps.get-previous-tag.outputs.current-tag }}"
# Get commit log with detailed format
COMMIT_LOG=$(git log --pretty=format:"- %s (%h by %an)" $PREVIOUS_TAG..$CURRENT_TAG --no-merges)
# Get changed files summary
CHANGED_FILES=$(git diff --name-status $PREVIOUS_TAG..$CURRENT_TAG | head -20)
# Save to files for AI processing
echo "$COMMIT_LOG" > commits.txt
echo "$CHANGED_FILES" > changes.txt
echo "commits-file=commits.txt" >> $GITHUB_OUTPUT
echo "changes-file=changes.txt" >> $GITHUB_OUTPUT
- name: Generate release notes with AI
id: generate-notes
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
with:
prompt-file: commits.txt
system-prompt: |
You are an expert technical writer tasked with generating comprehensive release notes for Donut Browser, a powerful anti-detect browser.
Analyze the provided commit messages and generate well-structured release notes following this format:
## What's New in ${{ steps.get-previous-tag.outputs.current-tag }}
[Brief 1-2 sentence overview of the release]
### ✨ New Features
[List new features with brief descriptions]
### 🐛 Bug Fixes
[List bug fixes]
### 🔧 Improvements
[List improvements and enhancements]
### 📚 Documentation
[List documentation updates if any]
### 🔄 Dependencies
[List dependency updates if any]
### 🛠️ Developer Experience
[List development-related changes if any]
Guidelines:
- Use clear, user-friendly language
- Group related commits logically
- Omit minor commits like formatting, typos unless significant
- Focus on user-facing changes
- Use emojis sparingly and consistently
- Keep descriptions concise but informative
- If commits are unclear, infer the purpose from the context
The application is a desktop app built with Tauri + Next.js that helps users manage multiple browser profiles with proxy support.
model: gpt-4o
- name: Update release with generated notes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get the generated release notes
RELEASE_NOTES="${{ steps.generate-notes.outputs.response }}"
# Update the release with the generated notes
gh api --method PATCH /repos/${{ github.repository }}/releases/${{ github.event.release.id }} \
--field body="$RELEASE_NOTES"
echo "✅ Release notes updated successfully!"
- name: Cleanup
run: |
rm -f commits.txt changes.txt
+81 -36
View File
@@ -11,18 +11,55 @@ env:
STABLE_RELEASE: "true"
jobs:
security-scan:
name: Security Vulnerability Scan
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@b00f71e051ddddc6e46a193c31c8c0bf283bf9e6" # v2.1.0
with:
scan-args: |-
-r
--skip-git
--lockfile=pnpm-lock.yaml
--lockfile=src-tauri/Cargo.lock
--lockfile=nodecar/pnpm-lock.yaml
./
permissions:
security-events: write
contents: read
actions: read
lint-js:
name: Lint JavaScript/TypeScript
uses: ./.github/workflows/lint-js.yml
secrets: inherit
permissions:
contents: read
lint-rust:
name: Lint Rust
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
permissions:
contents: read
codeql:
name: CodeQL
uses: ./.github/workflows/codeql.yml
secrets: inherit
permissions:
security-events: write
contents: read
packages: read
actions: read
spellcheck:
name: Spell Check
uses: ./.github/workflows/spellcheck.yml
secrets: inherit
permissions:
contents: read
release:
needs: [lint-js, lint-rust]
needs: [security-scan, lint-js, lint-rust, codeql, spellcheck]
permissions:
contents: write
strategy:
@@ -30,38 +67,37 @@ jobs:
matrix:
include:
- platform: "macos-latest"
args: "--target aarch64-apple-darwin"
args: "--target aarch64-apple-darwin --verbose"
arch: "aarch64"
target: "aarch64-apple-darwin"
pkg_target: "latest-macos-arm64"
nodecar_script: "build:aarch64"
nodecar_script: "build:mac-aarch64"
- platform: "macos-latest"
args: "--target x86_64-apple-darwin"
args: "--target x86_64-apple-darwin --verbose"
arch: "x86_64"
target: "x86_64-apple-darwin"
pkg_target: "latest-macos-x64"
nodecar_script: "build:x86_64"
# Future platforms can be added here:
# - platform: "ubuntu-20.04"
# args: "--target x86_64-unknown-linux-gnu"
# arch: "x86_64"
# target: "x86_64-unknown-linux-gnu"
# pkg_target: "latest-linux-x64"
# nodecar_script: "build:linux-x64"
# - platform: "ubuntu-20.04"
# args: "--target aarch64-unknown-linux-gnu"
# arch: "aarch64"
# target: "aarch64-unknown-linux-gnu"
# pkg_target: "latest-linux-arm64"
# nodecar_script: "build:linux-arm64"
nodecar_script: "build:mac-x86_64"
- platform: "ubuntu-22.04"
args: "--target x86_64-unknown-linux-gnu --verbose"
arch: "x86_64"
target: "x86_64-unknown-linux-gnu"
pkg_target: "latest-linux-x64"
nodecar_script: "build:linux-x64"
- platform: "ubuntu-22.04-arm"
args: "--target aarch64-unknown-linux-gnu --verbose"
arch: "aarch64"
target: "aarch64-unknown-linux-gnu"
pkg_target: "latest-linux-arm64"
nodecar_script: "build:linux-arm64"
# - platform: "windows-latest"
# args: "--target x86_64-pc-windows-msvc"
# args: "--target x86_64-pc-windows-msvc --verbose"
# arch: "x86_64"
# target: "x86_64-pc-windows-msvc"
# pkg_target: "latest-win-x64"
# nodecar_script: "build:win-x64"
# - platform: "windows-latest"
# args: "--target aarch64-pc-windows-msvc"
# - platform: "windows-11-arm"
# args: "--target aarch64-pc-windows-msvc --verbose"
# arch: "aarch64"
# target: "aarch64-pc-windows-msvc"
# pkg_target: "latest-win-arm64"
@@ -69,40 +105,39 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
node-version-file: .node-version
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b #master
with:
toolchain: stable
targets: ${{ matrix.target }}
- name: Install dependencies (Ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
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@v2
uses: swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 #v2.8.0
with:
workdir: ./src-tauri
- name: Install banderole
run: cargo install banderole
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Install nodecar dependencies
working-directory: ./nodecar
run: |
pnpm install --ignore-workspace --frozen-lockfile
- name: Build nodecar sidecar
shell: bash
working-directory: ./nodecar
@@ -114,16 +149,20 @@ jobs:
run: |
mkdir -p src-tauri/binaries
if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then
cp nodecar/dist/nodecar.exe src-tauri/binaries/nodecar-${{ matrix.target }}.exe
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-${{ matrix.target }}.exe
else
cp nodecar/dist/nodecar src-tauri/binaries/nodecar-${{ matrix.target }}
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-${{ matrix.target }}
fi
# - name: Download Camoufox for testing
# run: npx camoufox-js fetch
# continue-on-error: true
- name: Build frontend
run: pnpm build
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
uses: tauri-apps/tauri-action@564aea5a8075c7a54c167bb0cf5b3255314a7f9d #v0.5.22
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REF_NAME: ${{ github.ref_name }}
@@ -134,3 +173,9 @@ jobs:
releaseDraft: false
prerelease: false
args: ${{ matrix.args }}
- name: Commit CHANGELOG.md
uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 #v6.0.1
with:
branch: main
commit_message: "docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]"
+104 -25
View File
@@ -10,18 +10,55 @@ env:
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
jobs:
security-scan:
name: Security Vulnerability Scan
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@b00f71e051ddddc6e46a193c31c8c0bf283bf9e6" # v2.1.0
with:
scan-args: |-
-r
--skip-git
--lockfile=pnpm-lock.yaml
--lockfile=src-tauri/Cargo.lock
--lockfile=nodecar/pnpm-lock.yaml
./
permissions:
security-events: write
contents: read
actions: read
lint-js:
name: Lint JavaScript/TypeScript
uses: ./.github/workflows/lint-js.yml
secrets: inherit
permissions:
contents: read
lint-rust:
name: Lint Rust
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
permissions:
contents: read
codeql:
name: CodeQL
uses: ./.github/workflows/codeql.yml
secrets: inherit
permissions:
security-events: write
contents: read
packages: read
actions: read
spellcheck:
name: Spell Check
uses: ./.github/workflows/spellcheck.yml
secrets: inherit
permissions:
contents: read
rolling-release:
needs: [lint-js, lint-rust]
needs: [security-scan, lint-js, lint-rust, codeql, spellcheck]
permissions:
contents: write
strategy:
@@ -29,48 +66,77 @@ jobs:
matrix:
include:
- platform: "macos-latest"
args: "--target aarch64-apple-darwin"
args: "--target aarch64-apple-darwin --verbose"
arch: "aarch64"
target: "aarch64-apple-darwin"
pkg_target: "latest-macos-arm64"
nodecar_script: "build:aarch64"
nodecar_script: "build:mac-aarch64"
- platform: "macos-latest"
args: "--target x86_64-apple-darwin"
args: "--target x86_64-apple-darwin --verbose"
arch: "x86_64"
target: "x86_64-apple-darwin"
pkg_target: "latest-macos-x64"
nodecar_script: "build:x86_64"
nodecar_script: "build:mac-x86_64"
- platform: "ubuntu-22.04"
args: "--target x86_64-unknown-linux-gnu --verbose"
arch: "x86_64"
target: "x86_64-unknown-linux-gnu"
pkg_target: "latest-linux-x64"
nodecar_script: "build:linux-x64"
- platform: "ubuntu-22.04-arm"
args: "--target aarch64-unknown-linux-gnu --verbose"
arch: "aarch64"
target: "aarch64-unknown-linux-gnu"
pkg_target: "latest-linux-arm64"
nodecar_script: "build:linux-arm64"
- platform: "windows-latest"
args: "--target x86_64-pc-windows-msvc --verbose"
arch: "x86_64"
target: "x86_64-pc-windows-msvc"
pkg_target: "latest-win-x64"
nodecar_script: "build:win-x64"
# - platform: "windows-11-arm"
# args: "--target aarch64-pc-windows-msvc --verbose"
# arch: "aarch64"
# target: "aarch64-pc-windows-msvc"
# pkg_target: "latest-win-arm64"
# nodecar_script: "build:win-arm64"
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
node-version-file: .node-version
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b #master
with:
toolchain: stable
targets: ${{ matrix.target }}
- name: Install dependencies (Ubuntu only)
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
run: |
sudo apt-get update
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@v2
uses: swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 #v2.8.0
with:
workdir: ./src-tauri
- name: Install banderole
run: cargo install banderole
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Install nodecar dependencies
working-directory: ./nodecar
run: |
pnpm install --ignore-workspace --frozen-lockfile
- name: Build nodecar sidecar
shell: bash
working-directory: ./nodecar
@@ -81,26 +147,39 @@ jobs:
shell: bash
run: |
mkdir -p src-tauri/binaries
cp nodecar/dist/nodecar src-tauri/binaries/nodecar-${{ matrix.target }}
if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-${{ matrix.target }}.exe
else
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-${{ matrix.target }}
fi
# - name: Download Camoufox for testing
# run: npx camoufox-js fetch
# continue-on-error: true
- name: Build frontend
run: pnpm build
- name: Get commit hash
id: commit
run: echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Generate nightly timestamp
id: timestamp
shell: bash
run: |
TIMESTAMP=$(date -u +"%Y-%m-%d")
COMMIT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-7)
echo "timestamp=${TIMESTAMP}-${COMMIT_HASH}" >> $GITHUB_OUTPUT
echo "Generated timestamp: ${TIMESTAMP}-${COMMIT_HASH}"
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
uses: tauri-apps/tauri-action@564aea5a8075c7a54c167bb0cf5b3255314a7f9d #v0.5.22
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_TAG: "nightly-${{ steps.commit.outputs.hash }}"
GITHUB_REF_NAME: "nightly-${{ steps.commit.outputs.hash }}"
BUILD_TAG: "nightly-${{ steps.timestamp.outputs.timestamp }}"
GITHUB_REF_NAME: "nightly-${{ steps.timestamp.outputs.timestamp }}"
GITHUB_SHA: ${{ github.sha }}
with:
tagName: "nightly-${{ steps.commit.outputs.hash }}"
releaseName: "Donut Browser Nightly (Build ${{ steps.commit.outputs.hash }})"
releaseBody: "⚠️ **Nightly Release** - This is an automatically generated pre-release build from the latest main branch. Use with caution.\n\nCommit: ${{ github.sha }}\nBuild: ${{ steps.commit.outputs.hash }}"
tagName: "nightly-${{ steps.timestamp.outputs.timestamp }}"
releaseName: "Donut Browser Nightly (Build ${{ steps.timestamp.outputs.timestamp }})"
releaseBody: "⚠️ **Nightly Release** - This is an automatically generated pre-release build from the latest main branch. Use with caution.\n\nCommit: ${{ github.sha }}\nBuild: ${{ steps.timestamp.outputs.timestamp }}"
releaseDraft: false
prerelease: true
args: ${{ matrix.args }}
+26
View File
@@ -0,0 +1,26 @@
name: Spell Check
permissions:
contents: read
on:
workflow_call:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
RUST_BACKTRACE: 1
CARGO_TERM_COLOR: always
CLICOLOR: 1
jobs:
spelling:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Spell Check Repo
uses: crate-ci/typos@52bd719c2c91f9d676e2aa359fc8e0db8925e6d8 #v1.35.3
+21
View File
@@ -0,0 +1,21 @@
name: Mark stale issues and pull requests
on:
schedule:
- cron: "35 23 * * *"
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.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."
stale-pr-message: "This pull request has been inactive for 60 days. Please respond to keep it open."
stale-issue-label: "stale"
stale-pr-label: "stale"
+10 -3
View File
@@ -5,6 +5,10 @@
/.pnp
.pnp.js
# npm/yarn lock files (project uses pnpm only)
**/package-lock.json
**/yarn.lock
# testing
/coverage
@@ -30,8 +34,8 @@ yarn-error.log*
.pnpm-debug.log*
# nodecar
nodecar/dist
nodecar/node_modules
**/dist
**/node_modules
# local env files
.env*.local
@@ -42,4 +46,7 @@ nodecar/node_modules
# typescript
*.tsbuildinfo
!**/.gitkeep
!**/.gitkeep
# nodecar
nodecar/nodecar-bin
+1 -1
View File
@@ -1 +1 @@
pnpm lint-staged
pnpm exec lint-staged
+1 -1
View File
@@ -1,2 +1,2 @@
22
23
+1
View File
@@ -0,0 +1 @@
23
+11
View File
@@ -0,0 +1,11 @@
{
"recommendations": [
"biomejs.biome",
"streetsidesoftware.code-spell-checker",
"usernamehw.errorlens",
"heybourn.headwind",
"yoavbls.pretty-ts-errors",
"rust-lang.rust-analyzer",
"bradlc.vscode-tailwindcss"
]
}
+165 -1
View File
@@ -1,23 +1,187 @@
{
"cSpell.words": [
"adwaita",
"ahooks",
"akhilmhdh",
"appimage",
"appindicator",
"applescript",
"asyncio",
"autoconfig",
"autologin",
"biomejs",
"breezedark",
"browserforge",
"busctl",
"CAMOU",
"camoufox",
"cdylib",
"certifi",
"CFURL",
"checkin",
"chrono",
"CLICOLOR",
"clippy",
"cmdk",
"codegen",
"codesign",
"commitish",
"CTYPE",
"daijro",
"dataclasses",
"datareporting",
"datas",
"dconf",
"devedition",
"distro",
"doctest",
"doesn",
"domcontentloaded",
"donutbrowser",
"doorhanger",
"dpkg",
"dtolnay",
"dyld",
"elif",
"errorlevel",
"esac",
"esbuild",
"etree",
"flate",
"frontmost",
"geoip",
"getcwd",
"gettimezone",
"gifs",
"gsettings",
"healthreport",
"hiddenimports",
"hkcu",
"hooksconfig",
"hookspath",
"icns",
"idlelib",
"idletime",
"idna",
"Inno",
"kdeglobals",
"keras",
"KHTML",
"Kolkata",
"kreadconfig",
"launchservices",
"letterboxing",
"libatk",
"libayatana",
"libcairo",
"libgdk",
"libglib",
"libpango",
"librsvg",
"libwebkit",
"libxdo",
"localtime",
"lxml",
"lzma",
"mmdb",
"mountpoint",
"msiexec",
"msvc",
"msys",
"Mullvad",
"mullvadbrowser",
"mypy",
"noarchive",
"nobrowse",
"noconfirm",
"nodecar",
"nodemon",
"norestart",
"NSIS",
"ntlm",
"numpy",
"objc",
"orhun",
"orjson",
"osascript",
"oscpu",
"outpath",
"pathex",
"pathlib",
"peerconnection",
"pids",
"pixbuf",
"plasmohq",
"platformdirs",
"prefs",
"propertylist",
"psutil",
"pycache",
"pydantic",
"pyee",
"pyinstaller",
"pyoxidizer",
"pytest",
"pyyaml",
"reqwest",
"ridedott",
"rlib",
"rustc",
"SARIF",
"scipy",
"screeninfo",
"serde",
"setuptools",
"shadcn",
"showcursor",
"shutil",
"signon",
"signum",
"sklearn",
"sonner",
"splitn",
"sspi",
"staticlib",
"stefanzweifel",
"subdirs",
"subkey",
"SUPPRESSMSGBOXES",
"swatinem",
"sysinfo",
"systempreferences",
"turbopack"
"systemsetup",
"taskkill",
"tasklist",
"tauri",
"TERX",
"testpass",
"testuser",
"timedatectl",
"titlebar",
"tkinter",
"Torbrowser",
"tqdm",
"trackingprotection",
"turbopack",
"turtledemo",
"udeps",
"unlisten",
"unminimize",
"unrs",
"urlencoding",
"urllib",
"venv",
"vercel",
"VERYSILENT",
"webgl",
"webrtc",
"winreg",
"wiremock",
"xattr",
"xfconf",
"xsettings",
"zhom",
"zipball",
"zoneinfo"
]
}
+8
View File
@@ -0,0 +1,8 @@
# Instructions for AI Agents
- After your changes, instead of running specific tests or linting specific files, run "pnpm format && pnpm lint && pnpm test". It means that you first format the code, then lint it, then test it, so that no part is broken after your changes.
- Don't leave comments that don't add value.
- Do not duplicate code unless you have a very good reason to do so. It is important that the same logic is not duplicated multiple times.
- Before finishing the task and showing summary, always run "pnpm format && pnpm lint && pnpm test" at the root of the project to ensure that you don't finish with broken application.
- Anytime you change nodecar's code and try to test, recompile it with "cd nodecar && pnpm build".
- If there is a global singleton of a struct, only use it inside a method while properly initializing it, unless I have explicitly specified in the request otherwise.
+1 -1
View File
@@ -23,6 +23,6 @@ Examples of unacceptable behavior by participants include:
## Enforcement
Violations of the Code of Conduct may be reported by pinging @zhom on Github. All reports will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Further details of specific enforcement policies may be posted separately.
Violations of the Code of Conduct may be reported to contact at donutbrowser dot com. All reports will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Further details of specific enforcement policies may be posted separately.
We hold the right and responsibility to remove comments or other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any members for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+6 -42
View File
@@ -26,6 +26,7 @@ Ensure you have the following dependencies installed:
- Node.js (see `.node-version` for exact version)
- pnpm package manager
- Latest Rust and Cargo toolchain
- [Banderole](https://github.com/zhom/banderole)
- [Tauri prerequisites guide](https://v2.tauri.app/start/prerequisites/).
## Run Locally
@@ -46,12 +47,13 @@ After having the above dependencies installed, proceed through the following ste
pnpm install
```
4. **Install nodecar dependencies**
4. **Build nodecar**
Building nodecar requires you to have `banderole` installed.
```bash
cd nodecar
pnpm install --ignore-workspace --frozen-lockfile
cd ..
pnpm build
```
5. **Start the development server**
@@ -105,7 +107,6 @@ Make sure the build completes successfully without errors.
## Testing
- Always test your changes on the target platform
- Test both development and production builds
- Verify that existing functionality still works
- Add tests for new features when possible
@@ -149,50 +150,13 @@ Refs #00000
- Ensure that "Allow edits from maintainers" option is checked
## Types of Contributions
### Bug Reports
When filing bug reports, please include:
- Clear description of the issue
- Steps to reproduce
- Expected vs actual behavior
- Environment details (OS, version, etc.)
- Screenshots or error logs if applicable
### Feature Requests
When suggesting new features:
- Explain the use case and why it's valuable
- Describe the desired behavior
- Consider alternatives you've thought of
- Check if it aligns with our roadmap
### Code Contributions
- Bug fixes
- New features
- Performance improvements
- Documentation updates
- Test coverage improvements
### Documentation
- README improvements
- Code comments
- API documentation
- Tutorial content
- Translation work
## Architecture Overview
Donut Browser is built with:
- **Frontend**: Next.js React application
- **Backend**: Tauri (Rust) for native functionality
- **Node.js Sidecar**: `nodecar` binary for proxy support
- **Node.js Sidecar**: `nodecar` binary for access to JavaScript ecosystem
- **Build System**: GitHub Actions for CI/CD
Understanding this architecture will help you contribute more effectively.
+57 -9
View File
@@ -1,41 +1,57 @@
<div align="center">
<img src="assets/logo.png" alt="Donut Browser Logo" width="150">
<h1>Donut Browser</h1>
<strong>A powerful browser orchestrator that puts you in control of your browsing experience. 🍩</strong>
<strong>A powerful anti-detect browser that puts you in control of your browsing experience. 🍩</strong>
</div>
<br>
<p align="center">
<a href="https://github.com/zhom/donutbrowser/releases/latest" target="_blank"><img alt="GitHub release" src="https://img.shields.io/github/v/release/zhom/donutbrowser">
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/releases/latest" target="_blank"><img alt="GitHub release" src="https://img.shields.io/github/v/release/zhom/donutbrowser">
</a>
<a href="https://github.com/zhom/donutbrowser/issues" target="_blank">
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/issues" target="_blank">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="PRs Welcome">
</a>
<a href="https://github.com/zhom/donutbrowser/blob/main/LICENSE" target="_blank">
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/blob/main/LICENSE" target="_blank">
<img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg" alt="License">
</a>
<a href="https://github.com/zhom/donutbrowser/stargazers" target="_blank">
<a href="https://app.codacy.com/gh/zhom/donutbrowser/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade">
<img src="https://app.codacy.com/project/badge/Grade/b9c9beafc92d4bc8bc7c5b42c6c4ba81"/>
</a>
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2Fzhom%2Fdonutbrowser?ref=badge_shield&issueType=security" alt="FOSSA Status">
<img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Fzhom%2Fdonutbrowser.svg?type=shield&issueType=security"/>
</a>
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/stargazers" target="_blank">
<img src="https://img.shields.io/github/stars/zhom/donutbrowser?style=social" alt="GitHub stars">
</a>
</p>
## Donut Browser
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/preview-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="assets/preview.png" />
<img alt="Preview" src="assets/preview.png" />
</picture>
> A free and open source browser orchestrator built with [Tauri](https://v2.tauri.app/).
## Features
![Donut Browser Preview](assets/preview.png)
- Create unlimited number of local browser profiles completely isolated from each other
- Safely use multiple accounts on one device by using anti-detect browser profiles, powered by [Camoufox](https://camoufox.com)
- Proxy support with basic auth for all browsers except for TOR Browser
- Import profiles from your existing browsers
- Automatic updates for browsers
- Set Donut Browser as your default browser to control in which profile to open links
## Download
> As of right now, the app is not signed by Apple. You need to have Gatekeeper disabled to run it. The app automatically checks for updates on each launch.
> For Linux, .deb and .rpm packages are available as well as standalone .AppImage files.
The app can be downloaded from the [releases page](https://github.com/zhom/donutbrowser/releases/latest).
## Supported Platforms
-**macOS** (Intel & Apple Silicon)
-**Linux** (x64 & arm64)
- 🔄 **Windows** (Planned)
- 🔄 **Linux** (Planned)
## Development
@@ -54,6 +70,38 @@ Have questions or want to contribute? We'd love to hear from you!
- **Issues**: [GitHub Issues](https://github.com/zhom/donutbrowser/issues)
- **Discussions**: [GitHub Discussions](https://github.com/zhom/donutbrowser/discussions)
## Star History
<a href="https://www.star-history.com/#zhom/donutbrowser&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date" />
</picture>
</a>
## Contributors
<!-- readme: collaborators,contributors -start -->
<table>
<tbody>
<tr>
<td align="center">
<a href="https://github.com/zhom">
<img src="https://avatars.githubusercontent.com/u/2717306?v=4" width="100;" alt="zhom"/>
<br />
<sub><b>zhom</b></sub>
</a>
</td>
</tr>
<tbody>
</table>
<!-- readme: collaborators,contributors -end -->
## Contact
Have an urgent question or want to report a security vulnerability? Send an email to contact at donutbrowser dot com and we'll get back to you as fast as possible.
## License
This project is licensed under the AGPL-3.0 License - see the [LICENSE](LICENSE) file for details.
+40
View File
@@ -0,0 +1,40 @@
# Security Policy
## Reporting Security Issues
Thanks for helping make Donut Browser safe for everyone! ❤️
We take the security of Donut Browser seriously. If you believe you have found a security vulnerability in Donut Browser, please report it to us through coordinated disclosure.
**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
Instead, please send an email to **contact at donutbrowser dot com** with the subject line "Security Vulnerability Report".
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
- The type of issue (e.g., buffer overflow, injection attack, privilege escalation, or cross-site scripting)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit the issue
- Your assessment of the severity level
This information will help us triage your report more quickly.
## What to Expect
- **Response Time**: We will acknowledge receipt of your vulnerability report within 72 hours.
- **Investigation**: We will investigate the issue and provide you with updates on our progress.
- **Resolution**: We aim to resolve critical security issues as fast as possible, but no longer than in 30 days after the initial report.
- **Disclosure**: We will coordinate with you on the timing of any public disclosure.
## Contact
For urgent security matters, please contact us at **contact at donutbrowser dot com**.
For general questions about this security policy, you can also reach out through:
- [GitHub Issues](https://github.com/zhom/donutbrowser/issues) (for non-security questions only)
- [GitHub Discussions](https://github.com/zhom/donutbrowser/discussions)
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 114 KiB

+3 -17
View File
@@ -1,22 +1,18 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": []
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
@@ -25,17 +21,7 @@
"useHookAtTopLevel": "error"
},
"nursery": {
"useGoogleFontDisplay": "error",
"noDocumentImportInPage": "error",
"noHeadElement": "error",
"noHeadImportInDocument": "error",
"noImgElement": "off",
"useComponentExportOnlyModules": {
"level": "error",
"options": {
"allowExportNames": ["metadata", "badgeVariants", "buttonVariants"]
}
}
"useUniqueElementIds": "off"
},
"a11y": {
"useSemanticElements": "off"
+2 -2
View File
@@ -4,7 +4,7 @@
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"config": "tailwind.config.js",
"css": "src/styles/globals.css",
"baseColor": "zinc",
"cssVariables": true,
@@ -18,4 +18,4 @@
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
}
-133
View File
@@ -1,133 +0,0 @@
import { FlatCompat } from "@eslint/eslintrc";
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
const compat = new FlatCompat({
baseDirectory: import.meta.dirname,
});
const eslintConfig = tseslint.config(
eslint.configs.recommended,
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
...compat.extends("next/core-web-vitals"),
{
// Disabled rules taken from https://biomejs.dev/linter/rules-sources for ones that
// are already handled by Prettier and TypeScript or are not needed
rules: {
// eslint-plugin-jsx-a11y rules - some disabled for performance/specific project needs
"jsx-a11y/alt-text": "off",
"jsx-a11y/anchor-has-content": "off",
"jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/aria-activedescendant-has-tabindex": "off",
"jsx-a11y/aria-props": "off",
"jsx-a11y/aria-proptypes": "off",
"jsx-a11y/aria-role": "off",
"jsx-a11y/aria-unsupported-elements": "off",
"jsx-a11y/autocomplete-valid": "off",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/heading-has-content": "off",
"jsx-a11y/html-has-lang": "off",
"jsx-a11y/iframe-has-title": "off",
"jsx-a11y/img-redundant-alt": "off",
"jsx-a11y/interactive-supports-focus": "off",
"jsx-a11y/label-has-associated-control": "off",
"jsx-a11y/lang": "off",
"jsx-a11y/media-has-caption": "off",
"jsx-a11y/mouse-events-have-key-events": "off",
"jsx-a11y/no-access-key": "off",
"jsx-a11y/no-aria-hidden-on-focusable": "off",
"jsx-a11y/no-autofocus": "off",
"jsx-a11y/no-distracting-elements": "off",
"jsx-a11y/no-interactive-element-to-noninteractive-role": "off",
"jsx-a11y/no-noninteractive-element-to-interactive-role": "off",
"jsx-a11y/no-noninteractive-tabindex": "off",
"jsx-a11y/no-redundant-roles": "off",
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/prefer-tag-over-role": "off",
"jsx-a11y/role-has-required-aria-props": "off",
"jsx-a11y/role-supports-aria-props": "off",
"jsx-a11y/scope": "off",
"jsx-a11y/tabindex-no-positive": "off",
// eslint-plugin-react rules - some disabled for performance/specific project needs
"react/button-has-type": "off",
"react/jsx-boolean-value": "off",
"react/jsx-curly-brace-presence": "off",
"react/jsx-fragments": "off",
"react/jsx-key": "off",
"react/jsx-no-comment-textnodes": "off",
"react/jsx-no-duplicate-props": "off",
"react/jsx-no-target-blank": "off",
"react/jsx-no-useless-fragment": "off",
"react/no-array-index-key": "off",
"react/no-children-prop": "off",
"react/no-danger": "off",
"react/no-danger-with-children": "off",
"react/void-dom-elements-no-children": "off",
// eslint-plugin-react-hooks rules - disabled for specific project needs
"react-hooks/exhaustive-deps": "off",
"react-hooks/rules-of-hooks": "off",
// typescript-eslint rules - some handled by TypeScript compiler or disabled for project needs
"@typescript-eslint/adjacent-overload-signatures": "off",
"@typescript-eslint/array-type": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/consistent-type-exports": "off",
"@typescript-eslint/consistent-type-imports": "off",
"@typescript-eslint/default-param-last": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-dupe-class-members": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-extra-non-null-assertion": "off",
"@typescript-eslint/no-extraneous-class": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-invalid-void-type": "off",
"@typescript-eslint/no-loss-of-precision": "off",
"@typescript-eslint/no-misused-new": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-redeclare": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-restricted-imports": "off",
"@typescript-eslint/no-restricted-types": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off",
"@typescript-eslint/no-unsafe-declaration-merging": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-useless-constructor": "off",
"@typescript-eslint/no-useless-empty-export": "off",
"@typescript-eslint/only-throw-error": "off",
"@typescript-eslint/parameter-properties": "off",
"@typescript-eslint/prefer-as-const": "off",
"@typescript-eslint/prefer-enum-initializers": "off",
"@typescript-eslint/prefer-for-of": "off",
"@typescript-eslint/prefer-function-type": "off",
"@typescript-eslint/prefer-literal-enum-member": "off",
"@typescript-eslint/prefer-namespace-keyword": "off",
"@typescript-eslint/prefer-optional-chain": "off",
"@typescript-eslint/require-await": "off",
// Custom rules
"@typescript-eslint/restrict-template-expressions": [
"error",
{
allowNumber: true,
allowBoolean: true,
allowNever: true,
},
],
},
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
);
export default eslintConfig;
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# Determine file extension based on platform
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" || "$OSTYPE" == "cygwin" ]]; then
EXT=".exe"
else
EXT=""
fi
# If architecture provided in the command line, use it to rename the binary in TARGET_TRIPLE
if [ -n "$1" ]; then
TARGET_TRIPLE="$1"
else
RUST_INFO=$(rustc -vV)
TARGET_TRIPLE=$(echo "$RUST_INFO" | grep -o 'host: [^ ]*' | cut -d' ' -f2)
fi
# Check if target triple was found
if [ -z "$TARGET_TRIPLE" ]; then
echo "Failed to determine platform target triple" >&2
exit 1
fi
# Copy the file with target triple suffix
cp "nodecar-bin" "../src-tauri/binaries/nodecar-${TARGET_TRIPLE}${EXT}"
# Also copy a generic version for Tauri to find
cp "nodecar-bin" "../src-tauri/binaries/nodecar${EXT}"
-131
View File
@@ -1,131 +0,0 @@
import { FlatCompat } from "@eslint/eslintrc";
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
const compat = new FlatCompat({
baseDirectory: import.meta.dirname,
});
const eslintConfig = tseslint.config(
eslint.configs.recommended,
...compat.extends("next/core-web-vitals"),
{
// Disabled rules taken from https://biomejs.dev/linter/rules-sources for ones that
// are already handled by Prettier and TypeScript or are not needed
rules: {
// eslint-plugin-jsx-a11y rules - some disabled for performance/specific project needs
"jsx-a11y/alt-text": "off",
"jsx-a11y/anchor-has-content": "off",
"jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/aria-activedescendant-has-tabindex": "off",
"jsx-a11y/aria-props": "off",
"jsx-a11y/aria-proptypes": "off",
"jsx-a11y/aria-role": "off",
"jsx-a11y/aria-unsupported-elements": "off",
"jsx-a11y/autocomplete-valid": "off",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/heading-has-content": "off",
"jsx-a11y/html-has-lang": "off",
"jsx-a11y/iframe-has-title": "off",
"jsx-a11y/img-redundant-alt": "off",
"jsx-a11y/interactive-supports-focus": "off",
"jsx-a11y/label-has-associated-control": "off",
"jsx-a11y/lang": "off",
"jsx-a11y/media-has-caption": "off",
"jsx-a11y/mouse-events-have-key-events": "off",
"jsx-a11y/no-access-key": "off",
"jsx-a11y/no-aria-hidden-on-focusable": "off",
"jsx-a11y/no-autofocus": "off",
"jsx-a11y/no-distracting-elements": "off",
"jsx-a11y/no-interactive-element-to-noninteractive-role": "off",
"jsx-a11y/no-noninteractive-element-to-interactive-role": "off",
"jsx-a11y/no-noninteractive-tabindex": "off",
"jsx-a11y/no-redundant-roles": "off",
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/prefer-tag-over-role": "off",
"jsx-a11y/role-has-required-aria-props": "off",
"jsx-a11y/role-supports-aria-props": "off",
"jsx-a11y/scope": "off",
"jsx-a11y/tabindex-no-positive": "off",
// eslint-plugin-react rules - some disabled for performance/specific project needs
"react/button-has-type": "off",
"react/jsx-boolean-value": "off",
"react/jsx-curly-brace-presence": "off",
"react/jsx-fragments": "off",
"react/jsx-key": "off",
"react/jsx-no-comment-textnodes": "off",
"react/jsx-no-duplicate-props": "off",
"react/jsx-no-target-blank": "off",
"react/jsx-no-useless-fragment": "off",
"react/no-array-index-key": "off",
"react/no-children-prop": "off",
"react/no-danger": "off",
"react/no-danger-with-children": "off",
"react/void-dom-elements-no-children": "off",
// eslint-plugin-react-hooks rules - disabled for specific project needs
"react-hooks/exhaustive-deps": "off",
"react-hooks/rules-of-hooks": "off",
// typescript-eslint rules - some handled by TypeScript compiler or disabled for project needs
"@typescript-eslint/adjacent-overload-signatures": "off",
"@typescript-eslint/array-type": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/consistent-type-exports": "off",
"@typescript-eslint/consistent-type-imports": "off",
"@typescript-eslint/default-param-last": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-dupe-class-members": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-extra-non-null-assertion": "off",
"@typescript-eslint/no-extraneous-class": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-invalid-void-type": "off",
"@typescript-eslint/no-loss-of-precision": "off",
"@typescript-eslint/no-misused-new": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-redeclare": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-restricted-imports": "off",
"@typescript-eslint/no-restricted-types": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off",
"@typescript-eslint/no-unsafe-declaration-merging": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-useless-constructor": "off",
"@typescript-eslint/no-useless-empty-export": "off",
"@typescript-eslint/only-throw-error": "off",
"@typescript-eslint/parameter-properties": "off",
"@typescript-eslint/prefer-as-const": "off",
"@typescript-eslint/prefer-enum-initializers": "off",
"@typescript-eslint/prefer-for-of": "off",
"@typescript-eslint/prefer-function-type": "off",
"@typescript-eslint/prefer-literal-enum-member": "off",
"@typescript-eslint/prefer-namespace-keyword": "off",
"@typescript-eslint/prefer-optional-chain": "off",
"@typescript-eslint/require-await": "off",
// Custom rules
"@typescript-eslint/restrict-template-expressions": [
"error",
{
allowNumber: true,
allowBoolean: true,
allowNever: true,
},
],
},
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
}
);
export default eslintConfig;
+24 -17
View File
@@ -2,32 +2,39 @@
"name": "nodecar",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"main": "dist/index.js",
"bin": "dist/index.js",
"scripts": {
"watch": "nodemon --exec ts-node --esm ./src/index.ts --watch src",
"dev": "node --loader ts-node/esm ./src/index.ts",
"start": "node --loader ts-node/esm ./src/index.ts",
"build": "tsc && pkg ./dist/index.js --targets latest-macos-arm64 --output dist/nodecar",
"build:aarch64": "tsc && pkg ./dist/index.js --targets latest-macos-arm64 --output dist/nodecar",
"build:x86_64": "tsc && pkg ./dist/index.js --targets latest-macos-x64 --output dist/nodecar",
"build:linux-x64": "tsc && pkg ./dist/index.js --targets latest-linux-x64 --output dist/nodecar",
"build:linux-arm64": "tsc && pkg ./dist/index.js --targets latest-linux-arm64 --output dist/nodecar",
"build:win-x64": "tsc && pkg ./dist/index.js --targets latest-win-x64 --output dist/nodecar",
"build:win-arm64": "tsc && pkg ./dist/index.js --targets latest-win-arm64 --output dist/nodecar"
"start": "tsc && node ./dist/index.js",
"rename-binary": "sh ./copy-binary.sh",
"build": "tsc && banderole bundle . --output nodecar-bin && pnpm rename-binary",
"build:mac-aarch64": "tsc && banderole bundle . --output nodecar-bin && pnpm rename-binary",
"build:mac-x86_64": "tsc && banderole bundle . --output nodecar-bin && pnpm rename-binary",
"build:linux-x64": "tsc && banderole bundle . --output nodecar-bin && pnpm rename-binary",
"build:linux-arm64": "tsc && banderole bundle . --output nodecar-bin && pnpm rename-binary",
"build:win-x64": "tsc && banderole bundle . --output nodecar-bin && pnpm rename-binary",
"build:win-arm64": "tsc && banderole bundle . --output nodecar-bin && pnpm rename-binary"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.6.1",
"license": "AGPL-3.0",
"dependencies": {
"@types/node": "^22.15.17",
"@yao-pkg/pkg": "^6.4.1",
"commander": "^13.1.0",
"dotenv": "^16.5.0",
"@types/node": "^24.2.1",
"commander": "^14.0.0",
"donutbrowser-camoufox-js": "^0.6.6",
"dotenv": "^17.2.1",
"fingerprint-generator": "^2.1.69",
"get-port": "^7.1.0",
"nodemon": "^3.1.10",
"proxy-chain": "^2.5.8",
"playwright-core": "^1.54.2",
"proxy-chain": "^2.5.9",
"tmp": "^0.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
"typescript": "^5.9.2"
},
"devDependencies": {
"@types/tmp": "^0.2.6"
}
}
-1304
View File
File diff suppressed because it is too large Load Diff
-14
View File
@@ -1,14 +0,0 @@
import { execSync } from "child_process";
import fs from "fs";
const ext = process.platform === "win32" ? ".exe" : "";
const rustInfo = execSync("rustc -vV");
const targetTriple = /host: (\S+)/g.exec(rustInfo)[1];
if (!targetTriple) {
console.error("Failed to determine platform target triple");
}
fs.renameSync(
`dist/nodecar${ext}`,
`../src-tauri/binaries/nodecar-${targetTriple}${ext}`
);
+459
View File
@@ -0,0 +1,459 @@
import { spawn } from "node:child_process";
import path from "node:path";
import { launchOptions } from "donutbrowser-camoufox-js";
import type { LaunchOptions } from "donutbrowser-camoufox-js/dist/utils.js";
import {
type CamoufoxConfig,
deleteCamoufoxConfig,
generateCamoufoxId,
getCamoufoxConfig,
listCamoufoxConfigs,
saveCamoufoxConfig,
} from "./camoufox-storage.js";
/**
* Convert camoufox fingerprint format to fingerprint-generator format
* @param camoufoxFingerprint The camoufox fingerprint object
* @returns fingerprint-generator object
*/
function convertCamoufoxToFingerprintGenerator(
camoufoxFingerprint: Record<string, any>,
): any {
const fingerprintObj: Record<string, any> = {
navigator: {},
screen: {},
videoCard: {},
headers: {},
battery: {},
};
// Mapping from camoufox keys to fingerprint-generator structure based on the YAML
const mappings: Record<string, string> = {
// Navigator properties
"navigator.userAgent": "navigator.userAgent",
"navigator.platform": "navigator.platform",
"navigator.hardwareConcurrency": "navigator.hardwareConcurrency",
"navigator.maxTouchPoints": "navigator.maxTouchPoints",
"navigator.doNotTrack": "navigator.doNotTrack",
"navigator.appCodeName": "navigator.appCodeName",
"navigator.appName": "navigator.appName",
"navigator.appVersion": "navigator.appVersion",
"navigator.oscpu": "navigator.oscpu",
"navigator.product": "navigator.product",
"navigator.language": "navigator.language",
"navigator.languages": "navigator.languages",
"navigator.globalPrivacyControl": "navigator.globalPrivacyControl",
// Screen properties
"screen.width": "screen.width",
"screen.height": "screen.height",
"screen.availWidth": "screen.availWidth",
"screen.availHeight": "screen.availHeight",
"screen.availTop": "screen.availTop",
"screen.availLeft": "screen.availLeft",
"screen.colorDepth": "screen.colorDepth",
"screen.pixelDepth": "screen.pixelDepth",
"window.outerWidth": "screen.outerWidth",
"window.outerHeight": "screen.outerHeight",
"window.innerWidth": "screen.innerWidth",
"window.innerHeight": "screen.innerHeight",
"window.screenX": "screen.screenX",
"window.screenY": "screen.screenY",
"screen.pageXOffset": "screen.pageXOffset",
"screen.pageYOffset": "screen.pageYOffset",
"window.devicePixelRatio": "screen.devicePixelRatio",
"document.body.clientWidth": "screen.clientWidth",
"document.body.clientHeight": "screen.clientHeight",
// WebGL properties
"webGl:vendor": "videoCard.vendor",
"webGl:renderer": "videoCard.renderer",
// Headers
"headers.Accept-Encoding": "headers.Accept-Encoding",
// Battery
"battery:charging": "battery.charging",
"battery:chargingTime": "battery.chargingTime",
"battery:dischargingTime": "battery.dischargingTime",
};
// Apply mappings
for (const [camoufoxKey, fingerprintPath] of Object.entries(mappings)) {
if (camoufoxFingerprint[camoufoxKey] !== undefined) {
const pathParts = fingerprintPath.split(".");
let current = fingerprintObj;
// Navigate to the nested property, creating objects as needed
for (let i = 0; i < pathParts.length - 1; i++) {
const part = pathParts[i];
if (!current[part]) {
current[part] = {};
}
current = current[part];
}
// Set the final value
const finalKey = pathParts[pathParts.length - 1];
current[finalKey] = camoufoxFingerprint[camoufoxKey];
}
}
// Handle fonts separately
if (camoufoxFingerprint.fonts && Array.isArray(camoufoxFingerprint.fonts)) {
fingerprintObj.fonts = camoufoxFingerprint.fonts;
}
return { ...camoufoxFingerprint, ...fingerprintObj };
}
/**
* Start a Camoufox instance in a separate process
* @param options Camoufox launch options
* @param profilePath Profile directory path
* @param url Optional URL to open
* @returns Promise resolving to the Camoufox configuration
*/
export async function startCamoufoxProcess(
options: LaunchOptions = {},
profilePath?: string,
url?: string,
customConfig?: string,
): Promise<CamoufoxConfig> {
// Generate a unique ID for this instance
const id = generateCamoufoxId();
// Ensure profile path is absolute if provided
const absoluteProfilePath = profilePath
? path.resolve(profilePath)
: undefined;
// Create the Camoufox configuration
const config: CamoufoxConfig = {
id,
options: JSON.parse(JSON.stringify(options)), // Deep clone to avoid reference sharing
profilePath: absoluteProfilePath,
url,
customConfig,
};
// Save the configuration before starting the process
saveCamoufoxConfig(config);
// Build the command arguments
const args = [
path.join(__dirname, "index.js"),
"camoufox-worker",
"start",
"--id",
id,
];
// Spawn the process with proper detachment - similar to proxy implementation
const child = spawn(process.execPath, args, {
detached: true,
stdio: ["ignore", "pipe", "pipe"], // Capture stdout and stderr for startup feedback
cwd: process.cwd(),
env: {
...process.env,
NODE_ENV: "production",
// Ensure Camoufox can find its dependencies
NODE_PATH: process.env.NODE_PATH || "",
},
});
// Wait for the worker to start successfully or fail - with shorter timeout for quick response
return new Promise<CamoufoxConfig>((resolve, reject) => {
let resolved = false;
let stdoutBuffer = "";
let stderrBuffer = "";
// Shorter timeout for quick startup feedback
const timeout = setTimeout(() => {
if (!resolved) {
resolved = true;
child.kill("SIGKILL");
reject(
new Error(`Camoufox worker ${id} startup timeout after 5 seconds`),
);
}
}, 5000);
// Handle stdout - look for success JSON
if (child.stdout) {
child.stdout.on("data", (data) => {
const output = data.toString();
stdoutBuffer += output;
// Look for success JSON message
const lines = stdoutBuffer.split("\n");
for (const line of lines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line.trim());
if (parsed.success && parsed.id === id && parsed.processId) {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
config.processId = parsed.processId;
saveCamoufoxConfig(config);
// Unref immediately after success to detach properly
child.unref();
resolve(config);
return;
}
}
} catch {
// Not JSON, continue
}
}
}
});
}
// Handle stderr - look for error JSON
if (child.stderr) {
child.stderr.on("data", (data) => {
const output = data.toString();
stderrBuffer += output;
// Look for error JSON message
const lines = stderrBuffer.split("\n");
for (const line of lines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line.trim());
if (parsed.error && parsed.id === id) {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
reject(
new Error(
`Camoufox worker failed: ${parsed.message || parsed.error}`,
),
);
return;
}
}
} catch {
// Not JSON, continue
}
}
}
});
}
child.on("exit", (code, signal) => {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
if (code !== 0) {
reject(
new Error(
`Camoufox worker ${id} exited with code ${code} and signal ${signal}. Stderr: ${stderrBuffer}`,
),
);
} else {
// Process exited successfully but we didn't get success message
reject(
new Error(
`Camoufox worker ${id} exited without success confirmation`,
),
);
}
}
});
});
}
/**
* Stop a Camoufox process
* @param id The Camoufox ID to stop
* @returns Promise resolving to true if stopped, false if not found
*/
export async function stopCamoufoxProcess(id: string): Promise<boolean> {
const config = getCamoufoxConfig(id);
if (!config) {
return false;
}
try {
// Method 1: If we have a process ID, kill by PID with proper signal sequence
if (config.processId) {
try {
// First try SIGTERM for graceful shutdown
process.kill(config.processId, "SIGTERM");
// Give it more time to terminate gracefully (increased from 2s to 5s)
await new Promise((resolve) => setTimeout(resolve, 5000));
// Check if process is still running
try {
process.kill(config.processId, 0); // Signal 0 checks if process exists
process.kill(config.processId, "SIGKILL");
} catch {}
} catch {}
}
// Method 2: Pattern-based kill as fallback
const killByPattern = spawn(
"pkill",
["-TERM", "-f", `camoufox-worker.*${id}`],
{
stdio: "ignore",
},
);
// Wait for pattern-based kill command to complete
await new Promise<void>((resolve) => {
killByPattern.on("exit", () => resolve());
// Timeout after 3 seconds
setTimeout(() => resolve(), 3000);
});
// Final cleanup with SIGKILL if needed
setTimeout(() => {
spawn("pkill", ["-KILL", "-f", `camoufox-worker.*${id}`], {
stdio: "ignore",
});
}, 1000);
// Delete the configuration
deleteCamoufoxConfig(id);
return true;
} catch {
// Delete the configuration even if stopping failed
deleteCamoufoxConfig(id);
return false;
}
}
/**
* Stop all Camoufox processes
* @returns Promise resolving when all instances are stopped
*/
export async function stopAllCamoufoxProcesses(): Promise<void> {
const configs = listCamoufoxConfigs();
const stopPromises = configs.map((config) => stopCamoufoxProcess(config.id));
await Promise.all(stopPromises);
}
interface GenerateConfigOptions {
proxy?: string;
maxWidth?: number;
maxHeight?: number;
minWidth?: number;
minHeight?: number;
geoip?: string | boolean;
blockImages?: boolean;
blockWebrtc?: boolean;
blockWebgl?: boolean;
executablePath?: string;
fingerprint?: string;
}
/**
* Generate Camoufox configuration using launchOptions
* @param options Configuration options
* @returns Promise resolving to the generated config JSON string
*/
export async function generateCamoufoxConfig(
options: GenerateConfigOptions,
): Promise<string> {
try {
const launchOpts: any = {
headless: false,
i_know_what_im_doing: true,
config: {
disableTheming: true,
showcursor: false,
},
};
if (options.geoip) {
launchOpts.geoip = true;
}
if (options.blockImages) {
launchOpts.block_images = true;
}
if (options.blockWebrtc) {
launchOpts.block_webrtc = true;
}
if (options.blockWebgl) {
launchOpts.block_webgl = true;
}
if (options.executablePath) {
launchOpts.executable_path = options.executablePath;
}
if (options.proxy) {
launchOpts.proxy = options.proxy;
}
// If fingerprint is provided, use it and ignore other options except executable_path and block_*
if (options.fingerprint) {
try {
const camoufoxFingerprint = JSON.parse(options.fingerprint);
if (camoufoxFingerprint.timezone) {
launchOpts.config.timezone = camoufoxFingerprint.timezone;
}
// Convert camoufox fingerprint format to fingerprint-generator format
const fingerprintObj =
convertCamoufoxToFingerprintGenerator(camoufoxFingerprint);
launchOpts.fingerprint = fingerprintObj;
} catch (error) {
throw new Error(`Invalid fingerprint JSON: ${error}`);
}
} else {
// Use individual options to build configuration
// Build screen configuration with min/max dimensions
const screen: {
minWidth?: number;
maxWidth?: number;
minHeight?: number;
maxHeight?: number;
} = {};
if (options.minWidth) screen.minWidth = options.minWidth;
if (options.maxWidth) screen.maxWidth = options.maxWidth;
if (options.minHeight) screen.minHeight = options.minHeight;
if (options.maxHeight) screen.maxHeight = options.maxHeight;
if (Object.keys(screen).length > 0) {
launchOpts.screen = screen;
}
}
// Generate the configuration using launchOptions
const generatedOptions = await launchOptions(launchOpts);
// Extract the environment variables that contain the config
const envVars = generatedOptions.env || {};
// Reconstruct the config from environment variables using getEnvVars utility
let configStr = "";
let chunkIndex = 1;
while (envVars[`CAMOU_CONFIG_${chunkIndex}`]) {
configStr += envVars[`CAMOU_CONFIG_${chunkIndex}`];
chunkIndex++;
}
if (!configStr) {
throw new Error("No configuration generated");
}
// Parse and return the config as JSON string
const config = JSON.parse(configStr);
return JSON.stringify(config);
} catch (error) {
throw new Error(`Failed to generate Camoufox config: ${error}`);
}
}
+153
View File
@@ -0,0 +1,153 @@
import fs from "node:fs";
import path from "node:path";
import type { LaunchOptions } from "donutbrowser-camoufox-js/dist/utils.js";
import tmp from "tmp";
export interface CamoufoxConfig {
id: string;
options: LaunchOptions;
profilePath?: string;
url?: string;
processId?: number;
customConfig?: string; // JSON string of the fingerprint config
}
const STORAGE_DIR = path.join(tmp.tmpdir, "donutbrowser", "camoufox");
if (!fs.existsSync(STORAGE_DIR)) {
fs.mkdirSync(STORAGE_DIR, { recursive: true });
}
/**
* Save a Camoufox configuration to disk
* @param config The Camoufox configuration to save
*/
export function saveCamoufoxConfig(config: CamoufoxConfig): void {
const filePath = path.join(STORAGE_DIR, `${config.id}.json`);
fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
}
/**
* Get a Camoufox configuration by ID
* @param id The Camoufox ID
* @returns The Camoufox configuration or null if not found
*/
export function getCamoufoxConfig(id: string): CamoufoxConfig | null {
const filePath = path.join(STORAGE_DIR, `${id}.json`);
if (!fs.existsSync(filePath)) {
return null;
}
try {
const content = fs.readFileSync(filePath, "utf-8");
return JSON.parse(content) as CamoufoxConfig;
} catch (error) {
console.error({
message: `Error reading Camoufox config ${id}`,
error: (error as Error).message,
});
return null;
}
}
/**
* Delete a Camoufox configuration
* @param id The Camoufox ID to delete
* @returns True if deleted, false if not found
*/
export function deleteCamoufoxConfig(id: string): boolean {
const filePath = path.join(STORAGE_DIR, `${id}.json`);
if (!fs.existsSync(filePath)) {
return false;
}
try {
fs.unlinkSync(filePath);
return true;
} catch (error) {
console.error({
message: `Error deleting Camoufox config ${id}`,
error: (error as Error).message,
});
return false;
}
}
/**
* List all saved Camoufox configurations
* @returns Array of Camoufox configurations
*/
export function listCamoufoxConfigs(): CamoufoxConfig[] {
if (!fs.existsSync(STORAGE_DIR)) {
return [];
}
try {
return fs
.readdirSync(STORAGE_DIR)
.filter((file) => file.endsWith(".json"))
.map((file) => {
try {
const content = fs.readFileSync(
path.join(STORAGE_DIR, file),
"utf-8",
);
return JSON.parse(content) as CamoufoxConfig;
} catch (error) {
console.error({
message: `Error reading Camoufox config ${file}`,
error,
});
return null;
}
})
.filter((config): config is CamoufoxConfig => config !== null)
.map((config) => {
config.options = "Removed for logging" as any;
config.customConfig = "Removed for logging" as any;
return config;
});
} catch (error) {
console.error({ message: "Error listing Camoufox configs:", error });
return [];
}
}
/**
* Update a Camoufox configuration
* @param config The Camoufox configuration to update
* @returns True if updated, false if not found
*/
export function updateCamoufoxConfig(config: CamoufoxConfig): boolean {
const filePath = path.join(STORAGE_DIR, `${config.id}.json`);
try {
fs.readFileSync(filePath, "utf-8");
fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
console.error({
message: `Config ${config.id} was deleted while the app was running`,
});
return false;
}
console.error({
message: `Error updating Camoufox config ${config.id}`,
error,
});
return false;
}
}
/**
* Generate a unique ID for a Camoufox instance
* @returns A unique ID string
*/
export function generateCamoufoxId(): string {
// Include process ID to ensure uniqueness across multiple processes
return `camoufox_${Date.now()}_${process.pid}_${Math.floor(Math.random() * 10000)}`;
}
+307
View File
@@ -0,0 +1,307 @@
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 { getCamoufoxConfig, saveCamoufoxConfig } from "./camoufox-storage.js";
import { getEnvVars, parseProxyString } from "./utils.js";
/**
* Run a Camoufox browser server as a worker process
* @param id The Camoufox configuration ID
*/
export async function runCamoufoxWorker(id: string): Promise<void> {
// Get the Camoufox configuration
const config = getCamoufoxConfig(id);
if (!config) {
console.error(
JSON.stringify({
error: "Configuration not found",
id: id,
}),
);
process.exit(1);
}
config.processId = process.pid;
saveCamoufoxConfig(config);
console.log(
JSON.stringify({
success: true,
id: id,
processId: process.pid,
profilePath: config.profilePath,
message: "Camoufox worker started successfully",
}),
);
// Launch browser in background - this can take time and may fail
setImmediate(async () => {
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 () => {
try {
// Clear any intervals first
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
// Close browser context and server if they exist
if (context && !context.pages) {
// Context is already closed
} else if (context) {
await context.close();
}
if (browser?.isConnected()) {
await browser.close();
}
} catch {
// Ignore cleanup errors during shutdown
}
process.exit(0);
};
// Handle various quit signals for proper macOS Command+Q support
process.on("SIGTERM", () => void gracefulShutdown());
process.on("SIGINT", () => void gracefulShutdown());
process.on("SIGHUP", () => void gracefulShutdown());
process.on("SIGQUIT", () => void gracefulShutdown());
// Handle uncaught exceptions and unhandled rejections
process.on("uncaughtException", () => void gracefulShutdown());
process.on("unhandledRejection", () => void gracefulShutdown());
try {
// Deep clone to avoid reference sharing and ensure fresh configuration for each instance
const camoufoxOptions: LaunchOptions = JSON.parse(
JSON.stringify(config.options || {}),
);
// Add profile path if provided
if (config.profilePath) {
camoufoxOptions.user_data_dir = config.profilePath;
}
// Ensure block options are properly set
if (camoufoxOptions.block_images) {
camoufoxOptions.block_images = true;
}
if (camoufoxOptions.block_webgl) {
camoufoxOptions.block_webgl = true;
}
if (camoufoxOptions.block_webrtc) {
camoufoxOptions.block_webrtc = true;
}
// Check for headless mode from config (no environment variable check)
if (camoufoxOptions.headless) {
camoufoxOptions.headless = true;
}
// Always set these defaults - ensure they are applied for each instance
camoufoxOptions.i_know_what_im_doing = true;
camoufoxOptions.config = {
disableTheming: true,
showcursor: false,
...(camoufoxOptions.config || {}),
};
// Generate fresh options for this specific instance
const generatedOptions = await launchOptions(camoufoxOptions);
// Start with process environment to ensure proper inheritance
let finalEnv = { ...process.env };
// Add generated options environment variables
if (generatedOptions.env) {
finalEnv = { ...finalEnv, ...generatedOptions.env };
}
// If we have a custom config from Rust, use it directly as environment variables
if (config.customConfig) {
try {
// Parse the custom config JSON string
const customConfigObj = JSON.parse(config.customConfig);
// Ensure default config values are preserved even with custom config
const mergedConfig = {
...customConfigObj,
disableTheming: true,
showcursor: false,
};
// Convert merged config to environment variables using getEnvVars
const customEnvVars = getEnvVars(mergedConfig);
// Merge custom config with generated config (custom takes precedence)
finalEnv = { ...finalEnv, ...customEnvVars };
} catch (error) {
console.error(
`Camoufox worker ${id}: Failed to parse custom config, using generated config:`,
error,
);
return;
}
}
// Prepare profile path for persistent context
const profilePath = config.profilePath || "";
// Launch persistent context with the final configuration
const finalOptions: any = {
...generatedOptions,
env: finalEnv,
};
// If a custom executable path was provided, ensure Playwright uses it
if (
(camoufoxOptions as any).executable_path &&
typeof (camoufoxOptions as any).executable_path === "string"
) {
finalOptions.executablePath = (camoufoxOptions as any)
.executable_path as string;
}
// Only add proxy if it exists and is valid
if (camoufoxOptions.proxy) {
try {
finalOptions.proxy = parseProxyString(camoufoxOptions.proxy);
} catch (error) {
console.error({
message: "Failed to parse proxy, launching without proxy",
error,
});
return;
}
}
// Use launchPersistentContext instead of launchServer
context = await firefox.launchPersistentContext(
profilePath,
finalOptions,
);
// Get the browser instance from context
browser = context.browser();
// Handle browser disconnection for proper cleanup
if (browser) {
browser.on("disconnected", () => void gracefulShutdown());
}
// Handle context close for proper cleanup
context.on("close", () => void gracefulShutdown());
saveCamoufoxConfig(config);
// Monitor for window closure
const startWindowMonitoring = () => {
windowCheckInterval = setInterval(async () => {
try {
// Check if context is still active
if (!context?.pages || context.pages().length === 0) {
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
await gracefulShutdown();
return;
}
// Check if browser is still connected (if available)
if (browser && !browser.isConnected()) {
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
await gracefulShutdown();
return;
}
// Check pages in the persistent context
const pages = context.pages();
if (pages.length === 0) {
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
await gracefulShutdown();
}
} catch {
// If we can't check windows, assume browser is closing
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
await gracefulShutdown();
}
}, 1000); // Check every second
};
// Handle URL opening if provided
if (config.url) {
try {
const pages = await context.pages();
if (pages.length) {
const page = pages[0];
await page.goto(config.url, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
// Start monitoring after page is created
startWindowMonitoring();
}
} catch (urlError) {
console.error({
message: "Failed to open URL",
error: urlError,
});
// URL opening failure doesn't affect startup success
// Still start monitoring
startWindowMonitoring();
}
} else {
// Start monitoring after page is created
startWindowMonitoring();
}
// Monitor browser/context connection
const keepAlive = setInterval(async () => {
try {
// Check if context is still active
if (!context?.pages) {
clearInterval(keepAlive);
await gracefulShutdown();
return;
}
// Check browser connection if available
if (browser && !browser.isConnected()) {
clearInterval(keepAlive);
await gracefulShutdown();
return;
}
} catch (error) {
console.error({
message: "Error in keepAlive check",
error,
});
clearInterval(keepAlive);
await gracefulShutdown();
}
}, 2000);
} catch (error) {
console.error({
message: "Failed to launch Camoufox",
error,
});
// Browser launch failed, attempt cleanup
await gracefulShutdown();
}
});
// Keep process alive
process.stdin.resume();
}
+387 -26
View File
@@ -1,8 +1,17 @@
import { program } from "commander";
import type { LaunchOptions } from "donutbrowser-camoufox-js/dist/utils.js";
import {
generateCamoufoxConfig,
startCamoufoxProcess,
stopAllCamoufoxProcesses,
stopCamoufoxProcess,
} from "./camoufox-launcher.js";
import { listCamoufoxConfigs } from "./camoufox-storage.js";
import { runCamoufoxWorker } from "./camoufox-worker.js";
import {
startProxyProcess,
stopProxyProcess,
stopAllProxyProcesses,
stopProxyProcess,
} from "./proxy-runner";
import { listProxyConfigs } from "./proxy-storage";
import { runProxyWorker } from "./proxy-worker";
@@ -11,79 +20,117 @@ import { runProxyWorker } from "./proxy-worker";
program
.command("proxy")
.argument("<action>", "start, stop, or list proxies")
.option(
"-u, --upstream <url>",
"upstream proxy URL (protocol://[username:password@]host:port)"
)
.option("--host <host>", "upstream proxy host")
.option("--proxy-port <port>", "upstream proxy port", Number.parseInt)
.option("--type <type>", "proxy type (http, https, socks4, socks5)")
.option("--username <username>", "proxy username")
.option("--password <password>", "proxy password")
.option(
"-p, --port <number>",
"local port to use (random if not specified)",
Number.parseInt
Number.parseInt,
)
.option("--ignore-certificate", "ignore certificate errors for HTTPS proxies")
.option("--id <id>", "proxy ID for stop command")
.option(
"-u, --upstream <url>",
"upstream proxy URL (protocol://[username:password@]host:port)",
)
.description("manage proxy servers")
.action(
async (
action: string,
options: {
upstream?: string;
host?: string;
proxyPort?: number;
type?: string;
username?: string;
password?: string;
port?: number;
ignoreCertificate?: boolean;
id?: string;
}
upstream?: string;
},
) => {
if (action === "start") {
if (!options.upstream) {
console.error("Error: Upstream proxy URL is required");
console.log(
"Example: proxy start -u http://username:password@proxy.example.com:8080"
);
return;
let upstreamUrl: string | undefined;
// Build upstream URL from individual components if provided
if (options.host && options.proxyPort && options.type) {
// Preserve provided scheme (http, https, socks4, socks5)
const protocol = String(options.type).toLowerCase();
const auth =
options.username && options.password
? `${encodeURIComponent(options.username)}:${encodeURIComponent(
options.password,
)}@`
: "";
upstreamUrl = `${protocol}://${auth}${options.host}:${options.proxyPort}`;
} else if (options.upstream) {
upstreamUrl = options.upstream;
}
// If no upstream is provided, create a direct proxy
try {
const config = await startProxyProcess(options.upstream, {
const config = await startProxyProcess(upstreamUrl, {
port: options.port,
ignoreProxyCertificate: options.ignoreCertificate,
});
console.log(JSON.stringify(config));
} catch (error: any) {
console.error(`Failed to start proxy: ${error.message}`);
// Output the configuration as JSON for the Rust side to parse
console.log(
JSON.stringify({
id: config.id,
localPort: config.localPort,
localUrl: config.localUrl,
upstreamUrl: config.upstreamUrl,
}),
);
// Exit successfully to allow the process to detach
process.exit(0);
} catch (error: unknown) {
console.error(
`Failed to start proxy: ${
error instanceof Error ? error.message : JSON.stringify(error)
}`,
);
process.exit(1);
}
} else if (action === "stop") {
if (options.id) {
const stopped = await stopProxyProcess(options.id);
console.log(`{
"success": ${stopped}}`);
console.log(JSON.stringify({ success: stopped }));
} else if (options.upstream) {
// Find proxies with this upstream URL
const configs = listProxyConfigs().filter(
(config) => config.upstreamUrl === options.upstream
(config) => config.upstreamUrl === options.upstream,
);
if (configs.length === 0) {
console.error(`No proxies found for ${options.upstream}`);
process.exit(1);
return;
}
for (const config of configs) {
const stopped = await stopProxyProcess(config.id);
console.log(`{
"success": ${stopped}}`);
console.log(JSON.stringify({ success: stopped }));
}
} else {
await stopAllProxyProcesses();
console.log(`{
"success": true}`);
console.log(JSON.stringify({ success: true }));
}
process.exit(0);
} else if (action === "list") {
const configs = listProxyConfigs();
console.log(JSON.stringify(configs));
process.exit(0);
} else {
console.error("Invalid action. Use 'start', 'stop', or 'list'");
process.exit(1);
}
}
},
);
// Command for proxy worker (internal use)
@@ -101,4 +148,318 @@ program
}
});
// Command for Camoufox management
program
.command("camoufox")
.argument(
"<action>",
"start, stop, list, or generate-config Camoufox instances",
)
.option("--id <id>", "Camoufox ID for stop command")
.option("--profile-path <path>", "profile directory path")
.option("--url <url>", "URL to open")
// Config generation options
.option("--proxy <proxy>", "proxy URL for config generation")
.option("--max-width <width>", "maximum screen width", parseInt)
.option("--max-height <height>", "maximum screen height", parseInt)
.option("--min-width <width>", "minimum screen width", parseInt)
.option("--min-height <height>", "minimum screen height", parseInt)
.option("--geoip", "enable geoip")
.option("--block-images", "block images")
.option("--block-webrtc", "block WebRTC")
.option("--block-webgl", "block WebGL")
.option("--executable-path <path>", "executable path")
.option("--fingerprint <json>", "fingerprint JSON string")
.option("--headless", "run in headless mode")
.option("--custom-config <json>", "custom config JSON string")
.description("manage Camoufox browser instances")
.action(
async (
action: string,
options: Record<string, string | number | boolean | undefined>,
) => {
if (action === "start") {
try {
// Build Camoufox options in the format expected by camoufox-js
const camoufoxOptions: LaunchOptions = {};
// OS fingerprinting
if (options.os && typeof options.os === "string") {
camoufoxOptions.os = options.os.includes(",")
? (options.os.split(",") as ("windows" | "macos" | "linux")[])
: (options.os as "windows" | "macos" | "linux");
}
// Blocking options
if (options.blockImages) camoufoxOptions.block_images = true;
if (options.blockWebrtc) camoufoxOptions.block_webrtc = true;
if (options.blockWebgl) camoufoxOptions.block_webgl = true;
// Security options
if (options.disableCoop) camoufoxOptions.disable_coop = true;
if (options.geoip) {
camoufoxOptions.geoip = true;
}
if (options.latitude && options.longitude) {
camoufoxOptions.geolocation = {
latitude: options.latitude as number,
longitude: options.longitude as number,
accuracy: 100,
};
}
if (options.country)
camoufoxOptions.country = options.country as string;
if (options.timezone)
camoufoxOptions.timezone = options.timezone as string;
if (options.humanize)
camoufoxOptions.humanize = options.humanize as boolean;
if (options.headless) camoufoxOptions.headless = true;
// Localization
if (options.locale && typeof options.locale === "string") {
camoufoxOptions.locale = options.locale.includes(",")
? options.locale.split(",")
: options.locale;
}
// Extensions and fonts
if (options.addons && typeof options.addons === "string")
camoufoxOptions.addons = options.addons.split(",");
if (options.fonts && typeof options.fonts === "string")
camoufoxOptions.fonts = options.fonts.split(",");
if (options.customFontsOnly) camoufoxOptions.custom_fonts_only = true;
if (
options.excludeAddons &&
typeof options.excludeAddons === "string"
)
camoufoxOptions.exclude_addons = options.excludeAddons.split(
",",
) as "UBO"[];
// Executable path: forward through to camoufox-js and ultimately Playwright
if (
options.executablePath &&
typeof options.executablePath === "string"
) {
// camoufox-js uses snake_case for this option
(camoufoxOptions as any).executable_path =
options.executablePath as string;
}
// Screen and window
const screen: {
minWidth?: number;
maxWidth?: number;
minHeight?: number;
maxHeight?: number;
} = {};
if (options.screenMinWidth)
screen.minWidth = options.screenMinWidth as number;
if (options.screenMaxWidth)
screen.maxWidth = options.screenMaxWidth as number;
if (options.screenMinHeight)
screen.minHeight = options.screenMinHeight as number;
if (options.screenMaxHeight)
screen.maxHeight = options.screenMaxHeight as number;
if (Object.keys(screen).length > 0) camoufoxOptions.screen = screen;
if (options.windowWidth && options.windowHeight) {
camoufoxOptions.window = [
options.windowWidth as number,
options.windowHeight as number,
];
}
// Advanced options
if (options.ffVersion)
camoufoxOptions.ff_version = options.ffVersion as number;
if (options.mainWorldEval) camoufoxOptions.main_world_eval = true;
if (options.webglVendor && options.webglRenderer) {
camoufoxOptions.webgl_config = [
options.webglVendor as string,
options.webglRenderer as string,
];
}
// Proxy
if (options.proxy) camoufoxOptions.proxy = options.proxy as string;
// Cache and performance - default to enabled
camoufoxOptions.enable_cache = !options.disableCache;
// Environment and debugging
if (options.virtualDisplay)
camoufoxOptions.virtual_display = options.virtualDisplay as string;
if (options.debug) camoufoxOptions.debug = true;
// Handle headless mode via flag instead of environment variable
if (options.headless) {
camoufoxOptions.headless = true;
}
if (options.args && typeof options.args === "string")
camoufoxOptions.args = options.args.split(",");
if (options.env && typeof options.env === "string") {
try {
camoufoxOptions.env = JSON.parse(options.env);
} catch (e) {
console.error(
JSON.stringify({
error: "Invalid JSON for --env option",
message: String(e),
}),
);
process.exit(1);
return;
}
}
// Firefox preferences
if (
options.firefoxPrefs &&
typeof options.firefoxPrefs === "string"
) {
try {
camoufoxOptions.firefox_user_prefs = JSON.parse(
options.firefoxPrefs,
);
} catch (e) {
console.error(
JSON.stringify({
error: "Invalid JSON for --firefox-prefs option",
message: String(e),
}),
);
process.exit(1);
}
}
const config = await startCamoufoxProcess(
camoufoxOptions,
typeof options.profilePath === "string"
? options.profilePath
: undefined,
typeof options.url === "string" ? options.url : undefined,
typeof options.customConfig === "string"
? options.customConfig
: undefined,
);
console.log(
JSON.stringify({
id: config.id,
processId: config.processId,
profilePath: config.profilePath,
url: config.url,
}),
);
process.exit(0);
} catch (error: unknown) {
console.error(
JSON.stringify({
error: "Failed to start Camoufox",
message: error instanceof Error ? error.message : String(error),
}),
);
process.exit(1);
}
} else if (action === "stop") {
if (options.id && typeof options.id === "string") {
const stopped = await stopCamoufoxProcess(options.id);
console.log(JSON.stringify({ success: stopped }));
} else {
await stopAllCamoufoxProcesses();
console.log(JSON.stringify({ success: true }));
}
process.exit(0);
} else if (action === "list") {
const configs = listCamoufoxConfigs();
console.log(JSON.stringify(configs));
process.exit(0);
} else if (action === "generate-config") {
try {
const config = await generateCamoufoxConfig({
proxy:
typeof options.proxy === "string" ? options.proxy : undefined,
maxWidth:
typeof options.maxWidth === "number"
? options.maxWidth
: undefined,
maxHeight:
typeof options.maxHeight === "number"
? options.maxHeight
: undefined,
minWidth:
typeof options.minWidth === "number"
? options.minWidth
: undefined,
minHeight:
typeof options.minHeight === "number"
? options.minHeight
: undefined,
geoip: Boolean(options.geoip),
blockImages:
typeof options.blockImages === "boolean"
? options.blockImages
: undefined,
blockWebrtc:
typeof options.blockWebrtc === "boolean"
? options.blockWebrtc
: undefined,
blockWebgl:
typeof options.blockWebgl === "boolean"
? options.blockWebgl
: undefined,
executablePath:
typeof options.executablePath === "string"
? options.executablePath
: undefined,
fingerprint:
typeof options.fingerprint === "string"
? options.fingerprint
: undefined,
});
console.log(config);
process.exit(0);
} catch (error: unknown) {
console.error({
error: "Failed to generate config",
message:
error instanceof Error ? error.message : JSON.stringify(error),
});
process.exit(1);
}
} else {
console.error({
error: "Invalid action",
message: "Use 'start', 'stop', 'list', or 'generate-config'",
});
process.exit(1);
}
},
);
// Command for Camoufox worker (internal use)
program
.command("camoufox-worker")
.argument("<action>", "start a Camoufox worker")
.requiredOption("--id <id>", "Camoufox configuration ID")
.description("run a Camoufox worker process")
.action(async (action: string, options: { id: string }) => {
if (action === "start") {
await runCamoufoxWorker(options.id);
} else {
console.error({
error: "Invalid action for camoufox-worker",
message: "Use 'start'",
});
process.exit(1);
}
});
program.parse();
+45 -33
View File
@@ -1,68 +1,71 @@
import { spawn } from "child_process";
import path from "path";
import { spawn } from "node:child_process";
import path from "node:path";
import getPort from "get-port";
import {
deleteProxyConfig,
generateProxyId,
getProxyConfig,
isProcessRunning,
listProxyConfigs,
type ProxyConfig,
saveProxyConfig,
getProxyConfig,
deleteProxyConfig,
isProcessRunning,
generateProxyId,
listProxyConfigs,
} from "./proxy-storage";
/**
* Start a proxy in a separate process
* @param upstreamUrl The upstream proxy URL
* @param upstreamUrl The upstream proxy URL (optional for direct proxy)
* @param options Optional configuration
* @returns Promise resolving to the proxy configuration
*/
export async function startProxyProcess(
upstreamUrl: string,
options: { port?: number; ignoreProxyCertificate?: boolean } = {}
upstreamUrl?: string,
options: { port?: number; ignoreProxyCertificate?: boolean } = {},
): Promise<ProxyConfig> {
// Generate a unique ID for this proxy
const id = generateProxyId();
// Get a random available port if not specified
const port = options.port || (await getPort());
const port = options.port ?? (await getPort());
// Create the proxy configuration
const config: ProxyConfig = {
id,
upstreamUrl,
upstreamUrl: upstreamUrl || "DIRECT",
localPort: port,
ignoreProxyCertificate: options.ignoreProxyCertificate || false,
ignoreProxyCertificate: options.ignoreProxyCertificate ?? false,
};
// Save the configuration before starting the process
saveProxyConfig(config);
// Build the command arguments
const args = ["proxy-worker", "start", "--id", id];
const args = [
path.join(__dirname, "index.js"),
"proxy-worker",
"start",
"--id",
id,
];
// Spawn the process
const child = spawn(
process.execPath,
[path.join(__dirname, "index.js"), ...args],
{
detached: true,
stdio: "ignore",
}
);
// Spawn the process with proper detachment
const child = spawn(process.execPath, args, {
detached: true,
stdio: ["ignore", "ignore", "ignore"], // Completely ignore all stdio
cwd: process.cwd(),
});
// Unref the child to allow the parent to exit independently
child.unref();
// Store the process ID
// Store the process ID and local URL
config.pid = child.pid;
config.localUrl = `http://localhost:${port}`;
config.localUrl = `http://127.0.0.1:${port}`;
// Update the configuration with the process ID
saveProxyConfig(config);
// Wait a bit to ensure the proxy has started
await new Promise((resolve) => setTimeout(resolve, 500));
// Give the worker a moment to start before returning
await new Promise((resolve) => setTimeout(resolve, 100));
return config;
}
@@ -75,7 +78,9 @@ export async function startProxyProcess(
export async function stopProxyProcess(id: string): Promise<boolean> {
const config = getProxyConfig(id);
if (!config || !config.pid) {
if (!config?.pid) {
// Try to delete the config anyway in case it exists without a PID
deleteProxyConfig(id);
return false;
}
@@ -83,10 +88,16 @@ export async function stopProxyProcess(id: string): Promise<boolean> {
// Check if the process is running
if (isProcessRunning(config.pid)) {
// Send SIGTERM to the process
process.kill(config.pid);
process.kill(config.pid, "SIGTERM");
// Wait a bit to ensure the process has terminated
await new Promise((resolve) => setTimeout(resolve, 300));
await new Promise((resolve) => setTimeout(resolve, 500));
// If still running, send SIGKILL
if (isProcessRunning(config.pid)) {
process.kill(config.pid, "SIGKILL");
await new Promise((resolve) => setTimeout(resolve, 200));
}
}
// Delete the configuration
@@ -95,6 +106,8 @@ export async function stopProxyProcess(id: string): Promise<boolean> {
return true;
} catch (error) {
console.error(`Error stopping proxy ${id}:`, error);
// Delete the configuration even if stopping failed
deleteProxyConfig(id);
return false;
}
}
@@ -106,7 +119,6 @@ export async function stopProxyProcess(id: string): Promise<boolean> {
export async function stopAllProxyProcesses(): Promise<void> {
const configs = listProxyConfigs();
for (const config of configs) {
await stopProxyProcess(config.id);
}
const stopPromises = configs.map((config) => stopProxyProcess(config.id));
await Promise.all(stopPromises);
}
+15 -14
View File
@@ -1,21 +1,18 @@
import fs from "fs";
import path from "path";
import os from "os";
import fs from "node:fs";
import path from "node:path";
import tmp from "tmp";
// Define the proxy configuration type
export interface ProxyConfig {
id: string;
upstreamUrl: string;
upstreamUrl: string; // Can be "DIRECT" for direct proxy
localPort?: number;
ignoreProxyCertificate?: boolean;
localUrl?: string;
pid?: number;
}
// Path to store proxy configurations
const STORAGE_DIR = path.join(os.tmpdir(), "donutbrowser", "proxies");
const STORAGE_DIR = path.join(tmp.tmpdir, "donutbrowser", "proxies");
// Ensure storage directory exists
if (!fs.existsSync(STORAGE_DIR)) {
fs.mkdirSync(STORAGE_DIR, { recursive: true });
}
@@ -88,7 +85,7 @@ export function listProxyConfigs(): ProxyConfig[] {
try {
const content = fs.readFileSync(
path.join(STORAGE_DIR, file),
"utf-8"
"utf-8",
);
return JSON.parse(content) as ProxyConfig;
} catch (error) {
@@ -111,14 +108,18 @@ export function listProxyConfigs(): ProxyConfig[] {
export function updateProxyConfig(config: ProxyConfig): boolean {
const filePath = path.join(STORAGE_DIR, `${config.id}.json`);
if (!fs.existsSync(filePath)) {
return false;
}
try {
fs.readFileSync(filePath, "utf-8");
fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
console.error(
`Config ${config.id} was deleted while the app was running`,
);
return false;
}
console.error(`Error updating proxy config ${config.id}:`, error);
return false;
}
@@ -135,7 +136,7 @@ export function isProcessRunning(pid: number): boolean {
// but checks if it exists
process.kill(pid, 0);
return true;
} catch (error) {
} catch {
return false;
}
}
+38 -19
View File
@@ -1,5 +1,5 @@
import { Server } from "proxy-chain";
import { getProxyConfig } from "./proxy-storage";
import { getProxyConfig, updateProxyConfig } from "./proxy-storage";
/**
* Run a proxy server as a worker process
@@ -8,44 +8,63 @@ import { getProxyConfig } from "./proxy-storage";
export async function runProxyWorker(id: string): Promise<void> {
// Get the proxy configuration
const config = getProxyConfig(id);
if (!config) {
console.error(`Proxy configuration ${id} not found`);
process.exit(1);
}
// Create a new proxy server
const server = new Server({
port: config.localPort,
host: "localhost",
host: "127.0.0.1",
prepareRequestFunction: () => {
// If upstreamUrl is "DIRECT", don't use upstream proxy
if (config.upstreamUrl === "DIRECT") {
return {};
}
return {
upstreamProxyUrl: config.upstreamUrl,
ignoreUpstreamProxyCertificate: config.ignoreProxyCertificate || false,
ignoreUpstreamProxyCertificate: config.ignoreProxyCertificate ?? false,
};
},
});
// Handle process termination
process.on("SIGTERM", async () => {
console.log(`Proxy worker ${id} received SIGTERM, shutting down...`);
await server.close(true);
// Handle process termination gracefully
const gracefulShutdown = async () => {
try {
await server.close(true);
} catch {}
process.exit(0);
};
process.on("SIGTERM", () => void gracefulShutdown());
process.on("SIGINT", () => void gracefulShutdown());
// Handle uncaught exceptions
process.on("uncaughtException", () => {
process.exit(1);
});
process.on("SIGINT", async () => {
console.log(`Proxy worker ${id} received SIGINT, shutting down...`);
await server.close(true);
process.exit(0);
process.on("unhandledRejection", () => {
process.exit(1);
});
// Start the server
try {
await server.listen();
console.log(`Proxy worker ${id} started on port ${server.port}`);
console.log(`Forwarding to upstream proxy: ${config.upstreamUrl}`);
// Update the config with the actual port (in case it was auto-assigned)
config.localPort = server.port;
config.localUrl = `http://127.0.0.1:${server.port}`;
updateProxyConfig(config);
// Keep the process alive
setInterval(() => {
// Do nothing, just keep the process alive
}, 60000);
} catch (error) {
console.error(`Failed to start proxy worker ${id}:`, error);
process.exit(1);
}
}
}
-73
View File
@@ -1,73 +0,0 @@
import {
startProxyProcess,
stopProxyProcess,
stopAllProxyProcesses
} from "./proxy-runner";
import { listProxyConfigs } from "./proxy-storage";
// Type definitions
interface ProxyOptions {
port?: number;
ignoreProxyCertificate?: boolean;
}
/**
* Start a local proxy server that forwards to an upstream proxy
* @param upstreamProxyUrl The upstream proxy URL (protocol://[username:password@]host:port)
* @param options Optional configuration
* @returns Promise resolving to the local proxy URL
*/
export async function startProxy(
upstreamProxyUrl: string,
options: ProxyOptions = {}
): Promise<string> {
const config = await startProxyProcess(upstreamProxyUrl, {
port: options.port,
ignoreProxyCertificate: options.ignoreProxyCertificate,
});
return config.localUrl || `http://localhost:${config.localPort}`;
}
/**
* Stop a specific proxy by its upstream URL
* @param upstreamProxyUrl The upstream proxy URL to stop
* @returns Promise resolving to true if proxy was found and stopped, false otherwise
*/
export async function stopProxy(upstreamProxyUrl: string): Promise<boolean> {
// Find all proxies with this upstream URL
const configs = listProxyConfigs().filter(
config => config.upstreamUrl === upstreamProxyUrl
);
if (configs.length === 0) {
return false;
}
// Stop all matching proxies
let success = true;
for (const config of configs) {
const stopped = await stopProxyProcess(config.id);
if (!stopped) {
success = false;
}
}
return success;
}
/**
* Get a list of all active proxy upstream URLs
* @returns Array of upstream proxy URLs
*/
export function getActiveProxies(): string[] {
return listProxyConfigs().map(config => config.upstreamUrl);
}
/**
* Stop all active proxies
* @returns Promise that resolves when all proxies are stopped
*/
export async function stopAllProxies(): Promise<void> {
await stopAllProxyProcesses();
}
+119
View File
@@ -0,0 +1,119 @@
import type { LaunchOptions } from "playwright-core";
const OS_MAP: { [key: string]: "mac" | "win" | "lin" } = {
darwin: "mac",
linux: "lin",
win32: "win",
};
const OS_NAME: "mac" | "win" | "lin" = OS_MAP[process.platform];
export function getEnvVars(configMap: Record<string, string>) {
const envVars: {
[key: string]: string | undefined;
} = {};
let updatedConfigData: Uint8Array;
try {
// Ensure we're working with a fresh copy of the config
const configCopy = JSON.parse(JSON.stringify(configMap));
updatedConfigData = new TextEncoder().encode(JSON.stringify(configCopy));
} catch (e) {
console.error(`Error updating config: ${e}`);
process.exit(1);
}
const chunkSize = OS_NAME === "win" ? 2047 : 32767;
const configStr = new TextDecoder().decode(updatedConfigData);
for (let i = 0; i < configStr.length; i += chunkSize) {
const chunk = configStr.slice(i, i + chunkSize);
const envName = `CAMOU_CONFIG_${Math.floor(i / chunkSize) + 1}`;
try {
envVars[envName] = chunk;
} catch (e) {
console.error(`Error setting ${envName}: ${e}`);
process.exit(1);
}
}
return envVars;
}
export function parseProxyString(proxyString: LaunchOptions["proxy"] | string) {
if (typeof proxyString === "object") {
return proxyString;
}
if (!proxyString || typeof proxyString !== "string") {
throw new Error("Invalid proxy string provided");
}
// Remove any leading/trailing whitespace
const trimmed = proxyString.trim();
// Handle different proxy string formats:
// 1. http://username:password@host:port
// 2. host:port
// 3. protocol://host:port
// 4. username:password@host:port
let server = "";
let username: string | undefined;
let password: string | undefined;
try {
// Try parsing as URL first (handles protocol://username:password@host:port)
if (trimmed.includes("://")) {
const url = new URL(trimmed);
server = `${url.hostname}:${url.port}`;
if (url.username) {
username = decodeURIComponent(url.username);
}
if (url.password) {
password = decodeURIComponent(url.password);
}
} else {
// Handle formats without protocol
let workingString = trimmed;
// Check for username:password@ prefix
const authMatch = workingString.match(/^([^:@]+):([^@]+)@(.+)$/);
if (authMatch) {
username = authMatch[1];
password = authMatch[2];
workingString = authMatch[3];
}
// The remaining part should be host:port
server = workingString;
}
// Validate that we have a server
if (!server) {
throw new Error("Could not extract server information");
}
// Basic validation for host:port format
if (!server.includes(":") || server.split(":").length !== 2) {
throw new Error("Server must be in host:port format");
}
const result: LaunchOptions["proxy"] = { server };
if (username !== undefined) {
result.username = username;
}
if (password !== undefined) {
result.password = password;
}
return result;
} catch (error) {
throw new Error(
`Failed to parse proxy string: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
+39 -35
View File
@@ -1,22 +1,27 @@
{
"name": "donutbrowser",
"private": true,
"version": "0.2.4",
"license": "AGPL-3.0",
"version": "0.9.2",
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"test": "pnpm test:rust",
"test:rust": "cd src-tauri && cargo test",
"lint": "pnpm lint:js && pnpm lint:rust",
"lint:js": "biome check src/ && tsc --noEmit && next lint",
"lint:js": "biome check src/ && tsc --noEmit",
"lint:rust": "cd src-tauri && cargo clippy --all-targets --all-features -- -D warnings -D clippy::all && cargo fmt --all",
"tauri": "tauri",
"shadcn:add": "pnpm dlx shadcn@latest add",
"prepare": "husky",
"prepare": "husky && husky install",
"format:rust": "cd src-tauri && cargo clippy --fix --allow-dirty --all-targets --all-features -- -D warnings -D clippy::all && cargo fmt --all",
"format:js": "biome check src/ --fix",
"format:js": "biome check src/ --write --unsafe",
"format": "pnpm format:js && pnpm format:rust",
"cargo": "cd src-tauri && cargo"
"cargo": "cd src-tauri && cargo",
"unused-exports:js": "ts-unused-exports tsconfig.json",
"check-unused-commands": "cd src-tauri && cargo run --bin check_unused_commands"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.2",
@@ -25,51 +30,50 @@
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-table": "^8.21.3",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-fs": "~2.3.0",
"@tauri-apps/plugin-opener": "^2.2.7",
"@tauri-apps/api": "^2.7.0",
"@tauri-apps/plugin-deep-link": "^2.4.1",
"@tauri-apps/plugin-dialog": "^2.3.2",
"@tauri-apps/plugin-fs": "~2.4.1",
"@tauri-apps/plugin-opener": "^2.4.0",
"ahooks": "^3.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"next": "^15.3.2",
"motion": "^12.23.12",
"next": "^15.4.6",
"next-themes": "^0.4.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-icons": "^5.5.0",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0"
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tauri-plugin-macos-permissions-api": "^2.3.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.27.0",
"@next/eslint-plugin-next": "^15.3.2",
"@tailwindcss/postcss": "^4.1.7",
"@tauri-apps/cli": "^2.5.0",
"@types/node": "^22.15.21",
"@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"@vitejs/plugin-react": "^4.5.0",
"eslint": "^9.27.0",
"eslint-config-next": "^15.3.2",
"eslint-plugin-react-hooks": "^5.2.0",
"@biomejs/biome": "2.1.4",
"@tailwindcss/postcss": "^4.1.11",
"@tauri-apps/cli": "^2.7.1",
"@types/node": "^24.2.1",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"husky": "^9.1.7",
"lint-staged": "^15.3.0",
"tailwindcss": "^4.1.7",
"tw-animate-css": "^1.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.32.1"
"lint-staged": "^16.1.5",
"tailwindcss": "^4.1.11",
"ts-unused-exports": "^11.0.1",
"tw-animate-css": "^1.3.6",
"typescript": "~5.9.2"
},
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx,json,css,md}": [
"**/*.{js,jsx,ts,tsx,json,css}": [
"biome check --fix"
],
"src-tauri/**/*.rs": [
+2855 -3401
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -1,5 +1,9 @@
packages:
- nodecar
onlyBuiltDependencies:
- '@biomejs/biome'
- '@tailwindcss/oxide'
- esbuild
- sharp
- sqlite3
- unrs-resolver
+1143 -547
View File
File diff suppressed because it is too large Load Diff
+51 -9
View File
@@ -1,9 +1,10 @@
[package]
name = "donutbrowser"
version = "0.2.4"
description = "Browser Orchestrator"
version = "0.9.2"
description = "Simple Yet Powerful Anti-Detect Browser"
authors = ["zhom@github"]
edition = "2021"
default-run = "donutbrowser"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -13,6 +14,7 @@ edition = "2021"
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "donutbrowser"
crate-type = ["staticlib", "cdylib", "rlib"]
doctest = false
[build-dependencies]
tauri-build = { version = "2", features = [] }
@@ -25,28 +27,68 @@ tauri-plugin-opener = "2"
tauri-plugin-fs = "2"
tauri-plugin-shell = "2"
tauri-plugin-deep-link = "2"
tauri-plugin-dialog = "2"
tauri-plugin-macos-permissions = "2"
directories = "6"
reqwest = { version = "0.12", features = ["json", "stream"] }
tokio = { version = "1", features = ["full"] }
sysinfo = "0.35"
tokio = { version = "1", features = ["full", "sync"] }
sysinfo = "0.36"
lazy_static = "1.4"
base64 = "0.22"
zip = "4"
async-trait = "0.1"
futures-util = "0.3"
zip = "4"
tar = "0"
bzip2 = "0"
flate2 = "1"
lzma-rs = "0"
msi-extract = "0"
uuid = { version = "1.0", features = ["v4", "serde"] }
url = "2.5"
chrono = { version = "0.4", features = ["serde"] }
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation="0.10"
core-foundation = "0.10"
objc2 = "0.6.1"
objc2-app-kit = { version = "0.3.1", features = ["NSWindow"] }
[target.'cfg(target_os = "windows")'.dependencies]
winreg = "0.55"
windows = { version = "0.61", features = [
"Win32_Foundation",
"Win32_System_ProcessStatus",
"Win32_System_Threading",
"Win32_System_Diagnostics_Debug",
"Win32_System_SystemInformation",
"Win32_Security",
"Win32_Storage_FileSystem",
"Win32_System_Registry",
"Win32_UI_Shell",
] }
[dev-dependencies]
tempfile = "3.13.0"
tokio-test = "0.4.4"
wiremock = "0.6"
hyper = { version = "1.0", features = ["full"] }
hyper-util = { version = "0.1", features = ["full"] }
http-body-util = "0.1"
tower = "0.5"
tower-http = { version = "0.6", features = ["fs", "trace"] }
futures-util = "0.3"
# Integration test configuration
[[test]]
name = "nodecar_integration"
path = "tests/nodecar_integration.rs"
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` points to the filesystem
default = [ "custom-protocol" ]
default = ["custom-protocol"]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = [ "tauri/custom-protocol" ]
custom-protocol = ["tauri/custom-protocol"]
+29 -19
View File
@@ -2,47 +2,57 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Donut Browser needs camera access to enable camera functionality in web browsers. Each website will still ask for your permission individually.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Donut Browser needs microphone access to enable microphone functionality in web browsers. Each website will still ask for your permission individually.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Donut Browser has proxy functionality that requires local network access. You can deny this functionality if you don't plan on setting proxies for browser profiles.</string>
<key>CFBundleDisplayName</key>
<string>Donut Browser</string>
<key>CFBundleName</key>
<string>Donut Browser</string>
<key>CFBundleIdentifier</key>
<string>com.donutbrowser</string>
<key>CFBundleURLName</key>
<string>com.donutbrowser</string>
<key>CFBundleExecutable</key>
<string>donutbrowser</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>0.2.4</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleIconFile</key>
<string>icon.icns</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleIconFile</key>
<string>icon.icns</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2025 Donut Browser</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>HTML document</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.html</string>
<string>public.xhtml</string>
</array>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Web Browser</string>
<string>Web site URL</string>
<key>CFBundleURLSchemes</key>
<array>
<string>http</string>
<string>https</string>
</array>
<key>CFBundleURLIconFile</key>
<string>icon.icns</string>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
</array>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2025 Donut Browser</string>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
</dict>
</plist>
+1 -12
View File
@@ -1,14 +1,3 @@
function FindProxyForURL(url, host) {
const proxyString = "{{proxy_url}}";
// Split the proxy string to get the credentials part
const parts = proxyString.split(" ")[1].split("@");
if (parts.length > 1) {
const credentials = parts[0];
const encodedCredentials = encodeURIComponent(credentials);
// Replace the original credentials with encoded ones
return proxyString.replace(credentials, encodedCredentials);
}
return proxyString;
return "{{proxy_url}}";
}
+1 -1
View File
@@ -17,7 +17,7 @@ fn main() {
let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.1.0".to_string());
println!("cargo:rustc-env=BUILD_VERSION=v{version}");
} else if let Ok(commit_hash) = std::env::var("GITHUB_SHA") {
// For nightly builds, use commit hash
// For nightly builds, use timestamp format or fallback to commit hash
let short_hash = &commit_hash[0..7.min(commit_hash.len())];
println!("cargo:rustc-env=BUILD_VERSION=nightly-{short_hash}");
} else {
+17 -1
View File
@@ -6,6 +6,11 @@
"permissions": [
"core:default",
"core:event:default",
"core:window:default",
"core:window:allow-start-dragging",
"core:window:allow-close",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"opener:default",
"fs:default",
"shell:allow-execute",
@@ -13,6 +18,17 @@
"shell:allow-open",
"shell:allow-spawn",
"shell:allow-stdin-write",
"deep-link:default"
"deep-link:default",
"deep-link:allow-register",
"deep-link:allow-unregister",
"deep-link:allow-is-registered",
"deep-link:allow-get-current",
"dialog:default",
"dialog:allow-open",
"macos-permissions:default",
"macos-permissions:allow-request-microphone-permission",
"macos-permissions:allow-request-camera-permission",
"macos-permissions:allow-check-microphone-permission",
"macos-permissions:allow-check-camera-permission"
]
}
+13
View File
@@ -0,0 +1,13 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Donut Browser
Comment=Simple Yet Powerful Anti-Detect Browser
Exec=donutbrowser %u
Icon=donutbrowser
StartupNotify=true
NoDisplay=false
Categories=Network;WebBrowser;
MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml;
StartupWMClass=donutbrowser
Keywords=browser;web;internet;productivity;
+22
View File
@@ -12,5 +12,27 @@
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.audio-output</key>
<true/>
<key>com.apple.security.device.microphone</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.device.usb</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+327 -237
View File
@@ -1,10 +1,12 @@
use crate::browser_runner::{BrowserProfile, BrowserRunner};
use crate::browser_version_service::{BrowserVersionInfo, BrowserVersionService};
use crate::api_client::is_browser_version_nightly;
use crate::browser_version_manager::{BrowserVersionInfo, BrowserVersionManager};
use crate::profile::BrowserProfile;
use crate::settings_manager::SettingsManager;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
use tauri::Emitter;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UpdateNotification {
@@ -27,50 +29,52 @@ pub struct AutoUpdateState {
}
pub struct AutoUpdater {
version_service: BrowserVersionService,
browser_runner: BrowserRunner,
settings_manager: SettingsManager,
version_service: &'static BrowserVersionManager,
settings_manager: &'static SettingsManager,
}
impl AutoUpdater {
pub fn new() -> Self {
fn new() -> Self {
Self {
version_service: BrowserVersionService::new(),
browser_runner: BrowserRunner::new(),
settings_manager: SettingsManager::new(),
version_service: BrowserVersionManager::instance(),
settings_manager: SettingsManager::instance(),
}
}
pub fn instance() -> &'static AutoUpdater {
&AUTO_UPDATER
}
/// Check for updates for all profiles
pub async fn check_for_updates(
&self,
) -> Result<Vec<UpdateNotification>, Box<dyn std::error::Error + Send + Sync>> {
// Check if auto-updates are enabled
let settings = self
.settings_manager
.load_settings()
.map_err(|e| format!("Failed to load settings: {e}"))?;
if !settings.auto_updates_enabled {
return Ok(Vec::new());
}
let profiles = self
.browser_runner
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let mut notifications = Vec::new();
let mut browser_versions: HashMap<String, Vec<BrowserVersionInfo>> = HashMap::new();
// Group profiles by browser type
// Group profiles by browser
let profile_manager = crate::profile::ProfileManager::instance();
let profiles = profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let mut browser_profiles: HashMap<String, Vec<BrowserProfile>> = HashMap::new();
for profile in profiles {
// Only check supported browsers
if !self
.version_service
.is_browser_supported(&profile.browser)
.unwrap_or(false)
{
continue;
}
browser_profiles
.entry(profile.browser.clone())
.or_default()
.push(profile);
}
// Check each browser type
for (browser, profiles) in browser_profiles {
// Get cached versions first, then try to fetch if needed
let versions = if let Some(cached) = self
@@ -97,7 +101,26 @@ impl AutoUpdater {
// Check each profile for updates
for profile in profiles {
if let Some(update) = self.check_profile_update(&profile, &versions)? {
notifications.push(update);
// Apply chromium threshold logic
if browser == "chromium" {
// For chromium, only show notifications if there are 400+ new versions
let current_version = &profile.version.parse::<u32>().unwrap();
let new_version = &update.new_version.parse::<u32>().unwrap();
let result = new_version - current_version;
println!(
"Current version: {current_version}, New version: {new_version}, Result: {result}"
);
if result > 400 {
notifications.push(update);
} else {
println!(
"Skipping chromium update notification: only {result} new versions (need 400+)"
);
}
} else {
notifications.push(update);
}
}
}
}
@@ -105,6 +128,93 @@ impl AutoUpdater {
Ok(notifications)
}
pub async fn check_for_updates_with_progress(&self, app_handle: &tauri::AppHandle) {
println!("Starting auto-update check with progress...");
// Check for browser updates and trigger auto-downloads
match self.check_for_updates().await {
Ok(update_notifications) => {
if !update_notifications.is_empty() {
println!(
"Found {} browser updates to auto-download",
update_notifications.len()
);
// Trigger automatic downloads for each update
for notification in update_notifications {
println!(
"Auto-downloading {} version {}",
notification.browser, notification.new_version
);
// Clone app_handle for the async task
let app_handle_clone = app_handle.clone();
let browser = notification.browser.clone();
let new_version = notification.new_version.clone();
let notification_id = notification.id.clone();
let affected_profiles = notification.affected_profiles.clone();
// Spawn async task to handle the download and auto-update
tokio::spawn(async move {
// First, check if browser already exists
match crate::browser_runner::is_browser_downloaded(
browser.clone(),
new_version.clone(),
) {
true => {
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(
browser.clone(),
new_version.clone(),
)
.await
{
Ok(updated_profiles) => {
println!(
"Auto-update completed for {} profiles: {:?}",
updated_profiles.len(),
updated_profiles
);
}
Err(e) => {
eprintln!("Failed to complete auto-update for {browser}: {e}");
}
}
}
false => {
println!("Downloading browser {browser} version {new_version}...");
// Emit the auto-update event to trigger frontend handling
let auto_update_event = serde_json::json!({
"browser": browser,
"new_version": new_version,
"notification_id": notification_id,
"affected_profiles": affected_profiles
});
if let Err(e) =
app_handle_clone.emit("browser-auto-update-available", &auto_update_event)
{
eprintln!("Failed to emit auto-update event for {browser}: {e}");
} else {
println!("Emitted auto-update event for {browser}");
}
}
}
});
}
} else {
println!("No browser updates needed");
}
}
Err(e) => {
eprintln!("Failed to check for browser updates: {e}");
}
}
}
/// Check if a specific profile has an available update
fn check_profile_update(
&self,
@@ -112,16 +222,15 @@ impl AutoUpdater {
available_versions: &[BrowserVersionInfo],
) -> Result<Option<UpdateNotification>, Box<dyn std::error::Error + Send + Sync>> {
let current_version = &profile.version;
let is_current_stable = !self.is_alpha_version(current_version);
let is_current_nightly = is_browser_version_nightly(&profile.browser, current_version, None);
// Find the best available update
let best_update = available_versions
.iter()
.filter(|v| {
// Only consider versions newer than current
self.is_version_newer(&v.version, current_version) &&
// Respect version type preference
is_current_stable != v.is_prerelease
self.is_version_newer(&v.version, current_version)
&& is_browser_version_nightly(&profile.browser, &v.version, None) == is_current_nightly
})
.max_by(|a, b| self.compare_versions(&a.version, &b.version));
@@ -181,85 +290,14 @@ impl AutoUpdater {
result
}
/// Mark download as auto-update
pub fn mark_auto_update_download(
&self,
browser: &str,
version: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut state = self.load_auto_update_state()?;
let download_key = format!("{browser}-{version}");
state.auto_update_downloads.insert(download_key);
self.save_auto_update_state(&state)?;
Ok(())
}
/// Remove auto-update download tracking
pub fn remove_auto_update_download(
&self,
browser: &str,
version: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut state = self.load_auto_update_state()?;
let download_key = format!("{browser}-{version}");
state.auto_update_downloads.remove(&download_key);
self.save_auto_update_state(&state)?;
Ok(())
}
/// Check if download is marked as auto-update
pub fn is_auto_update_download(
&self,
browser: &str,
version: &str,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let state = self.load_auto_update_state()?;
let download_key = format!("{browser}-{version}");
Ok(state.auto_update_downloads.contains(&download_key))
}
/// Start browser update process
pub async fn start_browser_update(
&self,
browser: &str,
new_version: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Add browser to disabled list to prevent conflicts during update
let mut state = self.load_auto_update_state()?;
state.disabled_browsers.insert(browser.to_string());
// Mark this download as auto-update for toast suppression
let download_key = format!("{browser}-{new_version}");
state.auto_update_downloads.insert(download_key);
self.save_auto_update_state(&state)?;
// The actual download will be triggered by the frontend
// This function now just marks the browser as updating to prevent conflicts
Ok(())
}
/// Complete browser update process
pub async fn complete_browser_update(
&self,
browser: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Remove browser from disabled list
let mut state = self.load_auto_update_state()?;
state.disabled_browsers.remove(browser);
self.save_auto_update_state(&state)?;
Ok(())
}
/// Automatically update all affected profile versions after browser download
pub async fn auto_update_profile_versions(
&self,
browser: &str,
new_version: &str,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let profiles = self
.browser_runner
let profile_manager = crate::profile::ProfileManager::instance();
let profiles = profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
@@ -276,10 +314,7 @@ impl AutoUpdater {
// Check if this is an update (newer version)
if self.is_version_newer(new_version, &profile.version) {
// Update the profile version
match self
.browser_runner
.update_profile_version(&profile.name, new_version)
{
match profile_manager.update_profile_version(&profile.name, new_version) {
Ok(_) => {
updated_profiles.push(profile.name);
}
@@ -312,9 +347,46 @@ 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,
@@ -335,34 +407,18 @@ impl AutoUpdater {
Ok(())
}
// Helper methods
fn is_alpha_version(&self, version: &str) -> bool {
version.contains("alpha")
|| version.contains("beta")
|| version.contains("rc")
|| version.contains("a")
|| version.contains("b")
|| version.contains("dev")
}
fn is_version_newer(&self, version1: &str, version2: &str) -> bool {
self.compare_versions(version1, version2) == std::cmp::Ordering::Greater
// 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
}
fn compare_versions(&self, version1: &str, version2: &str) -> std::cmp::Ordering {
// Basic semantic version comparison
let v1_parts = self.parse_version(version1);
let v2_parts = self.parse_version(version2);
v1_parts.cmp(&v2_parts)
}
fn parse_version(&self, version: &str) -> Vec<u32> {
version
.split(&['.', 'a', 'b', '-', '_'][..])
.filter_map(|part| part.parse::<u32>().ok())
.collect()
// 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)
}
fn get_auto_update_state_file(&self) -> PathBuf {
@@ -372,7 +428,7 @@ impl AutoUpdater {
.join("auto_update_state.json")
}
fn load_auto_update_state(
pub fn load_auto_update_state(
&self,
) -> Result<AutoUpdateState, Box<dyn std::error::Error + Send + Sync>> {
let state_file = self.get_auto_update_state_file();
@@ -386,7 +442,7 @@ impl AutoUpdater {
Ok(state)
}
fn save_auto_update_state(
pub fn save_auto_update_state(
&self,
state: &AutoUpdateState,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
@@ -405,7 +461,7 @@ impl AutoUpdater {
#[tauri::command]
pub async fn check_for_browser_updates() -> Result<Vec<UpdateNotification>, String> {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
let notifications = updater
.check_for_updates()
.await
@@ -414,27 +470,9 @@ pub async fn check_for_browser_updates() -> Result<Vec<UpdateNotification>, Stri
Ok(grouped)
}
#[tauri::command]
pub async fn start_browser_update(browser: String, new_version: String) -> Result<(), String> {
let updater = AutoUpdater::new();
updater
.start_browser_update(&browser, &new_version)
.await
.map_err(|e| format!("Failed to start browser update: {e}"))
}
#[tauri::command]
pub async fn complete_browser_update(browser: String) -> Result<(), String> {
let updater = AutoUpdater::new();
updater
.complete_browser_update(&browser)
.await
.map_err(|e| format!("Failed to complete browser update: {e}"))
}
#[tauri::command]
pub async fn is_browser_disabled_for_update(browser: String) -> Result<bool, String> {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
updater
.is_browser_disabled(&browser)
.map_err(|e| format!("Failed to check browser status: {e}"))
@@ -442,7 +480,7 @@ pub async fn is_browser_disabled_for_update(browser: String) -> Result<bool, Str
#[tauri::command]
pub async fn dismiss_update_notification(notification_id: String) -> Result<(), String> {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
updater
.dismiss_update_notification(&notification_id)
.map_err(|e| format!("Failed to dismiss notification: {e}"))
@@ -453,7 +491,7 @@ pub async fn complete_browser_update_with_auto_update(
browser: String,
new_version: String,
) -> Result<Vec<String>, String> {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
updater
.complete_browser_update_with_auto_update(&browser, &new_version)
.await
@@ -461,27 +499,9 @@ pub async fn complete_browser_update_with_auto_update(
}
#[tauri::command]
pub async fn mark_auto_update_download(browser: String, version: String) -> Result<(), String> {
let updater = AutoUpdater::new();
updater
.mark_auto_update_download(&browser, &version)
.map_err(|e| format!("Failed to mark auto-update download: {e}"))
}
#[tauri::command]
pub async fn remove_auto_update_download(browser: String, version: String) -> Result<(), String> {
let updater = AutoUpdater::new();
updater
.remove_auto_update_download(&browser, &version)
.map_err(|e| format!("Failed to remove auto-update download: {e}"))
}
#[tauri::command]
pub async fn is_auto_update_download(browser: String, version: String) -> Result<bool, String> {
let updater = AutoUpdater::new();
updater
.is_auto_update_download(&browser, &version)
.map_err(|e| format!("Failed to check auto-update download: {e}"))
pub async fn check_for_updates_with_progress(app_handle: tauri::AppHandle) {
let updater = AutoUpdater::instance();
updater.check_for_updates_with_progress(&app_handle).await;
}
#[cfg(test)]
@@ -490,13 +510,16 @@ mod tests {
fn create_test_profile(name: &str, browser: &str, version: &str) -> BrowserProfile {
BrowserProfile {
id: uuid::Uuid::new_v4(),
name: name.to_string(),
browser: browser.to_string(),
version: version.to_string(),
profile_path: format!("/tmp/{name}"),
process_id: None,
proxy: None,
proxy_id: None,
last_launch: None,
release_type: "stable".to_string(),
camoufox_config: None,
group_id: None,
}
}
@@ -508,24 +531,9 @@ mod tests {
}
}
#[test]
fn test_is_alpha_version() {
let updater = AutoUpdater::new();
assert!(updater.is_alpha_version("1.0.0-alpha"));
assert!(updater.is_alpha_version("1.0.0-beta"));
assert!(updater.is_alpha_version("1.0.0-rc"));
assert!(updater.is_alpha_version("1.0.0a1"));
assert!(updater.is_alpha_version("1.0.0b1"));
assert!(updater.is_alpha_version("1.0.0-dev"));
assert!(!updater.is_alpha_version("1.0.0"));
assert!(!updater.is_alpha_version("1.2.3"));
}
#[test]
fn test_compare_versions() {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
assert_eq!(
updater.compare_versions("1.0.0", "1.0.0"),
@@ -551,7 +559,7 @@ mod tests {
#[test]
fn test_is_version_newer() {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
assert!(updater.is_version_newer("1.0.1", "1.0.0"));
assert!(updater.is_version_newer("2.0.0", "1.9.9"));
@@ -559,9 +567,71 @@ mod tests {
assert!(!updater.is_version_newer("1.0.0", "1.0.0"));
}
#[test]
fn test_camoufox_beta_version_comparison() {
let updater = AutoUpdater::instance();
// Test the exact user-reported scenario: 135.0.1beta24 vs 135.0beta22
assert!(
updater.is_version_newer("135.0.1beta24", "135.0beta22"),
"135.0.1beta24 should be newer than 135.0beta22"
);
assert_eq!(
updater.compare_versions("135.0.1beta24", "135.0beta22"),
std::cmp::Ordering::Greater,
"135.0.1beta24 should compare as greater than 135.0beta22"
);
// Test other camoufox beta version combinations
assert!(
updater.is_version_newer("135.0.5beta24", "135.0.5beta22"),
"135.0.5beta24 should be newer than 135.0.5beta22"
);
assert!(
updater.is_version_newer("135.0.1beta1", "135.0beta1"),
"135.0.1beta1 should be newer than 135.0beta1 due to patch version"
);
// Test that older versions are not considered newer
assert!(
!updater.is_version_newer("135.0beta22", "135.0.1beta24"),
"135.0beta22 should NOT be newer than 135.0.1beta24"
);
}
#[test]
fn test_beta_version_ordering_comprehensive() {
let updater = AutoUpdater::instance();
// Test various beta version patterns that could appear in camoufox
let test_cases = vec![
("135.0.1beta24", "135.0beta22", true), // User reported case
("135.0.5beta24", "135.0.5beta22", true), // Same patch, different beta
("135.1beta1", "135.0beta99", true), // Higher minor beats beta number
("136.0beta1", "135.9.9beta99", true), // Higher major beats everything
("135.0.1beta1", "135.0beta1", true), // Patch version matters
("135.0beta22", "135.0.1beta24", false), // Reverse of user case
];
for (newer, older, should_be_newer) in test_cases {
let result = updater.is_version_newer(newer, older);
assert_eq!(
result,
should_be_newer,
"Expected {} {} {} but got {}",
newer,
if should_be_newer { ">" } else { "<=" },
older,
if result { "true" } else { "false" }
);
}
}
#[test]
fn test_check_profile_update_stable_to_stable() {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
let profile = create_test_profile("test", "firefox", "1.0.0");
let versions = vec![
create_test_version_info("1.0.1", false), // stable, newer
@@ -579,7 +649,7 @@ mod tests {
#[test]
fn test_check_profile_update_alpha_to_alpha() {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
let profile = create_test_profile("test", "firefox", "1.0.0-alpha");
let versions = vec![
create_test_version_info("1.0.1", false), // stable, should be included
@@ -598,7 +668,7 @@ mod tests {
#[test]
fn test_check_profile_update_no_update_available() {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
let profile = create_test_profile("test", "firefox", "1.0.0");
let versions = vec![
create_test_version_info("0.9.0", false), // older
@@ -611,7 +681,7 @@ mod tests {
#[test]
fn test_group_update_notifications() {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
let notifications = vec![
UpdateNotification {
id: "firefox_1.0.0_to_1.1.0_profile1".to_string(),
@@ -709,13 +779,15 @@ mod tests {
let state_file = test_settings_manager
.get_settings_dir()
.join("auto_update_state.json");
std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap();
let json = serde_json::to_string_pretty(&state).unwrap();
std::fs::write(&state_file, json).unwrap();
std::fs::create_dir_all(test_settings_manager.get_settings_dir())
.expect("Failed to create settings directory");
let json = serde_json::to_string_pretty(&state).expect("Failed to serialize state");
std::fs::write(&state_file, json).expect("Failed to write state file");
// Load state
let content = std::fs::read_to_string(&state_file).unwrap();
let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap();
let content = std::fs::read_to_string(&state_file).expect("Failed to read state file");
let loaded_state: AutoUpdateState =
serde_json::from_str(&content).expect("Failed to deserialize state");
assert_eq!(loaded_state.disabled_browsers.len(), 1);
assert!(loaded_state.disabled_browsers.contains("firefox"));
@@ -753,11 +825,15 @@ mod tests {
let state_file = test_settings_manager
.get_settings_dir()
.join("auto_update_state.json");
std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap();
std::fs::create_dir_all(test_settings_manager.get_settings_dir())
.expect("Failed to create settings directory");
// Initially not disabled (empty state file means default state)
let state = AutoUpdateState::default();
assert!(!state.disabled_browsers.contains("firefox"));
assert!(
!state.disabled_browsers.contains("firefox"),
"Firefox should not be disabled initially"
);
// Start update (should disable)
let mut state = AutoUpdateState::default();
@@ -765,27 +841,41 @@ mod tests {
state
.auto_update_downloads
.insert("firefox-1.1.0".to_string());
let json = serde_json::to_string_pretty(&state).unwrap();
std::fs::write(&state_file, json).unwrap();
let json = serde_json::to_string_pretty(&state).expect("Failed to serialize state");
std::fs::write(&state_file, json).expect("Failed to write state file");
// Check that it's disabled
let content = std::fs::read_to_string(&state_file).unwrap();
let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap();
assert!(loaded_state.disabled_browsers.contains("firefox"));
assert!(loaded_state.auto_update_downloads.contains("firefox-1.1.0"));
let content = std::fs::read_to_string(&state_file).expect("Failed to read state file");
let loaded_state: AutoUpdateState =
serde_json::from_str(&content).expect("Failed to deserialize state");
assert!(
loaded_state.disabled_browsers.contains("firefox"),
"Firefox should be disabled"
);
assert!(
loaded_state.auto_update_downloads.contains("firefox-1.1.0"),
"Firefox download should be tracked"
);
// Complete update (should enable)
let mut state = loaded_state;
state.disabled_browsers.remove("firefox");
state.auto_update_downloads.remove("firefox-1.1.0");
let json = serde_json::to_string_pretty(&state).unwrap();
std::fs::write(&state_file, json).unwrap();
let json = serde_json::to_string_pretty(&state).expect("Failed to serialize final state");
std::fs::write(&state_file, json).expect("Failed to write final state file");
// Check that it's enabled again
let content = std::fs::read_to_string(&state_file).unwrap();
let final_state: AutoUpdateState = serde_json::from_str(&content).unwrap();
assert!(!final_state.disabled_browsers.contains("firefox"));
assert!(!final_state.auto_update_downloads.contains("firefox-1.1.0"));
let content = std::fs::read_to_string(&state_file).expect("Failed to read final state file");
let final_state: AutoUpdateState =
serde_json::from_str(&content).expect("Failed to deserialize final state");
assert!(
!final_state.disabled_browsers.contains("firefox"),
"Firefox should be enabled again"
);
assert!(
!final_state.auto_update_downloads.contains("firefox-1.1.0"),
"Firefox download should not be tracked anymore"
);
}
#[test]
@@ -793,7 +883,7 @@ mod tests {
use tempfile::TempDir;
// Create a temporary directory for testing
let temp_dir = TempDir::new().unwrap();
let temp_dir = TempDir::new().expect("Failed to create temp directory");
// Create a mock settings manager that uses the temp directory
struct TestSettingsManager {
@@ -827,31 +917,31 @@ mod tests {
let state_file = test_settings_manager
.get_settings_dir()
.join("auto_update_state.json");
std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap();
let json = serde_json::to_string_pretty(&state).unwrap();
std::fs::write(&state_file, json).unwrap();
std::fs::create_dir_all(test_settings_manager.get_settings_dir())
.expect("Failed to create settings directory");
let json = serde_json::to_string_pretty(&state).expect("Failed to serialize initial state");
std::fs::write(&state_file, json).expect("Failed to write initial state file");
// Dismiss notification (remove from pending updates)
state
.pending_updates
.retain(|n| n.id != "test_notification");
let json = serde_json::to_string_pretty(&state).unwrap();
std::fs::write(&state_file, json).unwrap();
let json = serde_json::to_string_pretty(&state).expect("Failed to serialize updated state");
std::fs::write(&state_file, json).expect("Failed to write updated state file");
// Check that it's removed
let content = std::fs::read_to_string(&state_file).unwrap();
let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap();
assert_eq!(loaded_state.pending_updates.len(), 0);
}
#[test]
fn test_parse_version() {
let updater = AutoUpdater::new();
assert_eq!(updater.parse_version("1.2.3"), vec![1, 2, 3]);
assert_eq!(updater.parse_version("1.2.3-alpha"), vec![1, 2, 3]);
assert_eq!(updater.parse_version("1.2.3a1"), vec![1, 2, 3, 1]);
assert_eq!(updater.parse_version("1.2.3b2"), vec![1, 2, 3, 2]);
assert_eq!(updater.parse_version("10.0.0"), vec![10, 0, 0]);
let content = std::fs::read_to_string(&state_file).expect("Failed to read updated state file");
let loaded_state: AutoUpdateState =
serde_json::from_str(&content).expect("Failed to deserialize updated state");
assert_eq!(
loaded_state.pending_updates.len(),
0,
"Pending updates should be empty after dismissal"
);
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref AUTO_UPDATER: AutoUpdater = AutoUpdater::new();
}
+908 -249
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+488
View File
@@ -0,0 +1,488 @@
use crate::profile::BrowserProfile;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tauri::AppHandle;
use tauri_plugin_shell::ShellExt;
use tokio::sync::Mutex as AsyncMutex;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CamoufoxConfig {
pub proxy: Option<String>,
pub screen_max_width: Option<u32>,
pub screen_max_height: Option<u32>,
pub screen_min_width: Option<u32>,
pub screen_min_height: Option<u32>,
pub geoip: Option<serde_json::Value>, // Can be String or bool
pub block_images: Option<bool>,
pub block_webrtc: Option<bool>,
pub block_webgl: Option<bool>,
pub executable_path: Option<String>,
pub fingerprint: Option<String>, // JSON string of the complete fingerprint config
}
impl Default for CamoufoxConfig {
fn default() -> Self {
Self {
proxy: None,
screen_max_width: None,
screen_max_height: None,
screen_min_width: None,
screen_min_height: None,
geoip: Some(serde_json::Value::Bool(true)),
block_images: None,
block_webrtc: None,
block_webgl: None,
executable_path: None,
fingerprint: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct CamoufoxLaunchResult {
pub id: String,
#[serde(alias = "process_id")]
pub processId: Option<u32>,
#[serde(alias = "profile_path")]
pub profilePath: Option<String>,
pub url: Option<String>,
}
#[derive(Debug)]
struct CamoufoxInstance {
#[allow(dead_code)]
id: String,
process_id: Option<u32>,
profile_path: Option<String>,
url: Option<String>,
}
struct CamoufoxNodecarLauncherInner {
instances: HashMap<String, CamoufoxInstance>,
}
pub struct CamoufoxNodecarLauncher {
inner: Arc<AsyncMutex<CamoufoxNodecarLauncherInner>>,
}
impl CamoufoxNodecarLauncher {
fn new() -> Self {
Self {
inner: Arc::new(AsyncMutex::new(CamoufoxNodecarLauncherInner {
instances: HashMap::new(),
})),
}
}
pub fn instance() -> &'static CamoufoxNodecarLauncher {
&CAMOUFOX_NODECAR_LAUNCHER
}
/// Generate Camoufox fingerprint configuration during profile creation
pub async fn generate_fingerprint_config(
&self,
app_handle: &AppHandle,
profile: &crate::profile::BrowserProfile,
config: &CamoufoxConfig,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let mut config_args = vec!["camoufox".to_string(), "generate-config".to_string()];
// Always ensure executable_path is set to the user's binary location
let executable_path = if let Some(path) = &config.executable_path {
path.clone()
} else {
// Use the browser runner helper with the real profile
let browser_runner = crate::browser_runner::BrowserRunner::instance();
browser_runner
.get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?
.to_string_lossy()
.to_string()
};
config_args.extend(["--executable-path".to_string(), executable_path]);
// Pass existing fingerprint if provided (for advanced form partial fingerprints)
if let Some(fingerprint) = &config.fingerprint {
config_args.extend(["--fingerprint".to_string(), fingerprint.clone()]);
}
if let Some(serde_json::Value::Bool(true)) = &config.geoip {
config_args.push("--geoip".to_string());
}
// Add proxy if provided (can be passed directly during fingerprint generation)
if let Some(proxy) = &config.proxy {
config_args.extend(["--proxy".to_string(), proxy.clone()]);
}
// Add screen dimensions if provided
if let Some(max_width) = config.screen_max_width {
config_args.extend(["--max-width".to_string(), max_width.to_string()]);
}
if let Some(max_height) = config.screen_max_height {
config_args.extend(["--max-height".to_string(), max_height.to_string()]);
}
if let Some(min_width) = config.screen_min_width {
config_args.extend(["--min-width".to_string(), min_width.to_string()]);
}
if let Some(min_height) = config.screen_min_height {
config_args.extend(["--min-height".to_string(), min_height.to_string()]);
}
// Add block_* options
if let Some(block_images) = config.block_images {
if block_images {
config_args.push("--block-images".to_string());
}
}
if let Some(block_webrtc) = config.block_webrtc {
if block_webrtc {
config_args.push("--block-webrtc".to_string());
}
}
if let Some(block_webgl) = config.block_webgl {
if block_webgl {
config_args.push("--block-webgl".to_string());
}
}
// Execute config generation command
let mut config_sidecar = self.get_nodecar_sidecar(app_handle)?;
for arg in &config_args {
config_sidecar = config_sidecar.arg(arg);
}
let config_output = config_sidecar.output().await?;
if !config_output.status.success() {
let stderr = String::from_utf8_lossy(&config_output.stderr);
return Err(format!("Failed to generate camoufox fingerprint config: {stderr}").into());
}
Ok(String::from_utf8_lossy(&config_output.stdout).to_string())
}
/// Get the nodecar sidecar command
fn get_nodecar_sidecar(
&self,
app_handle: &AppHandle,
) -> Result<tauri_plugin_shell::process::Command, Box<dyn std::error::Error + Send + Sync>> {
let shell = app_handle.shell();
let sidecar_command = shell
.sidecar("nodecar")
.map_err(|e| format!("Failed to create nodecar sidecar: {e}"))?;
Ok(sidecar_command)
}
/// Launch Camoufox browser using nodecar sidecar
pub async fn launch_camoufox(
&self,
app_handle: &AppHandle,
profile: &crate::profile::BrowserProfile,
profile_path: &str,
config: &CamoufoxConfig,
url: Option<&str>,
) -> Result<CamoufoxLaunchResult, Box<dyn std::error::Error + Send + Sync>> {
let custom_config = if let Some(existing_fingerprint) = &config.fingerprint {
println!("Using existing fingerprint from profile metadata");
existing_fingerprint.clone()
} else {
return Err("No fingerprint provided".into());
};
// Always ensure executable_path is set to the user's binary location
let executable_path = if let Some(path) = &config.executable_path {
path.clone()
} else {
// Use the browser runner helper with the real profile
let browser_runner = crate::browser_runner::BrowserRunner::instance();
browser_runner
.get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?
.to_string_lossy()
.to_string()
};
// Build nodecar command arguments
let mut args = vec!["camoufox".to_string(), "start".to_string()];
// Add profile path - ensure it's an absolute path
let absolute_profile_path = std::path::Path::new(profile_path)
.canonicalize()
.unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf())
.to_string_lossy()
.to_string();
args.extend(["--profile-path".to_string(), absolute_profile_path]);
// Add URL if provided
if let Some(url) = url {
args.extend(["--url".to_string(), url.to_string()]);
}
// Always add the executable path
args.extend(["--executable-path".to_string(), executable_path]);
// Always add the generated custom config
args.extend(["--custom-config".to_string(), custom_config]);
// Add proxy if provided
if let Some(proxy) = &config.proxy {
args.extend(["--proxy".to_string(), proxy.clone()]);
}
// Add headless flag for tests
if std::env::var("CAMOUFOX_HEADLESS").is_ok() {
args.push("--headless".to_string());
}
// Get the nodecar sidecar command
let mut sidecar_command = self.get_nodecar_sidecar(app_handle)?;
// Add all arguments to the sidecar command
for arg in &args {
sidecar_command = sidecar_command.arg(arg);
}
// Execute nodecar sidecar command
println!("Executing nodecar command with args: {args:?}");
let output = sidecar_command.output().await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
println!("nodecar camoufox failed - stdout: {stdout}, stderr: {stderr}");
return Err(format!("nodecar camoufox failed: {stderr}").into());
}
let stdout = String::from_utf8_lossy(&output.stdout);
println!("nodecar camoufox output: {stdout}");
// Parse the JSON output
let launch_result: CamoufoxLaunchResult = serde_json::from_str(&stdout)
.map_err(|e| format!("Failed to parse nodecar output as JSON: {e}\nOutput was: {stdout}"))?;
// Store the instance
let instance = CamoufoxInstance {
id: launch_result.id.clone(),
process_id: launch_result.processId,
profile_path: launch_result.profilePath.clone(),
url: launch_result.url.clone(),
};
{
let mut inner = self.inner.lock().await;
inner.instances.insert(launch_result.id.clone(), instance);
}
Ok(launch_result)
}
/// Stop a Camoufox process by ID
pub async fn stop_camoufox(
&self,
app_handle: &AppHandle,
id: &str,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
// Get the nodecar sidecar command
let sidecar_command = self
.get_nodecar_sidecar(app_handle)?
.arg("camoufox")
.arg("stop")
.arg("--id")
.arg(id);
// Execute nodecar stop command
let output = sidecar_command.output().await?;
if !output.status.success() {
let _stderr = String::from_utf8_lossy(&output.stderr);
return Ok(false);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let result: serde_json::Value = serde_json::from_str(&stdout)
.map_err(|e| format!("Failed to parse nodecar stop output: {e}"))?;
let success = result
.get("success")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if success {
// Remove from our tracking
let mut inner = self.inner.lock().await;
inner.instances.remove(id);
}
Ok(success)
}
/// Find Camoufox server by profile path (for integration with browser_runner)
pub async fn find_camoufox_by_profile(
&self,
profile_path: &str,
) -> Result<Option<CamoufoxLaunchResult>, Box<dyn std::error::Error + Send + Sync>> {
// First clean up any dead instances
self.cleanup_dead_instances().await?;
let inner = self.inner.lock().await;
// Convert paths to canonical form for comparison
let target_path = std::path::Path::new(profile_path)
.canonicalize()
.unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf());
for (id, instance) in inner.instances.iter() {
if let Some(instance_profile_path) = &instance.profile_path {
let instance_path = std::path::Path::new(instance_profile_path)
.canonicalize()
.unwrap_or_else(|_| std::path::Path::new(instance_profile_path).to_path_buf());
if instance_path == target_path {
// Verify the server is actually running by checking the process
if let Some(process_id) = instance.process_id {
if self.is_server_running(process_id).await {
// Found running Camoufox instance
return Ok(Some(CamoufoxLaunchResult {
id: id.clone(),
processId: instance.process_id,
profilePath: instance.profile_path.clone(),
url: instance.url.clone(),
}));
} else {
// Camoufox instance found but process is not running
}
}
}
}
}
Ok(None)
}
/// Check if servers are still alive and clean up dead instances
pub async fn cleanup_dead_instances(
&self,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let mut dead_instances = Vec::new();
let mut instances_to_remove = Vec::new();
{
let inner = self.inner.lock().await;
for (id, instance) in inner.instances.iter() {
if let Some(process_id) = instance.process_id {
// Check if the process is still alive
if !self.is_server_running(process_id).await {
// Process is dead
// Camoufox instance is no longer running
dead_instances.push(id.clone());
instances_to_remove.push(id.clone());
}
} else {
// No process_id means it's likely a dead instance
// Camoufox instance has no PID, marking as dead
dead_instances.push(id.clone());
instances_to_remove.push(id.clone());
}
}
}
// Remove dead instances
if !instances_to_remove.is_empty() {
let mut inner = self.inner.lock().await;
for id in &instances_to_remove {
inner.instances.remove(id);
// Removed dead Camoufox instance
}
}
Ok(dead_instances)
}
/// Check if a Camoufox server is running with the given process ID
async fn is_server_running(&self, process_id: u32) -> bool {
// Check if the process is still running
use sysinfo::{Pid, System};
let system = System::new_all();
if let Some(process) = system.process(Pid::from(process_id as usize)) {
// Check if this is actually a Camoufox process by looking at the command line
let cmd = process.cmd();
let is_camoufox = cmd.iter().any(|arg| {
let arg_str = arg.to_str().unwrap_or("");
arg_str.contains("camoufox-worker") || arg_str.contains("camoufox")
});
if is_camoufox {
// Found running Camoufox process
return true;
}
}
false
}
}
impl CamoufoxNodecarLauncher {
pub async fn launch_camoufox_profile_nodecar(
&self,
app_handle: AppHandle,
profile: BrowserProfile,
config: CamoufoxConfig,
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 profile_path = profile.get_profile_data_path(&profiles_dir);
let profile_path_str = profile_path.to_string_lossy();
// Check if there's already a running instance for this profile
if let Ok(Some(existing)) = self.find_camoufox_by_profile(&profile_path_str).await {
// If there's an existing instance, stop it first to avoid conflicts
let _ = self.stop_camoufox(&app_handle, &existing.id).await;
}
// Clean up any dead instances before launching
let _ = self.cleanup_dead_instances().await;
self
.launch_camoufox(
&app_handle,
&profile,
&profile_path_str,
&config,
url.as_deref(),
)
.await
.map_err(|e| format!("Failed to launch Camoufox via nodecar: {e}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let default_config = CamoufoxConfig::default();
// Verify defaults
assert_eq!(default_config.geoip, Some(serde_json::Value::Bool(true)));
assert_eq!(default_config.proxy, None);
assert_eq!(default_config.fingerprint, None);
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref CAMOUFOX_NODECAR_LAUNCHER: CamoufoxNodecarLauncher = CamoufoxNodecarLauncher::new();
}
+485 -81
View File
@@ -1,5 +1,77 @@
use tauri::command;
pub struct DefaultBrowser;
impl DefaultBrowser {
fn new() -> Self {
Self
}
pub fn instance() -> &'static DefaultBrowser {
&DEFAULT_BROWSER
}
pub async fn is_default_browser(&self) -> Result<bool, String> {
#[cfg(target_os = "macos")]
return macos::is_default_browser();
#[cfg(target_os = "windows")]
return windows::is_default_browser();
#[cfg(target_os = "linux")]
return linux::is_default_browser();
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
Err("Unsupported platform".to_string())
}
pub async fn set_as_default_browser(&self) -> Result<(), String> {
#[cfg(target_os = "macos")]
return macos::set_as_default_browser();
#[cfg(target_os = "windows")]
return windows::set_as_default_browser();
#[cfg(target_os = "linux")]
return linux::set_as_default_browser();
#[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_name: 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.name == profile_name)
.ok_or_else(|| format!("Profile '{profile_name}' not found"))?;
println!("Opening URL '{url}' with profile '{profile_name}'");
// 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_name}': {e}");
format!("Failed to open URL with profile: {e}")
})?;
println!("Successfully opened URL '{url}' with profile '{profile_name}'");
Ok(())
}
}
#[cfg(target_os = "macos")]
mod macos {
use core_foundation::base::OSStatus;
@@ -65,56 +137,438 @@ mod macos {
#[cfg(target_os = "windows")]
mod windows {
use std::path::Path;
use winreg::enums::*;
use winreg::RegKey;
const APP_NAME: &str = "DonutBrowser";
const PROG_ID: &str = "DonutBrowser.HTML";
pub fn is_default_browser() -> Result<bool, String> {
// Windows implementation would go here
Err("Windows support not implemented yet".to_string())
let schemes = ["http", "https"];
for scheme in schemes {
// Check if our browser is set as the default handler for this scheme
if !is_default_for_scheme(scheme)? {
return Ok(false);
}
}
Ok(true)
}
pub fn set_as_default_browser() -> Result<(), String> {
Err("Windows support not implemented yet".to_string())
// Get the current executable path
let exe_path = std::env::current_exe()
.map_err(|e| format!("Failed to get current executable path: {}", e))?;
let exe_path_str = exe_path
.to_str()
.ok_or("Failed to convert executable path to string")?;
// Verify the executable exists
if !Path::new(exe_path_str).exists() {
return Err(format!("Executable not found at: {}", exe_path_str));
}
// Register the application
register_application(exe_path_str)?;
// Set as default for HTTP and HTTPS
set_default_for_scheme("http")?;
set_default_for_scheme("https")?;
// Register file associations for HTML files
register_html_file_association(exe_path_str)?;
// Notify the system of changes
notify_system_of_changes();
Ok(())
}
fn is_default_for_scheme(scheme: &str) -> Result<bool, String> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
// Check Software\Microsoft\Windows\Shell\Associations\UrlAssociations\{scheme}\UserChoice
let path = format!(
"Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\{}\\UserChoice",
scheme
);
match hkcu.open_subkey(&path) {
Ok(key) => match key.get_value::<String, _>("ProgId") {
Ok(prog_id) => Ok(prog_id == PROG_ID),
Err(_) => Ok(false),
},
Err(_) => Ok(false),
}
}
fn register_application(exe_path: &str) -> Result<(), String> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
// Register in Software\RegisteredApplications
let (registered_apps, _) = hkcu
.create_subkey("Software\\RegisteredApplications")
.map_err(|e| format!("Failed to create RegisteredApplications key: {}", e))?;
registered_apps
.set_value(APP_NAME, &format!("Software\\{}", APP_NAME))
.map_err(|e| format!("Failed to set registered application: {}", e))?;
// Create application key
let (app_key, _) = hkcu
.create_subkey(&format!("Software\\{}", APP_NAME))
.map_err(|e| format!("Failed to create application key: {}", e))?;
// Set application properties
app_key
.set_value("ApplicationName", &APP_NAME)
.map_err(|e| format!("Failed to set ApplicationName: {}", e))?;
app_key
.set_value(
"ApplicationDescription",
&"Donut Browser - Simple Yet Powerful Anti-Detect Browser",
)
.map_err(|e| format!("Failed to set ApplicationDescription: {}", e))?;
app_key
.set_value("ApplicationIcon", &format!("{},0", exe_path))
.map_err(|e| format!("Failed to set ApplicationIcon: {}", e))?;
// Create Capabilities key
let (capabilities, _) = app_key
.create_subkey("Capabilities")
.map_err(|e| format!("Failed to create Capabilities key: {}", e))?;
capabilities
.set_value(
"ApplicationDescription",
&"Donut Browser - Simple Yet Powerful Anti-Detect Browser",
)
.map_err(|e| format!("Failed to set Capabilities description: {}", e))?;
// Set URL associations
let (url_assoc, _) = capabilities
.create_subkey("URLAssociations")
.map_err(|e| format!("Failed to create URLAssociations key: {}", e))?;
url_assoc
.set_value("http", &PROG_ID)
.map_err(|e| format!("Failed to set http association: {}", e))?;
url_assoc
.set_value("https", &PROG_ID)
.map_err(|e| format!("Failed to set https association: {}", e))?;
// Set file associations
let (file_assoc, _) = capabilities
.create_subkey("FileAssociations")
.map_err(|e| format!("Failed to create FileAssociations key: {}", e))?;
file_assoc
.set_value(".html", &PROG_ID)
.map_err(|e| format!("Failed to set .html association: {}", e))?;
file_assoc
.set_value(".htm", &PROG_ID)
.map_err(|e| format!("Failed to set .htm association: {}", e))?;
// Register the ProgID
register_prog_id(exe_path)?;
Ok(())
}
fn register_prog_id(exe_path: &str) -> Result<(), String> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
// Create ProgID key
let (prog_id_key, _) = hkcu
.create_subkey(&format!("Software\\Classes\\{}", PROG_ID))
.map_err(|e| format!("Failed to create ProgID key: {}", e))?;
prog_id_key
.set_value("", &"Donut Browser Document")
.map_err(|e| format!("Failed to set ProgID default value: {}", e))?;
prog_id_key
.set_value("FriendlyTypeName", &"Donut Browser Document")
.map_err(|e| format!("Failed to set FriendlyTypeName: {}", e))?;
// Create DefaultIcon key
let (icon_key, _) = prog_id_key
.create_subkey("DefaultIcon")
.map_err(|e| format!("Failed to create DefaultIcon key: {}", e))?;
icon_key
.set_value("", &format!("{},0", exe_path))
.map_err(|e| format!("Failed to set default icon: {}", e))?;
// Create shell\open\command key
let (command_key, _) = prog_id_key
.create_subkey("shell\\open\\command")
.map_err(|e| format!("Failed to create command key: {}", e))?;
command_key
.set_value("", &format!("\"{}\" \"%1\"", exe_path))
.map_err(|e| format!("Failed to set command: {}", e))?;
Ok(())
}
fn set_default_for_scheme(scheme: &str) -> Result<(), String> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
// Set in Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.html\UserChoice
// Note: On Windows 10+, this might require elevated permissions or user interaction
// through the Settings app due to security restrictions
// Try to set the association in the user's choice
let user_choice_path = format!(
"Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\{}\\UserChoice",
scheme
);
// Note: Setting UserChoice directly may not work on Windows 10+ due to hash verification
// The user may need to manually set the default browser through Windows Settings
match hkcu.create_subkey(&user_choice_path) {
Ok((user_choice, _)) => {
// Attempt to set the ProgId
if let Err(_) = user_choice.set_value("ProgId", &PROG_ID) {
// If we can't set UserChoice, that's expected on newer Windows versions
// The registration is still valuable for the "Open with" menu
}
}
Err(_) => {
// Expected on newer Windows versions - user must set manually
}
}
Ok(())
}
fn register_html_file_association(_exe_path: &str) -> Result<(), String> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
// Register .html and .htm file associations
for ext in &[".html", ".htm"] {
let ext_path = format!("Software\\Classes\\{}", ext);
match hkcu.create_subkey(&ext_path) {
Ok((ext_key, _)) => {
// Set the default value to our ProgID
let _ = ext_key.set_value("", &PROG_ID);
}
Err(_) => {
// Continue if we can't set the file association
}
}
}
Ok(())
}
fn notify_system_of_changes() {
// Use Windows API to notify the system of association changes
// This helps refresh the system's understanding of the changes
unsafe {
use std::ffi::c_void;
// Declare the Windows API functions
type UINT = u32;
type DWORD = u32;
type LPARAM = isize;
type WPARAM = usize;
const HWND_BROADCAST: *mut c_void = 0xffff as *mut c_void;
const WM_SETTINGCHANGE: UINT = 0x001A;
const SMTO_ABORTIFHUNG: UINT = 0x0002;
// Link to user32.dll functions
extern "system" {
fn SendMessageTimeoutA(
hWnd: *mut c_void,
Msg: UINT,
wParam: WPARAM,
lParam: LPARAM,
fuFlags: UINT,
uTimeout: UINT,
lpdwResult: *mut DWORD,
) -> isize;
}
let mut result: DWORD = 0;
// Notify about file associations change
SendMessageTimeoutA(
HWND_BROADCAST,
WM_SETTINGCHANGE,
0,
"Software\\Classes\0".as_ptr() as LPARAM,
SMTO_ABORTIFHUNG,
1000,
&mut result,
);
}
}
}
#[cfg(target_os = "linux")]
mod linux {
use std::process::Command;
const APP_DESKTOP_NAME: &str = "donutbrowser.desktop";
pub fn is_default_browser() -> Result<bool, String> {
// Linux implementation would go here
Err("Linux support not implemented yet".to_string())
// Check if xdg-mime is available
if !is_xdg_mime_available() {
return Err("xdg-mime utility not found. Please install xdg-utils package.".to_string());
}
let schemes = ["http", "https"];
for scheme in schemes {
let mime_type = format!("x-scheme-handler/{}", scheme);
// Query the current default handler for this scheme
let output = Command::new("xdg-mime")
.args(["query", "default", &mime_type])
.output()
.map_err(|e| format!("Failed to query default handler for {}: {}", scheme, e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("xdg-mime query failed for {}: {}", scheme, stderr));
}
let current_handler = String::from_utf8_lossy(&output.stdout).trim().to_string();
// Check if our app is the default handler
if current_handler != APP_DESKTOP_NAME {
return Ok(false);
}
}
Ok(true)
}
pub fn set_as_default_browser() -> Result<(), String> {
Err("Linux support not implemented yet".to_string())
// Check if xdg-mime is available
if !is_xdg_mime_available() {
return Err("xdg-mime utility not found. Please install xdg-utils package.".to_string());
}
// Check if the desktop file exists in common locations
if !check_desktop_file_exists() {
return Err(format!(
"Desktop file '{}' not found in standard locations. Please ensure the application is properly installed. You can manually set Donut Browser as the default browser in your system settings.",
APP_DESKTOP_NAME
));
}
let schemes = ["http", "https"];
let mut all_succeeded = true;
let mut error_messages = Vec::new();
for scheme in schemes {
let mime_type = format!("x-scheme-handler/{}", scheme);
// Set our app as the default handler for this scheme
let output = Command::new("xdg-mime")
.args(["default", APP_DESKTOP_NAME, &mime_type])
.output()
.map_err(|e| format!("Failed to set default handler for {}: {}", scheme, e))?;
if !output.status.success() {
all_succeeded = false;
let stderr = String::from_utf8_lossy(&output.stderr);
error_messages.push(format!("Failed to set default for {}: {}", scheme, stderr));
}
}
if !all_succeeded {
return Err(format!(
"Some xdg-mime commands failed:\n{}\n\nYou may need to:\n1. Run with appropriate permissions\n2. Manually set the default browser in your desktop environment settings\n3. Restart your desktop session",
error_messages.join("\n")
));
}
// Give the system a moment to process the changes
std::thread::sleep(std::time::Duration::from_millis(500));
// Verify the changes took effect
match is_default_browser() {
Ok(true) => Ok(()),
Ok(false) => {
// This is the common case where commands succeed but verification fails
Err(format!(
"The xdg-mime commands completed successfully, but Donut Browser is not yet set as the default. This is common on some Linux distributions. Please try one of these options:\n\n1. Restart your desktop session and try again\n2. Log out and log back in\n3. Manually set Donut Browser as the default in your system settings:\n - GNOME: Settings > Default Applications > Web\n - KDE: System Settings > Applications > Default Applications > Web Browser\n - XFCE: Settings > Preferred Applications > Web Browser\n - Or run: xdg-settings set default-web-browser {}\n\nThe changes may take effect automatically after a desktop restart.",
APP_DESKTOP_NAME
))
}
Err(e) => Err(format!(
"Set as default completed, but verification failed: {}. The changes may still be in effect after restarting your desktop session.",
e
))
}
}
fn is_xdg_mime_available() -> bool {
Command::new("which")
.arg("xdg-mime")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn check_desktop_file_exists() -> bool {
let desktop_locations = [
"~/.local/share/applications/",
"/usr/share/applications/",
"/usr/local/share/applications/",
"/var/lib/flatpak/exports/share/applications/",
"~/.local/share/flatpak/exports/share/applications/",
];
for location in &desktop_locations {
let path = if location.starts_with('~') {
if let Ok(home) = std::env::var("HOME") {
location.replace('~', &home)
} else {
continue;
}
} else {
location.to_string()
};
let full_path = format!("{}{}", path, APP_DESKTOP_NAME);
if std::path::Path::new(&full_path).exists() {
return true;
}
}
false
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref DEFAULT_BROWSER: DefaultBrowser = DefaultBrowser::new();
}
#[command]
pub async fn is_default_browser() -> Result<bool, String> {
#[cfg(target_os = "macos")]
return macos::is_default_browser();
#[cfg(target_os = "windows")]
return windows::is_default_browser();
#[cfg(target_os = "linux")]
return linux::is_default_browser();
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
Err("Unsupported platform".to_string())
let default_browser = DefaultBrowser::instance();
default_browser.is_default_browser().await
}
#[command]
pub async fn set_as_default_browser() -> Result<(), String> {
#[cfg(target_os = "macos")]
return macos::set_as_default_browser();
#[cfg(target_os = "windows")]
return windows::set_as_default_browser();
#[cfg(target_os = "linux")]
return linux::set_as_default_browser();
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
Err("Unsupported platform".to_string())
let default_browser = DefaultBrowser::instance();
default_browser.set_as_default_browser().await
}
#[tauri::command]
@@ -123,58 +577,8 @@ pub async fn open_url_with_profile(
profile_name: String,
url: String,
) -> Result<(), String> {
use crate::browser_runner::BrowserRunner;
let runner = BrowserRunner::new();
// 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.name == profile_name)
.ok_or_else(|| format!("Profile '{profile_name}' not found"))?;
println!("Opening URL '{url}' with profile '{profile_name}'");
// 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()))
let default_browser = DefaultBrowser::instance();
default_browser
.open_url_with_profile(app_handle, profile_name, url)
.await
.map_err(|e| {
println!("Failed to open URL with profile '{profile_name}': {e}");
format!("Failed to open URL with profile: {e}")
})?;
println!("Successfully opened URL '{url}' with profile '{profile_name}'");
Ok(())
}
#[tauri::command]
pub async fn smart_open_url(
_app_handle: tauri::AppHandle,
_url: String,
_is_startup: Option<bool>,
) -> Result<String, String> {
use crate::browser_runner::BrowserRunner;
let runner = BrowserRunner::new();
// Get all profiles
let profiles = runner
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
if profiles.is_empty() {
return Err("no_profiles".to_string());
}
println!(
"URL opening - Total profiles: {}, showing profile selector",
profiles.len()
);
// Always show the profile selector so the user can choose
Err("show_selector".to_string())
}
+390 -416
View File
@@ -1,13 +1,12 @@
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io;
use std::path::{Path, PathBuf};
use tauri::Emitter;
use crate::api_client::ApiClient;
use crate::browser::BrowserType;
use crate::browser_version_service::DownloadInfo;
use crate::browser_version_manager::DownloadInfo;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DownloadProgress {
@@ -23,22 +22,26 @@ pub struct DownloadProgress {
pub struct Downloader {
client: Client,
api_client: ApiClient,
api_client: &'static ApiClient,
}
impl Downloader {
pub fn new() -> Self {
fn new() -> Self {
Self {
client: Client::new(),
api_client: ApiClient::new(),
api_client: ApiClient::instance(),
}
}
pub fn instance() -> &'static Downloader {
&DOWNLOADER
}
#[cfg(test)]
pub fn new_with_api_client(api_client: ApiClient) -> Self {
pub fn new_with_api_client(_api_client: ApiClient) -> Self {
Self {
client: Client::new(),
api_client,
api_client: ApiClient::instance(),
}
}
@@ -51,7 +54,7 @@ impl Downloader {
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
match browser_type {
BrowserType::Brave => {
// For Brave, we need to find the actual macOS asset
// For Brave, we need to find the actual platform-specific asset
let releases = self
.api_client
.fetch_brave_releases_with_caching(true)
@@ -65,39 +68,63 @@ impl Downloader {
})
.ok_or(format!("Brave version {version} not found"))?;
// Find the universal macOS DMG asset
let asset = release
.assets
.iter()
.find(|asset| asset.name.contains(".dmg") && asset.name.contains("universal"))
// Get platform and architecture info
let (os, arch) = Self::get_platform_info();
// Find the appropriate asset based on platform and architecture
let asset_url = self
.find_brave_asset(&release.assets, &os, &arch)
.ok_or(format!(
"No universal macOS DMG asset found for Brave version {version}"
"No compatible asset found for Brave version {version} on {os}/{arch}"
))?;
Ok(asset.browser_download_url.clone())
Ok(asset_url)
}
BrowserType::Zen => {
// For Zen, verify the asset exists
let releases = self
.api_client
.fetch_zen_releases_with_caching(true)
.await?;
// For Zen, verify the asset exists and handle different naming patterns
let releases = match self.api_client.fetch_zen_releases_with_caching(true).await {
Ok(releases) => releases,
Err(e) => {
eprintln!("Failed to fetch Zen releases: {e}");
return Err(format!("Failed to fetch Zen releases from GitHub API: {e}. This might be due to GitHub API rate limiting or network issues. Please try again later.").into());
}
};
let release = releases
.iter()
.find(|r| r.tag_name == version)
.ok_or(format!("Zen version {version} not found"))?;
.ok_or_else(|| {
format!(
"Zen version {} not found. Available versions: {}",
version,
releases
.iter()
.take(5)
.map(|r| r.tag_name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
})?;
// Find the macOS universal DMG asset
let asset = release
.assets
.iter()
.find(|asset| asset.name == "zen.macos-universal.dmg")
.ok_or(format!(
"No macOS universal asset found for Zen version {version}"
))?;
// Get platform and architecture info
let (os, arch) = Self::get_platform_info();
Ok(asset.browser_download_url.clone())
// Find the appropriate asset
let asset_url = self
.find_zen_asset(&release.assets, &os, &arch)
.ok_or_else(|| {
let available_assets: Vec<&str> =
release.assets.iter().map(|a| a.name.as_str()).collect();
format!(
"No compatible asset found for Zen version {} on {}/{}. Available assets: {}",
version,
os,
arch,
available_assets.join(", ")
)
})?;
Ok(asset_url)
}
BrowserType::MullvadBrowser => {
// For Mullvad, verify the asset exists
@@ -111,16 +138,41 @@ impl Downloader {
.find(|r| r.tag_name == version)
.ok_or(format!("Mullvad version {version} not found"))?;
// Find the macOS DMG asset
let asset = release
.assets
.iter()
.find(|asset| asset.name.contains(".dmg") && asset.name.contains("mac"))
// Get platform and architecture info
let (os, arch) = Self::get_platform_info();
// Find the appropriate asset
let asset_url = self
.find_mullvad_asset(&release.assets, &os, &arch)
.ok_or(format!(
"No macOS asset found for Mullvad version {version}"
"No compatible asset found for Mullvad version {version} on {os}/{arch}"
))?;
Ok(asset.browser_download_url.clone())
Ok(asset_url)
}
BrowserType::Camoufox => {
// For Camoufox, verify the asset exists and find the correct download URL
let releases = self
.api_client
.fetch_camoufox_releases_with_caching(true)
.await?;
let release = releases
.iter()
.find(|r| r.tag_name == version)
.ok_or(format!("Camoufox version {version} not found"))?;
// Get platform and architecture info
let (os, arch) = Self::get_platform_info();
// Find the appropriate asset
let asset_url = self
.find_camoufox_asset(&release.assets, &os, &arch)
.ok_or(format!(
"No compatible asset found for Camoufox version {version} on {os}/{arch}"
))?;
Ok(asset_url)
}
_ => {
// For other browsers, use the provided URL
@@ -129,6 +181,202 @@ impl Downloader {
}
}
/// Get platform and architecture information
fn get_platform_info() -> (String, String) {
let os = if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "macos") {
"macos"
} else {
"unknown"
};
let arch = if cfg!(target_arch = "x86_64") {
"x64"
} else if cfg!(target_arch = "aarch64") {
"arm64"
} else {
"unknown"
};
(os.to_string(), arch.to_string())
}
/// Find the appropriate Brave asset for the current platform and architecture
fn find_brave_asset(
&self,
assets: &[crate::browser::GithubAsset],
os: &str,
arch: &str,
) -> Option<String> {
// Brave asset naming patterns:
// Windows: BraveBrowserStandaloneNightlySetup.exe, BraveBrowserStandaloneSilentNightlySetup.exe
// macOS: Brave-Browser-Nightly-universal.dmg, Brave-Browser-Nightly-universal.pkg
// Linux: brave-browser-1.79.119-linux-arm64.zip, brave-browser-1.79.119-linux-amd64.zip
let asset = match os {
"windows" => {
// For Windows, look for standalone setup EXE (not the auto-updater one)
assets
.iter()
.find(|asset| {
let name = asset.name.to_lowercase();
name.contains("standalone") && name.ends_with(".exe") && !name.contains("silent")
})
.or_else(|| {
// Fallback to any EXE if standalone not found
assets.iter().find(|asset| asset.name.ends_with(".exe"))
})
}
"macos" => {
// For macOS, prefer universal DMG
assets
.iter()
.find(|asset| {
let name = asset.name.to_lowercase();
name.contains("universal") && name.ends_with(".dmg")
})
.or_else(|| {
// Fallback to any DMG
assets.iter().find(|asset| asset.name.ends_with(".dmg"))
})
}
"linux" => {
// For Linux, be strict about architecture matching - same logic as has_compatible_brave_asset
let arch_pattern = if arch == "arm64" { "arm64" } else { "amd64" };
assets.iter().find(|asset| {
let name = asset.name.to_lowercase();
name.contains("linux") && name.contains(arch_pattern) && name.ends_with(".zip")
})
}
_ => None,
};
asset.map(|a| a.browser_download_url.clone())
}
/// Find the appropriate Zen asset for the current platform and architecture
fn find_zen_asset(
&self,
assets: &[crate::browser::GithubAsset],
os: &str,
arch: &str,
) -> Option<String> {
// Zen asset naming patterns:
// Windows: zen.installer.exe, zen.installer-arm64.exe
// macOS: zen.macos-universal.dmg
// Linux: zen.linux-x86_64.tar.xz, zen.linux-aarch64.tar.xz, zen-x86_64.AppImage, zen-aarch64.AppImage
let asset = match (os, arch) {
("windows", "x64") => assets
.iter()
.find(|asset| asset.name == "zen.installer.exe"),
("windows", "arm64") => assets
.iter()
.find(|asset| asset.name == "zen.installer-arm64.exe"),
("macos", _) => assets
.iter()
.find(|asset| asset.name == "zen.macos-universal.dmg"),
("linux", "x64") => {
// Prefer tar.xz, fallback to AppImage
assets
.iter()
.find(|asset| asset.name == "zen.linux-x86_64.tar.xz")
.or_else(|| {
assets
.iter()
.find(|asset| asset.name == "zen-x86_64.AppImage")
})
}
("linux", "arm64") => {
// Prefer tar.xz, fallback to AppImage
assets
.iter()
.find(|asset| asset.name == "zen.linux-aarch64.tar.xz")
.or_else(|| {
assets
.iter()
.find(|asset| asset.name == "zen-aarch64.AppImage")
})
}
_ => None,
};
asset.map(|a| a.browser_download_url.clone())
}
/// Find the appropriate Mullvad asset for the current platform and architecture
fn find_mullvad_asset(
&self,
assets: &[crate::browser::GithubAsset],
os: &str,
arch: &str,
) -> Option<String> {
// Mullvad asset naming patterns:
// Windows: mullvad-browser-windows-x86_64-VERSION.exe
// macOS: mullvad-browser-macos-VERSION.dmg
// Linux: mullvad-browser-x86_64-VERSION.tar.xz
let asset = match (os, arch) {
("windows", "x64") => assets.iter().find(|asset| {
asset.name.contains("windows")
&& asset.name.contains("x86_64")
&& asset.name.ends_with(".exe")
}),
("windows", "arm64") => {
// Mullvad doesn't support ARM64 on Windows
None
}
("macos", _) => assets
.iter()
.find(|asset| asset.name.contains("macos") && asset.name.ends_with(".dmg")),
("linux", "x64") => assets.iter().find(|asset| {
asset.name.contains("x86_64")
&& asset.name.ends_with(".tar.xz")
&& !asset.name.contains("windows")
}),
("linux", "arm64") => {
// Mullvad doesn't support ARM64 on Linux
None
}
_ => None,
};
asset.map(|a| a.browser_download_url.clone())
}
/// Find the appropriate Camoufox asset for the current platform and architecture
fn find_camoufox_asset(
&self,
assets: &[crate::browser::GithubAsset],
os: &str,
arch: &str,
) -> Option<String> {
// Camoufox asset naming pattern: camoufox-{version}-{release}-{os}.{arch}.zip
let (os_name, arch_name) = match (os, arch) {
("windows", "x64") => ("win", "x86_64"),
("windows", "arm64") => ("win", "arm64"),
("linux", "x64") => ("lin", "x86_64"),
("linux", "arm64") => ("lin", "arm64"),
("macos", "x64") => ("mac", "x86_64"),
("macos", "arm64") => ("mac", "arm64"),
_ => return None,
};
// Look for assets matching the pattern
let asset = assets.iter().find(|asset| {
let name = asset.name.to_lowercase();
name.starts_with("camoufox-")
&& name.contains(&format!("-{os_name}.{arch_name}.zip"))
&& name.ends_with(".zip")
});
asset.map(|a| a.browser_download_url.clone())
}
pub async fn download_browser<R: tauri::Runtime>(
&self,
app_handle: &tauri::AppHandle<R>,
@@ -148,43 +396,104 @@ impl Downloader {
let is_twilight =
browser_type == BrowserType::Zen && version.to_lowercase().contains("twilight");
// Emit initial progress
// Determine if we have a partial file to resume
let mut existing_size: u64 = 0;
if let Ok(meta) = std::fs::metadata(&file_path) {
existing_size = meta.len();
}
// Build request, add Range only if we have bytes
let mut request = self
.client
.get(&download_url)
.header(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
);
if existing_size > 0 {
request = request.header("Range", format!("bytes={existing_size}-"));
}
// Start download (or resume)
let response = request.send().await?;
// Check if the response is successful
if !(response.status().is_success() || response.status().as_u16() == 206) {
return Err(format!("Download failed with status: {}", response.status()).into());
}
// Determine total size
let mut total_size = response.content_length();
// If resuming (206) and Content-Range is present, parse total
if response.status().as_u16() == 206 {
if let Some(content_range) = response.headers().get(reqwest::header::CONTENT_RANGE) {
if let Ok(cr) = content_range.to_str() {
// Format: bytes start-end/total
if let Some((_, total_str)) = cr.split('/').collect::<Vec<_>>().split_first() {
if let Some(total_str) = total_str.first() {
if let Ok(total) = total_str.parse::<u64>() {
total_size = Some(total);
}
}
}
}
} else if let Some(len) = response.headers().get(reqwest::header::CONTENT_LENGTH) {
// Fallback: total = existing + incoming length
if let Ok(len_str) = len.to_str() {
if let Ok(incoming) = len_str.parse::<u64>() {
total_size = Some(existing_size + incoming);
}
}
}
} else if existing_size > 0 && response.status().is_success() {
// Server ignored range or we asked from 0; if 200 and existing file has content, start fresh
// Truncate existing file so we don't append duplicate bytes
let _ = std::fs::remove_file(&file_path);
existing_size = 0;
}
let mut downloaded = existing_size;
let start_time = std::time::Instant::now();
let mut last_update = start_time;
// Emit initial progress AFTER we've established total size and resume state
let initial_percentage = if let Some(total) = total_size {
if total > 0 {
(existing_size as f64 / total as f64) * 100.0
} else {
0.0
}
} else {
0.0
};
let initial_stage = if is_twilight {
"downloading (twilight rolling release)".to_string()
} else {
"downloading".to_string()
};
let progress = DownloadProgress {
browser: browser_type.as_str().to_string(),
version: version.to_string(),
downloaded_bytes: 0,
total_bytes: None,
percentage: 0.0,
downloaded_bytes: existing_size,
total_bytes: total_size,
percentage: initial_percentage,
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: if is_twilight {
"downloading (twilight rolling release)".to_string()
} else {
"downloading".to_string()
},
stage: initial_stage,
};
let _ = app_handle.emit("download-progress", &progress);
// Start download
let response = self
.client
.get(&download_url)
.header("User-Agent", "donutbrowser")
.send()
.await?;
// Check if the response is successful
if !response.status().is_success() {
return Err(format!("Download failed with status: {}", response.status()).into());
}
let total_size = response.content_length();
let mut downloaded = 0u64;
let start_time = std::time::Instant::now();
let mut last_update = start_time;
let mut file = File::create(&file_path)?;
// Open file in append mode (resuming) or create new
use std::fs::OpenOptions;
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&file_path)?;
let mut stream = response.bytes_stream();
use futures_util::StreamExt;
@@ -197,13 +506,19 @@ impl Downloader {
// Update progress every 100ms to avoid too many events
if now.duration_since(last_update).as_millis() >= 100 {
let elapsed = start_time.elapsed().as_secs_f64();
// Compute speed based only on bytes downloaded in this session to avoid inflated values when resuming
let downloaded_since_start = downloaded.saturating_sub(existing_size);
let speed = if elapsed > 0.0 {
downloaded as f64 / elapsed
downloaded_since_start as f64 / elapsed
} else {
0.0
};
let percentage = if let Some(total) = total_size {
(downloaded as f64 / total as f64) * 100.0
if total > 0 {
(downloaded as f64 / total as f64) * 100.0
} else {
0.0
}
} else {
0.0
};
@@ -244,10 +559,10 @@ mod tests {
use super::*;
use crate::api_client::ApiClient;
use crate::browser::BrowserType;
use crate::browser_version_service::DownloadInfo;
use crate::browser_version_manager::DownloadInfo;
use tempfile::TempDir;
use wiremock::matchers::{header, method, path};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn setup_mock_server() -> MockServer {
@@ -262,157 +577,13 @@ mod tests {
base_url.clone(), // github_api_base
base_url.clone(), // chromium_api_base
base_url.clone(), // tor_archive_base
base_url.clone(), // mozilla_download_base
)
}
#[tokio::test]
async fn test_resolve_brave_download_url() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "v1.81.9",
"name": "Brave Release 1.81.9",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "brave-v1.81.9-universal.dmg",
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
"size": 200000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/brave/brave-browser/releases"))
.and(header("user-agent", "donutbrowser"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "brave-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::Brave, "v1.81.9", &download_info)
.await;
assert!(result.is_ok());
let url = result.unwrap();
assert_eq!(url, "https://example.com/brave-1.81.9-universal.dmg");
}
#[tokio::test]
async fn test_resolve_zen_download_url() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "1.11b",
"name": "Zen Browser 1.11b",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "zen.macos-universal.dmg",
"browser_download_url": "https://example.com/zen-1.11b-universal.dmg",
"size": 120000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/zen-browser/desktop/releases"))
.and(header("user-agent", "donutbrowser"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "zen-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::Zen, "1.11b", &download_info)
.await;
assert!(result.is_ok());
let url = result.unwrap();
assert_eq!(url, "https://example.com/zen-1.11b-universal.dmg");
}
#[tokio::test]
async fn test_resolve_mullvad_download_url() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "14.5a6",
"name": "Mullvad Browser 14.5a6",
"prerelease": true,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "mullvad-browser-macos-14.5a6.dmg",
"browser_download_url": "https://example.com/mullvad-14.5a6.dmg",
"size": 100000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/mullvad/mullvad-browser/releases"))
.and(header("user-agent", "donutbrowser"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "mullvad-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::MullvadBrowser, "14.5a6", &download_info)
.await;
assert!(result.is_ok());
let url = result.unwrap();
assert_eq!(url, "https://example.com/mullvad-14.5a6.dmg");
}
#[tokio::test]
async fn test_resolve_firefox_download_url() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
@@ -473,106 +644,6 @@ mod tests {
assert_eq!(url, download_info.url);
}
#[tokio::test]
async fn test_resolve_brave_version_not_found() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "v1.81.8",
"name": "Brave Release 1.81.8",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "brave-v1.81.8-universal.dmg",
"browser_download_url": "https://example.com/brave-1.81.8-universal.dmg",
"size": 200000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/brave/brave-browser/releases"))
.and(header("user-agent", "donutbrowser"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "brave-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::Brave, "v1.81.9", &download_info)
.await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Brave version v1.81.9 not found"));
}
#[tokio::test]
async fn test_resolve_zen_asset_not_found() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "1.11b",
"name": "Zen Browser 1.11b",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "zen.linux-universal.tar.bz2",
"browser_download_url": "https://example.com/zen-1.11b-linux.tar.bz2",
"size": 150000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/zen-browser/desktop/releases"))
.and(header("user-agent", "donutbrowser"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "zen-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::Zen, "1.11b", &download_info)
.await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No macOS universal asset found"));
}
#[tokio::test]
async fn test_download_browser_with_progress() {
let server = setup_mock_server().await;
@@ -589,7 +660,6 @@ mod tests {
// Mock the download endpoint
Mock::given(method("GET"))
.and(path("/test-download"))
.and(header("user-agent", "donutbrowser"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(test_content)
@@ -640,7 +710,6 @@ mod tests {
// Mock a 404 response
Mock::given(method("GET"))
.and(path("/missing-file"))
.and(header("user-agent", "donutbrowser"))
.respond_with(ResponseTemplate::new(404))
.mount(&server)
.await;
@@ -667,105 +736,6 @@ mod tests {
assert!(result.is_err());
}
#[tokio::test]
async fn test_resolve_mullvad_asset_not_found() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "14.5a6",
"name": "Mullvad Browser 14.5a6",
"prerelease": true,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "mullvad-browser-linux-14.5a6.tar.xz",
"browser_download_url": "https://example.com/mullvad-14.5a6.tar.xz",
"size": 80000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/mullvad/mullvad-browser/releases"))
.and(header("user-agent", "donutbrowser"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "mullvad-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::MullvadBrowser, "14.5a6", &download_info)
.await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No macOS asset found"));
}
#[tokio::test]
async fn test_brave_version_with_v_prefix() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "v1.81.9",
"name": "Brave Release 1.81.9",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "brave-v1.81.9-universal.dmg",
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
"size": 200000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/brave/brave-browser/releases"))
.and(header("user-agent", "donutbrowser"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "brave-test.dmg".to_string(),
is_archive: true,
};
// Test with version without v prefix
let result = downloader
.resolve_download_url(BrowserType::Brave, "1.81.9", &download_info)
.await;
assert!(result.is_ok());
let url = result.unwrap();
assert_eq!(url, "https://example.com/brave-1.81.9-universal.dmg");
}
#[tokio::test]
async fn test_download_browser_chunked_response() {
let server = setup_mock_server().await;
@@ -780,7 +750,6 @@ mod tests {
Mock::given(method("GET"))
.and(path("/chunked-download"))
.and(header("user-agent", "donutbrowser"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(test_content.clone())
@@ -817,3 +786,8 @@ mod tests {
assert_eq!(downloaded_content.len(), test_content.len());
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref DOWNLOADER: Downloader = Downloader::new();
}
+399 -125
View File
@@ -3,43 +3,51 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DownloadedBrowserInfo {
pub browser: String,
pub version: String,
pub download_date: u64,
pub file_path: PathBuf,
pub verified: bool,
pub actual_version: Option<String>, // For browsers like Chromium where we track the actual version
pub file_size: Option<u64>, // For tracking file size changes (useful for rolling releases)
#[serde(default)] // Add default value (false) for backwards compatibility
pub is_rolling_release: bool, // True for Zen's twilight releases and other rolling releases
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct DownloadedBrowsersRegistry {
struct RegistryData {
pub browsers: HashMap<String, HashMap<String, DownloadedBrowserInfo>>, // browser -> version -> info
}
pub struct DownloadedBrowsersRegistry {
data: Mutex<RegistryData>,
}
impl DownloadedBrowsersRegistry {
pub fn new() -> Self {
Self::default()
fn new() -> Self {
Self {
data: Mutex::new(RegistryData::default()),
}
}
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
pub fn instance() -> &'static DownloadedBrowsersRegistry {
&DOWNLOADED_BROWSERS_REGISTRY
}
pub fn load(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let registry_path = Self::get_registry_path()?;
if !registry_path.exists() {
return Ok(Self::new());
return Ok(());
}
let content = fs::read_to_string(&registry_path)?;
let registry: DownloadedBrowsersRegistry = serde_json::from_str(&content)?;
Ok(registry)
let registry_data: RegistryData = serde_json::from_str(&content)?;
let mut data = self.data.lock().unwrap();
*data = registry_data;
Ok(())
}
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
pub fn save(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let registry_path = Self::get_registry_path()?;
// Ensure parent directory exists
@@ -47,12 +55,13 @@ impl DownloadedBrowsersRegistry {
fs::create_dir_all(parent)?;
}
let content = serde_json::to_string_pretty(self)?;
let data = self.data.lock().unwrap();
let content = serde_json::to_string_pretty(&*data)?;
fs::write(&registry_path, content)?;
Ok(())
}
fn get_registry_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
fn get_registry_path() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let mut path = base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
@@ -65,116 +74,389 @@ impl DownloadedBrowsersRegistry {
Ok(path)
}
pub fn add_browser(&mut self, info: DownloadedBrowserInfo) {
self
pub fn add_browser(&self, info: DownloadedBrowserInfo) {
let mut data = self.data.lock().unwrap();
data
.browsers
.entry(info.browser.clone())
.or_default()
.insert(info.version.clone(), info);
}
pub fn remove_browser(&mut self, browser: &str, version: &str) -> Option<DownloadedBrowserInfo> {
self.browsers.get_mut(browser)?.remove(version)
pub fn remove_browser(&self, browser: &str, version: &str) -> Option<DownloadedBrowserInfo> {
let mut data = self.data.lock().unwrap();
data.browsers.get_mut(browser)?.remove(version)
}
pub fn is_browser_downloaded(&self, browser: &str, version: &str) -> bool {
self
let data = self.data.lock().unwrap();
data
.browsers
.get(browser)
.and_then(|versions| versions.get(version))
.map(|info| info.verified)
.unwrap_or(false)
.is_some()
}
pub fn get_downloaded_versions(&self, browser: &str) -> Vec<String> {
self
let data = self.data.lock().unwrap();
data
.browsers
.get(browser)
.map(|versions| {
versions
.iter()
.filter(|(_, info)| info.verified)
.map(|(version, _)| version.clone())
.collect()
})
.map(|versions| versions.keys().cloned().collect())
.unwrap_or_default()
}
pub fn mark_download_started(&mut self, browser: &str, version: &str, file_path: PathBuf) {
let is_rolling = Self::is_rolling_release(browser, version);
pub fn mark_download_started(&self, browser: &str, version: &str, file_path: PathBuf) {
let info = DownloadedBrowserInfo {
browser: browser.to_string(),
version: version.to_string(),
download_date: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
file_path,
verified: false,
actual_version: None,
file_size: None,
is_rolling_release: is_rolling,
};
self.add_browser(info);
}
pub fn mark_download_completed_with_actual_version(
&mut self,
browser: &str,
version: &str,
actual_version: Option<String>,
) -> Result<(), String> {
if let Some(info) = self
pub fn mark_download_completed(&self, browser: &str, version: &str) -> Result<(), String> {
let data = self.data.lock().unwrap();
if data
.browsers
.get_mut(browser)
.and_then(|versions| versions.get_mut(version))
.get(browser)
.and_then(|versions| versions.get(version))
.is_some()
{
info.verified = true;
info.actual_version = actual_version;
Ok(())
} else {
Err(format!("Browser {browser}:{version} not found in registry"))
}
}
fn is_rolling_release(browser: &str, version: &str) -> bool {
// Check if this is a rolling release like twilight
browser == "zen" && version.to_lowercase() == "twilight"
}
pub fn cleanup_failed_download(
&mut self,
&self,
browser: &str,
version: &str,
) -> Result<(), Box<dyn std::error::Error>> {
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Some(info) = self.remove_browser(browser, version) {
// Clean up any files that might have been left behind
// Clean up extracted binaries but preserve downloaded archives
if info.file_path.exists() {
if info.file_path.is_dir() {
fs::remove_dir_all(&info.file_path)?;
// Allowed archive extensions to preserve
let archive_exts = [
"zip", "dmg", "tar.xz", "tar.gz", "tar.bz2", "AppImage", "exe", "pkg", "msi",
];
for entry in fs::read_dir(&info.file_path)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
fs::remove_dir_all(&path)?;
continue;
}
// For files, preserve if they look like downloaded archives/installers
let keep = path
.file_name()
.and_then(|n| n.to_str())
.map(|name| {
// Match suffixes (handles multi-part extensions like .tar.xz)
archive_exts
.iter()
.any(|ext| name.to_lowercase().ends_with(&ext.to_lowercase()))
})
.unwrap_or(false);
if !keep {
fs::remove_file(&path)?;
}
}
} else {
fs::remove_file(&info.file_path)?;
// It's a file. If it's not an archive, remove it; otherwise preserve it.
let file_name = info
.file_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
let archive_exts = [
"zip", "dmg", "tar.xz", "tar.gz", "tar.bz2", "AppImage", "exe", "pkg", "msi",
];
let is_archive = archive_exts
.iter()
.any(|ext| file_name.to_lowercase().ends_with(&ext.to_lowercase()));
if !is_archive {
fs::remove_file(&info.file_path)?;
}
}
}
// Also clean up the browser directory if it exists
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let mut browser_dir = base_dirs.data_local_dir().to_path_buf();
browser_dir.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
browser_dir.push("binaries");
browser_dir.push(browser);
browser_dir.push(version);
if browser_dir.exists() {
fs::remove_dir_all(&browser_dir)?;
}
}
Ok(())
}
/// Find and remove unused browser binaries that are not referenced by any active profiles
pub fn cleanup_unused_binaries(
&self,
active_profiles: &[(String, String)], // (browser, version) pairs
running_profiles: &[(String, String)], // (browser, version) pairs for running profiles
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let active_set: std::collections::HashSet<(String, String)> =
active_profiles.iter().cloned().collect();
let running_set: std::collections::HashSet<(String, String)> =
running_profiles.iter().cloned().collect();
let mut cleaned_up = Vec::new();
// Collect all downloaded browsers that are not in active profiles
let mut to_remove = Vec::new();
{
let data = self.data.lock().unwrap();
for (browser, versions) in &data.browsers {
for version in versions.keys() {
let browser_version = (browser.clone(), version.clone());
// Don't remove if it's used by any active profile
if active_set.contains(&browser_version) {
println!("Keeping: {browser} {version} (in use by profile)");
continue;
}
// Don't remove if it's currently running (even if not in active profiles)
if running_set.contains(&browser_version) {
println!("Keeping: {browser} {version} (currently running)");
continue;
}
// Mark for removal
to_remove.push(browser_version);
println!("Marking for removal: {browser} {version} (not used by any profile)");
}
}
}
// Remove unused binaries
for (browser, version) in to_remove {
if let Err(e) = self.cleanup_failed_download(&browser, &version) {
eprintln!("Failed to cleanup unused binary {browser}:{version}: {e}");
} else {
cleaned_up.push(format!("{browser} {version}"));
println!("Successfully removed unused binary: {browser} {version}");
}
}
if cleaned_up.is_empty() {
println!("No unused binaries found to clean up");
} else {
println!("Cleaned up {} unused binaries", cleaned_up.len());
}
Ok(cleaned_up)
}
/// Get all browsers and versions referenced by active profiles
pub fn get_active_browser_versions(
&self,
profiles: &[crate::profile::BrowserProfile],
) -> Vec<(String, String)> {
profiles
.iter()
.map(|profile| (profile.browser.clone(), profile.version.clone()))
.collect()
}
/// 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 browsers_to_check: Vec<(String, String)> = {
let data = self.data.lock().unwrap();
data
.browsers
.iter()
.flat_map(|(browser, versions)| {
versions
.keys()
.map(|version| (browser.clone(), version.clone()))
})
.collect()
};
for (browser_str, version) in browsers_to_check {
if let Ok(browser_type) = BrowserType::from_str(&browser_str) {
let browser = create_browser(browser_type);
if !browser.is_version_downloaded(&version, &binaries_dir) {
// Files don't exist, remove from registry
if let Some(_removed) = self.remove_browser(&browser_str, &version) {
cleaned_up.push(format!("{browser_str} {version}"));
println!("Removed stale registry entry for {browser_str} {version}");
}
}
}
}
if !cleaned_up.is_empty() {
self.save()?;
}
Ok(cleaned_up)
}
/// Get all browsers and versions that are currently running
pub fn get_running_browser_versions(
&self,
profiles: &[crate::profile::BrowserProfile],
) -> Vec<(String, String)> {
profiles
.iter()
.filter(|profile| profile.process_id.is_some())
.map(|profile| (profile.browser.clone(), profile.version.clone()))
.collect()
}
/// Scan the binaries directory and sync with registry
/// This ensures the registry reflects what's actually on disk
pub fn sync_with_binaries_directory(
&self,
binaries_dir: &std::path::Path,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let mut changes = Vec::new();
if !binaries_dir.exists() {
return Ok(changes);
}
// Scan for actual browser directories
for browser_entry in fs::read_dir(binaries_dir)? {
let browser_entry = browser_entry?;
let browser_path = browser_entry.path();
if !browser_path.is_dir() {
continue;
}
let browser_name = browser_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if browser_name.is_empty() || browser_name.starts_with('.') {
continue;
}
// Scan for version directories within this browser
for version_entry in fs::read_dir(&browser_path)? {
let version_entry = version_entry?;
let version_path = version_entry.path();
if !version_path.is_dir() {
continue;
}
let version_name = version_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if version_name.is_empty() || version_name.starts_with('.') {
continue;
}
// Only add to registry if this looks like a valid installed browser, not just an archive
if !self.is_browser_downloaded(browser_name, version_name) {
if let Ok(browser_type) = crate::browser::BrowserType::from_str(browser_name) {
let browser = crate::browser::create_browser(browser_type);
if browser.is_version_downloaded(version_name, binaries_dir) {
let info = DownloadedBrowserInfo {
browser: browser_name.to_string(),
version: version_name.to_string(),
file_path: version_path.clone(),
};
self.add_browser(info);
changes.push(format!("Added {browser_name} {version_name} to registry"));
}
}
}
}
}
if !changes.is_empty() {
self.save()?;
}
Ok(changes)
}
/// Comprehensive cleanup that removes unused binaries and syncs registry
pub fn comprehensive_cleanup(
&self,
binaries_dir: &std::path::Path,
active_profiles: &[(String, String)],
running_profiles: &[(String, String)],
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let mut cleanup_results = Vec::new();
// First, sync registry with actual binaries on disk
let sync_results = self.sync_with_binaries_directory(binaries_dir)?;
cleanup_results.extend(sync_results);
// Then perform the regular cleanup
let regular_cleanup = self.cleanup_unused_binaries(active_profiles, running_profiles)?;
cleanup_results.extend(regular_cleanup);
// Finally, verify and cleanup stale entries
let stale_cleanup = self.verify_and_cleanup_stale_entries_simple(binaries_dir)?;
cleanup_results.extend(stale_cleanup);
if !cleanup_results.is_empty() {
self.save()?;
}
Ok(cleanup_results)
}
/// Simplified version of verify_and_cleanup_stale_entries that doesn't need BrowserRunner
pub fn verify_and_cleanup_stale_entries_simple(
&self,
binaries_dir: &std::path::Path,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let mut cleaned_up = Vec::new();
let mut browsers_to_remove = Vec::new();
{
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()));
}
}
}
}
// 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}"
));
}
}
Ok(cleaned_up)
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref DOWNLOADED_BROWSERS_REGISTRY: DownloadedBrowsersRegistry = {
let registry = DownloadedBrowsersRegistry::new();
if let Err(e) = registry.load() {
eprintln!("Warning: Failed to load downloaded browsers registry: {e}");
}
registry
};
}
#[cfg(test)]
@@ -184,21 +466,17 @@ mod tests {
#[test]
fn test_registry_creation() {
let registry = DownloadedBrowsersRegistry::new();
assert!(registry.browsers.is_empty());
let data = registry.data.lock().unwrap();
assert!(data.browsers.is_empty());
}
#[test]
fn test_add_and_get_browser() {
let mut registry = DownloadedBrowsersRegistry::new();
let registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path"),
verified: true,
actual_version: None,
file_size: None,
is_rolling_release: false,
};
registry.add_browser(info.clone());
@@ -210,39 +488,24 @@ mod tests {
#[test]
fn test_get_downloaded_versions() {
let mut registry = DownloadedBrowsersRegistry::new();
let registry = DownloadedBrowsersRegistry::new();
let info1 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path1"),
verified: true,
actual_version: None,
file_size: None,
is_rolling_release: false,
};
let info2 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "140.0".to_string(),
download_date: 1234567891,
file_path: PathBuf::from("/test/path2"),
verified: false, // Not verified, should not be included
actual_version: None,
file_size: None,
is_rolling_release: false,
};
let info3 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "141.0".to_string(),
download_date: 1234567892,
file_path: PathBuf::from("/test/path3"),
verified: true,
actual_version: None,
file_size: None,
is_rolling_release: false,
};
registry.add_browser(info1);
@@ -250,63 +513,74 @@ mod tests {
registry.add_browser(info3);
let versions = registry.get_downloaded_versions("firefox");
assert_eq!(versions.len(), 2);
assert_eq!(versions.len(), 3);
assert!(versions.contains(&"139.0".to_string()));
assert!(versions.contains(&"140.0".to_string()));
assert!(versions.contains(&"141.0".to_string()));
assert!(!versions.contains(&"140.0".to_string()));
}
#[test]
fn test_mark_download_lifecycle() {
let mut registry = DownloadedBrowsersRegistry::new();
let registry = DownloadedBrowsersRegistry::new();
// Mark download started
registry.mark_download_started("firefox", "139.0", PathBuf::from("/test/path"));
// Should not be considered downloaded yet
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
// Should be considered downloaded immediately
assert!(
registry.is_browser_downloaded("firefox", "139.0"),
"Browser should be considered downloaded after marking as started"
);
// Mark as completed
registry
.mark_download_completed_with_actual_version("firefox", "139.0", Some("139.0".to_string()))
.unwrap();
.mark_download_completed("firefox", "139.0")
.expect("Failed to mark download as completed");
// Now should be considered downloaded
assert!(registry.is_browser_downloaded("firefox", "139.0"));
// Should still be considered downloaded
assert!(
registry.is_browser_downloaded("firefox", "139.0"),
"Browser should still be considered downloaded after completion"
);
}
#[test]
fn test_remove_browser() {
let mut registry = DownloadedBrowsersRegistry::new();
let registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path"),
verified: true,
actual_version: None,
file_size: None,
is_rolling_release: false,
};
registry.add_browser(info);
assert!(registry.is_browser_downloaded("firefox", "139.0"));
assert!(
registry.is_browser_downloaded("firefox", "139.0"),
"Browser should be downloaded after adding"
);
let removed = registry.remove_browser("firefox", "139.0");
assert!(removed.is_some());
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
assert!(
removed.is_some(),
"Remove operation should return the removed browser info"
);
assert!(
!registry.is_browser_downloaded("firefox", "139.0"),
"Browser should not be downloaded after removal"
);
}
#[test]
fn test_twilight_rolling_release() {
let mut registry = DownloadedBrowsersRegistry::new();
fn test_twilight_download() {
let registry = DownloadedBrowsersRegistry::new();
// Mark twilight download started
registry.mark_download_started("zen", "twilight", PathBuf::from("/test/zen-twilight"));
// Check that it's marked as rolling release
let zen_versions = &registry.browsers["zen"];
let twilight_info = &zen_versions["twilight"];
assert!(twilight_info.is_rolling_release);
// Check that it's registered
assert!(
registry.is_browser_downloaded("zen", "twilight"),
"Zen twilight version should be registered as downloaded"
);
}
}
+1579 -202
View File
File diff suppressed because it is too large Load Diff
+360
View File
@@ -0,0 +1,360 @@
use crate::browser::GithubRelease;
use directories::BaseDirs;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tauri::Emitter;
use tokio::fs;
use tokio::io::AsyncWriteExt;
const MMDB_REPO: &str = "P3TERX/GeoLite.mmdb";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeoIPDownloadProgress {
pub stage: String, // "downloading", "extracting", "completed"
pub percentage: f64,
pub message: String,
// Extra fields to mirror browser download progress payload
pub downloaded_bytes: Option<u64>,
pub total_bytes: Option<u64>,
pub speed_bytes_per_sec: Option<f64>,
pub eta_seconds: Option<f64>,
}
pub struct GeoIPDownloader {
client: Client,
}
impl GeoIPDownloader {
fn new() -> Self {
Self {
client: Client::new(),
}
}
pub fn instance() -> &'static GeoIPDownloader {
&GEOIP_DOWNLOADER
}
/// Create a new downloader with custom client (for testing)
#[cfg(test)]
pub fn new_with_client(client: Client) -> Self {
Self { client }
}
fn get_cache_dir() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let base_dirs = BaseDirs::new().ok_or("Failed to determine base directories")?;
#[cfg(target_os = "windows")]
let cache_dir = base_dirs
.data_local_dir()
.join("camoufox")
.join("camoufox")
.join("Cache");
#[cfg(target_os = "macos")]
let cache_dir = base_dirs.cache_dir().join("camoufox");
#[cfg(target_os = "linux")]
let cache_dir = base_dirs.cache_dir().join("camoufox");
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
let cache_dir = base_dirs.cache_dir().join("camoufox");
Ok(cache_dir)
}
fn get_mmdb_file_path() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
Ok(Self::get_cache_dir()?.join("GeoLite2-City.mmdb"))
}
pub fn is_geoip_database_available() -> bool {
if let Ok(mmdb_path) = Self::get_mmdb_file_path() {
mmdb_path.exists()
} else {
false
}
}
fn find_city_mmdb_asset(&self, release: &GithubRelease) -> Option<String> {
for asset in &release.assets {
if asset.name.ends_with("-City.mmdb") {
return Some(asset.browser_download_url.clone());
}
}
None
}
pub async fn download_geoip_database(
&self,
app_handle: &tauri::AppHandle,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Emit initial progress
let _ = app_handle.emit(
"geoip-download-progress",
GeoIPDownloadProgress {
stage: "downloading".to_string(),
percentage: 0.0,
message: "Starting GeoIP database download".to_string(),
downloaded_bytes: Some(0),
total_bytes: None,
speed_bytes_per_sec: Some(0.0),
eta_seconds: None,
},
);
// Fetch latest release from GitHub
let releases = self.fetch_geoip_releases().await?;
let latest_release = releases.first().ok_or("No GeoIP database releases found")?;
let download_url = self
.find_city_mmdb_asset(latest_release)
.ok_or("No compatible GeoIP database asset found")?;
// Create cache directory
let cache_dir = Self::get_cache_dir()?;
fs::create_dir_all(&cache_dir).await?;
let mmdb_path = Self::get_mmdb_file_path()?;
// Download the file
let response = self.client.get(&download_url).send().await?;
if !response.status().is_success() {
return Err(
format!(
"Failed to download GeoIP database: HTTP {}",
response.status()
)
.into(),
);
}
let total_size = response.content_length().unwrap_or(0);
let mut downloaded: u64 = 0;
let mut file = fs::File::create(&mmdb_path).await?;
let mut stream = response.bytes_stream();
use futures_util::StreamExt;
use std::time::Instant;
let start_time = Instant::now();
let mut last_update = Instant::now();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
downloaded += chunk.len() as u64;
file.write_all(&chunk).await?;
let now = Instant::now();
if now.duration_since(last_update).as_millis() >= 100 {
let elapsed = start_time.elapsed().as_secs_f64();
let speed = if elapsed > 0.0 {
downloaded as f64 / elapsed
} else {
0.0
};
let percentage = if total_size > 0 {
(downloaded as f64 / total_size as f64) * 100.0
} else {
0.0
};
let eta = if speed > 0.0 && total_size > 0 {
Some((total_size.saturating_sub(downloaded)) as f64 / speed)
} else {
None
};
let _ = app_handle.emit(
"geoip-download-progress",
GeoIPDownloadProgress {
stage: "downloading".to_string(),
percentage,
message: format!("Downloaded {downloaded} / {total_size} bytes"),
downloaded_bytes: Some(downloaded),
total_bytes: Some(total_size),
speed_bytes_per_sec: Some(speed),
eta_seconds: eta,
},
);
last_update = now;
}
}
file.flush().await?;
// Emit completion
let _ = app_handle.emit(
"geoip-download-progress",
GeoIPDownloadProgress {
stage: "completed".to_string(),
percentage: 100.0,
message: "GeoIP database download completed".to_string(),
downloaded_bytes: Some(downloaded),
total_bytes: Some(total_size),
speed_bytes_per_sec: Some(0.0),
eta_seconds: Some(0.0),
},
);
Ok(())
}
async fn fetch_geoip_releases(
&self,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://api.github.com/repos/{MMDB_REPO}/releases");
let response = self
.client
.get(&url)
.header("User-Agent", "Mozilla/5.0 (compatible; donutbrowser)")
.send()
.await?;
if !response.status().is_success() {
return Err(format!("Failed to fetch releases: HTTP {}", response.status()).into());
}
let releases: Vec<GithubRelease> = response.json().await?;
Ok(releases)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::browser::GithubRelease;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn create_mock_release() -> GithubRelease {
GithubRelease {
tag_name: "v1.0.0".to_string(),
name: "Test Release".to_string(),
body: Some("Test release body".to_string()),
published_at: "2023-01-01T00:00:00Z".to_string(),
created_at: Some("2023-01-01T00:00:00Z".to_string()),
html_url: Some("https://example.com/release".to_string()),
tarball_url: Some("https://example.com/tarball".to_string()),
zipball_url: Some("https://example.com/zipball".to_string()),
draft: false,
prerelease: false,
is_nightly: false,
id: Some(1),
node_id: Some("test_node_id".to_string()),
target_commitish: None,
assets: vec![crate::browser::GithubAsset {
id: Some(1),
node_id: Some("test_asset_node_id".to_string()),
name: "GeoLite2-City.mmdb".to_string(),
label: None,
content_type: Some("application/octet-stream".to_string()),
state: Some("uploaded".to_string()),
size: 1024,
download_count: Some(0),
created_at: Some("2023-01-01T00:00:00Z".to_string()),
updated_at: Some("2023-01-01T00:00:00Z".to_string()),
browser_download_url: "https://example.com/GeoLite2-City.mmdb".to_string(),
}],
}
}
#[tokio::test]
async fn test_fetch_geoip_releases_success() {
let mock_server = MockServer::start().await;
let releases = vec![create_mock_release()];
Mock::given(method("GET"))
.and(path(format!("/repos/{MMDB_REPO}/releases")))
.respond_with(ResponseTemplate::new(200).set_body_json(&releases))
.mount(&mock_server)
.await;
let client = Client::builder()
.build()
.expect("Failed to create HTTP client");
let downloader = GeoIPDownloader::new_with_client(client);
// Override the URL for testing
let url = format!("{}/repos/{}/releases", mock_server.uri(), MMDB_REPO);
let response = downloader
.client
.get(&url)
.header("User-Agent", "Mozilla/5.0 (compatible; donutbrowser)")
.send()
.await
.expect("Request should succeed");
assert!(response.status().is_success());
let fetched_releases: Vec<GithubRelease> = response.json().await.expect("Should parse JSON");
assert_eq!(fetched_releases.len(), 1);
assert_eq!(fetched_releases[0].tag_name, "v1.0.0");
}
#[tokio::test]
async fn test_find_city_mmdb_asset() {
let downloader = GeoIPDownloader::new();
let release = create_mock_release();
let asset_url = downloader.find_city_mmdb_asset(&release);
assert!(asset_url.is_some());
assert_eq!(asset_url.unwrap(), "https://example.com/GeoLite2-City.mmdb");
}
#[tokio::test]
async fn test_find_city_mmdb_asset_not_found() {
let downloader = GeoIPDownloader::new();
let mut release = create_mock_release();
release.assets[0].name = "wrong-file.txt".to_string();
let asset_url = downloader.find_city_mmdb_asset(&release);
assert!(asset_url.is_none());
}
#[test]
fn test_get_cache_dir() {
let cache_dir = GeoIPDownloader::get_cache_dir();
assert!(cache_dir.is_ok());
let path = cache_dir.unwrap();
assert!(path.to_string_lossy().contains("camoufox"));
}
#[test]
fn test_get_mmdb_file_path() {
let mmdb_path = GeoIPDownloader::get_mmdb_file_path();
assert!(mmdb_path.is_ok());
let path = mmdb_path.unwrap();
assert!(path.to_string_lossy().ends_with("GeoLite2-City.mmdb"));
}
#[test]
fn test_is_geoip_database_available() {
// Test that the function works correctly regardless of file system state
let is_available = GeoIPDownloader::is_geoip_database_available();
// The function should return a boolean value (either true or false)
// The function should return a boolean value - we just verify it doesn't panic
// and returns the expected result based on file existence
// Verify the function logic by checking if the path resolution works
let mmdb_path_result = GeoIPDownloader::get_mmdb_file_path();
assert!(
mmdb_path_result.is_ok(),
"Should be able to get MMDB file path"
);
let mmdb_path = mmdb_path_result.unwrap();
let expected_available = mmdb_path.exists();
assert_eq!(
is_available, expected_available,
"Function result should match actual file existence"
);
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref GEOIP_DOWNLOADER: GeoIPDownloader = GeoIPDownloader::new();
}
+534
View File
@@ -0,0 +1,534 @@
use directories::BaseDirs;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileGroup {
pub id: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupWithCount {
pub id: String,
pub name: String,
pub count: usize,
}
#[derive(Debug, Serialize, Deserialize)]
struct GroupsData {
groups: Vec<ProfileGroup>,
}
pub struct GroupManager {
base_dirs: BaseDirs,
data_dir_override: Option<PathBuf>,
}
impl GroupManager {
pub fn new() -> Self {
Self {
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
data_dir_override: std::env::var("DONUTBROWSER_DATA_DIR")
.ok()
.map(PathBuf::from),
}
}
// Helper for tests to override data directory without global env var
#[allow(dead_code)]
pub fn with_data_dir_override(dir: &Path) -> Self {
Self {
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
data_dir_override: Some(dir.to_path_buf()),
}
}
fn get_groups_file_path(&self) -> PathBuf {
if let Some(dir) = &self.data_dir_override {
let mut override_path = dir.clone();
// Ensure the directory exists before returning the path
let _ = fs::create_dir_all(&override_path);
override_path.push("groups.json");
return override_path;
}
let mut path = self.base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
path.push("data");
path.push("groups.json");
path
}
fn load_groups_data(&self) -> Result<GroupsData, Box<dyn std::error::Error>> {
let groups_file = self.get_groups_file_path();
if !groups_file.exists() {
return Ok(GroupsData { groups: Vec::new() });
}
let content = fs::read_to_string(groups_file)?;
let groups_data: GroupsData = serde_json::from_str(&content)?;
Ok(groups_data)
}
fn save_groups_data(&self, groups_data: &GroupsData) -> Result<(), Box<dyn std::error::Error>> {
let groups_file = self.get_groups_file_path();
// Ensure the parent directory exists
if let Some(parent) = groups_file.parent() {
fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(groups_data)?;
fs::write(groups_file, json)?;
Ok(())
}
pub fn get_all_groups(&self) -> Result<Vec<ProfileGroup>, Box<dyn std::error::Error>> {
let groups_data = self.load_groups_data()?;
Ok(groups_data.groups)
}
pub fn create_group(&self, name: String) -> Result<ProfileGroup, Box<dyn std::error::Error>> {
let mut groups_data = self.load_groups_data()?;
// Check if group with this name already exists
if groups_data.groups.iter().any(|g| g.name == name) {
return Err(format!("Group with name '{name}' already exists").into());
}
let group = ProfileGroup {
id: uuid::Uuid::new_v4().to_string(),
name,
};
groups_data.groups.push(group.clone());
self.save_groups_data(&groups_data)?;
Ok(group)
}
pub fn update_group(
&self,
id: String,
name: String,
) -> Result<ProfileGroup, Box<dyn std::error::Error>> {
let mut groups_data = self.load_groups_data()?;
// Check if another group with this name already exists
if groups_data
.groups
.iter()
.any(|g| g.name == name && g.id != id)
{
return Err(format!("Group with name '{name}' already exists").into());
}
let group = groups_data
.groups
.iter_mut()
.find(|g| g.id == id)
.ok_or_else(|| format!("Group with id '{id}' not found"))?;
group.name = name;
let updated_group = group.clone();
self.save_groups_data(&groups_data)?;
Ok(updated_group)
}
pub fn delete_group(&self, id: String) -> Result<(), Box<dyn std::error::Error>> {
let mut groups_data = self.load_groups_data()?;
let initial_len = groups_data.groups.len();
groups_data.groups.retain(|g| g.id != id);
if groups_data.groups.len() == initial_len {
return Err(format!("Group with id '{id}' not found").into());
}
self.save_groups_data(&groups_data)?;
Ok(())
}
pub fn get_groups_with_profile_counts(
&self,
profiles: &[crate::profile::BrowserProfile],
) -> Result<Vec<GroupWithCount>, Box<dyn std::error::Error>> {
let groups = self.get_all_groups()?;
let mut group_counts = HashMap::new();
// Count profiles in each group
for profile in profiles {
if let Some(group_id) = &profile.group_id {
*group_counts.entry(group_id.clone()).or_insert(0) += 1;
}
}
// Create result including all groups (even those with 0 count)
let mut result = Vec::new();
for group in groups {
let count = group_counts.get(&group.id).copied().unwrap_or(0);
result.push(GroupWithCount {
id: group.id,
name: group.name,
count,
});
}
// Add default group count (profiles without group_id), always include even if 0
let default_count = profiles.iter().filter(|p| p.group_id.is_none()).count();
let default_group = GroupWithCount {
id: "default".to_string(),
name: "Default".to_string(),
count: default_count,
};
// Insert at the beginning for consistent ordering with UI expectations
result.insert(0, default_group);
Ok(result)
}
}
// Global instance
lazy_static::lazy_static! {
pub static ref GROUP_MANAGER: Mutex<GroupManager> = Mutex::new(GroupManager::new());
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use tempfile::TempDir;
fn create_test_group_manager() -> (GroupManager, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
// Set up a temporary home directory for testing
env::set_var("HOME", temp_dir.path());
// Use per-test isolated data directory without relying on global env vars
let data_override = temp_dir.path().join("donutbrowser_test_data");
let manager = GroupManager::with_data_dir_override(&data_override);
(manager, temp_dir)
}
#[test]
fn test_group_manager_creation() {
let (_manager, _temp_dir) = create_test_group_manager();
// Test passes if no panic occurs
}
#[test]
fn test_create_and_get_groups() {
let (manager, _temp_dir) = create_test_group_manager();
// Initially should have no groups
let groups = manager
.get_all_groups()
.expect("Should be able to get groups");
assert!(groups.is_empty(), "Should start with no groups");
// Create a group
let group_name = "Test Group".to_string();
let created_group = manager
.create_group(group_name.clone())
.expect("Should create group successfully");
assert_eq!(
created_group.name, group_name,
"Created group should have correct name"
);
assert!(
!created_group.id.is_empty(),
"Created group should have an ID"
);
// Verify group was saved
let groups = manager
.get_all_groups()
.expect("Should be able to get groups");
assert_eq!(groups.len(), 1, "Should have one group");
assert_eq!(
groups[0].name, group_name,
"Retrieved group should have correct name"
);
assert_eq!(
groups[0].id, created_group.id,
"Retrieved group should have correct ID"
);
}
#[test]
fn test_create_duplicate_group_fails() {
let (manager, _temp_dir) = create_test_group_manager();
let group_name = "Duplicate Group".to_string();
// Create first group
let _first_group = manager
.create_group(group_name.clone())
.expect("Should create first group");
// Try to create duplicate group
let result = manager.create_group(group_name.clone());
assert!(result.is_err(), "Should fail to create duplicate group");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("already exists"),
"Error should mention group already exists"
);
}
#[test]
fn test_update_group() {
let (manager, _temp_dir) = create_test_group_manager();
// Create a group
let original_name = "Original Name".to_string();
let created_group = manager
.create_group(original_name)
.expect("Should create group");
// Update the group
let new_name = "Updated Name".to_string();
let updated_group = manager
.update_group(created_group.id.clone(), new_name.clone())
.expect("Should update group successfully");
assert_eq!(
updated_group.name, new_name,
"Updated group should have new name"
);
assert_eq!(
updated_group.id, created_group.id,
"Updated group should keep same ID"
);
// Verify update was persisted
let groups = manager.get_all_groups().expect("Should get groups");
assert_eq!(groups.len(), 1, "Should still have one group");
assert_eq!(
groups[0].name, new_name,
"Persisted group should have updated name"
);
}
#[test]
fn test_update_nonexistent_group_fails() {
let (manager, _temp_dir) = create_test_group_manager();
let result = manager.update_group("nonexistent-id".to_string(), "New Name".to_string());
assert!(result.is_err(), "Should fail to update nonexistent group");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("not found"),
"Error should mention group not found"
);
}
#[test]
fn test_delete_group() {
let (manager, _temp_dir) = create_test_group_manager();
// Create a group
let group_name = "To Delete".to_string();
let created_group = manager
.create_group(group_name)
.expect("Should create group");
// Verify group exists
let groups = manager.get_all_groups().expect("Should get groups");
assert_eq!(groups.len(), 1, "Should have one group");
// Delete the group
manager
.delete_group(created_group.id)
.expect("Should delete group successfully");
// Verify group was deleted
let groups = manager.get_all_groups().expect("Should get groups");
assert!(groups.is_empty(), "Should have no groups after deletion");
}
#[test]
fn test_delete_nonexistent_group_fails() {
let (manager, _temp_dir) = create_test_group_manager();
let result = manager.delete_group("nonexistent-id".to_string());
assert!(result.is_err(), "Should fail to delete nonexistent group");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("not found"),
"Error should mention group not found"
);
}
#[test]
fn test_get_groups_with_profile_counts() {
let (manager, _temp_dir) = create_test_group_manager();
// Create test groups
let group1 = manager
.create_group("Group 1".to_string())
.expect("Should create group 1");
let _group2 = manager
.create_group("Group 2".to_string())
.expect("Should create group 2");
// Create mock profiles
let profiles = vec![
crate::profile::BrowserProfile {
id: uuid::Uuid::new_v4(),
name: "Profile 1".to_string(),
browser: "firefox".to_string(),
version: "1.0".to_string(),
proxy_id: None,
process_id: None,
last_launch: None,
release_type: "stable".to_string(),
camoufox_config: None,
group_id: Some(group1.id.clone()),
},
crate::profile::BrowserProfile {
id: uuid::Uuid::new_v4(),
name: "Profile 2".to_string(),
browser: "firefox".to_string(),
version: "1.0".to_string(),
proxy_id: None,
process_id: None,
last_launch: None,
release_type: "stable".to_string(),
camoufox_config: None,
group_id: Some(group1.id.clone()),
},
crate::profile::BrowserProfile {
id: uuid::Uuid::new_v4(),
name: "Profile 3".to_string(),
browser: "firefox".to_string(),
version: "1.0".to_string(),
proxy_id: None,
process_id: None,
last_launch: None,
release_type: "stable".to_string(),
camoufox_config: None,
group_id: None, // Default group
},
];
let groups_with_counts = manager
.get_groups_with_profile_counts(&profiles)
.expect("Should get groups with counts");
// Should have default group + group1 + group2 (group2 has 0 profiles but should still appear)
assert_eq!(
groups_with_counts.len(),
3,
"Should include all groups, even those with 0 profiles"
);
// Check default group
let default_group = groups_with_counts
.iter()
.find(|g| g.id == "default")
.expect("Should have default group");
assert_eq!(
default_group.count, 1,
"Default group should have 1 profile"
);
// Check group1
let group1_with_count = groups_with_counts
.iter()
.find(|g| g.id == group1.id)
.expect("Should have group1");
assert_eq!(group1_with_count.count, 2, "Group1 should have 2 profiles");
// Check that group2 exists with 0 profiles
let group2_with_count = groups_with_counts
.iter()
.find(|g| g.name == "Group 2")
.expect("Should have group2 present even with 0 profiles");
assert_eq!(group2_with_count.count, 0, "Group2 should have 0 profiles");
}
}
// Helper function to get groups with counts
pub fn get_groups_with_counts(profiles: &[crate::profile::BrowserProfile]) -> Vec<GroupWithCount> {
let group_manager = GROUP_MANAGER.lock().unwrap();
group_manager
.get_groups_with_profile_counts(profiles)
.unwrap_or_default()
}
// Tauri commands
#[tauri::command]
pub async fn get_profile_groups() -> Result<Vec<ProfileGroup>, String> {
let group_manager = GROUP_MANAGER.lock().unwrap();
group_manager
.get_all_groups()
.map_err(|e| format!("Failed to get profile groups: {e}"))
}
#[tauri::command]
pub async fn get_groups_with_profile_counts() -> Result<Vec<GroupWithCount>, String> {
let profile_manager = crate::profile::ProfileManager::instance();
let profiles = profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
Ok(get_groups_with_counts(&profiles))
}
#[tauri::command]
pub async fn create_profile_group(name: String) -> Result<ProfileGroup, String> {
let group_manager = GROUP_MANAGER.lock().unwrap();
group_manager
.create_group(name)
.map_err(|e| format!("Failed to create group: {e}"))
}
#[tauri::command]
pub async fn update_profile_group(group_id: String, name: String) -> Result<ProfileGroup, String> {
let group_manager = GROUP_MANAGER.lock().unwrap();
group_manager
.update_group(group_id, name)
.map_err(|e| format!("Failed to update group: {e}"))
}
#[tauri::command]
pub async fn delete_profile_group(group_id: String) -> Result<(), String> {
let group_manager = GROUP_MANAGER.lock().unwrap();
group_manager
.delete_group(group_id)
.map_err(|e| format!("Failed to delete group: {e}"))
}
#[tauri::command]
pub async fn assign_profiles_to_group(
profile_names: Vec<String>,
group_id: Option<String>,
) -> Result<(), String> {
let profile_manager = crate::profile::ProfileManager::instance();
profile_manager
.assign_profiles_to_group(profile_names, group_id)
.map_err(|e| format!("Failed to assign profiles to group: {e}"))
}
#[tauri::command]
pub async fn delete_selected_profiles(profile_names: Vec<String>) -> Result<(), String> {
let profile_manager = crate::profile::ProfileManager::instance();
profile_manager
.delete_multiple_profiles(profile_names)
.map_err(|e| format!("Failed to delete profiles: {e}"))
}
+543 -96
View File
@@ -1,7 +1,7 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use std::env;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{Emitter, Manager};
use tauri::{Emitter, Manager, Runtime, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
use tauri_plugin_deep_link::DeepLinkExt;
// Store pending URLs that need to be handled when the window is ready
@@ -12,57 +12,139 @@ mod app_auto_updater;
mod auto_updater;
mod browser;
mod browser_runner;
mod browser_version_service;
mod browser_version_manager;
mod camoufox;
mod default_browser;
mod download;
mod downloaded_browsers;
mod extraction;
mod geoip_downloader;
mod group_manager;
mod platform_browser;
mod profile;
mod profile_importer;
mod proxy_manager;
mod settings_manager;
// mod theme_detector; // removed: theme detection handled in webview via CSS prefers-color-scheme
mod version_updater;
extern crate lazy_static;
use browser_runner::{
check_browser_exists, check_browser_status, create_browser_profile, create_browser_profile_new,
delete_profile, download_browser, fetch_browser_versions, fetch_browser_versions_cached_first,
fetch_browser_versions_detailed, fetch_browser_versions_with_count,
fetch_browser_versions_with_count_cached_first, get_cached_browser_versions_detailed,
get_downloaded_browser_versions, get_saved_mullvad_releases, get_supported_browsers,
is_browser_downloaded, kill_browser_profile, launch_browser_profile, list_browser_profiles,
rename_profile, should_update_browser_cache, update_profile_proxy, update_profile_version,
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_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,
};
use settings_manager::{
disable_default_browser_prompt, get_app_settings, get_table_sorting_settings, save_app_settings,
save_table_sorting_settings, should_show_settings_on_startup,
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,
};
use default_browser::{
is_default_browser, open_url_with_profile, set_as_default_browser, smart_open_url,
};
use default_browser::{is_default_browser, open_url_with_profile, set_as_default_browser};
use version_updater::{
check_version_update_needed, force_version_update_check, get_version_update_status,
get_version_updater, trigger_manual_version_update,
get_version_update_status, get_version_updater, trigger_manual_version_update,
};
use auto_updater::{
check_for_browser_updates, complete_browser_update, complete_browser_update_with_auto_update,
dismiss_update_notification, is_auto_update_download, is_browser_disabled_for_update,
mark_auto_update_download, remove_auto_update_download, start_browser_update,
check_for_browser_updates, complete_browser_update_with_auto_update, dismiss_update_notification,
is_browser_disabled_for_update,
};
use app_auto_updater::{
check_for_app_updates, check_for_app_updates_manual, download_and_install_app_update,
get_app_version_info,
};
use profile_importer::{detect_existing_profiles, import_browser_profile};
// use theme_detector::get_system_theme;
use group_manager::{
assign_profiles_to_group, create_profile_group, delete_profile_group, delete_selected_profiles,
get_groups_with_profile_counts, get_profile_groups, update_profile_group,
};
use geoip_downloader::GeoIPDownloader;
use browser_version_manager::get_browser_release_types;
// Trait to extend WebviewWindow with transparent titlebar functionality
pub trait WindowExt {
#[cfg(target_os = "macos")]
fn set_transparent_titlebar(&self, transparent: bool) -> Result<(), String>;
}
impl<R: Runtime> WindowExt for WebviewWindow<R> {
#[cfg(target_os = "macos")]
fn set_transparent_titlebar(&self, transparent: bool) -> Result<(), String> {
use objc2::rc::Retained;
use objc2_app_kit::{NSWindow, NSWindowStyleMask, NSWindowTitleVisibility};
unsafe {
let ns_window: Retained<NSWindow> =
Retained::retain(self.ns_window().unwrap().cast()).unwrap();
if transparent {
// Hide the title text
ns_window.setTitleVisibility(NSWindowTitleVisibility(2)); // NSWindowTitleHidden
// Make titlebar transparent
ns_window.setTitlebarAppearsTransparent(true);
// Set full size content view
let current_mask = ns_window.styleMask();
let new_mask = NSWindowStyleMask(current_mask.0 | (1 << 15)); // NSFullSizeContentViewWindowMask
ns_window.setStyleMask(new_mask);
} else {
// Show the title text
ns_window.setTitleVisibility(NSWindowTitleVisibility(0)); // NSWindowTitleVisible
// Make titlebar opaque
ns_window.setTitlebarAppearsTransparent(false);
// Remove full size content view
let current_mask = ns_window.styleMask();
let new_mask = NSWindowStyleMask(current_mask.0 & !(1 << 15));
ns_window.setStyleMask(new_mask);
}
}
Ok(())
}
}
#[tauri::command]
fn greet() -> String {
let now = SystemTime::now();
let epoch_ms = now.duration_since(UNIX_EPOCH).unwrap().as_millis();
format!("Hello world from Rust! Current epoch: {epoch_ms}")
async fn warm_up_nodecar(app: tauri::AppHandle) -> Result<(), String> {
use tauri_plugin_shell::ShellExt;
use tokio::time::{timeout, Duration};
let start_time = std::time::Instant::now();
// Use sidecar to execute a fast, harmless command that ensures the binary is loaded
let cmd = app
.shell()
.sidecar("nodecar")
.map_err(|e| format!("Failed to create nodecar sidecar: {e}"))?
.arg("help");
let exec_future = async { cmd.output().await };
match timeout(Duration::from_secs(120), exec_future).await {
Ok(Ok(_output)) => {
let duration = start_time.elapsed();
println!(
"Nodecar warm-up (frontend-triggered) completed in {:.2}s",
duration.as_secs_f64()
);
Ok(())
}
Ok(Err(e)) => Err(format!("Failed to execute nodecar for warm-up: {e}")),
Err(_) => Err("Nodecar warm-up timed out after 120s".to_string()),
}
}
#[tauri::command]
@@ -71,20 +153,16 @@ async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), Strin
// Check if the main window exists and is ready
if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or(false) {
// Window is visible, emit event directly
println!("Main window is visible, emitting show-profile-selector event");
app
.emit("show-profile-selector", url.clone())
.map_err(|e| format!("Failed to emit URL open event: {e}"))?;
let _ = window.show();
let _ = window.set_focus();
} else {
// Window not visible yet - add to pending URLs
println!("Main window not visible, adding URL to pending list");
let mut pending = PENDING_URLS.lock().unwrap();
pending.push(url);
}
println!("Main window exists");
// Try to show and focus the window first
let _ = window.show();
let _ = window.set_focus();
let _ = window.unminimize();
app
.emit("show-profile-selector", url.clone())
.map_err(|e| format!("Failed to emit URL open event: {e}"))?;
} else {
// Window doesn't exist yet - add to pending URLs
println!("Main window doesn't exist, adding URL to pending list");
@@ -96,58 +174,123 @@ async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), Strin
}
#[tauri::command]
async fn check_and_handle_startup_url(app_handle: tauri::AppHandle) -> Result<bool, String> {
let pending_urls = {
let mut pending = PENDING_URLS.lock().unwrap();
let urls = pending.clone();
pending.clear(); // Clear after getting them
urls
};
async fn create_stored_proxy(
name: String,
proxy_settings: crate::browser::ProxySettings,
) -> Result<crate::proxy_manager::StoredProxy, String> {
crate::proxy_manager::PROXY_MANAGER
.create_stored_proxy(name, proxy_settings)
.map_err(|e| format!("Failed to create stored proxy: {e}"))
}
if !pending_urls.is_empty() {
println!(
"Handling {} pending URLs from frontend request",
pending_urls.len()
);
#[tauri::command]
async fn get_stored_proxies() -> Result<Vec<crate::proxy_manager::StoredProxy>, String> {
Ok(crate::proxy_manager::PROXY_MANAGER.get_stored_proxies())
}
for url in pending_urls {
println!("Emitting show-profile-selector event for URL: {url}");
if let Err(e) = app_handle.emit("show-profile-selector", url.clone()) {
eprintln!("Failed to emit URL event: {e}");
return Err(format!("Failed to emit URL event: {e}"));
}
}
#[tauri::command]
async fn update_stored_proxy(
proxy_id: String,
name: Option<String>,
proxy_settings: Option<crate::browser::ProxySettings>,
) -> Result<crate::proxy_manager::StoredProxy, String> {
crate::proxy_manager::PROXY_MANAGER
.update_stored_proxy(&proxy_id, name, proxy_settings)
.map_err(|e| format!("Failed to update stored proxy: {e}"))
}
return Ok(true);
}
#[tauri::command]
async fn delete_stored_proxy(proxy_id: String) -> Result<(), String> {
crate::proxy_manager::PROXY_MANAGER
.delete_stored_proxy(&proxy_id)
.map_err(|e| format!("Failed to delete stored proxy: {e}"))
}
Ok(false)
#[tauri::command]
async fn is_geoip_database_available() -> Result<bool, String> {
Ok(GeoIPDownloader::is_geoip_database_available())
}
#[tauri::command]
async fn download_geoip_database(app_handle: tauri::AppHandle) -> Result<(), String> {
let downloader = GeoIPDownloader::instance();
downloader
.download_geoip_database(&app_handle)
.await
.map_err(|e| format!("Failed to download GeoIP database: {e}"))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let args: Vec<String> = env::args().collect();
let startup_url = args.iter().find(|arg| arg.starts_with("http")).cloned();
if let Some(url) = startup_url.clone() {
println!("Found startup URL in command line: {url}");
let mut pending = PENDING_URLS.lock().unwrap();
pending.push(url.clone());
}
tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|_, args, _cwd| {
println!("Single instance triggered with args: {args:?}");
}))
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_macos_permissions::init())
.setup(|app| {
// Create the main window programmatically
#[allow(unused_variables)]
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.title("Donut Browser")
.inner_size(900.0, 600.0)
.resizable(false)
.fullscreen(false)
.center()
.focused(true)
.visible(true);
#[allow(unused_variables)]
let window = win_builder.build().unwrap();
// Set transparent titlebar for macOS
#[cfg(target_os = "macos")]
{
if let Err(e) = window.set_transparent_titlebar(true) {
eprintln!("Failed to set transparent titlebar: {e}");
}
}
// Set up deep link handler
let handle = app.handle().clone();
#[cfg(any(windows, target_os = "linux"))]
{
// For Windows and Linux, register all deep links at runtime for development
app.deep_link().register_all()?;
if let Err(e) = app.deep_link().register_all() {
eprintln!("Failed to register deep links: {e}");
}
}
#[cfg(target_os = "macos")]
{
// On macOS, try to register deep links for development builds
if let Err(e) = app.deep_link().register_all() {
eprintln!(
"Note: Deep link registration failed on macOS (this is normal for production): {e}"
);
}
}
// Handle deep links - this works for both scenarios:
// 1. App is running and URL is opened
// 2. App is not running and URL causes app to launch
app.deep_link().on_open_url({
let handle = handle.clone();
move |event| {
let urls = event.urls();
println!("Deep link event received with {} URLs", urls.len());
for url in urls {
let url_string = url.to_string();
println!("Deep link received: {url_string}");
@@ -165,27 +308,89 @@ pub fn run() {
}
});
if let Some(startup_url) = startup_url {
let handle_clone = handle.clone();
tauri::async_runtime::spawn(async move {
println!("Processing startup URL from command line: {startup_url}");
if let Err(e) = handle_url_open(handle_clone, startup_url.clone()).await {
eprintln!("Failed to handle startup URL: {e}");
}
});
}
// Initialize and start background version updater
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
let version_updater = get_version_updater();
let mut updater_guard = version_updater.lock().await;
// Set the app handle
updater_guard.set_app_handle(app_handle).await;
{
let mut updater_guard = version_updater.lock().await;
updater_guard.set_app_handle(app_handle);
}
// Start the background updates
updater_guard.start_background_updates().await;
// Run startup check without holding the lock
{
let updater_guard = version_updater.lock().await;
if let Err(e) = updater_guard.start_background_updates().await {
eprintln!("Failed to start background updates: {e}");
}
}
});
// Start the background update task separately
tauri::async_runtime::spawn(async move {
version_updater::VersionUpdater::run_background_task().await;
});
let app_handle_auto_updater = app.handle().clone();
// Start the auto-update check task separately
tauri::async_runtime::spawn(async move {
auto_updater::check_for_updates_with_progress(app_handle_auto_updater).await;
});
// Handle any pending URLs that were received before the window was ready
let handle_pending = handle.clone();
tauri::async_runtime::spawn(async move {
// Wait a bit for the window to be fully ready
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
let pending_urls = {
let mut pending = PENDING_URLS.lock().unwrap();
let urls = pending.clone();
pending.clear();
urls
};
for url in pending_urls {
println!("Processing pending URL: {url}");
if let Err(e) = handle_url_open(handle_pending.clone(), url).await {
eprintln!("Failed to handle pending URL: {e}");
}
}
});
// Start periodic cleanup task for unused binaries
tauri::async_runtime::spawn(async move {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(43200)); // Every 12 hours
loop {
interval.tick().await;
let browser_runner = crate::browser_runner::BrowserRunner::instance();
if let Err(e) = browser_runner.cleanup_unused_binaries_internal() {
eprintln!("Periodic cleanup failed: {e}");
} else {
println!("Periodic cleanup completed successfully");
}
}
});
// Check for app updates at startup
let app_handle_update = app.handle().clone();
tauri::async_runtime::spawn(async move {
// Add a small delay to ensure the app is fully loaded
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
println!("Starting app update check at startup...");
let updater = app_auto_updater::AppAutoUpdater::new();
let updater = app_auto_updater::AppAutoUpdater::instance();
match updater.check_for_updates().await {
Ok(Some(update_info)) => {
println!(
@@ -208,63 +413,305 @@ 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 mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
loop {
interval.tick().await;
match launcher.cleanup_dead_instances().await {
Ok(_dead_instances) => {
// Cleanup completed silently
}
Err(e) => {
eprintln!("Error during Camoufox cleanup: {e}");
}
}
}
});
// Check and download GeoIP database at startup if needed
let app_handle_geoip = app.handle().clone();
tauri::async_runtime::spawn(async move {
// 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() {
Ok(true) => {
println!("GeoIP database is missing for Camoufox profiles, downloading at startup...");
let geoip_downloader = GeoIPDownloader::instance();
if let Err(e) = geoip_downloader
.download_geoip_database(&app_handle_geoip)
.await
{
eprintln!("Failed to download GeoIP database at startup: {e}");
} else {
println!("GeoIP database downloaded successfully at startup");
}
}
Ok(false) => {
// No Camoufox profiles or GeoIP database already available
}
Err(e) => {
eprintln!("Failed to check GeoIP database status at startup: {e}");
}
}
});
// Start proxy cleanup task for dead browser processes
let app_handle_proxy_cleanup = app.handle().clone();
tauri::async_runtime::spawn(async move {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
loop {
interval.tick().await;
match crate::proxy_manager::PROXY_MANAGER
.cleanup_dead_proxies(app_handle_proxy_cleanup.clone())
.await
{
Ok(dead_pids) => {
if !dead_pids.is_empty() {
println!(
"Cleaned up proxies for {} dead browser processes",
dead_pids.len()
);
}
}
Err(e) => {
eprintln!("Error during proxy cleanup: {e}");
}
}
}
});
// Nodecar warm-up is now triggered from the frontend to allow UI blocking overlay
Ok(())
})
.invoke_handler(tauri::generate_handler![
greet,
get_supported_browsers,
is_browser_supported_on_platform,
download_browser,
delete_profile,
is_browser_downloaded,
check_browser_exists,
create_browser_profile_new,
create_browser_profile,
list_browser_profiles,
launch_browser_profile,
fetch_browser_versions,
fetch_browser_versions_detailed,
fetch_browser_versions_with_count,
fetch_browser_versions_cached_first,
fetch_browser_versions_with_count_cached_first,
get_cached_browser_versions_detailed,
should_update_browser_cache,
get_downloaded_browser_versions,
get_saved_mullvad_releases,
get_browser_release_types,
update_profile_proxy,
update_profile_version,
check_browser_status,
kill_browser_profile,
rename_profile,
get_app_settings,
save_app_settings,
should_show_settings_on_startup,
disable_default_browser_prompt,
get_table_sorting_settings,
save_table_sorting_settings,
clear_all_version_cache_and_refetch,
is_default_browser,
open_url_with_profile,
set_as_default_browser,
smart_open_url,
handle_url_open,
check_and_handle_startup_url,
trigger_manual_version_update,
get_version_update_status,
check_version_update_needed,
force_version_update_check,
check_for_browser_updates,
start_browser_update,
complete_browser_update,
is_browser_disabled_for_update,
dismiss_update_notification,
complete_browser_update_with_auto_update,
mark_auto_update_download,
remove_auto_update_download,
is_auto_update_download,
check_for_app_updates,
check_for_app_updates_manual,
download_and_install_app_update,
get_app_version_info,
// get_system_theme, // removed
detect_existing_profiles,
import_browser_profile,
check_missing_binaries,
check_missing_geoip_database,
ensure_all_binaries_exist,
create_stored_proxy,
get_stored_proxies,
update_stored_proxy,
delete_stored_proxy,
update_camoufox_config,
get_profile_groups,
get_groups_with_profile_counts,
create_profile_group,
update_profile_group,
delete_profile_group,
assign_profiles_to_group,
delete_selected_profiles,
is_geoip_database_available,
download_geoip_database,
warm_up_nodecar,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[cfg(test)]
mod tests {
use std::fs;
#[test]
fn test_no_unused_tauri_commands() {
check_unused_commands(false); // Run in strict mode for CI
}
#[test]
fn test_unused_tauri_commands_detailed() {
check_unused_commands(true); // Run in verbose mode for development
}
fn check_unused_commands(verbose: bool) {
// Extract command names from the generate_handler! macro in this file
let lib_rs_content = fs::read_to_string("src/lib.rs").expect("Failed to read lib.rs");
let commands = extract_tauri_commands(&lib_rs_content);
// Get all frontend files
let frontend_files = get_frontend_files("../src");
// Check which commands are actually used
let mut unused_commands = Vec::new();
let mut used_commands = Vec::new();
for command in &commands {
let mut is_used = false;
for file_content in &frontend_files {
// More comprehensive search for command usage
if is_command_used(file_content, command) {
is_used = true;
break;
}
}
if is_used {
used_commands.push(command.clone());
if verbose {
println!("{command}");
}
} else {
unused_commands.push(command.clone());
if verbose {
println!("{command} (UNUSED)");
}
}
}
if verbose {
println!("\n📊 Summary:");
println!(" ✅ Used commands: {}", used_commands.len());
println!(" ❌ Unused commands: {}", unused_commands.len());
}
if !unused_commands.is_empty() {
let message = format!(
"Found {} unused Tauri commands: {}\n\nThese commands are exported in generate_handler! but not used in the frontend.\nConsider removing them or add them to the allowlist if they're used elsewhere.\n\nRun `pnpm check-unused-commands` for detailed analysis.",
unused_commands.len(),
unused_commands.join(", ")
);
if verbose {
println!("\n🚨 {message}");
} else {
panic!("{}", message);
}
} else if verbose {
println!("\n🎉 All exported commands are being used!");
} else {
println!(
"✅ All {} exported Tauri commands are being used in the frontend",
commands.len()
);
}
}
fn is_command_used(content: &str, command: &str) -> bool {
// Check various patterns for invoke usage
let patterns = vec![
format!("invoke<{}>(\"{}\"", "", command), // invoke<Type>("command"
format!("invoke(\"{}\"", command), // invoke("command"
format!("invoke<{}>(\"{}\",", "", command), // invoke<Type>("command",
format!("invoke(\"{}\",", command), // invoke("command",
format!("\"{}\"", command), // Just the command name in quotes
];
for pattern in patterns {
if content.contains(&pattern) {
return true;
}
}
// Also check for the command name appearing after "invoke" within a reasonable distance
if let Some(invoke_pos) = content.find("invoke") {
let after_invoke = &content[invoke_pos..];
if let Some(cmd_pos) = after_invoke.find(&format!("\"{command}\"")) {
// If the command appears within 100 characters of "invoke", consider it used
if cmd_pos < 100 {
return true;
}
}
}
false
}
fn extract_tauri_commands(content: &str) -> Vec<String> {
let mut commands = Vec::new();
// Find the generate_handler! macro
if let Some(start) = content.find("tauri::generate_handler![") {
if let Some(end) = content[start..].find("])") {
let handler_content = &content[start + 25..start + end]; // Skip "tauri::generate_handler!["
// Extract command names
for line in handler_content.lines() {
let line = line.trim();
if !line.is_empty() && !line.starts_with("//") {
// Remove trailing comma and whitespace
let command = line.trim_end_matches(',').trim();
if !command.is_empty() {
commands.push(command.to_string());
}
}
}
}
}
commands
}
fn get_frontend_files(src_dir: &str) -> Vec<String> {
let mut files_content = Vec::new();
if let Ok(entries) = fs::read_dir(src_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
// Recursively read subdirectories
let subdir_files = get_frontend_files(&path.to_string_lossy());
files_content.extend(subdir_files);
} else if let Some(extension) = path.extension() {
if matches!(
extension.to_str(),
Some("ts") | Some("tsx") | Some("js") | Some("jsx")
) {
if let Ok(content) = fs::read_to_string(&path) {
files_content.push(content);
}
}
}
}
}
files_content
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
pub mod manager;
pub mod types;
pub use manager::ProfileManager;
pub use types::BrowserProfile;
+34
View File
@@ -0,0 +1,34 @@
use crate::camoufox::CamoufoxConfig;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrowserProfile {
pub id: uuid::Uuid,
pub name: String,
pub browser: String,
pub version: String,
#[serde(default)]
pub proxy_id: Option<String>, // Reference to stored proxy
#[serde(default)]
pub process_id: Option<u32>,
#[serde(default)]
pub last_launch: Option<u64>,
#[serde(default = "default_release_type")]
pub release_type: String, // "stable" or "nightly"
#[serde(default)]
pub camoufox_config: Option<CamoufoxConfig>, // Camoufox configuration
#[serde(default)]
pub group_id: Option<String>, // Reference to profile group
}
pub fn default_release_type() -> String {
"stable".to_string()
}
impl BrowserProfile {
/// Get the path to the profile data directory (profiles/{uuid}/profile)
pub fn get_profile_data_path(&self, profiles_dir: &Path) -> PathBuf {
profiles_dir.join(self.id.to_string()).join("profile")
}
}
+845
View File
@@ -0,0 +1,845 @@
use directories::BaseDirs;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs::{self, create_dir_all};
use std::path::{Path, PathBuf};
use crate::browser::BrowserType;
use crate::browser_runner::BrowserRunner;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DetectedProfile {
pub browser: String,
pub name: String,
pub path: String,
pub description: String,
}
pub struct ProfileImporter {
base_dirs: BaseDirs,
}
impl ProfileImporter {
fn new() -> Self {
Self {
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
}
}
pub fn instance() -> &'static ProfileImporter {
&PROFILE_IMPORTER
}
/// Detect existing browser profiles on the system
pub fn detect_existing_profiles(
&self,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut detected_profiles = Vec::new();
// Detect Firefox profiles
detected_profiles.extend(self.detect_firefox_profiles()?);
// Detect Chrome profiles
detected_profiles.extend(self.detect_chrome_profiles()?);
// Detect Brave profiles
detected_profiles.extend(self.detect_brave_profiles()?);
// Detect Firefox Developer Edition profiles
detected_profiles.extend(self.detect_firefox_developer_profiles()?);
// Detect Chromium profiles
detected_profiles.extend(self.detect_chromium_profiles()?);
// Detect Zen Browser profiles
detected_profiles.extend(self.detect_zen_browser_profiles()?);
// NOTE: Mullvad and Tor Browser profile imports are no longer supported.
// We intentionally do not detect these profiles to avoid offering them in the UI.
// Remove duplicates based on path
let mut seen_paths = HashSet::new();
let unique_profiles: Vec<DetectedProfile> = detected_profiles
.into_iter()
.filter(|profile| seen_paths.insert(profile.path.clone()))
.collect();
Ok(unique_profiles)
}
/// Detect Firefox profiles
fn detect_firefox_profiles(&self) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
let firefox_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/Firefox/Profiles");
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?);
}
#[cfg(target_os = "windows")]
{
// Primary location in AppData\Roaming
let app_data = self.base_dirs.data_dir();
let firefox_dir = app_data.join("Mozilla/Firefox/Profiles");
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?);
// Also check AppData\Local for portable installations
let local_app_data = self.base_dirs.data_local_dir();
let firefox_local_dir = local_app_data.join("Mozilla/Firefox/Profiles");
if firefox_local_dir.exists() {
profiles.extend(self.scan_firefox_profiles_dir(&firefox_local_dir, "firefox")?);
}
}
#[cfg(target_os = "linux")]
{
let firefox_dir = self.base_dirs.home_dir().join(".mozilla/firefox");
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?);
}
Ok(profiles)
}
/// Detect Firefox Developer Edition profiles
fn detect_firefox_developer_profiles(
&self,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
// Firefox Developer Edition on macOS uses separate profile directories
let firefox_dev_alt_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/Firefox Developer Edition/Profiles");
// Only scan the dedicated dev edition directory if it exists, otherwise skip to avoid duplicates
if firefox_dev_alt_dir.exists() {
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dev_alt_dir, "firefox-developer")?);
}
}
#[cfg(target_os = "windows")]
{
let app_data = self.base_dirs.data_dir();
// Firefox Developer Edition on Windows typically uses separate directories
let firefox_dev_dir = app_data.join("Mozilla/Firefox Developer Edition/Profiles");
if firefox_dev_dir.exists() {
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dev_dir, "firefox-developer")?);
}
}
#[cfg(target_os = "linux")]
{
// Firefox Developer Edition on Linux uses separate directories
let firefox_dev_dir = self
.base_dirs
.home_dir()
.join(".mozilla/firefox-dev-edition");
if firefox_dev_dir.exists() {
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dev_dir, "firefox-developer")?);
}
}
Ok(profiles)
}
/// Detect Chrome profiles
fn detect_chrome_profiles(&self) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
let chrome_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/Google/Chrome");
profiles.extend(self.scan_chrome_profiles_dir(&chrome_dir, "chromium")?);
}
#[cfg(target_os = "windows")]
{
let local_app_data = self.base_dirs.data_local_dir();
let chrome_dir = local_app_data.join("Google/Chrome/User Data");
profiles.extend(self.scan_chrome_profiles_dir(&chrome_dir, "chromium")?);
}
#[cfg(target_os = "linux")]
{
let chrome_dir = self.base_dirs.home_dir().join(".config/google-chrome");
profiles.extend(self.scan_chrome_profiles_dir(&chrome_dir, "chromium")?);
}
Ok(profiles)
}
/// Detect Chromium profiles
fn detect_chromium_profiles(&self) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
let chromium_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/Chromium");
profiles.extend(self.scan_chrome_profiles_dir(&chromium_dir, "chromium")?);
}
#[cfg(target_os = "windows")]
{
let local_app_data = self.base_dirs.data_local_dir();
let chromium_dir = local_app_data.join("Chromium/User Data");
profiles.extend(self.scan_chrome_profiles_dir(&chromium_dir, "chromium")?);
}
#[cfg(target_os = "linux")]
{
let chromium_dir = self.base_dirs.home_dir().join(".config/chromium");
profiles.extend(self.scan_chrome_profiles_dir(&chromium_dir, "chromium")?);
}
Ok(profiles)
}
/// Detect Brave profiles
fn detect_brave_profiles(&self) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
let brave_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/BraveSoftware/Brave-Browser");
profiles.extend(self.scan_chrome_profiles_dir(&brave_dir, "brave")?);
}
#[cfg(target_os = "windows")]
{
let local_app_data = self.base_dirs.data_local_dir();
let brave_dir = local_app_data.join("BraveSoftware/Brave-Browser/User Data");
profiles.extend(self.scan_chrome_profiles_dir(&brave_dir, "brave")?);
}
#[cfg(target_os = "linux")]
{
let brave_dir = self
.base_dirs
.home_dir()
.join(".config/BraveSoftware/Brave-Browser");
profiles.extend(self.scan_chrome_profiles_dir(&brave_dir, "brave")?);
}
Ok(profiles)
}
/// Detect Zen Browser profiles
fn detect_zen_browser_profiles(
&self,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
let zen_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/Zen/Profiles");
profiles.extend(self.scan_firefox_profiles_dir(&zen_dir, "zen")?);
}
#[cfg(target_os = "windows")]
{
let app_data = self.base_dirs.data_dir();
let zen_dir = app_data.join("Zen/Profiles");
profiles.extend(self.scan_firefox_profiles_dir(&zen_dir, "zen")?);
}
#[cfg(target_os = "linux")]
{
let zen_dir = self.base_dirs.home_dir().join(".zen");
profiles.extend(self.scan_firefox_profiles_dir(&zen_dir, "zen")?);
}
Ok(profiles)
}
/// Scan Firefox-style profiles directory
fn scan_firefox_profiles_dir(
&self,
profiles_dir: &Path,
browser_type: &str,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
if !profiles_dir.exists() {
return Ok(profiles);
}
// Read profiles.ini file if it exists
let profiles_ini = profiles_dir
.parent()
.unwrap_or(profiles_dir)
.join("profiles.ini");
if profiles_ini.exists() {
if let Ok(content) = fs::read_to_string(&profiles_ini) {
profiles.extend(self.parse_firefox_profiles_ini(&content, profiles_dir, browser_type)?);
}
}
// Also scan directory for any profile folders not in profiles.ini
if let Ok(entries) = fs::read_dir(profiles_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let prefs_file = path.join("prefs.js");
if prefs_file.exists() {
let profile_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("Unknown Profile");
// Check if this profile was already found in profiles.ini
let already_added = profiles.iter().any(|p| p.path == path.to_string_lossy());
if !already_added {
profiles.push(DetectedProfile {
browser: browser_type.to_string(),
name: format!(
"{} Profile - {}",
self.get_browser_display_name(browser_type),
profile_name
),
path: path.to_string_lossy().to_string(),
description: format!("Profile folder: {profile_name}"),
});
}
}
}
}
}
Ok(profiles)
}
/// Parse Firefox profiles.ini file
fn parse_firefox_profiles_ini(
&self,
content: &str,
profiles_dir: &Path,
browser_type: &str,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
let mut current_section = String::new();
let mut profile_name = String::new();
let mut profile_path = String::new();
let mut is_relative = true;
for line in content.lines() {
let line = line.trim();
if line.starts_with('[') && line.ends_with(']') {
// Save previous profile if complete
if !current_section.is_empty()
&& current_section.starts_with("Profile")
&& !profile_path.is_empty()
{
let full_path = if is_relative {
profiles_dir.join(&profile_path)
} else {
PathBuf::from(&profile_path)
};
if full_path.exists() {
let display_name = if profile_name.is_empty() {
format!("{} Profile", self.get_browser_display_name(browser_type))
} else {
format!(
"{} - {}",
self.get_browser_display_name(browser_type),
profile_name
)
};
profiles.push(DetectedProfile {
browser: browser_type.to_string(),
name: display_name,
path: full_path.to_string_lossy().to_string(),
description: format!("Profile: {profile_name}"),
});
}
}
// Start new section
current_section = line[1..line.len() - 1].to_string();
profile_name.clear();
profile_path.clear();
is_relative = true;
} else if line.contains('=') {
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts.len() == 2 {
let key = parts[0].trim();
let value = parts[1].trim();
match key {
"Name" => profile_name = value.to_string(),
"Path" => profile_path = value.to_string(),
"IsRelative" => is_relative = value == "1",
_ => {}
}
}
}
}
// Handle last profile
if !current_section.is_empty()
&& current_section.starts_with("Profile")
&& !profile_path.is_empty()
{
let full_path = if is_relative {
profiles_dir.join(&profile_path)
} else {
PathBuf::from(&profile_path)
};
if full_path.exists() {
let display_name = if profile_name.is_empty() {
format!("{} Profile", self.get_browser_display_name(browser_type))
} else {
format!(
"{} - {}",
self.get_browser_display_name(browser_type),
profile_name
)
};
profiles.push(DetectedProfile {
browser: browser_type.to_string(),
name: display_name,
path: full_path.to_string_lossy().to_string(),
description: format!("Profile: {profile_name}"),
});
}
}
Ok(profiles)
}
/// Scan Chrome-style profiles directory
fn scan_chrome_profiles_dir(
&self,
browser_dir: &Path,
browser_type: &str,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
if !browser_dir.exists() {
return Ok(profiles);
}
// Check for Default profile
let default_profile = browser_dir.join("Default");
if default_profile.exists() && default_profile.join("Preferences").exists() {
profiles.push(DetectedProfile {
browser: browser_type.to_string(),
name: format!(
"{} - Default Profile",
self.get_browser_display_name(browser_type)
),
path: default_profile.to_string_lossy().to_string(),
description: "Default profile".to_string(),
});
}
// Check for Profile X directories
if let Ok(entries) = fs::read_dir(browser_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if dir_name.starts_with("Profile ") && path.join("Preferences").exists() {
let profile_number = &dir_name[8..]; // Remove "Profile " prefix
profiles.push(DetectedProfile {
browser: browser_type.to_string(),
name: format!(
"{} - Profile {}",
self.get_browser_display_name(browser_type),
profile_number
),
path: path.to_string_lossy().to_string(),
description: format!("Profile {profile_number}"),
});
}
}
}
}
Ok(profiles)
}
/// Get browser display name
fn get_browser_display_name(&self, browser_type: &str) -> &str {
match browser_type {
"firefox" => "Firefox",
"firefox-developer" => "Firefox Developer",
"chromium" => "Chrome/Chromium",
"brave" => "Brave",
"mullvad-browser" => "Mullvad Browser",
"zen" => "Zen Browser",
"tor-browser" => "Tor Browser",
_ => "Unknown Browser",
}
}
/// Import a profile from an existing browser profile
pub fn import_profile(
&self,
source_path: &str,
browser_type: &str,
new_profile_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Disable imports for Mullvad and Tor browsers
if browser_type == "mullvad-browser" || browser_type == "tor-browser" {
return Err("Importing Mullvad Browser or Tor Browser profiles is not supported".into());
}
// Validate that source path exists
let source_path = Path::new(source_path);
if !source_path.exists() {
return Err("Source profile path does not exist".into());
}
// Validate browser type
let _browser_type = BrowserType::from_str(browser_type)
.map_err(|_| format!("Invalid browser type: {browser_type}"))?;
// Check if a profile with this name already exists
let existing_profiles = BrowserRunner::instance().list_profiles()?;
if existing_profiles
.iter()
.any(|p| p.name.to_lowercase() == new_profile_name.to_lowercase())
{
return Err(format!("Profile with name '{new_profile_name}' already exists").into());
}
// 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 new_profile_uuid_dir = profiles_dir.join(profile_id.to_string());
let new_profile_data_dir = new_profile_uuid_dir.join("profile");
create_dir_all(&new_profile_uuid_dir)?;
create_dir_all(&new_profile_data_dir)?;
// Copy all files from source to destination profile subdirectory
Self::copy_directory_recursive(source_path, &new_profile_data_dir)?;
// Create the profile metadata without overwriting the imported data
// We need to find a suitable version for this browser type
let available_versions = self.get_default_version_for_browser(browser_type)?;
let profile = crate::profile::BrowserProfile {
id: profile_id,
name: new_profile_name.to_string(),
browser: browser_type.to_string(),
version: available_versions,
proxy_id: None,
process_id: None,
last_launch: None,
release_type: "stable".to_string(),
camoufox_config: None,
group_id: None,
};
// Save the profile metadata
BrowserRunner::instance().save_profile(&profile)?;
println!(
"Successfully imported profile '{}' from '{}'",
new_profile_name,
source_path.display()
);
Ok(())
}
/// Get a default version for a browser type
fn get_default_version_for_browser(
&self,
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);
if let Some(version) = downloaded_versions.first() {
return Ok(version.clone());
}
// If no downloaded versions found, return an error
Err(format!(
"No downloaded versions found for browser '{}'. Please download a version of {} first before importing profiles.",
browser_type,
self.get_browser_display_name(browser_type)
).into())
}
/// Recursively copy directory contents
fn copy_directory_recursive(
source: &Path,
destination: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
if !destination.exists() {
create_dir_all(destination)?;
}
for entry in fs::read_dir(source)? {
let entry = entry?;
let source_path = entry.path();
let dest_path = destination.join(entry.file_name());
if source_path.is_dir() {
Self::copy_directory_recursive(&source_path, &dest_path)?;
} else {
fs::copy(&source_path, &dest_path)?;
}
}
Ok(())
}
}
// Tauri commands
#[tauri::command]
pub async fn detect_existing_profiles() -> Result<Vec<DetectedProfile>, String> {
let importer = ProfileImporter::instance();
importer
.detect_existing_profiles()
.map_err(|e| format!("Failed to detect existing profiles: {e}"))
}
#[tauri::command]
pub async fn import_browser_profile(
source_path: String,
browser_type: String,
new_profile_name: String,
) -> Result<(), String> {
let importer = ProfileImporter::instance();
importer
.import_profile(&source_path, &browser_type, &new_profile_name)
.map_err(|e| format!("Failed to import profile: {e}"))
}
// Global singleton instance
lazy_static::lazy_static! {
static ref PROFILE_IMPORTER: ProfileImporter = ProfileImporter::new();
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use tempfile::TempDir;
fn create_test_profile_importer() -> (ProfileImporter, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
// Set up a temporary home directory for testing
env::set_var("HOME", temp_dir.path());
let importer = ProfileImporter::new();
(importer, temp_dir)
}
#[test]
fn test_profile_importer_creation() {
let (_importer, _temp_dir) = create_test_profile_importer();
// Test passes if no panic occurs
}
#[test]
fn test_get_browser_display_name() {
let (importer, _temp_dir) = create_test_profile_importer();
assert_eq!(importer.get_browser_display_name("firefox"), "Firefox");
assert_eq!(
importer.get_browser_display_name("firefox-developer"),
"Firefox Developer"
);
assert_eq!(
importer.get_browser_display_name("chromium"),
"Chrome/Chromium"
);
assert_eq!(importer.get_browser_display_name("brave"), "Brave");
assert_eq!(
importer.get_browser_display_name("mullvad-browser"),
"Mullvad Browser"
);
assert_eq!(importer.get_browser_display_name("zen"), "Zen Browser");
assert_eq!(
importer.get_browser_display_name("tor-browser"),
"Tor Browser"
);
assert_eq!(
importer.get_browser_display_name("unknown"),
"Unknown Browser"
);
}
#[test]
fn test_detect_existing_profiles_no_panic() {
let (importer, _temp_dir) = create_test_profile_importer();
// This should not panic even if no browser profiles exist
let result = importer.detect_existing_profiles();
assert!(result.is_ok(), "detect_existing_profiles should not fail");
let _profiles = result.unwrap();
// We can't assert specific profiles since they depend on the system
// but we can verify the result is a valid Vec
// We can't assert specific profiles since they depend on the system
// but we can verify the result is a valid Vec (length check is always true for Vec, but shows intent)
}
#[test]
fn test_scan_firefox_profiles_dir_nonexistent() {
let (importer, temp_dir) = create_test_profile_importer();
let nonexistent_dir = temp_dir.path().join("nonexistent");
let result = importer.scan_firefox_profiles_dir(&nonexistent_dir, "firefox");
assert!(
result.is_ok(),
"Should handle nonexistent directory gracefully"
);
let profiles = result.unwrap();
assert!(
profiles.is_empty(),
"Should return empty vector for nonexistent directory"
);
}
#[test]
fn test_scan_chrome_profiles_dir_nonexistent() {
let (importer, temp_dir) = create_test_profile_importer();
let nonexistent_dir = temp_dir.path().join("nonexistent");
let result = importer.scan_chrome_profiles_dir(&nonexistent_dir, "chromium");
assert!(
result.is_ok(),
"Should handle nonexistent directory gracefully"
);
let profiles = result.unwrap();
assert!(
profiles.is_empty(),
"Should return empty vector for nonexistent directory"
);
}
#[test]
fn test_parse_firefox_profiles_ini_empty() {
let (importer, _temp_dir) = create_test_profile_importer();
let empty_content = "";
let profiles_dir = Path::new("/tmp");
let result = importer.parse_firefox_profiles_ini(empty_content, profiles_dir, "firefox");
assert!(result.is_ok(), "Should handle empty profiles.ini");
let profiles = result.unwrap();
assert!(
profiles.is_empty(),
"Should return empty vector for empty content"
);
}
#[test]
fn test_parse_firefox_profiles_ini_valid() {
let (importer, temp_dir) = create_test_profile_importer();
// Create a mock profile directory
let profiles_dir = temp_dir.path().join("profiles");
let profile_dir = profiles_dir.join("test.profile");
fs::create_dir_all(&profile_dir).expect("Should create profile directory");
// Create a prefs.js file to make it look like a valid profile
let prefs_file = profile_dir.join("prefs.js");
fs::write(&prefs_file, "// Firefox preferences").expect("Should create prefs.js");
let profiles_ini_content = r#"
[Profile0]
Name=Test Profile
IsRelative=1
Path=test.profile
"#;
let result =
importer.parse_firefox_profiles_ini(profiles_ini_content, &profiles_dir, "firefox");
assert!(result.is_ok(), "Should parse valid profiles.ini");
let profiles = result.unwrap();
assert_eq!(profiles.len(), 1, "Should find one profile");
assert_eq!(profiles[0].name, "Firefox - Test Profile");
assert_eq!(profiles[0].browser, "firefox");
}
#[test]
fn test_copy_directory_recursive() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
// Create source directory structure
let source_dir = temp_dir.path().join("source");
let source_subdir = source_dir.join("subdir");
fs::create_dir_all(&source_subdir).expect("Should create source directories");
// Create some test files
let source_file1 = source_dir.join("file1.txt");
let source_file2 = source_subdir.join("file2.txt");
fs::write(&source_file1, "content1").expect("Should create file1");
fs::write(&source_file2, "content2").expect("Should create file2");
// Create destination directory
let dest_dir = temp_dir.path().join("dest");
// Copy recursively
let result = ProfileImporter::copy_directory_recursive(&source_dir, &dest_dir);
assert!(result.is_ok(), "Should copy directory successfully");
// Verify files were copied
let dest_file1 = dest_dir.join("file1.txt");
let dest_file2 = dest_dir.join("subdir").join("file2.txt");
assert!(dest_file1.exists(), "file1.txt should be copied");
assert!(dest_file2.exists(), "file2.txt should be copied");
let content1 = fs::read_to_string(&dest_file1).expect("Should read file1");
let content2 = fs::read_to_string(&dest_file2).expect("Should read file2");
assert_eq!(content1, "content1", "file1 content should match");
assert_eq!(content2, "content2", "file2 content should match");
}
#[test]
fn test_get_default_version_for_browser_no_versions() {
let (importer, _temp_dir) = create_test_profile_importer();
// This should fail since no versions are downloaded in test environment
let result = importer.get_default_version_for_browser("firefox");
assert!(
result.is_err(),
"Should fail when no versions are available"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("No downloaded versions found"),
"Error should mention no versions found"
);
}
}
File diff suppressed because it is too large Load Diff
+279 -43
View File
@@ -3,6 +3,9 @@ use serde::{Deserialize, Serialize};
use std::fs::{self, create_dir_all};
use std::path::PathBuf;
use crate::api_client::ApiClient;
use crate::version_updater;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TableSortingSettings {
pub column: String, // Column to sort by: "name", "browser", "status"
@@ -22,33 +25,19 @@ impl Default for TableSortingSettings {
pub struct AppSettings {
#[serde(default)]
pub set_as_default_browser: bool,
#[serde(default = "default_show_settings_on_startup")]
pub show_settings_on_startup: bool,
#[serde(default = "default_theme")]
pub theme: String, // "light", "dark", or "system"
#[serde(default = "default_auto_updates_enabled")]
pub auto_updates_enabled: bool,
}
fn default_show_settings_on_startup() -> bool {
true
}
fn default_theme() -> String {
"system".to_string()
}
fn default_auto_updates_enabled() -> bool {
true
}
impl Default for AppSettings {
fn default() -> Self {
Self {
set_as_default_browser: false,
show_settings_on_startup: default_show_settings_on_startup(),
theme: default_theme(),
auto_updates_enabled: default_auto_updates_enabled(),
theme: "system".to_string(),
}
}
}
@@ -58,12 +47,16 @@ pub struct SettingsManager {
}
impl SettingsManager {
pub fn new() -> Self {
fn new() -> Self {
Self {
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
}
}
pub fn instance() -> &'static SettingsManager {
&SETTINGS_MANAGER
}
pub fn get_settings_dir(&self) -> PathBuf {
let mut path = self.base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
@@ -155,26 +148,14 @@ impl SettingsManager {
}
pub fn should_show_settings_on_startup(&self) -> Result<bool, Box<dyn std::error::Error>> {
let settings = self.load_settings()?;
// Show prompt if:
// 1. User wants to see the prompt
// 2. Donut Browser is not set as default
// 3. User hasn't explicitly disabled the default browser setting
Ok(settings.show_settings_on_startup && !settings.set_as_default_browser)
}
pub fn disable_default_browser_prompt(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut settings = self.load_settings()?;
settings.show_settings_on_startup = false;
self.save_settings(&settings)?;
Ok(())
// Always return false - we don't show settings on startup anymore
Ok(false)
}
}
#[tauri::command]
pub async fn get_app_settings() -> Result<AppSettings, String> {
let manager = SettingsManager::new();
let manager = SettingsManager::instance();
manager
.load_settings()
.map_err(|e| format!("Failed to load settings: {e}"))
@@ -182,7 +163,7 @@ pub async fn get_app_settings() -> Result<AppSettings, String> {
#[tauri::command]
pub async fn save_app_settings(settings: AppSettings) -> Result<(), String> {
let manager = SettingsManager::new();
let manager = SettingsManager::instance();
manager
.save_settings(&settings)
.map_err(|e| format!("Failed to save settings: {e}"))
@@ -190,23 +171,15 @@ pub async fn save_app_settings(settings: AppSettings) -> Result<(), String> {
#[tauri::command]
pub async fn should_show_settings_on_startup() -> Result<bool, String> {
let manager = SettingsManager::new();
let manager = SettingsManager::instance();
manager
.should_show_settings_on_startup()
.map_err(|e| format!("Failed to check prompt setting: {e}"))
}
#[tauri::command]
pub async fn disable_default_browser_prompt() -> Result<(), String> {
let manager = SettingsManager::new();
manager
.disable_default_browser_prompt()
.map_err(|e| format!("Failed to disable prompt: {e}"))
}
#[tauri::command]
pub async fn get_table_sorting_settings() -> Result<TableSortingSettings, String> {
let manager = SettingsManager::new();
let manager = SettingsManager::instance();
manager
.load_table_sorting()
.map_err(|e| format!("Failed to load table sorting settings: {e}"))
@@ -214,8 +187,271 @@ pub async fn get_table_sorting_settings() -> Result<TableSortingSettings, String
#[tauri::command]
pub async fn save_table_sorting_settings(sorting: TableSortingSettings) -> Result<(), String> {
let manager = SettingsManager::new();
let manager = SettingsManager::instance();
manager
.save_table_sorting(&sorting)
.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();
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use tempfile::TempDir;
fn create_test_settings_manager() -> (SettingsManager, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
// Set up a temporary home directory for testing
env::set_var("HOME", temp_dir.path());
let manager = SettingsManager::new();
(manager, temp_dir)
}
#[test]
fn test_settings_manager_creation() {
let (_manager, _temp_dir) = create_test_settings_manager();
// Test passes if no panic occurs
}
#[test]
fn test_default_app_settings() {
let default_settings = AppSettings::default();
assert!(
!default_settings.set_as_default_browser,
"Default should not set as default browser"
);
assert_eq!(
default_settings.theme, "system",
"Default theme should be system"
);
}
#[test]
fn test_default_table_sorting_settings() {
let default_sorting = TableSortingSettings::default();
assert_eq!(
default_sorting.column, "name",
"Default sort column should be name"
);
assert_eq!(
default_sorting.direction, "asc",
"Default sort direction should be asc"
);
}
#[test]
fn test_load_settings_nonexistent_file() {
let (manager, _temp_dir) = create_test_settings_manager();
let result = manager.load_settings();
assert!(
result.is_ok(),
"Should handle nonexistent settings file gracefully"
);
let settings = result.unwrap();
assert!(
!settings.set_as_default_browser,
"Should return default settings"
);
assert_eq!(settings.theme, "system", "Should return default theme");
}
#[test]
fn test_save_and_load_settings() {
let (manager, _temp_dir) = create_test_settings_manager();
let test_settings = AppSettings {
set_as_default_browser: true,
theme: "dark".to_string(),
};
// Save settings
let save_result = manager.save_settings(&test_settings);
assert!(save_result.is_ok(), "Should save settings successfully");
// Load settings back
let load_result = manager.load_settings();
assert!(load_result.is_ok(), "Should load settings successfully");
let loaded_settings = load_result.unwrap();
assert!(
loaded_settings.set_as_default_browser,
"Loaded settings should match saved"
);
assert_eq!(
loaded_settings.theme, "dark",
"Loaded theme should match saved"
);
}
#[test]
fn test_load_table_sorting_nonexistent_file() {
let (manager, _temp_dir) = create_test_settings_manager();
let result = manager.load_table_sorting();
assert!(
result.is_ok(),
"Should handle nonexistent sorting file gracefully"
);
let sorting = result.unwrap();
assert_eq!(sorting.column, "name", "Should return default sorting");
assert_eq!(sorting.direction, "asc", "Should return default direction");
}
#[test]
fn test_save_and_load_table_sorting() {
let (manager, _temp_dir) = create_test_settings_manager();
let test_sorting = TableSortingSettings {
column: "browser".to_string(),
direction: "desc".to_string(),
};
// Save sorting
let save_result = manager.save_table_sorting(&test_sorting);
assert!(save_result.is_ok(), "Should save sorting successfully");
// Load sorting back
let load_result = manager.load_table_sorting();
assert!(load_result.is_ok(), "Should load sorting successfully");
let loaded_sorting = load_result.unwrap();
assert_eq!(
loaded_sorting.column, "browser",
"Loaded column should match saved"
);
assert_eq!(
loaded_sorting.direction, "desc",
"Loaded direction should match saved"
);
}
#[test]
fn test_should_show_settings_on_startup() {
let (manager, _temp_dir) = create_test_settings_manager();
let result = manager.should_show_settings_on_startup();
assert!(result.is_ok(), "Should not fail");
let should_show = result.unwrap();
assert!(
!should_show,
"Should always return false as per implementation"
);
}
#[test]
fn test_load_corrupted_settings_file() {
let (manager, _temp_dir) = create_test_settings_manager();
// Create settings directory
let settings_dir = manager.get_settings_dir();
fs::create_dir_all(&settings_dir).expect("Should create settings directory");
// Write corrupted JSON
let settings_file = manager.get_settings_file();
fs::write(&settings_file, "{ invalid json }").expect("Should write corrupted file");
// Should handle corrupted file gracefully
let result = manager.load_settings();
assert!(
result.is_ok(),
"Should handle corrupted settings file gracefully"
);
let settings = result.unwrap();
assert!(
!settings.set_as_default_browser,
"Should return default settings for corrupted file"
);
assert_eq!(
settings.theme, "system",
"Should return default theme for corrupted file"
);
}
#[test]
fn test_settings_file_paths() {
let (manager, _temp_dir) = create_test_settings_manager();
let settings_dir = manager.get_settings_dir();
let settings_file = manager.get_settings_file();
let sorting_file = manager.get_table_sorting_file();
assert!(
settings_dir.to_string_lossy().contains("settings"),
"Settings dir should contain 'settings'"
);
assert!(
settings_file
.to_string_lossy()
.ends_with("app_settings.json"),
"Settings file should end with app_settings.json"
);
assert!(
sorting_file
.to_string_lossy()
.ends_with("table_sorting.json"),
"Sorting file should end with table_sorting.json"
);
}
}
+236 -138
View File
@@ -1,4 +1,3 @@
use crate::browser_version_service::BrowserVersionService;
use directories::BaseDirs;
use serde::{Deserialize, Serialize};
use std::fs;
@@ -8,7 +7,10 @@ use std::sync::OnceLock;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tauri::Emitter;
use tokio::sync::Mutex;
use tokio::time::{interval, Interval};
use tokio::time::interval;
use crate::auto_updater::AutoUpdater;
use crate::browser_version_manager::BrowserVersionManager;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct VersionUpdateProgress {
@@ -39,29 +41,30 @@ impl Default for BackgroundUpdateState {
fn default() -> Self {
Self {
last_update_time: 0,
update_interval_hours: 3,
update_interval_hours: 12,
}
}
}
pub struct VersionUpdater {
version_service: BrowserVersionService,
app_handle: Arc<Mutex<Option<tauri::AppHandle>>>,
update_interval: Interval,
version_service: &'static BrowserVersionManager,
auto_updater: &'static AutoUpdater,
app_handle: Option<tauri::AppHandle>,
}
impl VersionUpdater {
pub fn new() -> Self {
let mut update_interval = interval(Duration::from_secs(5 * 60)); // Check every 5 minutes
update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
Self {
version_service: BrowserVersionService::new(),
app_handle: Arc::new(Mutex::new(None)),
update_interval,
version_service: BrowserVersionManager::instance(),
auto_updater: AutoUpdater::instance(),
app_handle: None,
}
}
pub fn set_app_handle(&mut self, app_handle: tauri::AppHandle) {
self.app_handle = Some(app_handle);
}
fn get_cache_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let app_name = if cfg!(debug_assertions) {
@@ -143,11 +146,6 @@ impl VersionUpdater {
should_update
}
pub async fn set_app_handle(&self, app_handle: tauri::AppHandle) {
let mut handle = self.app_handle.lock().await;
*handle = Some(app_handle);
}
pub async fn check_and_run_startup_update(
&self,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
@@ -157,15 +155,10 @@ impl VersionUpdater {
return Ok(());
}
let app_handle = {
let handle_guard = self.app_handle.lock().await;
handle_guard.clone()
};
if let Some(handle) = app_handle {
if let Some(ref app_handle) = self.app_handle {
println!("Running startup version update...");
match self.update_all_browser_versions(&handle).await {
match self.update_all_browser_versions(app_handle).await {
Ok(_) => {
// Update the persistent state after successful update
let state = BackgroundUpdateState {
@@ -191,7 +184,9 @@ impl VersionUpdater {
Ok(())
}
pub async fn start_background_updates(&mut self) {
pub async fn start_background_updates(
&self,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!(
"Starting background version update service (checking every 5 minutes for 3-hour intervals)"
);
@@ -201,41 +196,54 @@ impl VersionUpdater {
eprintln!("Startup version update failed: {e}");
}
Ok(())
}
pub async fn run_background_task() {
let mut update_interval = interval(Duration::from_secs(5 * 60)); // Check every 5 minutes
update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
self.update_interval.tick().await;
update_interval.tick().await;
// Check if we should run an update based on persistent state
if !Self::should_run_background_update() {
continue;
}
// Check if we have an app handle
let app_handle = {
let handle_guard = self.app_handle.lock().await;
handle_guard.clone()
};
println!("Starting background version update...");
if let Some(handle) = app_handle {
println!("Starting background version update...");
// Get the updater instance for this update cycle
let updater = get_version_updater();
let result = {
let updater_guard = updater.lock().await;
if let Some(ref app_handle) = updater_guard.app_handle {
updater_guard.update_all_browser_versions(app_handle).await
} else {
Err("App handle not available for background update".into())
}
}; // Release the lock here
match self.update_all_browser_versions(&handle).await {
Ok(_) => {
// Update the persistent state after successful update
let state = BackgroundUpdateState {
last_update_time: Self::get_current_timestamp(),
update_interval_hours: 3,
};
match result {
Ok(_) => {
// Update the persistent state after successful update
let state = BackgroundUpdateState {
last_update_time: Self::get_current_timestamp(),
update_interval_hours: 3,
};
if let Err(e) = Self::save_background_update_state(&state) {
eprintln!("Failed to save background update state: {e}");
} else {
println!("Background version update completed successfully");
}
if let Err(e) = Self::save_background_update_state(&state) {
eprintln!("Failed to save background update state: {e}");
} else {
println!("Background version update completed successfully");
}
Err(e) => {
eprintln!("Background version update failed: {e}");
}
Err(e) => {
eprintln!("Background version update failed: {e}");
// Emit error event
// Try to emit error event if we have an app handle
let updater_guard = updater.lock().await;
if let Some(ref app_handle) = updater_guard.app_handle {
let progress = VersionUpdateProgress {
current_browser: "".to_string(),
total_browsers: 0,
@@ -244,11 +252,9 @@ impl VersionUpdater {
browser_new_versions: 0,
status: "error".to_string(),
};
let _ = handle.emit("version-update-progress", &progress);
let _ = app_handle.emit("version-update-progress", &progress);
}
}
} else {
println!("App handle not available, skipping background update");
}
}
}
@@ -257,107 +263,108 @@ impl VersionUpdater {
&self,
app_handle: &tauri::AppHandle,
) -> Result<Vec<BackgroundUpdateResult>, Box<dyn std::error::Error + Send + Sync>> {
println!("Starting background version update for all browsers");
let browsers = [
"firefox",
"firefox-developer",
"mullvad-browser",
"zen",
"brave",
"chromium",
"tor-browser",
];
let total_browsers = browsers.len();
let supported_browsers = self.version_service.get_supported_browsers();
let total_browsers = supported_browsers.len();
let mut results = Vec::new();
let mut total_new_versions = 0;
// Emit start event
let progress = VersionUpdateProgress {
current_browser: "".to_string(),
// Emit initial progress
let initial_progress = VersionUpdateProgress {
current_browser: String::new(),
total_browsers,
completed_browsers: 0,
new_versions_found: 0,
browser_new_versions: 0,
status: "updating".to_string(),
};
let _ = app_handle.emit("version-update-progress", &progress);
for (index, browser) in browsers.iter().enumerate() {
// Check if individual browser cache is expired before updating
if !self.version_service.should_update_cache(browser) {
println!("Skipping {browser} - cache is still fresh");
if let Err(e) = app_handle.emit("version-update-progress", &initial_progress) {
eprintln!("Failed to emit initial progress: {e}");
}
let browser_result = BackgroundUpdateResult {
browser: browser.to_string(),
new_versions_count: 0,
total_versions_count: 0,
updated_successfully: true,
error: None,
};
results.push(browser_result);
continue;
}
for (index, browser) in supported_browsers.iter().enumerate() {
println!("Updating browser versions for: {browser}");
println!("Updating versions for browser: {browser}");
// Emit progress for current browser
// Emit progress update for current browser
let progress = VersionUpdateProgress {
current_browser: browser.to_string(),
current_browser: browser.clone(),
total_browsers,
completed_browsers: index,
new_versions_found: total_new_versions,
browser_new_versions: 0,
status: "updating".to_string(),
};
let _ = app_handle.emit("version-update-progress", &progress);
let result = self.update_browser_versions(browser).await;
if let Err(e) = app_handle.emit("version-update-progress", &progress) {
eprintln!("Failed to emit progress for {browser}: {e}");
}
match result {
Ok(new_count) => {
total_new_versions += new_count;
let browser_result = BackgroundUpdateResult {
browser: browser.to_string(),
new_versions_count: new_count,
total_versions_count: 0, // We'll update this if needed
match self.update_browser_versions(browser).await {
Ok(new_versions_count) => {
results.push(BackgroundUpdateResult {
browser: browser.clone(),
new_versions_count,
total_versions_count: 0, // We don't track total for background updates
updated_successfully: true,
error: None,
};
results.push(browser_result);
});
println!("Found {new_count} new versions for {browser}");
total_new_versions += new_versions_count;
// Emit progress update with new versions found
let progress = VersionUpdateProgress {
current_browser: browser.clone(),
total_browsers,
completed_browsers: index,
new_versions_found: total_new_versions,
browser_new_versions: new_versions_count,
status: "updating".to_string(),
};
if let Err(e) = app_handle.emit("version-update-progress", &progress) {
eprintln!("Failed to emit progress with versions for {browser}: {e}");
}
}
Err(e) => {
eprintln!("Failed to update versions for {browser}: {e}");
let browser_result = BackgroundUpdateResult {
browser: browser.to_string(),
results.push(BackgroundUpdateResult {
browser: browser.clone(),
new_versions_count: 0,
total_versions_count: 0,
updated_successfully: false,
error: Some(e.to_string()),
};
results.push(browser_result);
});
}
}
// Small delay between browsers to avoid overwhelming APIs
tokio::time::sleep(Duration::from_millis(500)).await;
}
// Emit completion event
let progress = VersionUpdateProgress {
current_browser: "".to_string(),
// Emit completion
let final_progress = VersionUpdateProgress {
current_browser: String::new(),
total_browsers,
completed_browsers: total_browsers,
new_versions_found: total_new_versions,
browser_new_versions: 0,
status: "completed".to_string(),
};
let _ = app_handle.emit("version-update-progress", &progress);
println!("Background version update completed. Found {total_new_versions} new versions total");
if let Err(e) = app_handle.emit("version-update-progress", &final_progress) {
eprintln!("Failed to emit completion progress: {e}");
}
// After all version updates are complete, trigger auto-update check
if total_new_versions > 0 {
println!(
"Found {total_new_versions} new versions across all browsers. Checking for auto-updates..."
);
// Trigger auto-update check which will automatically download browsers
self
.auto_updater
.check_for_updates_with_progress(app_handle)
.await;
} else {
println!("No new versions found, skipping auto-update check");
}
Ok(results)
}
@@ -448,22 +455,6 @@ pub async fn get_version_update_status() -> Result<(Option<u64>, u64), String> {
Ok((last_update, time_until_next))
}
#[tauri::command]
pub async fn check_version_update_needed() -> Result<bool, String> {
Ok(VersionUpdater::should_run_background_update())
}
#[tauri::command]
pub async fn force_version_update_check(_app_handle: tauri::AppHandle) -> Result<bool, String> {
let updater = get_version_updater();
let updater_guard = updater.lock().await;
match updater_guard.check_and_run_startup_update().await {
Ok(_) => Ok(true),
Err(e) => Err(format!("Failed to run version update check: {e}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -528,31 +519,138 @@ mod tests {
#[test]
fn test_should_run_background_update_logic() {
// Note: This test uses the shared state file, so results may vary
// depending on previous test runs. This is expected behavior.
// Create isolated test states to avoid interference
let current_time = VersionUpdater::get_current_timestamp();
// Test with recent update (should not update)
let recent_state = BackgroundUpdateState {
last_update_time: VersionUpdater::get_current_timestamp() - 60, // 1 minute ago
last_update_time: current_time - 60, // 1 minute ago
update_interval_hours: 3,
};
VersionUpdater::save_background_update_state(&recent_state).unwrap();
assert!(!VersionUpdater::should_run_background_update());
// Save and test recent state
let save_result = VersionUpdater::save_background_update_state(&recent_state);
assert!(save_result.is_ok(), "Should save recent state successfully");
let should_update_recent = VersionUpdater::should_run_background_update();
assert!(
!should_update_recent,
"Should not update when last update was recent"
);
// Test with old update (should update)
let old_state = BackgroundUpdateState {
last_update_time: VersionUpdater::get_current_timestamp() - (4 * 60 * 60), // 4 hours ago
last_update_time: current_time - (4 * 60 * 60), // 4 hours ago
update_interval_hours: 3,
};
VersionUpdater::save_background_update_state(&old_state).unwrap();
assert!(VersionUpdater::should_run_background_update());
// Save and test old state
let save_result = VersionUpdater::save_background_update_state(&old_state);
assert!(save_result.is_ok(), "Should save old state successfully");
let should_update_old = VersionUpdater::should_run_background_update();
assert!(should_update_old, "Should update when last update was old");
// Test with never updated (should update)
let never_updated_state = BackgroundUpdateState {
last_update_time: 0,
update_interval_hours: 3,
};
let save_result = VersionUpdater::save_background_update_state(&never_updated_state);
assert!(
save_result.is_ok(),
"Should save never updated state successfully"
);
let should_update_never = VersionUpdater::should_run_background_update();
assert!(
should_update_never,
"Should update when never updated before"
);
}
#[test]
fn test_cache_dir_creation() {
// This should not panic and should create the directory if it doesn't exist
let cache_dir = VersionUpdater::get_cache_dir().unwrap();
assert!(cache_dir.exists());
assert!(cache_dir.is_dir());
let cache_dir_result = VersionUpdater::get_cache_dir();
assert!(
cache_dir_result.is_ok(),
"Should successfully get cache directory"
);
let cache_dir = cache_dir_result.unwrap();
assert!(
cache_dir.exists(),
"Cache directory should exist after creation"
);
assert!(cache_dir.is_dir(), "Cache directory should be a directory");
// Verify the path contains expected components
let path_str = cache_dir.to_string_lossy();
assert!(
path_str.contains("version_cache"),
"Path should contain version_cache"
);
// Test that calling it again returns the same directory
let cache_dir2 = VersionUpdater::get_cache_dir().unwrap();
assert_eq!(
cache_dir, cache_dir2,
"Multiple calls should return same directory"
);
}
#[test]
fn test_version_updater_creation() {
let updater = VersionUpdater::new();
// Should have valid references to services
assert!(
!std::ptr::eq(updater.version_service as *const _, std::ptr::null()),
"Version service should not be null"
);
assert!(
!std::ptr::eq(updater.auto_updater as *const _, std::ptr::null()),
"Auto updater should not be null"
);
assert!(
updater.app_handle.is_none(),
"App handle should initially be None"
);
}
#[test]
fn test_get_current_timestamp() {
let timestamp1 = VersionUpdater::get_current_timestamp();
// Should be a reasonable timestamp (after year 2020)
assert!(
timestamp1 > 1577836800,
"Timestamp should be after 2020-01-01"
); // 2020-01-01 00:00:00 UTC
// Should be before year 2100
assert!(
timestamp1 < 4102444800,
"Timestamp should be before 2100-01-01"
); // 2100-01-01 00:00:00 UTC
// Wait a tiny bit and check it increases
std::thread::sleep(std::time::Duration::from_millis(1));
let timestamp2 = VersionUpdater::get_current_timestamp();
assert!(timestamp2 >= timestamp1, "Timestamp should not decrease");
}
#[test]
fn test_get_version_updater_singleton() {
let updater1 = get_version_updater();
let updater2 = get_version_updater();
// Should return the same Arc instance
assert!(
Arc::ptr_eq(&updater1, &updater2),
"Should return same singleton instance"
);
}
}
+25 -12
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut Browser",
"version": "0.2.4",
"version": "0.9.2",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm dev",
@@ -10,15 +10,7 @@
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "Donut Browser",
"width": 900,
"height": 600,
"resizable": false,
"fullscreen": false
}
],
"windows": [],
"security": {
"csp": null
}
@@ -26,6 +18,7 @@
"bundle": {
"active": true,
"targets": "all",
"category": "Productivity",
"externalBin": ["binaries/nodecar"],
"icon": [
"icons/32x32.png",
@@ -45,12 +38,32 @@
"files": {
"Info.plist": "Info.plist"
}
},
"linux": {
"deb": {
"depends": ["xdg-utils"],
"files": {
"/usr/share/applications/donutbrowser.desktop": "donutbrowser.desktop"
}
},
"rpm": {
"depends": ["xdg-utils"],
"files": {
"/usr/share/applications/donutbrowser.desktop": "donutbrowser.desktop"
}
},
"appimage": {
"files": {
"usr/share/applications/donutbrowser.desktop": "donutbrowser.desktop"
}
}
}
},
"plugins": {
"deep-link": {
"schemes": ["http", "https"],
"domains": []
"desktop": {
"schemes": ["http", "https"]
}
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
Hello, World!
Binary file not shown.
+133
View File
@@ -0,0 +1,133 @@
use std::env;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
/// Utility functions for integration tests
pub struct TestUtils;
impl TestUtils {
/// Build the nodecar binary if it doesn't exist
pub async fn ensure_nodecar_binary() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>>
{
let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR")?;
let project_root = PathBuf::from(cargo_manifest_dir)
.parent()
.unwrap()
.to_path_buf();
let nodecar_dir = project_root.join("nodecar");
let nodecar_binary = nodecar_dir.join("nodecar-bin");
// Check if binary already exists
if nodecar_binary.exists() {
return Ok(nodecar_binary);
}
println!("Building nodecar binary for integration tests...");
// Install dependencies
let install_status = Command::new("pnpm")
.args(["install", "--frozen-lockfile"])
.current_dir(&nodecar_dir)
.status()?;
if !install_status.success() {
return Err("Failed to install nodecar dependencies".into());
}
// Build the binary
let build_status = Command::new("pnpm")
.args(["run", "build"])
.current_dir(&nodecar_dir)
.status()?;
if !build_status.success() {
return Err("Failed to build nodecar binary".into());
}
if !nodecar_binary.exists() {
return Err("Nodecar binary was not created successfully".into());
}
Ok(nodecar_binary)
}
/// Execute a nodecar command with timeout
pub async fn execute_nodecar_command(
binary_path: &PathBuf,
args: &[&str],
) -> Result<std::process::Output, Box<dyn std::error::Error + Send + Sync>> {
let mut cmd = Command::new(binary_path);
cmd.args(args);
let output = tokio::process::Command::from(cmd).output().await?;
Ok(output)
}
/// Check if a port is available
pub async fn is_port_available(port: u16) -> bool {
tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
.await
.is_ok()
}
/// Wait for a port to become available or occupied
pub async fn wait_for_port_state(port: u16, should_be_occupied: bool, timeout_secs: u64) -> bool {
let start = std::time::Instant::now();
while start.elapsed().as_secs() < timeout_secs {
let is_available = Self::is_port_available(port).await;
if should_be_occupied && !is_available {
return true; // Port is occupied as expected
} else if !should_be_occupied && is_available {
return true; // Port is available as expected
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
false
}
/// Create a temporary directory for test files
pub fn create_temp_dir() -> Result<tempfile::TempDir, Box<dyn std::error::Error + Send + Sync>> {
Ok(tempfile::tempdir()?)
}
/// Clean up specific nodecar processes by IDs (for targeted test cleanup)
pub async fn cleanup_specific_processes(
nodecar_path: &PathBuf,
proxy_ids: &[String],
camoufox_ids: &[String],
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("Cleaning up specific test processes...");
// Stop specific proxies
for proxy_id in proxy_ids {
let stop_args = ["proxy", "stop", "--id", proxy_id];
if let Ok(output) = Self::execute_nodecar_command(nodecar_path, &stop_args).await {
if output.status.success() {
println!("Stopped test proxy: {proxy_id}");
}
}
}
// Stop specific camoufox instances
for camoufox_id in camoufox_ids {
let stop_args = ["camoufox", "stop", "--id", camoufox_id];
if let Ok(output) = Self::execute_nodecar_command(nodecar_path, &stop_args).await {
if output.status.success() {
println!("Stopped test camoufox instance: {camoufox_id}");
}
}
}
// Give processes time to clean up
tokio::time::sleep(Duration::from_millis(500)).await;
println!("Test process cleanup completed");
Ok(())
}
}
+999
View File
@@ -0,0 +1,999 @@
mod common;
use common::TestUtils;
use serde_json::Value;
/// Setup function to ensure clean state before tests
async fn setup_test() -> Result<std::path::PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = TestUtils::ensure_nodecar_binary().await?;
// Only clean up test-specific processes, not all processes
// This prevents interfering with actual app usage during testing
println!("Setting up test environment...");
Ok(nodecar_path)
}
/// Helper to track and cleanup specific test resources
struct TestResourceTracker {
proxy_ids: Vec<String>,
camoufox_ids: Vec<String>,
nodecar_path: std::path::PathBuf,
}
impl TestResourceTracker {
fn new(nodecar_path: std::path::PathBuf) -> Self {
Self {
proxy_ids: Vec::new(),
camoufox_ids: Vec::new(),
nodecar_path,
}
}
fn track_proxy(&mut self, proxy_id: String) {
self.proxy_ids.push(proxy_id);
}
fn track_camoufox(&mut self, camoufox_id: String) {
self.camoufox_ids.push(camoufox_id);
}
async fn cleanup_all(&self) {
// Use targeted cleanup to only stop test-specific processes
let _ = TestUtils::cleanup_specific_processes(
&self.nodecar_path,
&self.proxy_ids,
&self.camoufox_ids,
)
.await;
}
}
impl Drop for TestResourceTracker {
fn drop(&mut self) {
// Ensure cleanup happens even if test panics
let proxy_ids = self.proxy_ids.clone();
let camoufox_ids = self.camoufox_ids.clone();
let nodecar_path = self.nodecar_path.clone();
tokio::spawn(async move {
let _ = TestUtils::cleanup_specific_processes(&nodecar_path, &proxy_ids, &camoufox_ids).await;
});
}
}
/// Integration tests for nodecar proxy functionality
#[tokio::test]
async fn test_nodecar_proxy_lifecycle() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let mut tracker = TestResourceTracker::new(nodecar_path.clone());
// Test proxy start with a known working upstream
let args = [
"proxy",
"start",
"--host",
"httpbin.org",
"--proxy-port",
"80",
"--type",
"http",
];
println!("Starting proxy with nodecar...");
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args).await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
tracker.cleanup_all().await;
return Err(format!("Proxy start failed - stdout: {stdout}, stderr: {stderr}").into());
}
let stdout = String::from_utf8(output.stdout)?;
let config: Value = serde_json::from_str(&stdout)?;
// Verify proxy configuration structure
assert!(config["id"].is_string(), "Proxy ID should be a string");
assert!(
config["localPort"].is_number(),
"Local port should be a number"
);
assert!(
config["localUrl"].is_string(),
"Local URL should be a string"
);
let proxy_id = config["id"].as_str().unwrap().to_string();
let local_port = config["localPort"].as_u64().unwrap() as u16;
tracker.track_proxy(proxy_id.clone());
println!("Proxy started with ID: {proxy_id} on port: {local_port}");
// Wait for the proxy to start listening
let is_listening = TestUtils::wait_for_port_state(local_port, true, 10).await;
assert!(
is_listening,
"Proxy should be listening on the assigned port"
);
// Test stopping the proxy
let stop_args = ["proxy", "stop", "--id", &proxy_id];
let stop_output = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args).await?;
assert!(stop_output.status.success(), "Proxy stop should succeed");
let port_available = TestUtils::wait_for_port_state(local_port, false, 5).await;
assert!(
port_available,
"Port should be available after stopping proxy"
);
tracker.cleanup_all().await;
Ok(())
}
/// Test proxy with authentication
#[tokio::test]
async fn test_nodecar_proxy_with_auth() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let mut tracker = TestResourceTracker::new(nodecar_path.clone());
let args = [
"proxy",
"start",
"--host",
"httpbin.org",
"--proxy-port",
"80",
"--type",
"http",
"--username",
"testuser",
"--password",
"testpass",
];
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args).await?;
if output.status.success() {
let stdout = String::from_utf8(output.stdout)?;
let config: Value = serde_json::from_str(&stdout)?;
let proxy_id = config["id"].as_str().unwrap().to_string();
tracker.track_proxy(proxy_id.clone());
// Verify upstream URL contains encoded credentials
if let Some(upstream_url) = config["upstreamUrl"].as_str() {
assert!(
upstream_url.contains("testuser"),
"Upstream URL should contain username"
);
// Password might be encoded, so we check for the presence of auth info
assert!(
upstream_url.contains("@"),
"Upstream URL should contain auth separator"
);
}
}
tracker.cleanup_all().await;
Ok(())
}
/// Test proxy list functionality
#[tokio::test]
async fn test_nodecar_proxy_list() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let mut tracker = TestResourceTracker::new(nodecar_path.clone());
// Start a proxy first
let start_args = [
"proxy",
"start",
"--host",
"httpbin.org",
"--proxy-port",
"80",
"--type",
"http",
];
let start_output = TestUtils::execute_nodecar_command(&nodecar_path, &start_args).await?;
if start_output.status.success() {
let stdout = String::from_utf8(start_output.stdout)?;
let config: Value = serde_json::from_str(&stdout)?;
let proxy_id = config["id"].as_str().unwrap().to_string();
tracker.track_proxy(proxy_id.clone());
// Test list command
let list_args = ["proxy", "list"];
let list_output = TestUtils::execute_nodecar_command(&nodecar_path, &list_args).await?;
assert!(list_output.status.success(), "Proxy list should succeed");
let list_stdout = String::from_utf8(list_output.stdout)?;
let proxy_list: Value = serde_json::from_str(&list_stdout)?;
assert!(proxy_list.is_array(), "Proxy list should be an array");
let proxies = proxy_list.as_array().unwrap();
assert!(
!proxies.is_empty(),
"Should have at least one proxy in the list"
);
// Find our proxy in the list
let found_proxy = proxies.iter().find(|p| p["id"].as_str() == Some(&proxy_id));
assert!(found_proxy.is_some(), "Started proxy should be in the list");
}
tracker.cleanup_all().await;
Ok(())
}
/// Test Camoufox functionality
#[tokio::test]
async fn test_nodecar_camoufox_lifecycle() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let mut tracker = TestResourceTracker::new(nodecar_path.clone());
let temp_dir = TestUtils::create_temp_dir()?;
let profile_path = temp_dir.path().join("test_profile");
let args = [
"camoufox",
"start",
"--profile-path",
profile_path.to_str().unwrap(),
"--headless",
];
println!("Starting Camoufox with nodecar...");
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args).await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
// If Camoufox is not installed or times out, skip the test
if stderr.contains("not installed")
|| stderr.contains("not found")
|| stderr.contains("timeout")
|| stdout.contains("timeout")
{
println!("Skipping Camoufox test - Camoufox not available or timed out");
tracker.cleanup_all().await;
return Ok(());
}
tracker.cleanup_all().await;
return Err(format!("Camoufox start failed - stdout: {stdout}, stderr: {stderr}").into());
}
let stdout = String::from_utf8(output.stdout)?;
let config: Value = serde_json::from_str(&stdout)?;
// Verify Camoufox configuration structure
assert!(config["id"].is_string(), "Camoufox ID should be a string");
let camoufox_id = config["id"].as_str().unwrap().to_string();
tracker.track_camoufox(camoufox_id.clone());
println!("Camoufox started with ID: {camoufox_id}");
// Test stopping Camoufox
let stop_args = ["camoufox", "stop", "--id", &camoufox_id];
let stop_output = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args).await?;
assert!(stop_output.status.success(), "Camoufox stop should succeed");
tracker.cleanup_all().await;
Ok(())
}
/// Test Camoufox with URL opening
#[tokio::test]
async fn test_nodecar_camoufox_with_url() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let mut tracker = TestResourceTracker::new(nodecar_path.clone());
let temp_dir = TestUtils::create_temp_dir()?;
let profile_path = temp_dir.path().join("test_profile_url");
let args = [
"camoufox",
"start",
"--profile-path",
profile_path.to_str().unwrap(),
"--url",
"https://httpbin.org/get",
"--headless",
];
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args).await?;
if output.status.success() {
let stdout = String::from_utf8(output.stdout)?;
let config: Value = serde_json::from_str(&stdout)?;
let camoufox_id = config["id"].as_str().unwrap().to_string();
tracker.track_camoufox(camoufox_id.clone());
// Verify URL is set
if let Some(url) = config["url"].as_str() {
assert_eq!(
url, "https://httpbin.org/get",
"URL should match what was provided"
);
}
// Test stopping Camoufox explicitly
let stop_args = ["camoufox", "stop", "--id", &camoufox_id];
let stop_output = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args).await?;
assert!(stop_output.status.success(), "Camoufox stop should succeed");
} else {
println!("Skipping Camoufox URL test - likely not installed");
tracker.cleanup_all().await;
return Ok(());
}
tracker.cleanup_all().await;
Ok(())
}
/// Test Camoufox list functionality
#[tokio::test]
async fn test_nodecar_camoufox_list() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let tracker = TestResourceTracker::new(nodecar_path.clone());
// Test list command (should work even without Camoufox installed)
let list_args = ["camoufox", "list"];
let list_output = TestUtils::execute_nodecar_command(&nodecar_path, &list_args).await?;
assert!(list_output.status.success(), "Camoufox list should succeed");
let list_stdout = String::from_utf8(list_output.stdout)?;
let camoufox_list: Value = serde_json::from_str(&list_stdout)?;
assert!(camoufox_list.is_array(), "Camoufox list should be an array");
tracker.cleanup_all().await;
Ok(())
}
/// Test Camoufox process tracking and management
#[tokio::test]
async fn test_nodecar_camoufox_process_tracking(
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let mut tracker = TestResourceTracker::new(nodecar_path.clone());
let temp_dir = TestUtils::create_temp_dir()?;
let profile_path = temp_dir.path().join("test_profile_tracking");
// Start multiple Camoufox instances
let mut instance_ids: Vec<String> = Vec::new();
for i in 0..2 {
let instance_profile_path = format!("{}_instance_{}", profile_path.to_str().unwrap(), i);
let args = [
"camoufox",
"start",
"--profile-path",
&instance_profile_path,
"--headless",
];
println!("Starting Camoufox instance {i}...");
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args).await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
// If Camoufox is not installed, skip the test
if stderr.contains("not installed") || stderr.contains("not found") {
println!("Skipping Camoufox process tracking test - Camoufox not installed");
tracker.cleanup_all().await;
return Ok(());
}
tracker.cleanup_all().await;
return Err(
format!("Camoufox instance {i} start failed - stdout: {stdout}, stderr: {stderr}").into(),
);
}
let stdout = String::from_utf8(output.stdout)?;
let config: Value = serde_json::from_str(&stdout)?;
let camoufox_id = config["id"].as_str().unwrap().to_string();
instance_ids.push(camoufox_id.clone());
tracker.track_camoufox(camoufox_id.clone());
println!("Camoufox instance {i} started with ID: {camoufox_id}");
}
// Verify all instances are tracked
let list_args = ["camoufox", "list"];
let list_output = TestUtils::execute_nodecar_command(&nodecar_path, &list_args).await?;
assert!(list_output.status.success(), "Camoufox list should succeed");
let list_stdout = String::from_utf8(list_output.stdout)?;
println!("Camoufox list output: {list_stdout}");
let instances: Value = serde_json::from_str(&list_stdout)?;
let instances_array = instances.as_array().unwrap();
println!("Found {} instances in list", instances_array.len());
// Verify our instances are in the list
for instance_id in &instance_ids {
let instance_found = instances_array
.iter()
.any(|i| i["id"].as_str() == Some(instance_id));
if !instance_found {
println!("Instance {instance_id} not found in list. Available instances:");
for instance in instances_array {
if let Some(id) = instance["id"].as_str() {
println!(" - {id}");
}
}
}
assert!(
instance_found,
"Camoufox instance {instance_id} should be found in list"
);
}
// Stop all instances individually
for instance_id in &instance_ids {
println!("Stopping Camoufox instance: {instance_id}");
let stop_args = ["camoufox", "stop", "--id", instance_id];
let stop_output = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args).await?;
if stop_output.status.success() {
let stop_stdout = String::from_utf8(stop_output.stdout)?;
if let Ok(stop_result) = serde_json::from_str::<Value>(&stop_stdout) {
let success = stop_result["success"].as_bool().unwrap_or(false);
if !success {
println!("Warning: Stop command returned success=false for instance {instance_id}");
}
} else {
println!("Warning: Could not parse stop result for instance {instance_id}");
}
} else {
println!("Warning: Stop command failed for instance {instance_id}");
}
}
// Verify all instances are removed
let list_output_after = TestUtils::execute_nodecar_command(&nodecar_path, &list_args).await?;
let instances_after: Value = serde_json::from_str(&String::from_utf8(list_output_after.stdout)?)?;
let instances_after_array = instances_after.as_array().unwrap();
for instance_id in &instance_ids {
let instance_still_exists = instances_after_array
.iter()
.any(|i| i["id"].as_str() == Some(instance_id));
assert!(
!instance_still_exists,
"Stopped Camoufox instance {instance_id} should not be found in list"
);
}
println!("Camoufox process tracking test completed successfully");
tracker.cleanup_all().await;
Ok(())
}
/// Test Camoufox with various configuration options
#[tokio::test]
async fn test_nodecar_camoufox_configuration_options(
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let mut tracker = TestResourceTracker::new(nodecar_path.clone());
let temp_dir = TestUtils::create_temp_dir()?;
let profile_path = temp_dir.path().join("test_profile_config");
let args = [
"camoufox",
"start",
"--profile-path",
profile_path.to_str().unwrap(),
"--block-images",
"--max-width",
"1920",
"--max-height",
"1080",
"--headless",
];
println!("Starting Camoufox with configuration options...");
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args).await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
// If Camoufox is not installed, skip the test
if stderr.contains("not installed") || stderr.contains("not found") {
println!("Skipping Camoufox configuration test - Camoufox not installed");
tracker.cleanup_all().await;
return Ok(());
}
tracker.cleanup_all().await;
return Err(
format!("Camoufox with config start failed - stdout: {stdout}, stderr: {stderr}").into(),
);
}
let stdout = String::from_utf8(output.stdout)?;
let config: Value = serde_json::from_str(&stdout)?;
let camoufox_id = config["id"].as_str().unwrap().to_string();
tracker.track_camoufox(camoufox_id.clone());
println!("Camoufox with configuration started with ID: {camoufox_id}");
// Verify configuration was applied by checking the profile path
if let Some(returned_profile_path) = config["profilePath"].as_str() {
assert!(
returned_profile_path.contains("test_profile_config"),
"Profile path should match what was provided"
);
}
// Test stopping Camoufox explicitly
let stop_args = ["camoufox", "stop", "--id", &camoufox_id];
let stop_output = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args).await?;
assert!(stop_output.status.success(), "Camoufox stop should succeed");
println!("Camoufox configuration test completed successfully");
tracker.cleanup_all().await;
Ok(())
}
/// Test Camoufox generate-config command with basic options
#[ignore = "CI is rate limited for camoufox download"]
#[tokio::test]
async fn test_nodecar_camoufox_generate_config_basic(
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let tracker = TestResourceTracker::new(nodecar_path.clone());
let args = [
"camoufox",
"generate-config",
"--max-width",
"1920",
"--max-height",
"1080",
"--block-images",
];
println!("Testing Camoufox config generation with basic options...");
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args).await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
tracker.cleanup_all().await;
return Err(
format!("Camoufox generate-config failed - stdout: {stdout}, stderr: {stderr}").into(),
);
}
let stdout = String::from_utf8(output.stdout)?;
println!("Generated config output: {stdout}");
// Parse the generated config as JSON
let config: Value = serde_json::from_str(&stdout)?;
// Verify the config contains expected properties
assert!(
config.is_object(),
"Generated config should be a JSON object"
);
// Check for some expected fingerprint properties
assert!(
config.get("screen.width").is_some(),
"Config should contain screen.width"
);
assert!(
config.get("screen.height").is_some(),
"Config should contain screen.height"
);
assert!(
config.get("navigator.userAgent").is_some(),
"Config should contain navigator.userAgent"
);
println!("Camoufox generate-config basic test completed successfully");
tracker.cleanup_all().await;
Ok(())
}
/// Test Camoufox generate-config command with custom fingerprint
#[ignore = "CI is rate limited for camoufox download"]
#[tokio::test]
async fn test_nodecar_camoufox_generate_config_custom_fingerprint(
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let tracker = TestResourceTracker::new(nodecar_path.clone());
// Create a custom fingerprint JSON
let custom_fingerprint = r#"{
"screen.width": 1440,
"screen.height": 900,
"navigator.userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/140.0",
"navigator.platform": "TestPlatform",
"timezone": "America/New_York",
"locale:language": "en",
"locale:region": "US"
}"#;
let args = [
"camoufox",
"generate-config",
"--fingerprint",
custom_fingerprint,
"--block-webrtc",
];
println!("Testing Camoufox config generation with custom fingerprint...");
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args).await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
tracker.cleanup_all().await;
return Err(
format!("Camoufox generate-config with custom fingerprint failed - stdout: {stdout}, stderr: {stderr}").into(),
);
}
let stdout = String::from_utf8(output.stdout)?;
// Parse the generated config as JSON
let config: Value = serde_json::from_str(&stdout)?;
// Verify the config contains expected properties
assert!(
config.is_object(),
"Generated config should be a JSON object"
);
// Check that our custom values are preserved
assert_eq!(
config.get("screen.width").and_then(|v| v.as_u64()),
Some(1440),
"Custom screen width should be preserved"
);
assert_eq!(
config.get("screen.height").and_then(|v| v.as_u64()),
Some(900),
"Custom screen height should be preserved"
);
assert_eq!(
config.get("navigator.userAgent").and_then(|v| v.as_str()),
Some("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/140.0"),
"Custom user agent should be preserved"
);
assert_eq!(
config.get("timezone").and_then(|v| v.as_str()),
Some("America/New_York"),
"Custom timezone should be preserved"
);
println!("Camoufox generate-config custom fingerprint test completed successfully");
tracker.cleanup_all().await;
Ok(())
}
/// Test nodecar command validation
#[tokio::test]
async fn test_nodecar_command_validation() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let tracker = TestResourceTracker::new(nodecar_path.clone());
// Test invalid command
let invalid_args = ["invalid", "command"];
let output = TestUtils::execute_nodecar_command(&nodecar_path, &invalid_args).await?;
assert!(!output.status.success(), "Invalid command should fail");
tracker.cleanup_all().await;
Ok(())
}
/// Test concurrent proxy operations
#[tokio::test]
async fn test_nodecar_concurrent_proxies() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let mut tracker = TestResourceTracker::new(nodecar_path.clone());
// Start multiple proxies concurrently
let mut handles = vec![];
for i in 0..3 {
let nodecar_path_clone = nodecar_path.clone();
let handle = tokio::spawn(async move {
let args = [
"proxy",
"start",
"--host",
"httpbin.org",
"--proxy-port",
"80",
"--type",
"http",
];
TestUtils::execute_nodecar_command(&nodecar_path_clone, &args).await
});
handles.push((i, handle));
}
// Wait for all proxies to start
for (i, handle) in handles {
match handle.await.map_err(|e| format!("Join error: {e}"))? {
Ok(output) if output.status.success() => {
let stdout = String::from_utf8(output.stdout)?;
let config: Value = serde_json::from_str(&stdout)?;
let proxy_id = config["id"].as_str().unwrap().to_string();
tracker.track_proxy(proxy_id.clone());
println!("Proxy {i} started successfully");
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Proxy {i} failed to start: {stderr}");
}
Err(e) => {
println!("Proxy {i} error: {e}");
}
}
}
tracker.cleanup_all().await;
Ok(())
}
/// Test proxy with different upstream types
#[tokio::test]
async fn test_nodecar_proxy_types() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let mut tracker = TestResourceTracker::new(nodecar_path.clone());
let test_cases = vec![
("http", "httpbin.org", "80"),
("https", "httpbin.org", "443"),
];
for (proxy_type, host, port) in test_cases {
println!("Testing {proxy_type} proxy to {host}:{port}");
let args = [
"proxy",
"start",
"--host",
host,
"--proxy-port",
port,
"--type",
proxy_type,
];
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args).await?;
if output.status.success() {
let stdout = String::from_utf8(output.stdout)?;
let config: Value = serde_json::from_str(&stdout)?;
let proxy_id = config["id"].as_str().unwrap().to_string();
tracker.track_proxy(proxy_id.clone());
println!("{proxy_type} proxy test passed");
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
println!("{proxy_type} proxy test failed: {stderr}");
}
}
tracker.cleanup_all().await;
Ok(())
}
/// Test direct proxy (no upstream) functionality
#[tokio::test]
async fn test_nodecar_direct_proxy() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let nodecar_path = setup_test().await?;
let mut tracker = TestResourceTracker::new(nodecar_path.clone());
// Test starting a direct proxy (no upstream)
let args = ["proxy", "start"];
println!("Starting direct proxy with nodecar...");
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args).await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
tracker.cleanup_all().await;
return Err(format!("Direct proxy start failed - stdout: {stdout}, stderr: {stderr}").into());
}
let stdout = String::from_utf8(output.stdout)?;
let config: Value = serde_json::from_str(&stdout)?;
// Verify proxy configuration structure
assert!(config["id"].is_string(), "Proxy ID should be a string");
assert!(
config["localPort"].is_number(),
"Local port should be a number"
);
assert!(
config["localUrl"].is_string(),
"Local URL should be a string"
);
assert_eq!(
config["upstreamUrl"].as_str().unwrap(),
"DIRECT",
"Upstream URL should be DIRECT"
);
let proxy_id = config["id"].as_str().unwrap().to_string();
let local_port = config["localPort"].as_u64().unwrap() as u16;
tracker.track_proxy(proxy_id.clone());
println!("Direct proxy started with ID: {proxy_id} on port: {local_port}");
// Wait for the proxy to start listening
let is_listening = TestUtils::wait_for_port_state(local_port, true, 10).await;
assert!(
is_listening,
"Direct proxy should be listening on the assigned port"
);
// Test stopping the proxy
let stop_args = ["proxy", "stop", "--id", &proxy_id];
let stop_output = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args).await?;
assert!(
stop_output.status.success(),
"Direct proxy stop should succeed"
);
let port_available = TestUtils::wait_for_port_state(local_port, false, 5).await;
assert!(
port_available,
"Port should be available after stopping direct proxy"
);
println!("Direct proxy test completed successfully");
tracker.cleanup_all().await;
Ok(())
}
/// Test SOCKS5 proxy chaining - create two proxies where the second uses the first as upstream
#[tokio::test]
async fn test_nodecar_socks5_proxy_chaining() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
{
let nodecar_path = setup_test().await?;
let mut tracker = TestResourceTracker::new(nodecar_path.clone());
// Step 1: Start a SOCKS5 proxy with a known working upstream (httpbin.org)
let socks5_args = [
"proxy",
"start",
"--host",
"httpbin.org",
"--proxy-port",
"80",
"--type",
"http", // Use HTTP upstream for the first proxy
];
println!("Starting first proxy with HTTP upstream...");
let socks5_output = TestUtils::execute_nodecar_command(&nodecar_path, &socks5_args).await?;
if !socks5_output.status.success() {
let stderr = String::from_utf8_lossy(&socks5_output.stderr);
let stdout = String::from_utf8_lossy(&socks5_output.stdout);
tracker.cleanup_all().await;
return Err(format!("First proxy start failed - stdout: {stdout}, stderr: {stderr}").into());
}
let socks5_stdout = String::from_utf8(socks5_output.stdout)?;
let socks5_config: Value = serde_json::from_str(&socks5_stdout)?;
let socks5_proxy_id = socks5_config["id"].as_str().unwrap().to_string();
let socks5_local_port = socks5_config["localPort"].as_u64().unwrap() as u16;
tracker.track_proxy(socks5_proxy_id.clone());
println!("First proxy started with ID: {socks5_proxy_id} on port: {socks5_local_port}");
// Step 2: Start a second proxy that uses the first proxy as upstream
let http_proxy_args = [
"proxy",
"start",
"--upstream",
&format!("http://127.0.0.1:{socks5_local_port}"),
];
println!("Starting second proxy with first proxy as upstream...");
let http_output = TestUtils::execute_nodecar_command(&nodecar_path, &http_proxy_args).await?;
if !http_output.status.success() {
let stderr = String::from_utf8_lossy(&http_output.stderr);
let stdout = String::from_utf8_lossy(&http_output.stdout);
tracker.cleanup_all().await;
return Err(
format!("Second proxy with chained upstream failed - stdout: {stdout}, stderr: {stderr}")
.into(),
);
}
let http_stdout = String::from_utf8(http_output.stdout)?;
let http_config: Value = serde_json::from_str(&http_stdout)?;
let http_proxy_id = http_config["id"].as_str().unwrap().to_string();
let http_local_port = http_config["localPort"].as_u64().unwrap() as u16;
tracker.track_proxy(http_proxy_id.clone());
println!(
"Second proxy started with ID: {http_proxy_id} on port: {http_local_port} (chained through first proxy)"
);
// Verify both proxies are listening by waiting for them to be occupied
let socks5_listening = TestUtils::wait_for_port_state(socks5_local_port, true, 5).await;
let http_listening = TestUtils::wait_for_port_state(http_local_port, true, 5).await;
assert!(
socks5_listening,
"First proxy should be listening on port {socks5_local_port}"
);
assert!(
http_listening,
"Second proxy should be listening on port {http_local_port}"
);
// Clean up both proxies
let stop_http_args = ["proxy", "stop", "--id", &http_proxy_id];
let stop_socks5_args = ["proxy", "stop", "--id", &socks5_proxy_id];
let http_stop_result = TestUtils::execute_nodecar_command(&nodecar_path, &stop_http_args).await;
let socks5_stop_result =
TestUtils::execute_nodecar_command(&nodecar_path, &stop_socks5_args).await;
// Verify cleanup
assert!(
http_stop_result.is_ok() && http_stop_result.unwrap().status.success(),
"Second proxy stop should succeed"
);
assert!(
socks5_stop_result.is_ok() && socks5_stop_result.unwrap().status.success(),
"First proxy stop should succeed"
);
let http_port_available = TestUtils::wait_for_port_state(http_local_port, false, 5).await;
let socks5_port_available = TestUtils::wait_for_port_state(socks5_local_port, false, 5).await;
assert!(
http_port_available,
"Second proxy port should be available after stopping"
);
assert!(
socks5_port_available,
"First proxy port should be available after stopping"
);
println!("Proxy chaining test completed successfully");
tracker.cleanup_all().await;
Ok(())
}
+3 -1
View File
@@ -4,6 +4,7 @@ import "@/styles/globals.css";
import { CustomThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { WindowDragArea } from "@/components/window-drag-area";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -23,9 +24,10 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} antialiased overflow-hidden`}
>
<CustomThemeProvider>
<WindowDragArea />
<TooltipProvider>{children}</TooltipProvider>
<Toaster />
</CustomThemeProvider>
+616 -210
View File
@@ -1,26 +1,28 @@
"use client";
import { ChangeVersionDialog } from "@/components/change-version-dialog";
import { CreateProfileDialog } from "@/components/create-profile-dialog";
import { ProfilesDataTable } from "@/components/profile-data-table";
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
import { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
import { SettingsDialog } from "@/components/settings-dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
import { showErrorToast } from "@/lib/toast-utils";
import type { BrowserProfile, ProxySettings } from "@/types";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { getCurrent } from "@tauri-apps/plugin-deep-link";
import { useCallback, useEffect, useRef, useState } from "react";
import { GoGear, GoPlus } from "react-icons/go";
import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog";
import { CreateProfileDialog } from "@/components/create-profile-dialog";
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog";
import { GroupAssignmentDialog } from "@/components/group-assignment-dialog";
import { GroupBadges } from "@/components/group-badges";
import { GroupManagementDialog } from "@/components/group-management-dialog";
import HomeHeader from "@/components/home-header";
import { ImportProfileDialog } from "@/components/import-profile-dialog";
import { PermissionDialog } from "@/components/permission-dialog";
import { ProfilesDataTable } from "@/components/profile-data-table";
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
import { ProxyManagementDialog } from "@/components/proxy-management-dialog";
import { SettingsDialog } from "@/components/settings-dialog";
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
import type { PermissionType } from "@/hooks/use-permissions";
import { usePermissions } from "@/hooks/use-permissions";
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
import { showErrorToast, showToast } from "@/lib/toast-utils";
import type { BrowserProfile, CamoufoxConfig, GroupWithCount } from "@/types";
type BrowserTypeString =
| "mullvad-browser"
@@ -29,7 +31,8 @@ type BrowserTypeString =
| "chromium"
| "brave"
| "zen"
| "tor-browser";
| "tor-browser"
| "camoufox";
interface PendingUrl {
id: string;
@@ -37,17 +40,119 @@ interface PendingUrl {
}
export default function Home() {
const [isInitializing, setIsInitializing] = useState(true);
const [profiles, setProfiles] = useState<BrowserProfile[]>([]);
const [error, setError] = useState<string | null>(null);
const [proxyDialogOpen, setProxyDialogOpen] = useState(false);
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false);
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false);
const [proxyManagementDialogOpen, setProxyManagementDialogOpen] =
useState(false);
const [camoufoxConfigDialogOpen, setCamoufoxConfigDialogOpen] =
useState(false);
const [groupManagementDialogOpen, setGroupManagementDialogOpen] =
useState(false);
const [groupAssignmentDialogOpen, setGroupAssignmentDialogOpen] =
useState(false);
const [selectedGroupId, setSelectedGroupId] = useState<string>("default");
const [selectedProfilesForGroup, setSelectedProfilesForGroup] = useState<
string[]
>([]);
const [selectedProfiles, setSelectedProfiles] = useState<string[]>([]);
const [pendingUrls, setPendingUrls] = useState<PendingUrl[]>([]);
const [currentProfileForProxy, setCurrentProfileForProxy] =
useState<BrowserProfile | null>(null);
const [currentProfileForVersionChange, setCurrentProfileForVersionChange] =
const [currentProfileForCamoufoxConfig, setCurrentProfileForCamoufoxConfig] =
useState<BrowserProfile | null>(null);
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
const [groups, setGroups] = useState<GroupWithCount[]>([]);
const [areGroupsLoading, setGroupsLoading] = useState(true);
const [currentPermissionType, setCurrentPermissionType] =
useState<PermissionType>("microphone");
const [showBulkDeleteConfirmation, setShowBulkDeleteConfirmation] =
useState(false);
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } =
usePermissions();
const handleSelectGroup = useCallback((groupId: string) => {
setSelectedGroupId(groupId);
setSelectedProfiles([]);
}, []);
// Check for missing binaries and offer to download them
const checkMissingBinaries = useCallback(async () => {
try {
const missingBinaries = await invoke<[string, string, string][]>(
"check_missing_binaries",
);
// Also check for missing GeoIP database
const missingGeoIP = await invoke<boolean>(
"check_missing_geoip_database",
);
if (missingBinaries.length > 0 || missingGeoIP) {
if (missingBinaries.length > 0) {
console.log("Found missing binaries:", missingBinaries);
}
if (missingGeoIP) {
console.log("Found missing GeoIP database for Camoufox");
}
// Group missing binaries by browser type to avoid concurrent downloads
const browserMap = new Map<string, string[]>();
for (const [profileName, browser, version] of missingBinaries) {
if (!browserMap.has(browser)) {
browserMap.set(browser, []);
}
const versions = browserMap.get(browser);
if (versions) {
versions.push(`${version} (for ${profileName})`);
}
}
// Show a toast notification about missing binaries and auto-download them
let missingList = Array.from(browserMap.entries())
.map(([browser, versions]) => `${browser}: ${versions.join(", ")}`)
.join(", ");
if (missingGeoIP) {
if (missingList) {
missingList += ", GeoIP database for Camoufox";
} else {
missingList = "GeoIP database for Camoufox";
}
}
console.log(`Downloading missing components: ${missingList}`);
try {
// Download missing binaries and GeoIP database sequentially to prevent conflicts
const downloaded = await invoke<string[]>(
"ensure_all_binaries_exist",
);
if (downloaded.length > 0) {
console.log(
"Successfully downloaded missing components:",
downloaded,
);
}
} catch (downloadError) {
console.error(
"Failed to download missing components:",
downloadError,
);
setError(
`Failed to download missing components: ${JSON.stringify(
downloadError,
)}`,
);
}
}
} catch (err: unknown) {
console.error("Failed to check missing components:", err);
}
}, []);
// Simple profiles loader without updates check (for use as callback)
const loadProfiles = useCallback(async () => {
@@ -56,13 +161,48 @@ export default function Home() {
"list_browser_profiles",
);
setProfiles(profileList);
// Check for missing binaries after loading profiles
await checkMissingBinaries();
} catch (err: unknown) {
console.error("Failed to load profiles:", err);
setError(`Failed to load profiles: ${JSON.stringify(err)}`);
}
}, []);
}, [checkMissingBinaries]);
// Auto-update functionality - pass loadProfiles to refresh profiles after updates
const [processingUrls, setProcessingUrls] = useState<Set<string>>(new Set());
const handleUrlOpen = useCallback(
async (url: string) => {
// Prevent duplicate processing of the same URL
if (processingUrls.has(url)) {
console.log("URL already being processed:", url);
return;
}
setProcessingUrls((prev) => new Set(prev).add(url));
try {
console.log("URL received for opening:", url);
// Always show profile selector for manual selection - never auto-open
// Replace any existing pending URL with the new one
setPendingUrls([{ id: Date.now().toString(), url }]);
} finally {
// Remove URL from processing set after a short delay to prevent rapid duplicates
setTimeout(() => {
setProcessingUrls((prev) => {
const next = new Set(prev);
next.delete(url);
return next;
});
}, 1000);
}
},
[processingUrls],
);
// Auto-update functionality - use the existing hook for compatibility
const updateNotifications = useUpdateNotifications(loadProfiles);
const { checkForUpdates, isUpdating } = updateNotifications;
@@ -76,40 +216,37 @@ export default function Home() {
// Check for updates after loading profiles
await checkForUpdates();
await checkMissingBinaries();
} catch (err: unknown) {
console.error("Failed to load profiles:", err);
setError(`Failed to load profiles: ${JSON.stringify(err)}`);
}
}, [checkForUpdates]);
}, [checkForUpdates, checkMissingBinaries]);
useAppUpdateNotifications();
useEffect(() => {
void loadProfilesWithUpdateCheck();
// Check for startup URLs but only process them once
const [hasCheckedStartupUrl, setHasCheckedStartupUrl] = useState(false);
const checkCurrentUrl = useCallback(async () => {
if (hasCheckedStartupUrl) return;
// Check for startup default browser prompt
void checkStartupPrompt();
try {
const currentUrl = await getCurrent();
if (currentUrl && currentUrl.length > 0) {
console.log("Startup URL detected:", currentUrl[0]);
void handleUrlOpen(currentUrl[0]);
}
} catch (error) {
console.error("Failed to check current URL:", error);
} finally {
setHasCheckedStartupUrl(true);
}
}, [handleUrlOpen, hasCheckedStartupUrl]);
// Listen for URL open events
void listenForUrlEvents();
const checkStartupPrompt = useCallback(async () => {
// Only check once during app startup to prevent reopening after dismissing notifications
if (hasCheckedStartupPrompt) return;
// Check for startup URLs (when app was launched as default browser)
void checkStartupUrls();
// Set up periodic update checks (every 30 minutes)
const updateInterval = setInterval(
() => {
void checkForUpdates();
},
30 * 60 * 1000,
);
return () => {
clearInterval(updateInterval);
};
}, [loadProfilesWithUpdateCheck, checkForUpdates]);
const checkStartupPrompt = async () => {
try {
const shouldShow = await invoke<boolean>(
"should_show_settings_on_startup",
@@ -119,23 +256,70 @@ export default function Home() {
}
} catch (error) {
console.error("Failed to check startup prompt:", error);
} finally {
setHasCheckedStartupPrompt(true);
}
};
}, [hasCheckedStartupPrompt]);
const checkStartupUrls = async () => {
// Warm up nodecar at startup and block UI until complete
useEffect(() => {
let cancelled = false;
(async () => {
try {
await invoke("warm_up_nodecar");
} catch (err) {
if (!cancelled) {
setError(
`Initialization failed: ${err instanceof Error ? err.message : String(err)}`,
);
}
} finally {
if (!cancelled) setIsInitializing(false);
}
})();
return () => {
cancelled = true;
};
}, []);
const checkAllPermissions = useCallback(async () => {
try {
const hasStartupUrl = await invoke<boolean>(
"check_and_handle_startup_url",
);
if (hasStartupUrl) {
console.log("Handled startup URL successfully");
// Wait for permissions to be initialized before checking
if (!isInitialized) {
return;
}
// Check if any permissions are not granted - prioritize missing permissions
if (!isMicrophoneAccessGranted) {
setCurrentPermissionType("microphone");
setPermissionDialogOpen(true);
} else if (!isCameraAccessGranted) {
setCurrentPermissionType("camera");
setPermissionDialogOpen(true);
}
} catch (error) {
console.error("Failed to check startup URLs:", error);
console.error("Failed to check permissions:", error);
}
};
}, [isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized]);
const listenForUrlEvents = async () => {
const checkNextPermission = useCallback(() => {
try {
if (!isMicrophoneAccessGranted) {
setCurrentPermissionType("microphone");
setPermissionDialogOpen(true);
} else if (!isCameraAccessGranted) {
setCurrentPermissionType("camera");
setPermissionDialogOpen(true);
} else {
setPermissionDialogOpen(false);
}
} catch (error) {
console.error("Failed to check next permission:", error);
}
}, [isMicrophoneAccessGranted, isCameraAccessGranted]);
const listenForUrlEvents = useCallback(async () => {
try {
// Listen for URL open events from the deep link handler (when app is already running)
await listen<string>("url-open-request", (event) => {
@@ -146,10 +330,7 @@ export default function Home() {
// Listen for show profile selector events
await listen<string>("show-profile-selector", (event) => {
console.log("Received show profile selector request:", event.payload);
setPendingUrls((prev) => [
...prev,
{ id: Date.now().toString(), url: event.payload },
]);
void handleUrlOpen(event.payload);
});
// Listen for show create profile dialog events
@@ -163,89 +344,100 @@ export default function Home() {
);
setCreateProfileDialogOpen(true);
});
// Listen for custom logo click events
const handleLogoUrlEvent = (event: CustomEvent) => {
console.log("Received logo URL event:", event.detail);
void handleUrlOpen(event.detail);
};
window.addEventListener(
"url-open-request",
handleLogoUrlEvent as EventListener,
);
// Return cleanup function
return () => {
window.removeEventListener(
"url-open-request",
handleLogoUrlEvent as EventListener,
);
};
} catch (error) {
console.error("Failed to setup URL listener:", error);
}
};
}, [handleUrlOpen]);
const handleUrlOpen = async (url: string) => {
try {
// Use smart profile selection
const result = await invoke<string>("smart_open_url", {
url,
});
console.log("Smart URL opening succeeded:", result);
// URL was handled successfully
} catch (error: unknown) {
console.log(
"Smart URL opening failed or requires profile selection:",
error,
);
// Show profile selector for manual selection
setPendingUrls((prev) => [...prev, { id: Date.now().toString(), url }]);
}
};
const openProxyDialog = useCallback((profile: BrowserProfile | null) => {
setCurrentProfileForProxy(profile);
setProxyDialogOpen(true);
const handleConfigureCamoufox = useCallback((profile: BrowserProfile) => {
setCurrentProfileForCamoufoxConfig(profile);
setCamoufoxConfigDialogOpen(true);
}, []);
const openChangeVersionDialog = useCallback((profile: BrowserProfile) => {
setCurrentProfileForVersionChange(profile);
setChangeVersionDialogOpen(true);
}, []);
const handleSaveProxy = useCallback(
async (proxySettings: ProxySettings) => {
setProxyDialogOpen(false);
const handleSaveCamoufoxConfig = useCallback(
async (profile: BrowserProfile, config: CamoufoxConfig) => {
setError(null);
try {
if (currentProfileForProxy) {
await invoke("update_profile_proxy", {
profileName: currentProfileForProxy.name,
proxy: proxySettings,
});
}
await invoke("update_camoufox_config", {
profileName: profile.name,
config,
});
await loadProfiles();
setCamoufoxConfigDialogOpen(false);
} catch (err: unknown) {
console.error("Failed to update proxy settings:", err);
setError(`Failed to update proxy settings: ${JSON.stringify(err)}`);
console.error("Failed to update camoufox config:", err);
setError(`Failed to update camoufox config: ${JSON.stringify(err)}`);
throw err;
}
},
[currentProfileForProxy, loadProfiles],
[loadProfiles],
);
const loadGroups = useCallback(async () => {
setGroupsLoading(true);
try {
const groupsWithCounts = await invoke<GroupWithCount[]>(
"get_groups_with_profile_counts",
);
setGroups(groupsWithCounts);
} catch (err) {
console.error("Failed to load groups with counts:", err);
setGroups([]);
} finally {
setGroupsLoading(false);
}
}, []);
const handleCreateProfile = useCallback(
async (profileData: {
name: string;
browserStr: BrowserTypeString;
version: string;
proxy?: ProxySettings;
releaseType: string;
proxyId?: string;
camoufoxConfig?: CamoufoxConfig;
groupId?: string;
}) => {
setError(null);
try {
const profile = await invoke<BrowserProfile>(
const _profile = await invoke<BrowserProfile>(
"create_browser_profile_new",
{
name: profileData.name,
browserStr: profileData.browserStr,
version: profileData.version,
releaseType: profileData.releaseType,
proxyId: profileData.proxyId,
camoufoxConfig: profileData.camoufoxConfig,
groupId:
profileData.groupId ||
(selectedGroupId !== "default" ? selectedGroupId : undefined),
},
);
// Update proxy if provided
if (profileData.proxy) {
await invoke("update_profile_proxy", {
profileName: profile.name,
proxy: profileData.proxy,
});
}
await loadProfiles();
await loadGroups();
// Trigger proxy data reload in the table
} catch (error) {
setError(
`Failed to create profile: ${
@@ -255,7 +447,7 @@ export default function Home() {
throw error;
}
},
[loadProfiles],
[loadProfiles, loadGroups, selectedGroupId],
);
const [runningProfiles, setRunningProfiles] = useState<Set<string>>(
@@ -273,6 +465,9 @@ export default function Home() {
const currentRunning = runningProfilesRef.current.has(profile.name);
if (isRunning !== currentRunning) {
console.log(
`Profile ${profile.name} (${profile.browser}) status changed: ${currentRunning} -> ${isRunning}`,
);
setRunningProfiles((prev) => {
const next = new Set(prev);
if (isRunning) {
@@ -327,43 +522,43 @@ export default function Home() {
[loadProfiles, checkBrowserStatus, isUpdating],
);
useEffect(() => {
if (profiles.length === 0) return;
const interval = setInterval(() => {
for (const profile of profiles) {
void checkBrowserStatus(profile);
}
}, 500);
return () => {
clearInterval(interval);
};
}, [profiles, checkBrowserStatus]);
useEffect(() => {
runningProfilesRef.current = runningProfiles;
}, [runningProfiles]);
useEffect(() => {
if (error) {
showErrorToast(error);
setError(null);
}
}, [error]);
const handleDeleteProfile = useCallback(
async (profile: BrowserProfile) => {
setError(null);
console.log("Attempting to delete profile:", profile.name);
try {
// First check if the browser is running for this profile
const isRunning = await invoke<boolean>("check_browser_status", {
profile,
});
if (isRunning) {
setError(
"Cannot delete profile while browser is running. Please stop the browser first.",
);
return;
}
// Attempt to delete the profile
await invoke("delete_profile", { profileName: profile.name });
console.log("Profile deletion command completed successfully");
// Give a small delay to ensure file system operations complete
await new Promise((resolve) => setTimeout(resolve, 500));
// Reload profiles and groups to ensure UI is updated
await loadProfiles();
await loadGroups();
console.log("Profile deleted and profiles reloaded successfully");
} catch (err: unknown) {
console.error("Failed to delete profile:", err);
setError(`Failed to delete profile: ${JSON.stringify(err)}`);
const errorMessage = err instanceof Error ? err.message : String(err);
setError(`Failed to delete profile: ${errorMessage}`);
}
},
[loadProfiles],
[loadProfiles, loadGroups],
);
const handleRenameProfile = useCallback(
@@ -395,71 +590,228 @@ export default function Home() {
[loadProfiles],
);
const handleDeleteSelectedProfiles = useCallback(
async (profileNames: string[]) => {
setError(null);
try {
await invoke("delete_selected_profiles", { profileNames });
await loadProfiles();
await loadGroups();
} catch (err: unknown) {
console.error("Failed to delete selected profiles:", err);
setError(`Failed to delete selected profiles: ${JSON.stringify(err)}`);
}
},
[loadProfiles, loadGroups],
);
const handleAssignProfilesToGroup = useCallback((profileNames: string[]) => {
setSelectedProfilesForGroup(profileNames);
setGroupAssignmentDialogOpen(true);
}, []);
const handleBulkDelete = useCallback(() => {
if (selectedProfiles.length === 0) return;
setShowBulkDeleteConfirmation(true);
}, [selectedProfiles]);
const confirmBulkDelete = useCallback(async () => {
if (selectedProfiles.length === 0) return;
setIsBulkDeleting(true);
try {
await invoke("delete_selected_profiles", {
profileNames: selectedProfiles,
});
await loadProfiles();
await loadGroups();
setSelectedProfiles([]);
setShowBulkDeleteConfirmation(false);
} catch (error) {
console.error("Failed to delete selected profiles:", error);
setError(`Failed to delete selected profiles: ${JSON.stringify(error)}`);
} finally {
setIsBulkDeleting(false);
}
}, [selectedProfiles, loadProfiles, loadGroups]);
const handleBulkGroupAssignment = useCallback(() => {
if (selectedProfiles.length === 0) return;
handleAssignProfilesToGroup(selectedProfiles);
setSelectedProfiles([]);
}, [selectedProfiles, handleAssignProfilesToGroup]);
const handleGroupAssignmentComplete = useCallback(async () => {
await loadProfiles();
await loadGroups();
setGroupAssignmentDialogOpen(false);
setSelectedProfilesForGroup([]);
}, [loadProfiles, loadGroups]);
const handleGroupManagementComplete = useCallback(async () => {
await loadGroups();
}, [loadGroups]);
useEffect(() => {
void loadProfilesWithUpdateCheck();
void loadGroups();
// Check for startup default browser prompt
void checkStartupPrompt();
// Listen for URL open events and get cleanup function
const setupListeners = async () => {
const cleanup = await listenForUrlEvents();
return cleanup;
};
let cleanup: (() => void) | undefined;
setupListeners().then((cleanupFn) => {
cleanup = cleanupFn;
});
// Check for startup URLs (when app was launched as default browser)
void checkCurrentUrl();
// Set up periodic update checks (every 30 minutes)
const updateInterval = setInterval(
() => {
void checkForUpdates();
},
30 * 60 * 1000,
);
return () => {
clearInterval(updateInterval);
if (cleanup) {
cleanup();
}
};
}, [
loadProfilesWithUpdateCheck,
checkForUpdates,
checkStartupPrompt,
listenForUrlEvents,
checkCurrentUrl,
loadGroups,
]);
// Show deprecation warning for unsupported profiles (with names)
useEffect(() => {
if (profiles.length === 0) return;
const deprecatedProfiles = profiles.filter(
(p) =>
["tor-browser", "mullvad-browser"].includes(p.browser) ||
(p.release_type === "nightly" && p.browser !== "firefox-developer"),
);
if (deprecatedProfiles.length > 0) {
const deprecatedNames = deprecatedProfiles.map((p) => p.name).join(", ");
// Use a stable id to avoid duplicate toasts on re-renders
showToast({
id: "deprecated-profiles-warning",
type: "error",
title: "Some profiles will be deprecated soon",
description: `The following profiles will be deprecated soon: ${deprecatedNames}. Tor Browser, Mullvad Browser, and nightly profiles (except Firefox Developers Edition) will be removed in upcoming versions. Please check GitHub for migration instructions.`,
duration: 15000,
action: {
label: "Learn more",
onClick: () => {
const event = new CustomEvent("url-open-request", {
detail: "https://github.com/zhom/donutbrowser/discussions/66",
});
window.dispatchEvent(event);
},
},
});
}
}, [profiles]);
useEffect(() => {
if (profiles.length === 0) return;
const interval = setInterval(() => {
for (const profile of profiles) {
void checkBrowserStatus(profile);
}
}, 500);
return () => {
clearInterval(interval);
};
}, [profiles, checkBrowserStatus]);
useEffect(() => {
runningProfilesRef.current = runningProfiles;
}, [runningProfiles]);
useEffect(() => {
if (error) {
showErrorToast(error);
setError(null);
}
}, [error]);
// Check permissions when they are initialized
useEffect(() => {
if (isInitialized) {
void checkAllPermissions();
}
}, [isInitialized, checkAllPermissions]);
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 gap-8 sm:p-12 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center w-full max-w-3xl">
<Card className="w-full">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Profiles</CardTitle>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={() => {
setSettingsDialogOpen(true);
}}
className="flex items-center gap-2"
>
<GoGear className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Settings</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
onClick={() => {
setCreateProfileDialogOpen(true);
}}
className="flex items-center gap-2"
>
<GoPlus className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Create a new profile</TooltipContent>
</Tooltip>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
<ProfilesDataTable
data={profiles}
onLaunchProfile={launchProfile}
onKillProfile={handleKillProfile}
onProxySettings={openProxyDialog}
onDeleteProfile={handleDeleteProfile}
onRenameProfile={handleRenameProfile}
onChangeVersion={openChangeVersionDialog}
runningProfiles={runningProfiles}
isUpdating={isUpdating}
/>
</CardContent>
</Card>
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen gap-8 font-[family-name:var(--font-geist-sans)] bg-white dark:bg-black">
<main className="flex flex-col row-start-2 gap-6 items-center w-full max-w-3xl">
<div className="w-full">
<HomeHeader
selectedProfiles={selectedProfiles}
onBulkDelete={handleBulkDelete}
onBulkGroupAssignment={handleBulkGroupAssignment}
onCreateProfileDialogOpen={setCreateProfileDialogOpen}
onGroupManagementDialogOpen={setGroupManagementDialogOpen}
onImportProfileDialogOpen={setImportProfileDialogOpen}
onProxyManagementDialogOpen={setProxyManagementDialogOpen}
onSettingsDialogOpen={setSettingsDialogOpen}
/>
</div>
<div className="space-y-4 w-full">
<GroupBadges
selectedGroupId={selectedGroupId}
onGroupSelect={handleSelectGroup}
groups={groups}
isLoading={areGroupsLoading}
/>
<ProfilesDataTable
data={profiles}
onLaunchProfile={launchProfile}
onKillProfile={handleKillProfile}
onDeleteProfile={handleDeleteProfile}
onRenameProfile={handleRenameProfile}
onConfigureCamoufox={handleConfigureCamoufox}
runningProfiles={runningProfiles}
isUpdating={isUpdating}
onDeleteSelectedProfiles={handleDeleteSelectedProfiles}
onAssignProfilesToGroup={handleAssignProfilesToGroup}
selectedGroupId={selectedGroupId}
selectedProfiles={selectedProfiles}
onSelectedProfilesChange={setSelectedProfiles}
/>
</div>
</main>
<ProxySettingsDialog
isOpen={proxyDialogOpen}
onClose={() => {
setProxyDialogOpen(false);
}}
onSave={(proxy: ProxySettings) => void handleSaveProxy(proxy)}
initialSettings={currentProfileForProxy?.proxy}
browserType={currentProfileForProxy?.browser}
/>
{isInitializing && (
<div className="fixed inset-0 z-[100000] backdrop-blur-sm bg-black/30 flex items-center justify-center">
<div className="bg-white dark:bg-neutral-900 rounded-xl p-6 shadow-xl border border-black/10 dark:border-white/10 w-[320px] text-center">
<div className="text-lg font-medium">Initializing</div>
<div className="mt-1 mb-2 text-sm text-gray-600 dark:text-gray-300">
Please don't close the app
</div>
<div className="mx-auto mb-4 w-8 h-8 rounded-full border-2 border-gray-300 animate-spin border-t-gray-900 dark:border-gray-700 dark:border-t-white" />
</div>
</div>
)}
<CreateProfileDialog
isOpen={createProfileDialogOpen}
@@ -467,6 +819,7 @@ export default function Home() {
setCreateProfileDialogOpen(false);
}}
onCreateProfile={handleCreateProfile}
selectedGroupId={selectedGroupId}
/>
<SettingsDialog
@@ -476,13 +829,19 @@ export default function Home() {
}}
/>
<ChangeVersionDialog
isOpen={changeVersionDialogOpen}
<ImportProfileDialog
isOpen={importProfileDialogOpen}
onClose={() => {
setChangeVersionDialogOpen(false);
setImportProfileDialogOpen(false);
}}
onImportComplete={() => void loadProfiles()}
/>
<ProxyManagementDialog
isOpen={proxyManagementDialogOpen}
onClose={() => {
setProxyManagementDialogOpen(false);
}}
profile={currentProfileForVersionChange}
onVersionChanged={() => void loadProfiles()}
/>
{pendingUrls.map((pendingUrl) => (
@@ -495,9 +854,56 @@ export default function Home() {
);
}}
url={pendingUrl.url}
isUpdating={isUpdating}
runningProfiles={runningProfiles}
/>
))}
<PermissionDialog
isOpen={permissionDialogOpen}
onClose={() => {
setPermissionDialogOpen(false);
}}
permissionType={currentPermissionType}
onPermissionGranted={checkNextPermission}
/>
<CamoufoxConfigDialog
isOpen={camoufoxConfigDialogOpen}
onClose={() => {
setCamoufoxConfigDialogOpen(false);
}}
profile={currentProfileForCamoufoxConfig}
onSave={handleSaveCamoufoxConfig}
/>
<GroupManagementDialog
isOpen={groupManagementDialogOpen}
onClose={() => {
setGroupManagementDialogOpen(false);
}}
onGroupManagementComplete={handleGroupManagementComplete}
/>
<GroupAssignmentDialog
isOpen={groupAssignmentDialogOpen}
onClose={() => {
setGroupAssignmentDialogOpen(false);
}}
selectedProfiles={selectedProfilesForGroup}
onAssignmentComplete={handleGroupAssignmentComplete}
/>
<DeleteConfirmationDialog
isOpen={showBulkDeleteConfirmation}
onClose={() => setShowBulkDeleteConfirmation(false)}
onConfirm={confirmBulkDelete}
title="Delete Selected Profiles"
description={`This action cannot be undone. This will permanently delete ${selectedProfiles.length} profile${selectedProfiles.length !== 1 ? "s" : ""} and all associated data.`}
confirmButtonText={`Delete ${selectedProfiles.length} Profile${selectedProfiles.length !== 1 ? "s" : ""}`}
isLoading={isBulkDeleting}
profileNames={selectedProfiles}
/>
</div>
);
}
+108 -52
View File
@@ -1,26 +1,52 @@
"use client";
import { FaDownload, FaTimes } from "react-icons/fa";
import { LuCheckCheck, LuCog, LuRefreshCw } from "react-icons/lu";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import React from "react";
import { FaDownload, FaTimes } from "react-icons/fa";
import { LuRefreshCw } from "react-icons/lu";
interface AppUpdateInfo {
current_version: string;
new_version: string;
release_notes: string;
download_url: string;
is_nightly: boolean;
published_at: string;
}
import type { AppUpdateInfo, AppUpdateProgress } from "@/types";
import { RippleButton } from "./ui/ripple";
interface AppUpdateToastProps {
updateInfo: AppUpdateInfo;
onUpdate: (updateInfo: AppUpdateInfo) => Promise<void>;
onDismiss: () => void;
isUpdating?: boolean;
updateProgress?: string;
updateProgress?: AppUpdateProgress | null;
}
function getStageIcon(stage?: string, isUpdating?: boolean) {
if (!isUpdating) {
return <FaDownload className="flex-shrink-0 w-5 h-5" />;
}
switch (stage) {
case "downloading":
return <FaDownload className="flex-shrink-0 w-5 h-5" />;
case "extracting":
return <LuRefreshCw className="flex-shrink-0 w-5 h-5 animate-spin" />;
case "installing":
return <LuCog className="flex-shrink-0 w-5 h-5 animate-spin" />;
case "completed":
return <LuCheckCheck className="flex-shrink-0 w-5 h-5" />;
default:
return <LuRefreshCw className="flex-shrink-0 w-5 h-5 animate-spin" />;
}
}
function getStageDisplayName(stage?: string) {
switch (stage) {
case "downloading":
return "Downloading";
case "extracting":
return "Extracting";
case "installing":
return "Installing";
case "completed":
return "Completed";
default:
return "Updating";
}
}
export function AppUpdateToast({
@@ -34,22 +60,32 @@ export function AppUpdateToast({
await onUpdate(updateInfo);
};
const showDownloadProgress =
isUpdating &&
updateProgress?.stage === "downloading" &&
updateProgress.percentage !== undefined;
const showOtherStageProgress =
isUpdating &&
updateProgress &&
(updateProgress.stage === "extracting" ||
updateProgress.stage === "installing" ||
updateProgress.stage === "completed");
return (
<div className="flex items-start w-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 shadow-lg max-w-md">
<div className="flex items-start p-4 w-full max-w-md rounded-lg border shadow-lg bg-card border-border text-card-foreground">
<div className="mr-3 mt-0.5">
{isUpdating ? (
<LuRefreshCw className="h-5 w-5 text-blue-500 animate-spin flex-shrink-0" />
) : (
<FaDownload className="h-5 w-5 text-blue-500 flex-shrink-0" />
)}
{getStageIcon(updateProgress?.stage, isUpdating)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="flex gap-2 justify-between items-start">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-semibold text-foreground text-sm">
Donut Browser Update Available
<div className="flex gap-2 items-center">
<span className="text-sm font-semibold text-foreground">
{isUpdating
? `${getStageDisplayName(updateProgress?.stage)} Donut Browser Update`
: "Donut Browser Update Available"}
</span>
<Badge
variant={updateInfo.is_nightly ? "secondary" : "default"}
@@ -59,8 +95,14 @@ export function AppUpdateToast({
</Badge>
</div>
<div className="text-xs text-muted-foreground">
Update from {updateInfo.current_version} to{" "}
<span className="font-medium">{updateInfo.new_version}</span>
{isUpdating ? (
updateProgress?.message || "Updating..."
) : (
<>
Update from {updateInfo.current_version} to{" "}
<span className="font-medium">{updateInfo.new_version}</span>
</>
)}
</div>
</div>
@@ -69,52 +111,66 @@ export function AppUpdateToast({
variant="ghost"
size="sm"
onClick={onDismiss}
className="h-6 w-6 p-0 shrink-0"
className="p-0 w-6 h-6 shrink-0"
>
<FaTimes className="h-3 w-3" />
<FaTimes className="w-3 h-3" />
</Button>
)}
</div>
{isUpdating && updateProgress && (
<div className="mt-2">
<p className="text-xs text-muted-foreground">{updateProgress}</p>
{/* Download progress */}
{showDownloadProgress && updateProgress && (
<div className="mt-2 space-y-1">
<div className="flex justify-between items-center">
<p className="flex-1 min-w-0 text-xs text-muted-foreground">
{updateProgress.percentage?.toFixed(1)}%
{updateProgress.speed && `${updateProgress.speed} MB/s`}
{updateProgress.eta && `${updateProgress.eta} remaining`}
</p>
</div>
<div className="w-full bg-muted rounded-full h-1.5">
<div
className="bg-primary h-1.5 rounded-full transition-all duration-300"
style={{ width: `${updateProgress.percentage}%` }}
/>
</div>
</div>
)}
{/* Other stage progress (with visual indicators) */}
{showOtherStageProgress && (
<div className="mt-2 space-y-1">
{/* Progress indicator for non-downloading stages */}
<div className="w-full bg-muted rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all duration-500 ${
updateProgress.stage === "completed"
? "bg-green-500 w-full"
: "bg-primary w-full animate-pulse"
}`}
/>
</div>
</div>
)}
{!isUpdating && (
<div className="flex items-center gap-2 mt-3">
<Button
<div className="flex gap-2 items-center mt-3">
<RippleButton
onClick={() => void handleUpdateClick()}
size="sm"
className="flex items-center gap-2 text-xs"
className="flex gap-2 items-center text-xs"
>
<FaDownload className="h-3 w-3" />
<FaDownload className="w-3 h-3" />
Update Now
</Button>
<Button
</RippleButton>
<RippleButton
variant="outline"
onClick={onDismiss}
size="sm"
className="text-xs"
>
Later
</Button>
</div>
)}
{updateInfo.release_notes && !isUpdating && (
<div className="mt-2">
<details className="text-xs">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
Release Notes
</summary>
<div className="mt-1 text-muted-foreground whitespace-pre-wrap max-h-32 overflow-y-auto">
{updateInfo.release_notes.length > 200
? `${updateInfo.release_notes.substring(0, 200)}...`
: updateInfo.release_notes}
</div>
</details>
</RippleButton>
</div>
)}
</div>
+135
View File
@@ -0,0 +1,135 @@
"use client";
import { useEffect, useState } from "react";
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import type { BrowserProfile, CamoufoxConfig } from "@/types";
import { LoadingButton } from "./loading-button";
import { RippleButton } from "./ui/ripple";
interface CamoufoxConfigDialogProps {
isOpen: boolean;
onClose: () => void;
profile: BrowserProfile | null;
onSave: (profile: BrowserProfile, config: CamoufoxConfig) => Promise<void>;
}
export function CamoufoxConfigDialog({
isOpen,
onClose,
profile,
onSave,
}: CamoufoxConfigDialogProps) {
const [config, setConfig] = useState<CamoufoxConfig>({
geoip: true,
});
const [isSaving, setIsSaving] = useState(false);
// Initialize config when profile changes
useEffect(() => {
if (profile && profile.browser === "camoufox") {
setConfig(
profile.camoufox_config || {
geoip: true,
},
);
}
}, [profile]);
const updateConfig = (key: keyof CamoufoxConfig, value: unknown) => {
setConfig((prev) => ({ ...prev, [key]: value }));
};
const handleSave = async () => {
if (!profile) return;
// Validate fingerprint JSON if it exists
if (config.fingerprint) {
try {
JSON.parse(config.fingerprint);
} catch (_error) {
const { toast } = await import("sonner");
toast.error("Invalid fingerprint configuration", {
description:
"The fingerprint configuration contains invalid JSON. Please check your advanced settings.",
});
return;
}
}
setIsSaving(true);
try {
await onSave(profile, config);
onClose();
} catch (error) {
console.error("Failed to save camoufox config:", error);
const { toast } = await import("sonner");
toast.error("Failed to save configuration", {
description:
error instanceof Error ? error.message : "Unknown error occurred",
});
} finally {
setIsSaving(false);
}
};
const handleClose = () => {
// Reset config to original when closing without saving
if (profile && profile.browser === "camoufox") {
setConfig(
profile.camoufox_config || {
geoip: true,
},
);
}
onClose();
};
if (!profile || profile.browser !== "camoufox") {
return null;
}
// No OS warning needed anymore since we removed OS selection
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-3xl max-h-[90vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>
Configure Fingerprint Settings - {profile.name}
</DialogTitle>
</DialogHeader>
<ScrollArea className="flex-1 h-[400px]">
<div className="py-4">
<SharedCamoufoxConfigForm
config={config}
onConfigChange={updateConfig}
forceAdvanced={true}
/>
</div>
</ScrollArea>
<DialogFooter className="flex-shrink-0 pt-4 border-t">
<RippleButton variant="outline" onClick={handleClose}>
Cancel
</RippleButton>
<LoadingButton
isLoading={isSaving}
onClick={handleSave}
disabled={isSaving}
>
Save
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
-196
View File
@@ -1,196 +0,0 @@
"use client";
import { LoadingButton } from "@/components/loading-button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { VersionSelector } from "@/components/version-selector";
import { useBrowserDownload } from "@/hooks/use-browser-download";
import type { BrowserProfile } from "@/types";
import { invoke } from "@tauri-apps/api/core";
import { useEffect, useState } from "react";
import { LuTriangleAlert } from "react-icons/lu";
interface ChangeVersionDialogProps {
isOpen: boolean;
onClose: () => void;
profile: BrowserProfile | null;
onVersionChanged: () => void;
}
export function ChangeVersionDialog({
isOpen,
onClose,
profile,
onVersionChanged,
}: ChangeVersionDialogProps) {
const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
const [isUpdating, setIsUpdating] = useState(false);
const [showDowngradeWarning, setShowDowngradeWarning] = useState(false);
const [acknowledgeDowngrade, setAcknowledgeDowngrade] = useState(false);
const {
availableVersions,
downloadedVersions,
isDownloading,
loadVersions,
loadDownloadedVersions,
downloadBrowser,
isVersionDownloaded,
} = useBrowserDownload();
useEffect(() => {
if (isOpen && profile) {
setSelectedVersion(profile.version);
setAcknowledgeDowngrade(false);
void loadVersions(profile.browser);
void loadDownloadedVersions(profile.browser);
}
}, [isOpen, profile, loadVersions, loadDownloadedVersions]);
useEffect(() => {
if (profile && selectedVersion) {
// Check if this is a downgrade
const currentVersionIndex = availableVersions.findIndex(
(v) => v.tag_name === profile.version,
);
const selectedVersionIndex = availableVersions.findIndex(
(v) => v.tag_name === selectedVersion,
);
// If selected version has a higher index, it's older (downgrade)
const isDowngrade =
currentVersionIndex !== -1 &&
selectedVersionIndex !== -1 &&
selectedVersionIndex > currentVersionIndex;
setShowDowngradeWarning(isDowngrade);
if (!isDowngrade) {
setAcknowledgeDowngrade(false);
}
}
}, [selectedVersion, profile, availableVersions]);
const handleDownload = async () => {
if (!profile || !selectedVersion) return;
await downloadBrowser(profile.browser, selectedVersion);
};
const handleVersionChange = async () => {
if (!profile || !selectedVersion) return;
setIsUpdating(true);
try {
await invoke("update_profile_version", {
profileName: profile.name,
version: selectedVersion,
});
onVersionChanged();
onClose();
} catch (error) {
console.error("Failed to update profile version:", error);
} finally {
setIsUpdating(false);
}
};
const canUpdate =
profile &&
selectedVersion &&
selectedVersion !== profile.version &&
selectedVersion &&
isVersionDownloaded(selectedVersion) &&
(!showDowngradeWarning || acknowledgeDowngrade);
if (!profile) return null;
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Change Browser Version</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label className="text-sm font-medium">Profile:</Label>
<div className="p-2 bg-muted rounded text-sm">{profile.name}</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">Current Version:</Label>
<div className="p-2 bg-muted rounded text-sm">
{profile.version}
</div>
</div>
{/* Version Selection */}
<div className="grid gap-2">
<Label>New Version</Label>
<VersionSelector
selectedVersion={selectedVersion}
onVersionSelect={setSelectedVersion}
availableVersions={availableVersions}
downloadedVersions={downloadedVersions}
isDownloading={isDownloading}
onDownload={() => {
void handleDownload();
}}
placeholder="Select version..."
/>
</div>
{/* Downgrade Warning */}
{showDowngradeWarning && (
<Alert className="border-orange-700">
<LuTriangleAlert className="h-4 w-4 text-orange-700" />
<AlertTitle className="text-orange-700">
Downgrade Warning
</AlertTitle>
<AlertDescription className="text-orange-700">
You are about to downgrade from version {profile.version} to{" "}
{selectedVersion}. This may lead to compatibility issues, data
loss, or unexpected behavior.
<div className="flex items-center space-x-2 mt-3">
<Checkbox
id="acknowledge-downgrade"
checked={acknowledgeDowngrade}
onCheckedChange={(checked) => {
setAcknowledgeDowngrade(checked as boolean);
}}
/>
<Label htmlFor="acknowledge-downgrade" className="text-sm">
I understand the risks and want to proceed
</Label>
</div>
</AlertDescription>
</Alert>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<LoadingButton
isLoading={isUpdating}
onClick={() => {
void handleVersionChange();
}}
disabled={!canUpdate}
>
{isUpdating ? "Updating..." : "Update Version"}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+119
View File
@@ -0,0 +1,119 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import { LoadingButton } from "@/components/loading-button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { ProfileGroup } from "@/types";
import { RippleButton } from "./ui/ripple";
interface CreateGroupDialogProps {
isOpen: boolean;
onClose: () => void;
onGroupCreated: (group: ProfileGroup) => void;
}
export function CreateGroupDialog({
isOpen,
onClose,
onGroupCreated,
}: CreateGroupDialogProps) {
const [groupName, setGroupName] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleCreate = useCallback(async () => {
if (!groupName.trim()) return;
setIsCreating(true);
setError(null);
try {
const newGroup = await invoke<ProfileGroup>("create_profile_group", {
name: groupName.trim(),
});
toast.success("Group created successfully");
onGroupCreated(newGroup);
setGroupName("");
onClose();
} catch (err) {
console.error("Failed to create group:", err);
const errorMessage =
err instanceof Error ? err.message : "Failed to create group";
setError(errorMessage);
toast.error(errorMessage);
} finally {
setIsCreating(false);
}
}, [groupName, onGroupCreated, onClose]);
const handleClose = useCallback(() => {
setGroupName("");
setError(null);
onClose();
}, [onClose]);
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Create New Group</DialogTitle>
<DialogDescription>
Create a new group to organize your browser profiles.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="group-name">Group Name</Label>
<Input
id="group-name"
placeholder="Enter group name..."
value={groupName}
onChange={(e) => setGroupName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && groupName.trim()) {
void handleCreate();
}
}}
disabled={isCreating}
/>
</div>
{error && (
<div className="p-3 text-sm text-red-600 bg-red-50 rounded-md dark:bg-red-900/20 dark:text-red-400">
{error}
</div>
)}
</div>
<DialogFooter>
<RippleButton
variant="outline"
onClick={handleClose}
disabled={isCreating}
>
Cancel
</RippleButton>
<LoadingButton
isLoading={isCreating}
onClick={() => void handleCreate()}
disabled={!groupName.trim()}
>
Create
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

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