Compare commits

..

26 Commits

Author SHA1 Message Date
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
33 changed files with 2315 additions and 435 deletions
+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
@@ -3,4 +3,4 @@ description:
globs:
alwaysApply: true
---
After your changes, instead of running specific tests or linting specific files, run "pnpm format && pnpm lint && pnpm test"
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.
+12 -13
View File
@@ -69,19 +69,18 @@ jobs:
target: "aarch64-unknown-linux-gnu"
pkg_target: "latest-linux-arm64"
nodecar_script: "build:linux-arm64"
# Future platforms can be added here:
# - platform: "windows-latest"
# args: "--target x86_64-pc-windows-msvc"
# 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"
# arch: "aarch64"
# target: "aarch64-pc-windows-msvc"
# pkg_target: "latest-win-arm64"
# nodecar_script: "build:win-arm64"
- platform: "windows-11"
args: "--target x86_64-pc-windows-msvc"
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"
arch: "aarch64"
target: "aarch64-pc-windows-msvc"
pkg_target: "latest-win-arm64"
nodecar_script: "build:win-arm64"
runs-on: ${{ matrix.platform }}
steps:
+12
View File
@@ -68,6 +68,18 @@ jobs:
target: "aarch64-unknown-linux-gnu"
pkg_target: "latest-linux-arm64"
nodecar_script: "build:linux-arm64"
- platform: "windows-11"
args: "--target x86_64-pc-windows-msvc"
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"
arch: "aarch64"
target: "aarch64-pc-windows-msvc"
pkg_target: "latest-win-arm64"
nodecar_script: "build:win-arm64"
runs-on: ${{ matrix.platform }}
steps:
+2
View File
@@ -15,6 +15,7 @@
"donutbrowser",
"dpkg",
"dtolnay",
"dyld",
"elif",
"esbuild",
"eslintcache",
@@ -66,6 +67,7 @@
"unlisten",
"unrs",
"vercel",
"winreg",
"wiremock",
"xattr",
"zhom"
+2 -1
View File
@@ -1,4 +1,5 @@
# Instructions for AI Agents
- If you want to run tests, only ever run them as "pnpm format && pnpm lint && pnpm test".
- 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
+4 -2
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.3.1",
"version": "0.3.3",
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
@@ -38,6 +38,7 @@
"@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "~2.3.0",
"@tauri-apps/plugin-opener": "^2.2.7",
"ahooks": "^3.8.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -47,7 +48,8 @@
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"sonner": "^2.0.5",
"tailwind-merge": "^3.3.0"
"tailwind-merge": "^3.3.0",
"tauri-plugin-macos-permissions-api": "^2.3.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
+159 -84
View File
@@ -53,6 +53,9 @@ importers:
'@tauri-apps/plugin-opener':
specifier: ^2.2.7
version: 2.2.7
ahooks:
specifier: ^3.8.5
version: 3.8.5(react@19.1.0)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -83,6 +86,9 @@ importers:
tailwind-merge:
specifier: ^3.3.0
version: 3.3.0
tauri-plugin-macos-permissions-api:
specifier: ^2.3.0
version: 2.3.0
devDependencies:
'@biomejs/biome':
specifier: 1.9.4
@@ -255,6 +261,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.27.6':
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
@@ -1168,103 +1178,103 @@ packages:
'@rolldown/pluginutils@1.0.0-beta.9':
resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==}
'@rollup/rollup-android-arm-eabi@4.41.1':
resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==}
'@rollup/rollup-android-arm-eabi@4.42.0':
resolution: {integrity: sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.41.1':
resolution: {integrity: sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==}
'@rollup/rollup-android-arm64@4.42.0':
resolution: {integrity: sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.41.1':
resolution: {integrity: sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==}
'@rollup/rollup-darwin-arm64@4.42.0':
resolution: {integrity: sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.41.1':
resolution: {integrity: sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==}
'@rollup/rollup-darwin-x64@4.42.0':
resolution: {integrity: sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.41.1':
resolution: {integrity: sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==}
'@rollup/rollup-freebsd-arm64@4.42.0':
resolution: {integrity: sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.41.1':
resolution: {integrity: sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==}
'@rollup/rollup-freebsd-x64@4.42.0':
resolution: {integrity: sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.41.1':
resolution: {integrity: sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==}
'@rollup/rollup-linux-arm-gnueabihf@4.42.0':
resolution: {integrity: sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.41.1':
resolution: {integrity: sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==}
'@rollup/rollup-linux-arm-musleabihf@4.42.0':
resolution: {integrity: sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.41.1':
resolution: {integrity: sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==}
'@rollup/rollup-linux-arm64-gnu@4.42.0':
resolution: {integrity: sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.41.1':
resolution: {integrity: sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==}
'@rollup/rollup-linux-arm64-musl@4.42.0':
resolution: {integrity: sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-loongarch64-gnu@4.41.1':
resolution: {integrity: sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==}
'@rollup/rollup-linux-loongarch64-gnu@4.42.0':
resolution: {integrity: sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-powerpc64le-gnu@4.41.1':
resolution: {integrity: sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==}
'@rollup/rollup-linux-powerpc64le-gnu@4.42.0':
resolution: {integrity: sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.41.1':
resolution: {integrity: sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==}
'@rollup/rollup-linux-riscv64-gnu@4.42.0':
resolution: {integrity: sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.41.1':
resolution: {integrity: sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==}
'@rollup/rollup-linux-riscv64-musl@4.42.0':
resolution: {integrity: sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.41.1':
resolution: {integrity: sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==}
'@rollup/rollup-linux-s390x-gnu@4.42.0':
resolution: {integrity: sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.41.1':
resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==}
'@rollup/rollup-linux-x64-gnu@4.42.0':
resolution: {integrity: sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.41.1':
resolution: {integrity: sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==}
'@rollup/rollup-linux-x64-musl@4.42.0':
resolution: {integrity: sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==}
cpu: [x64]
os: [linux]
'@rollup/rollup-win32-arm64-msvc@4.41.1':
resolution: {integrity: sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==}
'@rollup/rollup-win32-arm64-msvc@4.42.0':
resolution: {integrity: sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.41.1':
resolution: {integrity: sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==}
'@rollup/rollup-win32-ia32-msvc@4.42.0':
resolution: {integrity: sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.41.1':
resolution: {integrity: sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==}
'@rollup/rollup-win32-x64-msvc@4.42.0':
resolution: {integrity: sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==}
cpu: [x64]
os: [win32]
@@ -1690,6 +1700,12 @@ packages:
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
engines: {node: '>= 14'}
ahooks@3.8.5:
resolution: {integrity: sha512-Y+MLoJpBXVdjsnnBjE5rOSPkQ4DK+8i5aPDzLJdIOsCpo/fiAeXcBY1Y7oWgtOK0TpOz0gFa/XcyO1UGdoqLcw==}
engines: {node: '>=8.0.0'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1940,6 +1956,9 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@@ -2409,6 +2428,9 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
intersection-observer@0.12.2:
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
into-stream@6.0.0:
resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==}
engines: {node: '>=10'}
@@ -2556,6 +2578,10 @@ packages:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -2694,6 +2720,9 @@ packages:
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
log-update@6.1.0:
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
engines: {node: '>=18'}
@@ -2985,6 +3014,9 @@ packages:
peerDependencies:
react: ^19.1.0
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
react-icons@5.5.0:
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
peerDependencies:
@@ -3054,6 +3086,9 @@ packages:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
resize-observer-polyfill@1.5.1:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -3081,8 +3116,8 @@ packages:
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
rollup@4.41.1:
resolution: {integrity: sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==}
rollup@4.42.0:
resolution: {integrity: sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
@@ -3110,6 +3145,10 @@ packages:
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
screenfull@5.2.0:
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
engines: {node: '>=0.10.0'}
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
@@ -3330,6 +3369,9 @@ packages:
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
engines: {node: '>=18'}
tauri-plugin-macos-permissions-api@2.3.0:
resolution: {integrity: sha512-pZp0jmDySysBqrGueknd1a7Rr4XEO9aXpMv9TNrT2PDHP0MSH20njieOagsFYJ5MCVb8A+wcaK0cIkjUC2dOww==}
tinyglobby@0.2.14:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'}
@@ -3674,6 +3716,8 @@ snapshots:
'@babel/core': 7.27.4
'@babel/helper-plugin-utils': 7.27.1
'@babel/runtime@7.27.6': {}
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
@@ -4464,64 +4508,64 @@ snapshots:
'@rolldown/pluginutils@1.0.0-beta.9': {}
'@rollup/rollup-android-arm-eabi@4.41.1':
'@rollup/rollup-android-arm-eabi@4.42.0':
optional: true
'@rollup/rollup-android-arm64@4.41.1':
'@rollup/rollup-android-arm64@4.42.0':
optional: true
'@rollup/rollup-darwin-arm64@4.41.1':
'@rollup/rollup-darwin-arm64@4.42.0':
optional: true
'@rollup/rollup-darwin-x64@4.41.1':
'@rollup/rollup-darwin-x64@4.42.0':
optional: true
'@rollup/rollup-freebsd-arm64@4.41.1':
'@rollup/rollup-freebsd-arm64@4.42.0':
optional: true
'@rollup/rollup-freebsd-x64@4.41.1':
'@rollup/rollup-freebsd-x64@4.42.0':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.41.1':
'@rollup/rollup-linux-arm-gnueabihf@4.42.0':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.41.1':
'@rollup/rollup-linux-arm-musleabihf@4.42.0':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.41.1':
'@rollup/rollup-linux-arm64-gnu@4.42.0':
optional: true
'@rollup/rollup-linux-arm64-musl@4.41.1':
'@rollup/rollup-linux-arm64-musl@4.42.0':
optional: true
'@rollup/rollup-linux-loongarch64-gnu@4.41.1':
'@rollup/rollup-linux-loongarch64-gnu@4.42.0':
optional: true
'@rollup/rollup-linux-powerpc64le-gnu@4.41.1':
'@rollup/rollup-linux-powerpc64le-gnu@4.42.0':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.41.1':
'@rollup/rollup-linux-riscv64-gnu@4.42.0':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.41.1':
'@rollup/rollup-linux-riscv64-musl@4.42.0':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.41.1':
'@rollup/rollup-linux-s390x-gnu@4.42.0':
optional: true
'@rollup/rollup-linux-x64-gnu@4.41.1':
'@rollup/rollup-linux-x64-gnu@4.42.0':
optional: true
'@rollup/rollup-linux-x64-musl@4.41.1':
'@rollup/rollup-linux-x64-musl@4.42.0':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.41.1':
'@rollup/rollup-win32-arm64-msvc@4.42.0':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.41.1':
'@rollup/rollup-win32-ia32-msvc@4.42.0':
optional: true
'@rollup/rollup-win32-x64-msvc@4.41.1':
'@rollup/rollup-win32-x64-msvc@4.42.0':
optional: true
'@rtsao/scc@1.1.0': {}
@@ -4936,6 +4980,19 @@ snapshots:
agent-base@7.1.3: {}
ahooks@3.8.5(react@19.1.0):
dependencies:
'@babel/runtime': 7.27.6
dayjs: 1.11.13
intersection-observer: 0.12.2
js-cookie: 3.0.5
lodash: 4.17.21
react: 19.1.0
react-fast-compare: 3.2.2
resize-observer-polyfill: 1.5.1
screenfull: 5.2.0
tslib: 2.8.1
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -5231,6 +5288,8 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
dayjs@1.11.13: {}
debug@3.2.7:
dependencies:
ms: 2.1.3
@@ -5840,6 +5899,8 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
intersection-observer@0.12.2: {}
into-stream@6.0.0:
dependencies:
from2: 2.3.0
@@ -5993,6 +6054,8 @@ snapshots:
jiti@2.4.2: {}
js-cookie@3.0.5: {}
js-tokens@4.0.0: {}
js-yaml@4.1.0:
@@ -6120,6 +6183,8 @@ snapshots:
lodash.merge@4.6.2: {}
lodash@4.17.21: {}
log-update@6.1.0:
dependencies:
ansi-escapes: 7.0.0
@@ -6414,6 +6479,8 @@ snapshots:
react: 19.1.0
scheduler: 0.26.0
react-fast-compare@3.2.2: {}
react-icons@5.5.0(react@19.1.0):
dependencies:
react: 19.1.0
@@ -6493,6 +6560,8 @@ snapshots:
require-directory@2.1.1: {}
resize-observer-polyfill@1.5.1: {}
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@@ -6518,30 +6587,30 @@ snapshots:
rfdc@1.4.1: {}
rollup@4.41.1:
rollup@4.42.0:
dependencies:
'@types/estree': 1.0.7
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.41.1
'@rollup/rollup-android-arm64': 4.41.1
'@rollup/rollup-darwin-arm64': 4.41.1
'@rollup/rollup-darwin-x64': 4.41.1
'@rollup/rollup-freebsd-arm64': 4.41.1
'@rollup/rollup-freebsd-x64': 4.41.1
'@rollup/rollup-linux-arm-gnueabihf': 4.41.1
'@rollup/rollup-linux-arm-musleabihf': 4.41.1
'@rollup/rollup-linux-arm64-gnu': 4.41.1
'@rollup/rollup-linux-arm64-musl': 4.41.1
'@rollup/rollup-linux-loongarch64-gnu': 4.41.1
'@rollup/rollup-linux-powerpc64le-gnu': 4.41.1
'@rollup/rollup-linux-riscv64-gnu': 4.41.1
'@rollup/rollup-linux-riscv64-musl': 4.41.1
'@rollup/rollup-linux-s390x-gnu': 4.41.1
'@rollup/rollup-linux-x64-gnu': 4.41.1
'@rollup/rollup-linux-x64-musl': 4.41.1
'@rollup/rollup-win32-arm64-msvc': 4.41.1
'@rollup/rollup-win32-ia32-msvc': 4.41.1
'@rollup/rollup-win32-x64-msvc': 4.41.1
'@rollup/rollup-android-arm-eabi': 4.42.0
'@rollup/rollup-android-arm64': 4.42.0
'@rollup/rollup-darwin-arm64': 4.42.0
'@rollup/rollup-darwin-x64': 4.42.0
'@rollup/rollup-freebsd-arm64': 4.42.0
'@rollup/rollup-freebsd-x64': 4.42.0
'@rollup/rollup-linux-arm-gnueabihf': 4.42.0
'@rollup/rollup-linux-arm-musleabihf': 4.42.0
'@rollup/rollup-linux-arm64-gnu': 4.42.0
'@rollup/rollup-linux-arm64-musl': 4.42.0
'@rollup/rollup-linux-loongarch64-gnu': 4.42.0
'@rollup/rollup-linux-powerpc64le-gnu': 4.42.0
'@rollup/rollup-linux-riscv64-gnu': 4.42.0
'@rollup/rollup-linux-riscv64-musl': 4.42.0
'@rollup/rollup-linux-s390x-gnu': 4.42.0
'@rollup/rollup-linux-x64-gnu': 4.42.0
'@rollup/rollup-linux-x64-musl': 4.42.0
'@rollup/rollup-win32-arm64-msvc': 4.42.0
'@rollup/rollup-win32-ia32-msvc': 4.42.0
'@rollup/rollup-win32-x64-msvc': 4.42.0
fsevents: 2.3.3
run-parallel@1.2.0:
@@ -6573,6 +6642,8 @@ snapshots:
scheduler@0.26.0: {}
screenfull@5.2.0: {}
semver@6.3.1: {}
semver@7.7.2: {}
@@ -6861,6 +6932,10 @@ snapshots:
mkdirp: 3.0.1
yallist: 5.0.0
tauri-plugin-macos-permissions-api@2.3.0:
dependencies:
'@tauri-apps/api': 2.5.0
tinyglobby@0.2.14:
dependencies:
fdir: 6.4.5(picomatch@4.0.2)
@@ -7036,7 +7111,7 @@ snapshots:
dependencies:
esbuild: 0.25.5
postcss: 8.5.4
rollup: 4.41.1
rollup: 4.42.0
optionalDependencies:
'@types/node': 22.15.30
fsevents: 2.3.3
+55 -27
View File
@@ -405,9 +405,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.18.0"
version = "3.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b094a32014c3d1f3944e4808e0e7c70e97dae0660886a8eb6dbc52d745badc"
checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
[[package]]
name = "bytemuck"
@@ -518,9 +518,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.25"
version = "1.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
dependencies = [
"jobserver",
"libc",
@@ -993,7 +993,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.3.1"
version = "0.3.3"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -1012,11 +1012,14 @@ dependencies = [
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-macos-permissions",
"tauri-plugin-opener",
"tauri-plugin-shell",
"tempfile",
"tokio",
"tokio-test",
"windows",
"winreg",
"wiremock",
"zip",
]
@@ -1187,9 +1190,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.1.1"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"libz-rs-sys",
@@ -1826,9 +1829,9 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.27.6"
version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http",
"hyper",
@@ -2284,9 +2287,9 @@ dependencies = [
[[package]]
name = "liblzma-sys"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5839bad90c3cc2e0b8c4ed8296b80e86040240f81d46b9c0e9bc8dd51ddd3af1"
checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736"
dependencies = [
"cc",
"libc",
@@ -2305,9 +2308,9 @@ dependencies = [
[[package]]
name = "libz-rs-sys"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a"
checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
dependencies = [
"zlib-rs",
]
@@ -2346,6 +2349,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "macos-accessibility-client"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edf7710fbff50c24124331760978fb9086d6de6288dcdb38b25a97f8b1bdebbb"
dependencies = [
"core-foundation 0.9.4",
"core-foundation-sys",
]
[[package]]
name = "markup5ever"
version = "0.11.0"
@@ -3791,9 +3804,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.8"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
@@ -3948,9 +3961,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.15.0"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
@@ -4407,6 +4420,21 @@ dependencies = [
"url",
]
[[package]]
name = "tauri-plugin-macos-permissions"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5607e0707d37d7b20e287cf0ce396d1efebe7b833b8e9cbd2ea4257091d9c604"
dependencies = [
"macos-accessibility-client",
"objc2 0.6.1",
"objc2-foundation 0.3.1",
"serde",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.2.7"
@@ -4769,9 +4797,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.9"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
@@ -4814,9 +4842,9 @@ dependencies = [
[[package]]
name = "toml_write"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tower"
@@ -4876,9 +4904,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.28"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662"
dependencies = [
"proc-macro2",
"quote",
@@ -4887,9 +4915,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.33"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
]
@@ -5974,9 +6002,9 @@ dependencies = [
[[package]]
name = "zlib-rs"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
[[package]]
name = "zopfli"
+16 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.3.1"
version = "0.3.3"
description = "Simple Yet Powerful Browser Orchestrator"
authors = ["zhom@github"]
edition = "2021"
@@ -27,6 +27,7 @@ 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"] }
@@ -42,6 +43,20 @@ 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"
+6 -26
View File
@@ -2,47 +2,27 @@
<!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>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.3.1</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>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Web Browser</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>
+6 -1
View File
@@ -20,6 +20,11 @@
"shell:allow-stdin-write",
"deep-link:default",
"dialog:default",
"dialog:allow-open"
"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"
]
}
+16
View File
@@ -12,5 +12,21 @@
<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/>
</dict>
</plist>
+8 -14
View File
@@ -246,8 +246,7 @@ pub fn is_browser_version_nightly(
if let Some(name) = release_name {
!name.starts_with("Release")
} else {
// Fallback to version string analysis if no release name
is_nightly_version(version)
true
}
}
"firefox" | "firefox-developer" => {
@@ -806,22 +805,17 @@ impl ApiClient {
}) || assets.iter().any(|asset| asset.name.ends_with(".dmg"))
}
"linux" => {
// For Linux, check for architecture-specific packages (prefer ZIP for stable releases)
// For Linux, be strict about architecture matching - only allow assets that explicitly match the current architecture
let arch_pattern = if arch == "arm64" { "arm64" } else { "amd64" };
assets.iter().any(|asset| {
if assets.iter().any(|asset| {
let name = asset.name.to_lowercase();
name.contains("linux") && name.contains(arch_pattern) && name.ends_with(".zip")
}) || assets.iter().any(|asset| {
let name = asset.name.to_lowercase();
name.contains(arch_pattern) && (name.ends_with(".deb") || name.ends_with(".rpm"))
}) || assets.iter().any(|asset| {
let name = asset.name.to_lowercase();
name.contains("linux") && name.ends_with(".zip")
}) || assets.iter().any(|asset| {
let name = asset.name.to_lowercase();
name.ends_with(".deb") || name.ends_with(".rpm")
})
}) {
return true;
}
false
}
_ => false,
}
+407 -63
View File
@@ -398,6 +398,30 @@ impl AppAutoUpdater {
Err("DMG extraction is only supported on macOS".into())
}
}
"msi" => {
#[cfg(target_os = "windows")]
{
// For MSI files on Windows, we need to run the installer
// MSI files can't be extracted like archives, they need to be executed
// Return the path to the MSI file itself for installation
Ok(archive_path.to_path_buf())
}
#[cfg(not(target_os = "windows"))]
{
Err("MSI installation is only supported on Windows".into())
}
}
"exe" => {
#[cfg(target_os = "windows")]
{
// For exe installers on Windows, return the path for execution
Ok(archive_path.to_path_buf())
}
#[cfg(not(target_os = "windows"))]
{
Err("EXE installation is only supported on Windows".into())
}
}
"zip" => extractor.extract_zip(archive_path, dest_dir).await,
_ => Err(format!("Unsupported archive format: {extension}").into()),
}
@@ -406,71 +430,282 @@ impl AppAutoUpdater {
/// Install the update by replacing the current app
async fn install_update(
&self,
new_app_path: &Path,
installer_path: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Get the current application bundle path
let current_app_path = self.get_current_app_path()?;
#[cfg(target_os = "macos")]
{
// Get the current application bundle path
let current_app_path = self.get_current_app_path()?;
// Create a backup of the current app
let backup_path = current_app_path.with_extension("app.backup");
if backup_path.exists() {
fs::remove_dir_all(&backup_path)?;
// Create a backup of the current app
let backup_path = current_app_path.with_extension("app.backup");
if backup_path.exists() {
fs::remove_dir_all(&backup_path)?;
}
// Move current app to backup
fs::rename(&current_app_path, &backup_path)?;
// Move new app to current location
fs::rename(installer_path, &current_app_path)?;
// Remove quarantine attributes from the new app
let _ = Command::new("xattr")
.args([
"-dr",
"com.apple.quarantine",
current_app_path.to_str().unwrap(),
])
.output();
let _ = Command::new("xattr")
.args(["-cr", current_app_path.to_str().unwrap()])
.output();
// Clean up backup after successful installation
let _ = fs::remove_dir_all(&backup_path);
Ok(())
}
// Move current app to backup
fs::rename(&current_app_path, &backup_path)?;
#[cfg(target_os = "windows")]
{
let extension = installer_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
// Move new app to current location
fs::rename(new_app_path, &current_app_path)?;
println!("Installing Windows update with extension: {extension}");
// Remove quarantine attributes from the new app
let _ = Command::new("xattr")
.args([
"-dr",
"com.apple.quarantine",
current_app_path.to_str().unwrap(),
])
.output();
match extension {
"msi" => {
// Install MSI silently with enhanced error handling
println!("Running MSI installer: {}", installer_path.display());
let _ = Command::new("xattr")
.args(["-cr", current_app_path.to_str().unwrap()])
.output();
let mut cmd = Command::new("msiexec");
cmd.args([
"/i",
installer_path.to_str().unwrap(),
"/quiet",
"/norestart",
"REBOOT=ReallySuppress",
"/l*v", // Enable verbose logging
&format!("{}.log", installer_path.to_str().unwrap()),
]);
// Clean up backup after successful installation
let _ = fs::remove_dir_all(&backup_path);
let output = cmd.output()?;
Ok(())
if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr);
let exit_code = output.status.code().unwrap_or(-1);
// Try to read the log file for more details
let log_path = format!("{}.log", installer_path.to_str().unwrap());
let log_content = fs::read_to_string(&log_path).unwrap_or_default();
println!("MSI installation failed with exit code: {exit_code}");
println!("Error output: {error_msg}");
if !log_content.is_empty() {
println!(
"Log file content (last 500 chars): {}",
&log_content
.chars()
.rev()
.take(500)
.collect::<String>()
.chars()
.rev()
.collect::<String>()
);
}
return Err(
format!("MSI installation failed (exit code {exit_code}): {error_msg}").into(),
);
}
println!("MSI installation completed successfully");
}
"exe" => {
// Run exe installer silently with multiple fallback options
println!("Running EXE installer: {}", installer_path.display());
// Try NSIS silent flag first (most common for Tauri)
let mut success = false;
let mut last_error = String::new();
// NSIS installer flags (used by Tauri)
let nsis_args = vec![
vec!["/S"], // Standard NSIS silent flag
vec!["/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"], // Inno Setup flags
vec!["/quiet"], // Generic quiet flag
vec!["/silent"], // Alternative silent flag
];
for args in nsis_args {
println!("Trying installer with args: {:?}", args);
let output = Command::new(installer_path).args(&args).output();
match output {
Ok(output) if output.status.success() => {
println!(
"EXE installation completed successfully with args: {:?}",
args
);
success = true;
break;
}
Ok(output) => {
let error_msg = String::from_utf8_lossy(&output.stderr);
last_error = format!(
"Exit code {}: {}",
output.status.code().unwrap_or(-1),
error_msg
);
println!("Installer failed with args {:?}: {}", args, last_error);
}
Err(e) => {
last_error = format!("Failed to execute installer: {e}");
println!(
"Failed to execute installer with args {:?}: {}",
args, last_error
);
}
}
}
if !success {
return Err(
format!(
"EXE installation failed after trying multiple methods. Last error: {last_error}"
)
.into(),
);
}
}
"zip" => {
// Handle ZIP files by extracting and replacing the current executable
println!("Handling ZIP update: {}", installer_path.display());
let temp_extract_dir = installer_path.parent().unwrap().join("extracted");
fs::create_dir_all(&temp_extract_dir)?;
// Extract ZIP file
let extractor = crate::extraction::Extractor::new();
let extracted_path = extractor
.extract_zip(installer_path, &temp_extract_dir)
.await?;
// Find the executable in the extracted files
let current_exe = self.get_current_app_path()?;
let current_exe_name = current_exe.file_name().unwrap();
// Look for the new executable
let new_exe_path =
if extracted_path.is_file() && extracted_path.file_name() == Some(current_exe_name) {
extracted_path
} else {
// Search in extracted directory
let mut found_exe = None;
if let Ok(entries) = fs::read_dir(&extracted_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.file_name() == Some(current_exe_name) {
found_exe = Some(path);
break;
}
}
}
found_exe.ok_or("Could not find executable in ZIP file")?
};
// Create backup of current executable
let backup_path = current_exe.with_extension("exe.backup");
if backup_path.exists() {
fs::remove_file(&backup_path)?;
}
fs::copy(&current_exe, &backup_path)?;
// Replace current executable
fs::copy(&new_exe_path, &current_exe)?;
// Clean up
let _ = fs::remove_dir_all(&temp_extract_dir);
println!("ZIP update completed successfully");
}
_ => {
return Err(format!("Unsupported installer format: {extension}").into());
}
}
Ok(())
}
#[cfg(target_os = "linux")]
{
// For Linux, we would handle different package formats here
// This implementation would depend on the specific package type
Err("Linux auto-update installation not yet implemented".into())
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
Err("Auto-update installation not supported on this platform".into())
}
}
/// Get the current application bundle path
fn get_current_app_path(&self) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
// Get the current executable path
let exe_path = std::env::current_exe()?;
#[cfg(target_os = "macos")]
{
// Get the current executable path
let exe_path = std::env::current_exe()?;
// Navigate up to find the .app bundle
let mut current = exe_path.as_path();
while let Some(parent) = current.parent() {
if parent.extension().is_some_and(|ext| ext == "app") {
return Ok(parent.to_path_buf());
// Navigate up to find the .app bundle
let mut current = exe_path.as_path();
while let Some(parent) = current.parent() {
if parent.extension().is_some_and(|ext| ext == "app") {
return Ok(parent.to_path_buf());
}
current = parent;
}
current = parent;
Err("Could not find application bundle".into())
}
Err("Could not find application bundle".into())
#[cfg(target_os = "windows")]
{
// On Windows, just return the current executable path
std::env::current_exe().map_err(|e| e.into())
}
#[cfg(target_os = "linux")]
{
// On Linux, return the current executable path
std::env::current_exe().map_err(|e| e.into())
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
Err("Platform not supported".into())
}
}
/// Restart the application
async fn restart_application(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let app_path = self.get_current_app_path()?;
let current_pid = std::process::id();
#[cfg(target_os = "macos")]
{
let app_path = self.get_current_app_path()?;
let current_pid = std::process::id();
// Create a temporary restart script
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("donut_restart.sh");
// Create a temporary restart script
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("donut_restart.sh");
// Create the restart script content
let script_content = format!(
r#"#!/bin/bash
// Create the restart script content
let script_content = format!(
r#"#!/bin/bash
# Wait for the current process to exit
while kill -0 {} 2>/dev/null; do
sleep 0.5
@@ -485,37 +720,146 @@ open "{}"
# Clean up this script
rm "{}"
"#,
current_pid,
app_path.to_str().unwrap(),
script_path.to_str().unwrap()
);
current_pid,
app_path.to_str().unwrap(),
script_path.to_str().unwrap()
);
// Write the script to file
fs::write(&script_path, script_content)?;
// Write the script to file
fs::write(&script_path, script_content)?;
// Make the script executable
let _ = Command::new("chmod")
.args(["+x", script_path.to_str().unwrap()])
.output();
// Make the script executable
let _ = Command::new("chmod")
.args(["+x", script_path.to_str().unwrap()])
.output();
// Execute the restart script in the background
let mut cmd = Command::new("bash");
cmd.arg(script_path.to_str().unwrap());
// Execute the restart script in the background
let mut cmd = Command::new("bash");
cmd.arg(script_path.to_str().unwrap());
// Detach the process completely
#[cfg(unix)]
{
// Detach the process completely
use std::os::unix::process::CommandExt;
cmd.process_group(0);
let _child = cmd.spawn()?;
// Give the script a moment to start
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Exit the current process
std::process::exit(0);
}
let _child = cmd.spawn()?;
#[cfg(target_os = "windows")]
{
let app_path = self.get_current_app_path()?;
let current_pid = std::process::id();
// Give the script a moment to start
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Create a temporary restart batch script
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("donut_restart.bat");
// Exit the current process
std::process::exit(0);
// Create the restart script content
let script_content = format!(
r#"@echo off
rem Wait for the current process to exit
:wait_loop
tasklist /fi "PID eq {}" >nul 2>&1
if %errorlevel% equ 0 (
timeout /t 1 /nobreak >nul
goto wait_loop
)
rem Wait a bit more to ensure clean exit
timeout /t 2 /nobreak >nul
rem Start the new application
start "" "{}"
rem Clean up this script
del "%~f0"
"#,
current_pid,
app_path.to_str().unwrap()
);
// Write the script to file
fs::write(&script_path, script_content)?;
// Execute the restart script in the background
let mut cmd = Command::new("cmd");
cmd.args(["/C", script_path.to_str().unwrap()]);
// Start the process detached
let _child = cmd.spawn()?;
// Give the script a moment to start
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Exit the current process
std::process::exit(0);
}
#[cfg(target_os = "linux")]
{
let app_path = self.get_current_app_path()?;
let current_pid = std::process::id();
// Create a temporary restart script
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("donut_restart.sh");
// Create the restart script content
let script_content = format!(
r#"#!/bin/bash
# Wait for the current process to exit
while kill -0 {} 2>/dev/null; do
sleep 0.5
done
# Wait a bit more to ensure clean exit
sleep 1
# Start the new application
"{}" &
# Clean up this script
rm "{}"
"#,
current_pid,
app_path.to_str().unwrap(),
script_path.to_str().unwrap()
);
// Write the script to file
fs::write(&script_path, script_content)?;
// Make the script executable
let _ = Command::new("chmod")
.args(["+x", script_path.to_str().unwrap()])
.output();
// Execute the restart script in the background
let mut cmd = Command::new("bash");
cmd.arg(script_path.to_str().unwrap());
// Detach the process completely
use std::os::unix::process::CommandExt;
cmd.process_group(0);
let _child = cmd.spawn()?;
// Give the script a moment to start
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Exit the current process
std::process::exit(0);
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
Err("Application restart not supported on this platform".into())
}
}
}
+10 -22
View File
@@ -216,17 +216,13 @@ mod linux {
install_dir: &Path,
browser_type: &BrowserType,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
// Expected structure: install_dir/<browser>/<binary>
let browser_subdir = install_dir.join(browser_type.as_str());
let possible_executables = match browser_type {
BrowserType::Chromium => vec![
browser_subdir.join("chromium"),
browser_subdir.join("chrome"),
],
BrowserType::Chromium => vec![install_dir.join("chromium"), install_dir.join("chrome")],
BrowserType::Brave => vec![
browser_subdir.join("brave"),
browser_subdir.join("brave-browser"),
install_dir.join("brave"),
install_dir.join("brave-browser"),
install_dir.join("brave-browser-nightly"),
install_dir.join("brave-browser-beta"),
],
_ => vec![],
};
@@ -292,21 +288,13 @@ mod linux {
}
pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool {
// Expected structure: install_dir/<browser>/<binary>
let browser_subdir = install_dir.join(browser_type.as_str());
if !browser_subdir.exists() || !browser_subdir.is_dir() {
return false;
}
let possible_executables = match browser_type {
BrowserType::Chromium => vec![
browser_subdir.join("chromium"),
browser_subdir.join("chrome"),
],
BrowserType::Chromium => vec![install_dir.join("chromium"), install_dir.join("chrome")],
BrowserType::Brave => vec![
browser_subdir.join("brave"),
browser_subdir.join("brave-browser"),
install_dir.join("brave"),
install_dir.join("brave-browser"),
install_dir.join("brave-browser-nightly"),
install_dir.join("brave-browser-beta"),
],
_ => vec![],
};
+226 -39
View File
@@ -3,7 +3,6 @@ use directories::BaseDirs;
use serde::{Deserialize, Serialize};
use std::fs::{self, create_dir_all};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use sysinfo::{Pid, System};
use tauri::Emitter;
@@ -35,6 +34,7 @@ pub struct BrowserProfile {
mod macos {
use super::*;
use std::ffi::OsString;
use std::process::Command;
pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool {
match browser_type {
@@ -482,14 +482,42 @@ end try
mod windows {
use super::*;
use std::ffi::OsString;
use std::process::Command;
pub fn is_tor_or_mullvad_browser(
_exe_name: &str,
_cmd: &[OsString],
_browser_type: &str,
) -> bool {
// Windows implementation would go here
false
pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool {
let exe_lower = exe_name.to_lowercase();
// Check for Firefox-based browsers first by executable name
let is_firefox_family = exe_lower.contains("firefox") || exe_lower.contains(".exe");
if !is_firefox_family {
return false;
}
// Check command arguments for profile paths and browser-specific indicators
let cmd_line = cmd
.iter()
.map(|s| s.to_string_lossy().to_lowercase())
.collect::<Vec<_>>()
.join(" ");
match browser_type {
"tor-browser" => {
// Check for TOR browser specific paths and arguments
cmd_line.contains("tor")
|| cmd_line.contains("browser\\torbrowser")
|| cmd_line.contains("tor-browser")
|| cmd_line.contains("profile") && (cmd_line.contains("tor") || cmd_line.contains("tbb"))
}
"mullvad-browser" => {
// Check for Mullvad browser specific paths and arguments
cmd_line.contains("mullvad")
|| cmd_line.contains("browser\\mullvadbrowser")
|| cmd_line.contains("mullvad-browser")
|| cmd_line.contains("profile") && cmd_line.contains("mullvad")
}
_ => false,
}
}
pub async fn launch_browser_process(
@@ -500,7 +528,48 @@ mod windows {
"Launching browser on Windows: {:?} with args: {:?}",
executable_path, args
);
Ok(Command::new(executable_path).args(args).spawn()?)
// Check if the executable exists
if !executable_path.exists() {
return Err(format!("Browser executable not found: {:?}", executable_path).into());
}
// On Windows, set up the command with proper working directory
let mut cmd = Command::new(executable_path);
cmd.args(args);
// Set working directory to the executable's directory for better compatibility
if let Some(parent_dir) = executable_path.parent() {
cmd.current_dir(parent_dir);
}
// For Windows 7 compatibility, set some environment variables
cmd.env(
"PROCESSOR_ARCHITECTURE",
std::env::var("PROCESSOR_ARCHITECTURE").unwrap_or_else(|_| "x86".to_string()),
);
// Ensure proper PATH for DLL loading
if let Some(exe_dir) = executable_path.parent() {
let mut path_var = std::env::var("PATH").unwrap_or_default();
if !path_var.is_empty() {
path_var = format!("{};{}", exe_dir.display(), path_var);
} else {
path_var = exe_dir.display().to_string();
}
cmd.env("PATH", path_var);
}
// Launch the process
let child = cmd
.spawn()
.map_err(|e| format!("Failed to launch browser process: {}", e))?;
println!(
"Successfully launched browser process with PID: {}",
child.id()
);
Ok(child)
}
pub async fn open_url_in_existing_browser_firefox_like(
@@ -514,14 +583,88 @@ mod windows {
.get_executable_path(browser_dir)
.map_err(|e| format!("Failed to get executable path: {}", e))?;
let output = Command::new(executable_path)
.args(["-profile", &profile.profile_path, "-new-tab", url])
.output()?;
// For Windows, try using the -requestPending approach for Firefox
let mut cmd = Command::new(executable_path);
cmd.args([
"-profile",
&profile.profile_path,
"-requestPending",
"-new-tab",
url,
]);
// Set working directory
if let Some(parent_dir) = browser_dir
.parent()
.or_else(|| browser_dir.ancestors().nth(1))
{
cmd.current_dir(parent_dir);
}
let output = cmd.output()?;
if !output.status.success() {
// Fallback: try without -requestPending
let executable_path = browser
.get_executable_path(browser_dir)
.map_err(|e| format!("Failed to get executable path: {}", e))?;
let mut fallback_cmd = Command::new(executable_path);
fallback_cmd.args(["-profile", &profile.profile_path, "-new-tab", url]);
if let Some(parent_dir) = browser_dir
.parent()
.or_else(|| browser_dir.ancestors().nth(1))
{
fallback_cmd.current_dir(parent_dir);
}
let fallback_output = fallback_cmd.output()?;
if !fallback_output.status.success() {
return Err(
format!(
"Failed to open URL in existing browser: {}",
String::from_utf8_lossy(&fallback_output.stderr)
)
.into(),
);
}
}
Ok(())
}
pub async fn open_url_in_existing_browser_tor_mullvad(
profile: &BrowserProfile,
url: &str,
browser_type: BrowserType,
browser_dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// On Windows, TOR and Mullvad browsers can sometimes accept URLs via command line
// even with -no-remote, by launching a new instance that hands off to existing one
let browser = create_browser(browser_type.clone());
let executable_path = browser
.get_executable_path(browser_dir)
.map_err(|e| format!("Failed to get executable path: {}", e))?;
let mut cmd = Command::new(&executable_path);
cmd.args(["-profile", &profile.profile_path, url]);
// Set working directory
if let Some(parent_dir) = browser_dir
.parent()
.or_else(|| browser_dir.ancestors().nth(1))
{
cmd.current_dir(parent_dir);
}
let output = cmd.output()?;
if !output.status.success() {
return Err(
format!(
"Failed to open URL in existing browser: {}",
"Failed to open URL in existing {}: {}. Note: TOR and Mullvad browsers may require manual URL opening for security reasons.",
browser_type.as_str(),
String::from_utf8_lossy(&output.stderr)
)
.into(),
@@ -531,38 +674,57 @@ mod windows {
Ok(())
}
pub async fn open_url_in_existing_browser_tor_mullvad(
_profile: &BrowserProfile,
_url: &str,
_browser_type: BrowserType,
_browser_dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Err("Opening URLs in existing Firefox-based browsers is not supported on Windows when using -no-remote".into())
}
pub async fn open_url_in_existing_browser_chromium(
profile: &BrowserProfile,
url: &str,
browser_type: BrowserType,
browser_dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let browser = create_browser(browser_type);
let browser = create_browser(browser_type.clone());
let executable_path = browser
.get_executable_path(browser_dir)
.map_err(|e| format!("Failed to get executable path: {}", e))?;
let output = Command::new(executable_path)
.args([&format!("--user-data-dir={}", profile.profile_path), url])
.output()?;
let mut cmd = Command::new(&executable_path);
cmd.args([
&format!("--user-data-dir={}", profile.profile_path),
"--new-window",
url,
]);
// Set working directory
if let Some(parent_dir) = browser_dir
.parent()
.or_else(|| browser_dir.ancestors().nth(1))
{
cmd.current_dir(parent_dir);
}
let output = cmd.output()?;
if !output.status.success() {
return Err(
format!(
"Failed to open URL in existing Chromium-based browser: {}",
String::from_utf8_lossy(&output.stderr)
)
.into(),
);
// Try fallback without --new-window
let mut fallback_cmd = Command::new(&executable_path);
fallback_cmd.args([&format!("--user-data-dir={}", profile.profile_path), url]);
if let Some(parent_dir) = browser_dir
.parent()
.or_else(|| browser_dir.ancestors().nth(1))
{
fallback_cmd.current_dir(parent_dir);
}
let fallback_output = fallback_cmd.output()?;
if !fallback_output.status.success() {
return Err(
format!(
"Failed to open URL in existing Chromium-based browser: {}",
String::from_utf8_lossy(&fallback_output.stderr)
)
.into(),
);
}
}
Ok(())
@@ -571,17 +733,41 @@ mod windows {
pub async fn kill_browser_process_impl(
pid: u32,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// First try using sysinfo (cross-platform approach)
let system = System::new_all();
if let Some(process) = system.process(Pid::from(pid as usize)) {
if !process.kill() {
return Err(format!("Failed to kill process {}", pid).into());
if process.kill() {
println!("Successfully killed browser process with PID: {pid}");
return Ok(());
}
} else {
return Err(format!("Process {} not found", pid).into());
}
println!("Successfully killed browser process with PID: {pid}");
Ok(())
// Fallback to Windows-specific process termination
use std::process::Command;
// Try taskkill command as fallback
let output = Command::new("taskkill")
.args(["/F", "/PID", &pid.to_string()])
.output();
match output {
Ok(result) => {
if result.status.success() {
println!("Successfully killed browser process with PID: {pid} using taskkill");
Ok(())
} else {
Err(
format!(
"Failed to kill process {} with taskkill: {}",
pid,
String::from_utf8_lossy(&result.stderr)
)
.into(),
)
}
}
Err(e) => Err(format!("Failed to execute taskkill for process {}: {}", pid, e).into()),
}
}
}
@@ -589,6 +775,7 @@ mod windows {
mod linux {
use super::*;
use std::ffi::OsString;
use std::process::Command;
pub fn is_tor_or_mullvad_browser(
_exe_name: &str,
+7 -9
View File
@@ -442,8 +442,8 @@ impl BrowserVersionService {
format!("Firefox Setup {version}.exe"),
false,
),
("linux", "x64") => ("linux-x86_64", format!("firefox-{version}.tar.bz2"), true),
("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.bz2"), true),
("linux", "x64") => ("linux-x86_64", format!("firefox-{version}.tar.xz"), true),
("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.xz"), true),
("macos", _) => ("mac", format!("Firefox {version}.dmg"), true),
_ => {
return Err(
@@ -468,8 +468,8 @@ impl BrowserVersionService {
format!("Firefox Setup {version}.exe"),
false,
),
("linux", "x64") => ("linux-x86_64", format!("firefox-{version}.tar.bz2"), true),
("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.bz2"), true),
("linux", "x64") => ("linux-x86_64", format!("firefox-{version}.tar.xz"), true),
("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.xz"), true),
("macos", _) => ("mac", format!("Firefox {version}.dmg"), true),
_ => {
return Err(
@@ -568,8 +568,6 @@ impl BrowserVersionService {
})
}
"brave" => {
// Brave uses different asset naming conventions
// The actual URL will be resolved dynamically in the download service
let (filename, is_archive) = match (&os[..], &arch[..]) {
("windows", _) => (format!("brave-{version}.exe"), false),
("linux", "x64") => (format!("brave-browser-{version}-linux-amd64.zip"), true),
@@ -582,7 +580,7 @@ impl BrowserVersionService {
Ok(DownloadInfo {
url: format!(
"https://github.com/brave/brave-browser/releases/download/{version}/brave-placeholder"
"https://github.com/brave/brave-browser/releases/download/{version}/{filename}"
),
filename,
is_archive,
@@ -1521,10 +1519,10 @@ mod tests {
assert!(chromium_info.url.contains("chrome-mac.zip"));
assert!(chromium_info.is_archive);
// Test Brave
// Test Brave - Note: Brave uses dynamic URL resolution, so get_download_info provides a template URL
let brave_info = service.get_download_info("brave", "v1.81.9").unwrap();
assert_eq!(brave_info.filename, "Brave-Browser-universal.dmg");
assert!(brave_info.url.contains("brave-placeholder"));
assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/Brave-Browser-universal.dmg");
assert!(brave_info.is_archive);
// Test unsupported browser
+272 -3
View File
@@ -65,13 +65,282 @@ 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 Browser Orchestrator",
)
.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 Browser Orchestrator",
)
.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,
);
}
}
}
+5 -32
View File
@@ -195,40 +195,13 @@ impl Downloader {
})
}
"linux" => {
// For Linux, prefer ZIP files matching architecture (new format for stable releases)
// 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")
})
.or_else(|| {
// Fallback to DEB packages
assets.iter().find(|asset| {
let name = asset.name.to_lowercase();
name.contains(arch_pattern) && name.ends_with(".deb")
})
})
.or_else(|| {
// Fallback to any ZIP
assets.iter().find(|asset| {
let name = asset.name.to_lowercase();
name.contains("linux") && name.ends_with(".zip")
})
})
.or_else(|| {
// Fallback to any DEB
assets.iter().find(|asset| asset.name.ends_with(".deb"))
})
.or_else(|| {
// Last fallback to RPM if no ZIP or DEB found
assets.iter().find(|asset| {
let name = asset.name.to_lowercase();
name.contains("x86_64") && name.ends_with(".rpm")
})
})
assets.iter().find(|asset| {
let name = asset.name.to_lowercase();
name.contains("linux") && name.contains(arch_pattern) && name.ends_with(".zip")
})
}
_ => None,
};
+279 -42
View File
@@ -453,8 +453,13 @@ impl Extractor {
zip_path: &Path,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
// Use PowerShell's Expand-Archive on Windows
let output = Command::new("powershell")
println!("Extracting ZIP archive on Windows: {}", zip_path.display());
// Create destination directory if it doesn't exist
fs::create_dir_all(dest_dir)?;
// First try PowerShell's Expand-Archive (Windows 10+)
let powershell_result = Command::new("powershell")
.args([
"-Command",
&format!(
@@ -463,21 +468,81 @@ impl Extractor {
dest_dir.display()
),
])
.output()?;
.output();
if !output.status.success() {
return Err(
format!(
"Failed to extract zip with PowerShell: {}",
match powershell_result {
Ok(output) if output.status.success() => {
println!("Successfully extracted using PowerShell");
}
Ok(output) => {
println!(
"PowerShell extraction failed: {}, trying Rust zip crate fallback",
String::from_utf8_lossy(&output.stderr)
)
.into(),
);
);
// Fallback to Rust zip crate for Windows 7 compatibility
return self.extract_zip_with_rust_crate(zip_path, dest_dir).await;
}
Err(e) => {
println!("PowerShell not available: {}, using Rust zip crate", e);
// Fallback to Rust zip crate for Windows 7 compatibility
return self.extract_zip_with_rust_crate(zip_path, dest_dir).await;
}
}
self.find_extracted_executable(dest_dir).await
}
#[cfg(target_os = "windows")]
async fn extract_zip_with_rust_crate(
&self,
zip_path: &Path,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
println!("Using Rust zip crate for extraction (Windows 7+ compatibility)");
let file = fs::File::open(zip_path)?;
let mut archive = zip::ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = match file.enclosed_name() {
Some(path) => dest_dir.join(path),
None => continue,
};
// Handle directory creation
if file.name().ends_with('/') {
fs::create_dir_all(&outpath)?;
} else {
// Create parent directories
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(p)?;
}
}
// Extract file
let mut outfile = fs::File::create(&outpath)?;
std::io::copy(&mut file, &mut outfile)?;
// On Windows, verify executable files
if outpath
.extension()
.is_some_and(|ext| ext.to_string_lossy().to_lowercase() == "exe")
{
if let Ok(metadata) = fs::metadata(&outpath) {
if metadata.len() > 0 {
println!("Extracted executable: {}", outpath.display());
}
}
}
}
}
println!("ZIP extraction completed. Searching for executable...");
self.find_extracted_executable(dest_dir).await
}
#[cfg(not(target_os = "windows"))]
async fn extract_zip_unix(
&self,
@@ -514,24 +579,60 @@ impl Extractor {
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
create_dir_all(dest_dir)?;
// Use tar command for more reliable extraction
let output = Command::new("tar")
.args([
"-xf",
tar_path.to_str().unwrap(),
"-C",
dest_dir.to_str().unwrap(),
])
.output()?;
#[cfg(target_os = "windows")]
{
// On Windows, try multiple extraction methods for better compatibility
// First try using tar if available (Windows 10+)
let tar_result = Command::new("tar")
.args([
"-xf",
tar_path.to_str().unwrap(),
"-C",
dest_dir.to_str().unwrap(),
])
.output();
if !output.status.success() {
return Err(
format!(
"Failed to extract tar.xz: {}",
String::from_utf8_lossy(&output.stderr)
)
.into(),
);
match tar_result {
Ok(output) if output.status.success() => {
println!("Successfully extracted tar.xz using tar command");
}
Ok(output) => {
println!(
"tar command failed: {}, trying 7-Zip fallback",
String::from_utf8_lossy(&output.stderr)
);
// Try 7-Zip as fallback
return self.extract_with_7zip(tar_path, dest_dir).await;
}
Err(_) => {
println!("tar command not available, trying 7-Zip");
// Try 7-Zip as fallback
return self.extract_with_7zip(tar_path, dest_dir).await;
}
}
}
#[cfg(not(target_os = "windows"))]
{
// Use tar command for Unix-like systems
let output = Command::new("tar")
.args([
"-xf",
tar_path.to_str().unwrap(),
"-C",
dest_dir.to_str().unwrap(),
])
.output()?;
if !output.status.success() {
return Err(
format!(
"Failed to extract tar.xz: {}",
String::from_utf8_lossy(&output.stderr)
)
.into(),
);
}
}
// Find the extracted executable and set proper permissions
@@ -545,6 +646,44 @@ impl Extractor {
Ok(executable_path)
}
#[cfg(target_os = "windows")]
async fn extract_with_7zip(
&self,
archive_path: &Path,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
// Try to use 7-Zip for extraction (common on Windows)
let seven_zip_paths = [
"7z", // If 7z is in PATH
"C:\\Program Files\\7-Zip\\7z.exe",
"C:\\Program Files (x86)\\7-Zip\\7z.exe",
];
for seven_zip_path in &seven_zip_paths {
let result = Command::new(seven_zip_path)
.args([
"x", // Extract with full paths
archive_path.to_str().unwrap(),
&format!("-o{}", dest_dir.display()), // Output directory
"-y", // Yes to all
])
.output();
match result {
Ok(output) if output.status.success() => {
println!("Successfully extracted using 7-Zip: {}", seven_zip_path);
return self.find_extracted_executable(dest_dir).await;
}
Ok(_) => continue,
Err(_) => continue,
}
}
Err(
"No suitable extraction tool found. Please install 7-Zip or ensure tar is available.".into(),
)
}
pub async fn extract_tar_bz2(
&self,
tar_path: &Path,
@@ -827,40 +966,138 @@ impl Extractor {
&self,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
println!(
"Searching for Windows executable in: {}",
dest_dir.display()
);
// Look for .exe files, preferring main browser executables
let exe_names = [
"chrome.exe",
let priority_exe_names = [
"firefox.exe",
"chrome.exe",
"chromium.exe",
"zen.exe",
"brave.exe",
"tor-browser.exe",
"tor.exe",
"mullvad-browser.exe",
];
for exe_name in &exe_names {
// First try priority executable names
for exe_name in &priority_exe_names {
let exe_path = dest_dir.join(exe_name);
if exe_path.exists() {
println!("Found priority executable: {}", exe_path.display());
return Ok(exe_path);
}
}
// If no specific executable found, look for any .exe file
if let Ok(entries) = fs::read_dir(dest_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "exe") {
return Ok(path);
// Recursively search for executables with depth limit
match self.find_windows_executable_recursive(dest_dir, 0, 3).await {
Ok(exe_path) => {
println!(
"Found executable via recursive search: {}",
exe_path.display()
);
Ok(exe_path)
}
Err(_) => {
// List directory contents for debugging
if let Ok(entries) = fs::read_dir(dest_dir) {
println!("Directory contents:");
for entry in entries.flatten() {
let path = entry.path();
let metadata = if path.is_dir() { "dir" } else { "file" };
println!(" - {} ({})", path.display(), metadata);
}
}
// Check subdirectories
if path.is_dir() {
if let Ok(sub_result) = self.find_windows_executable(&path).await {
return Ok(sub_result);
Err("No executable found after extraction".into())
}
}
}
#[cfg(target_os = "windows")]
fn find_windows_executable_recursive<'a>(
&'a self,
dir: &'a Path,
depth: usize,
max_depth: usize,
) -> std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<PathBuf, Box<dyn std::error::Error + Send + Sync>>>
+ Send
+ 'a,
>,
> {
Box::pin(async move {
if depth > max_depth {
return Err("Maximum search depth reached".into());
}
if let Ok(entries) = fs::read_dir(dir) {
let mut dirs_to_search = Vec::new();
// First pass: look for .exe files in current directory
for entry in entries.flatten() {
let path = entry.path();
if path.is_file()
&& path
.extension()
.is_some_and(|ext| ext.to_string_lossy().to_lowercase() == "exe")
{
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_lowercase();
// Check if it's a browser executable
if file_name.contains("firefox")
|| file_name.contains("chrome")
|| file_name.contains("chromium")
|| file_name.contains("zen")
|| file_name.contains("brave")
|| file_name.contains("tor")
|| file_name.contains("mullvad")
|| file_name.contains("browser")
{
return Ok(path);
}
} else if path.is_dir() {
// Collect directories for later search
dirs_to_search.push(path);
}
}
// Second pass: search subdirectories
for subdir in dirs_to_search {
if let Ok(result) = self
.find_windows_executable_recursive(&subdir, depth + 1, max_depth)
.await
{
return Ok(result);
}
}
// Third pass: if no browser-specific executable found, return any .exe
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file()
&& path
.extension()
.is_some_and(|ext| ext.to_string_lossy().to_lowercase() == "exe")
{
return Ok(path);
}
}
}
}
}
Err("No executable found after extraction".into())
Err("No executable found".into())
})
}
#[cfg(target_os = "linux")]
+1
View File
@@ -172,6 +172,7 @@ pub fn run() {
.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)]
+141 -28
View File
@@ -55,6 +55,9 @@ impl ProfileImporter {
// Detect Zen Browser profiles
detected_profiles.extend(self.detect_zen_browser_profiles()?);
// Detect TOR Browser profiles
detected_profiles.extend(self.detect_tor_browser_profiles()?);
// Remove duplicates based on path
let mut seen_paths = HashSet::new();
let unique_profiles: Vec<DetectedProfile> = detected_profiles
@@ -80,9 +83,16 @@ impl ProfileImporter {
#[cfg(target_os = "windows")]
{
if let Some(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")?);
// 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")?);
}
}
@@ -117,12 +127,11 @@ impl ProfileImporter {
#[cfg(target_os = "windows")]
{
if let Some(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")?);
}
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")?);
}
}
@@ -156,10 +165,9 @@ impl ProfileImporter {
#[cfg(target_os = "windows")]
{
if let Some(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")?);
}
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")]
@@ -186,10 +194,9 @@ impl ProfileImporter {
#[cfg(target_os = "windows")]
{
if let Some(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")?);
}
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")]
@@ -216,10 +223,9 @@ impl ProfileImporter {
#[cfg(target_os = "windows")]
{
if let Some(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")?);
}
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")]
@@ -251,9 +257,16 @@ impl ProfileImporter {
#[cfg(target_os = "windows")]
{
if let Some(app_data) = self.base_dirs.data_dir() {
let mullvad_dir = app_data.join("MullvadBrowser/Profiles");
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
// Primary location in AppData\Roaming
let app_data = self.base_dirs.data_dir();
let mullvad_dir = app_data.join("MullvadBrowser/Profiles");
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
// Also check common installation locations
let local_app_data = self.base_dirs.data_local_dir();
let mullvad_local_dir = local_app_data.join("MullvadBrowser/Profiles");
if mullvad_local_dir.exists() {
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_local_dir, "mullvad-browser")?);
}
}
@@ -283,10 +296,9 @@ impl ProfileImporter {
#[cfg(target_os = "windows")]
{
if let Some(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")?);
}
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")]
@@ -298,6 +310,107 @@ impl ProfileImporter {
Ok(profiles)
}
/// Detect TOR Browser profiles
fn detect_tor_browser_profiles(
&self,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
// TOR Browser on macOS is typically in Applications
let tor_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/TorBrowser-Data/Browser/profile.default");
if tor_dir.exists() {
profiles.push(DetectedProfile {
browser: "tor-browser".to_string(),
name: "TOR Browser - Default Profile".to_string(),
path: tor_dir.to_string_lossy().to_string(),
description: "Default TOR Browser profile".to_string(),
});
}
}
#[cfg(target_os = "windows")]
{
// Check common TOR Browser installation locations on Windows
let possible_paths = [
// Default installation in user directory
(
"Desktop",
"Desktop/Tor Browser/Browser/TorBrowser/Data/Browser/profile.default",
),
// AppData locations
(
"AppData/Roaming",
"TorBrowser/Browser/TorBrowser/Data/Browser/profile.default",
),
(
"AppData/Local",
"TorBrowser/Browser/TorBrowser/Data/Browser/profile.default",
),
];
let home_dir = self.base_dirs.home_dir();
for (location_name, relative_path) in &possible_paths {
let tor_dir = home_dir.join(relative_path);
if tor_dir.exists() {
profiles.push(DetectedProfile {
browser: "tor-browser".to_string(),
name: format!("TOR Browser - {} Profile", location_name),
path: tor_dir.to_string_lossy().to_string(),
description: format!("TOR Browser profile from {}", location_name),
});
}
}
// Also check AppData directories if available
let app_data = self.base_dirs.data_dir();
let tor_app_data =
app_data.join("TorBrowser/Browser/TorBrowser/Data/Browser/profile.default");
if tor_app_data.exists() {
profiles.push(DetectedProfile {
browser: "tor-browser".to_string(),
name: "TOR Browser - AppData Profile".to_string(),
path: tor_app_data.to_string_lossy().to_string(),
description: "TOR Browser profile from AppData".to_string(),
});
}
}
#[cfg(target_os = "linux")]
{
// Common TOR Browser locations on Linux
let possible_paths = [
".local/share/torbrowser/tbb/x86_64/tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default",
"tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default",
".tor-browser/Browser/TorBrowser/Data/Browser/profile.default",
"Downloads/tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default",
];
let home_dir = self.base_dirs.home_dir();
for relative_path in &possible_paths {
let tor_dir = home_dir.join(relative_path);
if tor_dir.exists() {
profiles.push(DetectedProfile {
browser: "tor-browser".to_string(),
name: "TOR Browser - Default Profile".to_string(),
path: tor_dir.to_string_lossy().to_string(),
description: "TOR Browser profile".to_string(),
});
break; // Only add the first one found to avoid duplicates
}
}
}
Ok(profiles)
}
/// Scan Firefox-style profiles directory
fn scan_firefox_profiles_dir(
&self,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut Browser",
"version": "0.3.1",
"version": "0.3.3",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm dev",
+60
View File
@@ -3,6 +3,7 @@
import { ChangeVersionDialog } from "@/components/change-version-dialog";
import { CreateProfileDialog } from "@/components/create-profile-dialog";
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 { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
@@ -21,6 +22,8 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
import { usePermissions } from "@/hooks/use-permissions";
import type { PermissionType } from "@/hooks/use-permissions";
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
import { showErrorToast } from "@/lib/toast-utils";
import type { BrowserProfile, ProxySettings } from "@/types";
@@ -58,6 +61,11 @@ export default function Home() {
const [currentProfileForVersionChange, setCurrentProfileForVersionChange] =
useState<BrowserProfile | null>(null);
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
const [currentPermissionType, setCurrentPermissionType] =
useState<PermissionType>("microphone");
const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } =
usePermissions();
// Simple profiles loader without updates check (for use as callback)
const loadProfiles = useCallback(async () => {
@@ -119,6 +127,13 @@ export default function Home() {
};
}, [loadProfilesWithUpdateCheck, checkForUpdates]);
// Check permissions when they are initialized
useEffect(() => {
if (isInitialized) {
void checkAllPermissions();
}
}, [isInitialized]);
const checkStartupPrompt = async () => {
// Only check once during app startup to prevent reopening after dismissing notifications
if (hasCheckedStartupPrompt) return;
@@ -137,6 +152,42 @@ export default function Home() {
}
};
const checkAllPermissions = async () => {
try {
// 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 permissions:", error);
}
};
const checkNextPermission = () => {
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);
}
};
const checkStartupUrls = async () => {
try {
const hasStartupUrl = await invoke<boolean>(
@@ -533,6 +584,15 @@ export default function Home() {
runningProfiles={runningProfiles}
/>
))}
<PermissionDialog
isOpen={permissionDialogOpen}
onClose={() => {
setPermissionDialogOpen(false);
}}
permissionType={currentPermissionType}
onPermissionGranted={checkNextPermission}
/>
</div>
);
}
+1 -1
View File
@@ -293,7 +293,7 @@ export function CreateProfileDialog({
disabled={true}
className="opacity-50"
>
{displayName} (Not supported on this platform)
{displayName} (Not supported)
</SelectItem>
</TooltipTrigger>
<TooltipContent>
+15 -15
View File
@@ -116,31 +116,31 @@ type ToastProps =
function getToastIcon(type: ToastProps["type"], stage?: string) {
switch (type) {
case "success":
return <LuCheckCheck className="h-4 w-4 text-green-500 flex-shrink-0" />;
return <LuCheckCheck className="flex-shrink-0 w-4 h-4 text-green-500" />;
case "error":
return <LuTriangleAlert className="h-4 w-4 text-red-500 flex-shrink-0" />;
return <LuTriangleAlert className="flex-shrink-0 w-4 h-4 text-red-500" />;
case "download":
if (stage === "completed") {
return (
<LuCheckCheck className="h-4 w-4 text-green-500 flex-shrink-0" />
<LuCheckCheck className="flex-shrink-0 w-4 h-4 text-green-500" />
);
}
return <LuDownload className="h-4 w-4 text-blue-500 flex-shrink-0" />;
return <LuDownload className="flex-shrink-0 w-4 h-4 text-blue-500" />;
case "version-update":
return (
<LuRefreshCw className="h-4 w-4 text-blue-500 animate-spin flex-shrink-0" />
<LuRefreshCw className="flex-shrink-0 w-4 h-4 text-blue-500 animate-spin" />
);
case "fetching":
return (
<LuRefreshCw className="h-4 w-4 text-blue-500 animate-spin flex-shrink-0" />
<LuRefreshCw className="flex-shrink-0 w-4 h-4 text-blue-500 animate-spin" />
);
case "twilight-update":
return (
<LuRefreshCw className="h-4 w-4 text-purple-500 animate-spin flex-shrink-0" />
<LuRefreshCw className="flex-shrink-0 w-4 h-4 text-purple-500 animate-spin" />
);
default:
return (
<div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-500 border-t-transparent flex-shrink-0" />
<div className="flex-shrink-0 w-4 h-4 rounded-full border-2 border-blue-500 animate-spin border-t-transparent" />
);
}
}
@@ -151,10 +151,10 @@ export function UnifiedToast(props: ToastProps) {
const progress = "progress" in props ? props.progress : undefined;
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-3 shadow-lg">
<div className="flex items-start p-3 w-full bg-white rounded-lg border border-gray-200 shadow-lg dark:bg-gray-800 dark:border-gray-700">
<div className="mr-3 mt-0.5">{getToastIcon(type, stage)}</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-white leading-tight">
<p className="text-sm font-medium leading-tight text-gray-900 dark:text-white">
{title}
</p>
@@ -165,7 +165,7 @@ export function UnifiedToast(props: ToastProps) {
stage === "downloading" && (
<div className="mt-2 space-y-1">
<div className="flex justify-between items-center">
<p className="text-xs text-gray-600 dark:text-gray-300 min-w-0 flex-1">
<p className="flex-1 min-w-0 text-xs text-gray-600 dark:text-gray-300">
{progress.percentage.toFixed(1)}%
{progress.speed && `${progress.speed} MB/s`}
{progress.eta && `${progress.eta} remaining`}
@@ -195,7 +195,7 @@ export function UnifiedToast(props: ToastProps) {
}}
/>
</div>
<span className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap shrink-0 w-8 text-right">
<span className="w-8 text-xs text-right text-gray-500 whitespace-nowrap dark:text-gray-400 shrink-0">
{progress.current}/{progress.total}
</span>
</div>
@@ -211,7 +211,7 @@ export function UnifiedToast(props: ToastProps) {
: "Checking for twilight updates..."}
</p>
{props.browserName && (
<p className="text-xs text-purple-600 dark:text-purple-400 mt-1">
<p className="mt-1 text-xs text-purple-600 dark:text-purple-400">
{props.browserName} Rolling Release
</p>
)}
@@ -220,7 +220,7 @@ export function UnifiedToast(props: ToastProps) {
{/* Description */}
{description && (
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300 leading-tight">
<p className="mt-1 text-xs leading-tight text-gray-600 dark:text-gray-300">
{description}
</p>
)}
@@ -235,7 +235,7 @@ export function UnifiedToast(props: ToastProps) {
)}
{stage === "verifying" && (
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300">
Verifying installation...
Verifying browser files...
</p>
)}
{stage === "downloading (twilight rolling release)" && (
+2 -2
View File
@@ -458,7 +458,7 @@ export function ImportProfileDialog({
isLoading
}
>
Import Detected Profile
Import Profile
</LoadingButton>
) : (
<LoadingButton
@@ -472,7 +472,7 @@ export function ImportProfileDialog({
!manualProfileName.trim()
}
>
Import Manual Profile
Import Profile
</LoadingButton>
)}
</DialogFooter>
+182
View File
@@ -0,0 +1,182 @@
"use client";
import { LoadingButton } from "@/components/loading-button";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { usePermissions } from "@/hooks/use-permissions";
import type { PermissionType } from "@/hooks/use-permissions";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import { useEffect, useState } from "react";
import { BsCamera, BsMic } from "react-icons/bs";
interface PermissionDialogProps {
isOpen: boolean;
onClose: () => void;
permissionType: PermissionType;
onPermissionGranted?: () => void;
}
export function PermissionDialog({
isOpen,
onClose,
permissionType,
onPermissionGranted,
}: PermissionDialogProps) {
const [isRequesting, setIsRequesting] = useState(false);
const [isMacOS, setIsMacOS] = useState(false);
const {
requestPermission,
isMicrophoneAccessGranted,
isCameraAccessGranted,
} = usePermissions();
// Check if we're on macOS and close dialog if not
useEffect(() => {
const userAgent = navigator.userAgent;
const isMac = userAgent.includes("Mac");
setIsMacOS(isMac);
// If not macOS, close the dialog as permissions aren't needed
if (!isMac) {
onClose();
}
}, [onClose]);
// Get current permission status
const isCurrentPermissionGranted =
permissionType === "microphone"
? isMicrophoneAccessGranted
: isCameraAccessGranted;
// Auto-close dialog when permission is granted
useEffect(() => {
if (isCurrentPermissionGranted && isOpen) {
onPermissionGranted?.();
}
}, [isCurrentPermissionGranted, isOpen, onPermissionGranted]);
const getPermissionIcon = (type: PermissionType) => {
switch (type) {
case "microphone":
return <BsMic className="w-8 h-8" />;
case "camera":
return <BsCamera className="w-8 h-8" />;
}
};
const getPermissionTitle = (type: PermissionType) => {
switch (type) {
case "microphone":
return "Microphone Access Required";
case "camera":
return "Camera Access Required";
}
};
const getPermissionDescription = (type: PermissionType) => {
switch (type) {
case "microphone":
return "Donut Browser needs access to your microphone to enable microphone functionality in web browsers. Each website that wants to use your microphone will still ask for your permission individually.";
case "camera":
return "Donut Browser needs access to your camera to enable camera functionality in web browsers. Each website that wants to use your camera will still ask for your permission individually.";
}
};
const getStatusBadge = (isGranted: boolean) => {
if (isGranted) {
return (
<Badge variant="default" className="text-green-800 bg-green-100">
Granted
</Badge>
);
}
return <Badge variant="secondary">Not Granted</Badge>;
};
const handleRequestPermission = async () => {
setIsRequesting(true);
try {
await requestPermission(permissionType);
showSuccessToast(
`${getPermissionTitle(permissionType).replace(
" Required",
"",
)} permission requested`,
);
} catch (error) {
console.error("Failed to request permission:", error);
showErrorToast("Failed to request permission");
} finally {
setIsRequesting(false);
}
};
// Don't render if not macOS
if (!isMacOS) {
return null;
}
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader className="text-center">
<div className="flex justify-center items-center mx-auto mb-4 w-16 h-16 bg-blue-100 rounded-full dark:bg-blue-900">
{getPermissionIcon(permissionType)}
</div>
<DialogTitle className="text-xl">
{getPermissionTitle(permissionType)}
</DialogTitle>
<DialogDescription className="text-base">
{getPermissionDescription(permissionType)}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{isCurrentPermissionGranted && (
<div className="p-3 bg-green-50 rounded-lg dark:bg-green-900/20">
<p className="text-sm text-green-800 dark:text-green-200">
Permission granted! Browsers launched from Donut Browser can
now access your {permissionType}.
</p>
</div>
)}
{!isCurrentPermissionGranted && (
<div className="p-3 bg-amber-50 rounded-lg dark:bg-amber-900/20">
<p className="text-sm text-amber-800 dark:text-amber-200">
Permission not granted. Click the button below to request
access to your {permissionType}.
</p>
</div>
)}
</div>
<DialogFooter className="gap-2">
<Button variant="outline" onClick={onClose}>
{isCurrentPermissionGranted ? "Done" : "Cancel"}
</Button>
{!isCurrentPermissionGranted && (
<LoadingButton
isLoading={isRequesting}
onClick={() => {
handleRequestPermission().catch(console.error);
}}
className="min-w-24"
>
Grant Access
</LoadingButton>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+207 -8
View File
@@ -19,10 +19,13 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { usePermissions } from "@/hooks/use-permissions";
import type { PermissionType } from "@/hooks/use-permissions";
import { showSuccessToast } from "@/lib/toast-utils";
import { invoke } from "@tauri-apps/api/core";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { BsCamera, BsMic } from "react-icons/bs";
interface AppSettings {
set_as_default_browser: boolean;
@@ -32,6 +35,12 @@ interface AppSettings {
auto_delete_unused_binaries: boolean;
}
interface PermissionInfo {
permission_type: PermissionType;
isGranted: boolean;
description: string;
}
interface SettingsDialogProps {
isOpen: boolean;
onClose: () => void;
@@ -57,18 +66,46 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
const [isSaving, setIsSaving] = useState(false);
const [isSettingDefault, setIsSettingDefault] = useState(false);
const [isClearingCache, setIsClearingCache] = useState(false);
const [permissions, setPermissions] = useState<PermissionInfo[]>([]);
const [isLoadingPermissions, setIsLoadingPermissions] = useState(false);
const [requestingPermission, setRequestingPermission] =
useState<PermissionType | null>(null);
const [isMacOS, setIsMacOS] = useState(false);
const { setTheme } = useTheme();
const {
requestPermission,
isMicrophoneAccessGranted,
isCameraAccessGranted,
} = usePermissions();
const getPermissionDescription = useCallback((type: PermissionType) => {
switch (type) {
case "microphone":
return "Access to microphone for browser applications";
case "camera":
return "Access to camera for browser applications";
}
}, []);
useEffect(() => {
if (isOpen) {
void loadSettings();
void checkDefaultBrowserStatus();
loadSettings().catch(console.error);
checkDefaultBrowserStatus().catch(console.error);
// Check if we're on macOS
const userAgent = navigator.userAgent;
const isMac = userAgent.includes("Mac");
setIsMacOS(isMac);
if (isMac) {
loadPermissions().catch(console.error);
}
// Set up interval to check default browser status
const intervalId = setInterval(() => {
void checkDefaultBrowserStatus();
}, 500); // Check every 2 seconds
checkDefaultBrowserStatus().catch(console.error);
}, 500); // Check every 500ms
// Cleanup interval on component unmount or dialog close
return () => {
@@ -77,6 +114,32 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
}
}, [isOpen]);
// Update permissions when the permission states change
useEffect(() => {
if (isMacOS) {
const permissionList: PermissionInfo[] = [
{
permission_type: "microphone",
isGranted: isMicrophoneAccessGranted,
description: getPermissionDescription("microphone"),
},
{
permission_type: "camera",
isGranted: isCameraAccessGranted,
description: getPermissionDescription("camera"),
},
];
setPermissions(permissionList);
} else {
setPermissions([]);
}
}, [
isMacOS,
isMicrophoneAccessGranted,
isCameraAccessGranted,
getPermissionDescription,
]);
const loadSettings = async () => {
setIsLoading(true);
try {
@@ -90,6 +153,36 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
}
};
const loadPermissions = async () => {
setIsLoadingPermissions(true);
try {
if (!isMacOS) {
// On non-macOS platforms, don't show permissions
setPermissions([]);
return;
}
const permissionList: PermissionInfo[] = [
{
permission_type: "microphone",
isGranted: isMicrophoneAccessGranted,
description: getPermissionDescription("microphone"),
},
{
permission_type: "camera",
isGranted: isCameraAccessGranted,
description: getPermissionDescription("camera"),
},
];
setPermissions(permissionList);
} catch (error) {
console.error("Failed to load permissions:", error);
} finally {
setIsLoadingPermissions(false);
}
};
const checkDefaultBrowserStatus = async () => {
try {
const isDefault = await invoke<boolean>("is_default_browser");
@@ -127,6 +220,49 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
}
};
const handleRequestPermission = async (permissionType: PermissionType) => {
setRequestingPermission(permissionType);
try {
await requestPermission(permissionType);
showSuccessToast(
`${getPermissionDisplayName(permissionType)} access requested`,
);
} catch (error) {
console.error("Failed to request permission:", error);
} finally {
setRequestingPermission(null);
}
};
const getPermissionIcon = (type: PermissionType) => {
switch (type) {
case "microphone":
return <BsMic className="h-4 w-4" />;
case "camera":
return <BsCamera className="h-4 w-4" />;
}
};
const getPermissionDisplayName = (type: PermissionType) => {
switch (type) {
case "microphone":
return "Microphone";
case "camera":
return "Camera";
}
};
const getStatusBadge = (isGranted: boolean) => {
if (isGranted) {
return (
<Badge variant="default" className="bg-green-100 text-green-800">
Granted
</Badge>
);
}
return <Badge variant="secondary">Not Granted</Badge>;
};
const handleSave = async () => {
setIsSaving(true);
try {
@@ -204,7 +340,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
<LoadingButton
isLoading={isSettingDefault}
onClick={() => {
void handleSetDefaultBrowser();
handleSetDefaultBrowser().catch(console.error);
}}
disabled={isDefaultBrowser}
variant={isDefaultBrowser ? "outline" : "default"}
@@ -284,6 +420,69 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
</p>
</div>
{/* Permissions Section - Only show on macOS */}
{isMacOS && (
<div className="space-y-4">
<Label className="text-base font-medium">
System Permissions
</Label>
{isLoadingPermissions ? (
<div className="text-sm text-muted-foreground">
Loading permissions...
</div>
) : (
<div className="space-y-3">
{permissions.map((permission) => (
<div
key={permission.permission_type}
className="flex items-center justify-between p-3 border rounded-lg"
>
<div className="flex items-center space-x-3">
{getPermissionIcon(permission.permission_type)}
<div>
<div className="text-sm font-medium">
{getPermissionDisplayName(
permission.permission_type,
)}
</div>
<div className="text-xs text-muted-foreground">
{permission.description}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
{getStatusBadge(permission.isGranted)}
{!permission.isGranted && (
<LoadingButton
size="sm"
isLoading={
requestingPermission ===
permission.permission_type
}
onClick={() => {
handleRequestPermission(
permission.permission_type,
).catch(console.error);
}}
>
Grant
</LoadingButton>
)}
</div>
</div>
))}
</div>
)}
<p className="text-xs text-muted-foreground">
These permissions allow browsers launched from Donut Browser to
access system resources. Each website will still ask for your
permission individually.
</p>
</div>
)}
{/* Advanced Section */}
<div className="space-y-4">
<Label className="text-base font-medium">Advanced</Label>
@@ -291,7 +490,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
<LoadingButton
isLoading={isClearingCache}
onClick={() => {
void handleClearCache();
handleClearCache().catch(console.error);
}}
variant="outline"
className="w-full"
@@ -314,7 +513,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
<LoadingButton
isLoading={isSaving}
onClick={() => {
void handleSave();
handleSave().catch(console.error);
}}
disabled={isLoading || !hasChanges}
>
+174
View File
@@ -0,0 +1,174 @@
import { useCallback, useEffect, useRef, useState } from "react";
// Platform-specific imports
let macOSPermissions:
| typeof import("tauri-plugin-macos-permissions-api")
| null = null;
// Dynamically import macOS permissions only when needed
const loadMacOSPermissions = async () => {
if (macOSPermissions) return macOSPermissions;
try {
macOSPermissions = await import("tauri-plugin-macos-permissions-api");
return macOSPermissions;
} catch (error) {
console.warn("Failed to load macOS permissions API:", error);
return null;
}
};
export type PermissionType = "microphone" | "camera";
export interface UsePermissionsReturn {
requestPermission: (type: PermissionType) => Promise<void>;
isMicrophoneAccessGranted: boolean;
isCameraAccessGranted: boolean;
isInitialized: boolean;
}
export function usePermissions(): UsePermissionsReturn {
const [isMicrophoneAccessGranted, setIsMicrophoneAccessGranted] =
useState(false);
const [isCameraAccessGranted, setIsCameraAccessGranted] = useState(false);
const [currentPlatform, setCurrentPlatform] = useState<string | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
// Check permissions status
const checkPermissions = useCallback(async () => {
if (!currentPlatform) return;
if (currentPlatform !== "macos") {
// Windows/Linux - assume permissions are granted
setIsMicrophoneAccessGranted(true);
setIsCameraAccessGranted(true);
setIsInitialized(true);
return;
}
// macOS - use the permissions API
try {
const permissions = await loadMacOSPermissions();
if (permissions) {
const [micGranted, camGranted] = await Promise.all([
permissions.checkMicrophonePermission(),
permissions.checkCameraPermission(),
]);
setIsMicrophoneAccessGranted(micGranted);
setIsCameraAccessGranted(camGranted);
setIsInitialized(true);
}
} catch (error) {
console.error("Failed to check permissions on macOS:", error);
setIsInitialized(true);
}
}, [currentPlatform]);
// Request permission
const requestPermission = useCallback(
async (type: PermissionType): Promise<void> => {
if (!currentPlatform || currentPlatform !== "macos") return;
// macOS - use the permissions API
try {
const permissions = await loadMacOSPermissions();
if (!permissions) return;
if (type === "microphone") {
await permissions.requestMicrophonePermission();
// Poll for permission status change
const pollMicPermission = async () => {
const granted = await permissions.checkMicrophonePermission();
setIsMicrophoneAccessGranted(granted);
if (!granted) {
setTimeout(() => {
void pollMicPermission();
}, 1000);
}
};
await pollMicPermission();
}
if (type === "camera") {
await permissions.requestCameraPermission();
// Poll for permission status change
const pollCamPermission = async () => {
const granted = await permissions.checkCameraPermission();
setIsCameraAccessGranted(granted);
if (!granted) {
setTimeout(() => {
void pollCamPermission();
}, 1000);
}
};
await pollCamPermission();
}
} catch (error) {
console.error(`Failed to request ${type} permission on macOS:`, error);
}
},
[currentPlatform],
);
// Initialize platform detection and start interval checking
useEffect(() => {
const initializePlatform = async () => {
try {
// Detect platform - on macOS we need permissions, on others we don't
const userAgent = navigator.userAgent;
let platformName = "unknown";
if (userAgent.includes("Mac")) {
platformName = "macos";
} else if (userAgent.includes("Win")) {
platformName = "windows";
} else if (userAgent.includes("Linux")) {
platformName = "linux";
}
setCurrentPlatform(platformName);
} catch (error) {
console.error("Failed to detect platform:", error);
// Fallback - assume non-macOS
setCurrentPlatform("unknown");
}
};
initializePlatform().catch(console.error);
}, []);
// Set up interval checking when platform is determined
useEffect(() => {
if (!currentPlatform) return;
// Initial check
void checkPermissions();
// Set up 500ms interval for checking permissions
intervalRef.current = setInterval(() => {
void checkPermissions();
}, 500);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [currentPlatform, checkPermissions]);
return {
requestPermission,
isMicrophoneAccessGranted,
isCameraAccessGranted,
isInitialized,
};
}
+14
View File
@@ -40,3 +40,17 @@ export interface AppVersionInfo {
version: string;
is_nightly: boolean;
}
export type PermissionType = "microphone" | "camera" | "location";
export type PermissionStatus =
| "granted"
| "denied"
| "not_determined"
| "restricted";
export interface PermissionInfo {
permission_type: PermissionType;
status: PermissionStatus;
description: string;
}