Compare commits

...

32 Commits

Author SHA1 Message Date
zhom fb84068d30 docs: add features section to readme 2025-06-07 00:30:06 +04:00
zhom 5024eab062 chore: version bump 2025-06-07 00:01:01 +04:00
zhom 8137f9bf8d fix: adjust download logic to work with latest firefox cdn 2025-06-06 23:40:51 +04:00
zhom e2547c6ec7 docs: update agent instructions 2025-06-06 23:29:10 +04:00
zhom d8d59d2bd5 fix: extraction and version detection 2025-06-06 23:22:45 +04:00
zhom b84350eb13 docs: add ai agents instruction file 2025-06-06 23:22:29 +04:00
zhom 383cef916c docs: remove duplicate star history graph 2025-06-06 14:35:13 +04:00
zhom 743bc059be docs: update readme 2025-06-06 14:33:52 +04:00
zhom c46f54536b chore: linting 2025-06-06 04:52:05 +04:00
zhom 6cbc8627a1 chore: version bump 2025-06-06 04:43:22 +04:00
zhom a4f4cc2f27 style: clean up settings dialog 2025-06-06 04:41:50 +04:00
zhom 21c4d0a8ab build: use custom dependabot token for dependency automerge 2025-06-06 04:29:18 +04:00
zhom 9335149153 Merge pull request #12 from zhom/dependabot/cargo/src-tauri/rust-dependencies-ac9a5aa5e0
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 6 updates
2025-06-06 04:08:04 +04:00
zhom 6711659231 Merge pull request #13 from zhom/dependabot/npm_and_yarn/frontend-dependencies-1eccf95907
deps(deps): bump @types/node from 22.15.29 to 22.15.30 in the frontend-dependencies group
2025-06-06 04:07:37 +04:00
zhom 8a592e3d7d Merge pull request #14 from zhom/dependabot/npm_and_yarn/nodecar/nodecar-dependencies-1eccf95907
deps(nodecar)(deps): bump @types/node from 22.15.29 to 22.15.30 in /nodecar in the nodecar-dependencies group
2025-06-06 04:07:11 +04:00
zhom beea23307b build: install xdg-utils on ubuntu arm 2025-06-06 03:34:46 +04:00
zhom 19b66d006d build: switch to ubuntu-22.04-arm for linux arm build 2025-06-06 03:12:24 +04:00
zhom c7a36f6cd0 build: try to update mirrors if security.ubuntu.com is down 2025-06-06 02:51:39 +04:00
zhom 7404cb3ff8 build: install arm64 libraries on linux arm build worker 2025-06-06 02:12:52 +04:00
zhom ee91445fe1 feat: add ability to import existing profiles 2025-06-06 02:12:21 +04:00
zhom 77d53c7f32 build: install pkg-config on ubuntu 2025-06-06 01:28:34 +04:00
zhom a21f22a916 build: remove invalid package installation 2025-06-06 01:09:34 +04:00
dependabot[bot] 4aaf2eecbc deps(nodecar)(deps): bump @types/node
Bumps the nodecar-dependencies group in /nodecar with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

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

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


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

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

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

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


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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-05 21:00:03 +00:00
zhom 93eae1d77f build: set pkg-config variables for linux arm 2025-06-06 00:57:21 +04:00
zhom 82615c24bd build: set cross-compile build variables for linux arm 2025-06-06 00:39:05 +04:00
zhom 0769106a51 build: run dependabot on saturday 2025-06-06 00:32:56 +04:00
zhom 18aa3cb87b test: fix 2025-06-06 00:22:01 +04:00
zhom f066105c0f build: remove universal macos build until @yao-pkg/pkg has support for universal macos binaries 2025-06-06 00:10:20 +04:00
zhom 9d8b3629f6 docs: readme 2025-06-06 00:04:38 +04:00
zhom 353e149886 build: switch to ubuntu 22 for linux build 2025-06-05 23:59:44 +04:00
29 changed files with 2109 additions and 536 deletions
+4 -4
View File
@@ -5,7 +5,7 @@ updates:
directory: "/"
schedule:
interval: "weekly"
day: "monday"
day: "saturday"
time: "09:00"
allow:
- dependency-type: "all"
@@ -25,7 +25,7 @@ updates:
directory: "/nodecar"
schedule:
interval: "weekly"
day: "monday"
day: "saturday"
time: "09:00"
allow:
- dependency-type: "all"
@@ -42,7 +42,7 @@ updates:
directory: "/src-tauri"
schedule:
interval: "weekly"
day: "monday"
day: "saturday"
time: "09:00"
allow:
- dependency-type: "all"
@@ -59,7 +59,7 @@ updates:
directory: "/"
schedule:
interval: "weekly"
day: "monday"
day: "saturday"
time: "09:00"
groups:
github-actions:
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
- name: Auto-merge minor and patch updates
uses: ridedott/merge-me-action@v2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SECRET_DEPENDABOT_GITHUB_TOKEN }}
PRESET: DEPENDABOT_MINOR
MERGE_METHOD: SQUASH
timeout-minutes: 10
+4 -14
View File
@@ -57,19 +57,13 @@ jobs:
target: "x86_64-apple-darwin"
pkg_target: "latest-macos-x64"
nodecar_script: "build:mac-x86_64"
- platform: "macos-latest"
args: "--target universal-apple-darwin"
arch: "aarch64"
target: "aarch64-apple-darwin"
pkg_target: "universal"
nodecar_script: "build:mac-universal"
- platform: "ubuntu-20.04"
- platform: "ubuntu-22.04"
args: "--target x86_64-unknown-linux-gnu"
arch: "x86_64"
target: "x86_64-unknown-linux-gnu"
pkg_target: "latest-linux-x64"
nodecar_script: "build:linux-x64"
- platform: "ubuntu-20.04"
- platform: "ubuntu-22.04-arm"
args: "--target aarch64-unknown-linux-gnu"
arch: "aarch64"
target: "aarch64-unknown-linux-gnu"
@@ -107,14 +101,10 @@ jobs:
targets: ${{ matrix.target }}
- name: Install dependencies (Ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
# Install cross-compilation tools for ARM64
if [[ "${{ matrix.arch }}" == "aarch64" ]]; then
sudo apt-get install -y gcc-aarch64-linux-gnu
fi
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev pkg-config xdg-utils
- name: Rust cache
uses: swatinem/rust-cache@v2
+4 -14
View File
@@ -56,19 +56,13 @@ jobs:
target: "x86_64-apple-darwin"
pkg_target: "latest-macos-x64"
nodecar_script: "build:mac-x86_64"
- platform: "macos-latest"
args: "--target universal-apple-darwin"
arch: "aarch64"
target: "aarch64-apple-darwin,x86_64-apple-darwin"
pkg_target: "universal"
nodecar_script: "build:mac-universal"
- platform: "ubuntu-20.04"
- platform: "ubuntu-22.04"
args: "--target x86_64-unknown-linux-gnu"
arch: "x86_64"
target: "x86_64-unknown-linux-gnu"
pkg_target: "latest-linux-x64"
nodecar_script: "build:linux-x64"
- platform: "ubuntu-20.04"
- platform: "ubuntu-22.04-arm"
args: "--target aarch64-unknown-linux-gnu"
arch: "aarch64"
target: "aarch64-unknown-linux-gnu"
@@ -93,14 +87,10 @@ jobs:
targets: ${{ matrix.target }}
- name: Install dependencies (Ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
# Install cross-compilation tools for ARM64
if [[ "${{ matrix.arch }}" == "aarch64" ]]; then
sudo apt-get install -y gcc-aarch64-linux-gnu
fi
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev pkg-config xdg-utils
- name: Rust cache
uses: swatinem/rust-cache@v2
+9
View File
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"appimage",
"appindicator",
"applescript",
"autoconfig",
@@ -12,6 +13,7 @@
"codegen",
"devedition",
"donutbrowser",
"dpkg",
"dtolnay",
"elif",
"esbuild",
@@ -23,7 +25,12 @@
"idletime",
"KHTML",
"launchservices",
"libatk",
"libayatana",
"libcairo",
"libgdk",
"libglib",
"libpango",
"librsvg",
"libwebkit",
"mountpoint",
@@ -34,9 +41,11 @@
"ntlm",
"objc",
"osascript",
"pixbuf",
"plasmohq",
"propertylist",
"reqwest",
"ridedott",
"rlib",
"rustc",
"SARIF",
+4
View File
@@ -0,0 +1,4 @@
# Instructions for AI Agents
- If you want to run tests, only ever run them as "pnpm format && pnpm lint && pnpm test".
- Don't leave comments that don't add value
+36 -9
View File
@@ -1,18 +1,37 @@
# Donut Browser
<div align="center">
<img src="assets/logo.png" alt="Donut Browser Logo" width="150">
<h1>Donut Browser</h1>
<strong>A powerful browser orchestrator that puts you in control of your browsing experience. 🍩</strong>
</div>
<br>
![Donut Browser Logo](assets/logo.png)
<p align="center">
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/releases/latest" target="_blank"><img alt="GitHub release" src="https://img.shields.io/github/v/release/zhom/donutbrowser">
</a>
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/issues" target="_blank">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="PRs Welcome">
</a>
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/blob/main/LICENSE" target="_blank">
<img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg" alt="License">
</a>
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/stargazers" target="_blank">
<img src="https://img.shields.io/github/stars/zhom/donutbrowser?style=social" alt="GitHub stars">
</a>
</p>
## **A powerful browser orchestrator that puts you in control of your browsing experience. 🍩**
[![GitHub Release](https://img.shields.io/github/v/release/zhom/donutbrowser)](https://github.com/zhom/donutbrowser/releases/latest)
[![RPs Welcome!](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](https://github.com/zhom/donutbrowser/issues)
[![License](https://img.shields.io/badge/license-AGPL--3.0-blue.svg)](https://github.com/zhom/donutbrowser/blob/main/LICENSE)
[![GitHub Stars](https://img.shields.io/github/stars/zhom/donutbrowser?style=social)](https://github.com/zhom/donutbrowser/stargazers)
## Donut Browser
> A free and open source browser orchestrator built with [Tauri](https://v2.tauri.app/).
![Donut Browser Preview](assets/preview.png)
## Features
- Create unlimited number of local browser profiles completely isolated from each other
- Proxy support with basic auth for all browsers except for TOR Browser
- Import profiles from your existing browsers
- Set Donut Browser as your default browser to control in which profile to open links
## Download
> As of right now, the app is not signed by Apple. You need to have Gatekeeper disabled to run it. The app automatically checks for updates on each launch.
@@ -30,6 +49,8 @@ The app can be downloaded from the [releases page](https://github.com/zhom/donut
### Contributing
> Donut Browser is built with [Tauri](https://v2.tauri.app/).
See [CONTRIBUTING.md](CONTRIBUTING.md).
## Issues
@@ -45,7 +66,13 @@ Have questions or want to contribute? We'd love to hear from you!
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date)](https://www.star-history.com/#zhom/donutbrowser&Date)
<a href="https://www.star-history.com/#zhom/donutbrowser&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date" />
</picture>
</a>
## Contact
+1 -2
View File
@@ -11,7 +11,6 @@
"build": "tsc && pkg ./dist/index.js --targets latest-macos-arm64 --output dist/nodecar && pnpm rename-binary",
"build:mac-aarch64": "tsc && pkg ./dist/index.js --targets latest-macos-arm64 --output dist/nodecar && pnpm rename-binary",
"build:mac-x86_64": "tsc && pkg ./dist/index.js --targets latest-macos-x64 --output dist/nodecar && pnpm rename-binary",
"build:mac-universal": "pnpm build:mac-aarch64 && pnpm rename-binary && pnpm build:mac-x86_64 && pnpm rename-binary x86_64-apple-darwin",
"build:linux-x64": "tsc && pkg ./dist/index.js --targets latest-linux-x64 --output dist/nodecar && pnpm rename-binary",
"build:linux-arm64": "tsc && pkg ./dist/index.js --targets latest-linux-arm64 --output dist/nodecar && pnpm rename-binary",
"build:win-x64": "tsc && pkg ./dist/index.js --targets latest-win-x64 --output dist/nodecar && pnpm rename-binary",
@@ -21,7 +20,7 @@
"author": "",
"license": "AGPL-3.0",
"dependencies": {
"@types/node": "^22.15.29",
"@types/node": "^22.15.30",
"@yao-pkg/pkg": "^6.5.1",
"commander": "^14.0.0",
"dotenv": "^16.5.0",
+3 -2
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.2.5",
"version": "0.3.1",
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
@@ -35,6 +35,7 @@
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-table": "^8.21.3",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "~2.3.0",
"@tauri-apps/plugin-opener": "^2.2.7",
"class-variance-authority": "^0.7.1",
@@ -55,7 +56,7 @@
"@next/eslint-plugin-next": "^15.3.3",
"@tailwindcss/postcss": "^4.1.8",
"@tauri-apps/cli": "^2.5.0",
"@types/node": "^22.15.29",
"@types/node": "^22.15.30",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.6",
"@typescript-eslint/eslint-plugin": "^8.33.1",
+25 -15
View File
@@ -44,6 +44,9 @@ importers:
'@tauri-apps/api':
specifier: ^2.5.0
version: 2.5.0
'@tauri-apps/plugin-dialog':
specifier: ^2.2.2
version: 2.2.2
'@tauri-apps/plugin-fs':
specifier: ~2.3.0
version: 2.3.0
@@ -100,8 +103,8 @@ importers:
specifier: ^2.5.0
version: 2.5.0
'@types/node':
specifier: ^22.15.29
version: 22.15.29
specifier: ^22.15.30
version: 22.15.30
'@types/react':
specifier: ^19.1.6
version: 19.1.6
@@ -116,7 +119,7 @@ importers:
version: 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
'@vitejs/plugin-react':
specifier: ^4.5.1
version: 4.5.1(vite@6.2.0(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))
version: 4.5.1(vite@6.2.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))
eslint:
specifier: ^9.28.0
version: 9.28.0(jiti@2.4.2)
@@ -148,8 +151,8 @@ importers:
nodecar:
dependencies:
'@types/node':
specifier: ^22.15.29
version: 22.15.29
specifier: ^22.15.30
version: 22.15.30
'@yao-pkg/pkg':
specifier: ^6.5.1
version: 6.5.1
@@ -170,7 +173,7 @@ importers:
version: 2.5.9
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@22.15.29)(typescript@5.8.3)
version: 10.9.2(@types/node@22.15.30)(typescript@5.8.3)
typescript:
specifier: ^5.8.3
version: 5.8.3
@@ -1450,6 +1453,9 @@ packages:
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-dialog@2.2.2':
resolution: {integrity: sha512-Pm9qnXQq8ZVhAMFSEPwxvh+nWb2mk7LASVlNEHYaksHvcz8P6+ElR5U5dNL9Ofrm+uwhh1/gYKWswK8JJJAh6A==}
'@tauri-apps/plugin-fs@2.3.0':
resolution: {integrity: sha512-G9gEyYVUaaxhdRJBgQTTLmzAe0vtHYxYyN1oTQzU3zwvb8T+tVLcAqCdFMWHq0qGeGbmynI5whvYpcXo5LvZ1w==}
@@ -1492,8 +1498,8 @@ packages:
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/node@22.15.29':
resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==}
'@types/node@22.15.30':
resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==}
'@types/react-dom@19.1.6':
resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==}
@@ -4657,6 +4663,10 @@ snapshots:
'@tauri-apps/cli-win32-ia32-msvc': 2.5.0
'@tauri-apps/cli-win32-x64-msvc': 2.5.0
'@tauri-apps/plugin-dialog@2.2.2':
dependencies:
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-fs@2.3.0':
dependencies:
'@tauri-apps/api': 2.5.0
@@ -4705,7 +4715,7 @@ snapshots:
'@types/json5@0.0.29': {}
'@types/node@22.15.29':
'@types/node@22.15.30':
dependencies:
undici-types: 6.21.0
@@ -4862,7 +4872,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.7.9':
optional: true
'@vitejs/plugin-react@4.5.1(vite@6.2.0(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))':
'@vitejs/plugin-react@4.5.1(vite@6.2.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))':
dependencies:
'@babel/core': 7.27.4
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4)
@@ -4870,7 +4880,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.9
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 6.2.0(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)
vite: 6.2.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
@@ -6868,14 +6878,14 @@ snapshots:
dependencies:
typescript: 5.8.3
ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3):
ts-node@10.9.2(@types/node@22.15.30)(typescript@5.8.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 22.15.29
'@types/node': 22.15.30
acorn: 8.14.1
acorn-walk: 8.3.4
arg: 4.1.3
@@ -7022,13 +7032,13 @@ snapshots:
v8-compile-cache-lib@3.0.1: {}
vite@6.2.0(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0):
vite@6.2.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0):
dependencies:
esbuild: 0.25.5
postcss: 8.5.4
rollup: 4.41.1
optionalDependencies:
'@types/node': 22.15.29
'@types/node': 22.15.30
fsevents: 2.3.3
jiti: 2.4.2
lightningcss: 1.30.1
+122 -100
View File
@@ -82,6 +82,24 @@ dependencies = [
"derive_arbitrary",
]
[[package]]
name = "ashpd"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df"
dependencies = [
"enumflags2",
"futures-channel",
"futures-util",
"rand 0.9.1",
"raw-window-handle",
"serde",
"serde_repr",
"tokio",
"url",
"zbus",
]
[[package]]
name = "assert-json-diff"
version = "2.0.2"
@@ -387,9 +405,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.17.0"
version = "3.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
checksum = "c1b094a32014c3d1f3944e4808e0e7c70e97dae0660886a8eb6dbc52d745badc"
[[package]]
name = "bytemuck"
@@ -908,6 +926,18 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "dispatch2"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0"
dependencies = [
"bitflags 2.9.1",
"block2 0.6.1",
"libc",
"objc2 0.6.1",
]
[[package]]
name = "dispatch2"
version = "0.3.0"
@@ -963,7 +993,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.2.5"
version = "0.3.1"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -980,6 +1010,7 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-opener",
"tauri-plugin-shell",
@@ -1827,9 +1858,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.13"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -1848,7 +1879,7 @@ dependencies = [
"tokio",
"tower-service",
"tracing",
"windows-registry 0.4.0",
"windows-registry",
]
[[package]]
@@ -2599,7 +2630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags 2.9.1",
"dispatch2",
"dispatch2 0.3.0",
"objc2 0.6.1",
]
@@ -2610,7 +2641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
dependencies = [
"bitflags 2.9.1",
"dispatch2",
"dispatch2 0.3.0",
"objc2 0.6.1",
"objc2-core-foundation",
"objc2-io-surface",
@@ -3282,6 +3313,16 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
@@ -3302,6 +3343,16 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
]
[[package]]
name = "rand_core"
version = "0.5.1"
@@ -3320,6 +3371,15 @@ dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
@@ -3438,6 +3498,31 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfd"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d"
dependencies = [
"ashpd",
"block2 0.6.1",
"dispatch2 0.2.0",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -4029,9 +4114,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.35.1"
version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79251336d17c72d9762b8b54be4befe38d2db56fbbc0241396d70f173c39d47a"
checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e"
dependencies = [
"libc",
"memchr",
@@ -4278,10 +4363,28 @@ dependencies = [
"thiserror 2.0.12",
"tracing",
"url",
"windows-registry 0.5.2",
"windows-registry",
"windows-result",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33318fe222fc2a612961de8b0419e2982767f213f54a4d3a21b0d7b85c41df8"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.12",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.3.0"
@@ -4580,6 +4683,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
]
@@ -4731,9 +4835,9 @@ dependencies = [
[[package]]
name = "tower-http"
version = "0.6.5"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc2d9e086a412a451384326f521c8123a99a466b329941a9403696bff9b0da2"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [
"bitflags 2.9.1",
"bytes",
@@ -5284,7 +5388,7 @@ dependencies = [
"windows-interface",
"windows-link",
"windows-result",
"windows-strings 0.4.2",
"windows-strings",
]
[[package]]
@@ -5336,17 +5440,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-registry"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
]
[[package]]
name = "windows-registry"
version = "0.5.2"
@@ -5355,7 +5448,7 @@ checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820"
dependencies = [
"windows-link",
"windows-result",
"windows-strings 0.4.2",
"windows-strings",
]
[[package]]
@@ -5367,15 +5460,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
@@ -5436,29 +5520,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
@@ -5489,12 +5557,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -5507,12 +5569,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -5525,24 +5581,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -5555,12 +5599,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -5573,12 +5611,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -5591,12 +5623,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -5609,12 +5635,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.5.40"
@@ -5795,6 +5815,7 @@ dependencies = [
"ordered-stream",
"serde",
"serde_repr",
"tokio",
"tracing",
"uds_windows",
"windows-sys 0.59.0",
@@ -6006,6 +6027,7 @@ dependencies = [
"endi",
"enumflags2",
"serde",
"url",
"winnow 0.7.10",
"zvariant_derive",
"zvariant_utils",
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.2.5"
version = "0.3.1"
description = "Simple Yet Powerful Browser Orchestrator"
authors = ["zhom@github"]
edition = "2021"
@@ -26,6 +26,7 @@ tauri-plugin-opener = "2"
tauri-plugin-fs = "2"
tauri-plugin-shell = "2"
tauri-plugin-deep-link = "2"
tauri-plugin-dialog = "2"
directories = "6"
reqwest = { version = "0.12", features = ["json", "stream"] }
tokio = { version = "1", features = ["full"] }
+1 -1
View File
@@ -13,7 +13,7 @@
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>0.2.5</string>
<string>0.3.1</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleIconFile</key>
+3 -1
View File
@@ -18,6 +18,8 @@
"shell:allow-open",
"shell:allow-spawn",
"shell:allow-stdin-write",
"deep-link:default"
"deep-link:default",
"dialog:default",
"dialog:allow-open"
]
}
+49 -68
View File
@@ -229,10 +229,42 @@ pub fn is_nightly_version(version: &str) -> bool {
version_comp.pre_release.is_some()
}
// Browser-specific alpha version detection for Zen Browser
pub fn is_zen_nightly_version(version: &str) -> bool {
// For Zen Browser, only "twilight" is considered alpha/pre-release
version.to_lowercase() == "twilight"
/// Centralized function to determine if a browser version/release is nightly/prerelease
/// This is the single source of truth for nightly detection across the entire codebase
pub fn is_browser_version_nightly(
browser: &str,
version: &str,
release_name: Option<&str>,
) -> bool {
match browser {
"zen" => {
// For Zen Browser, only "twilight" is considered nightly
version.to_lowercase() == "twilight"
}
"brave" => {
// For Brave Browser, only releases titled "Release" are stable, everything else is 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)
}
}
"firefox" | "firefox-developer" => {
// For Firefox, use the category from the API response to determine stability
// This will be handled in the API parsing, so this fallback is for cached versions
is_nightly_version(version)
}
"mullvad-browser" | "tor-browser" => is_nightly_version(version),
"chromium" => {
// Chromium builds are generally stable snapshots
false
}
_ => {
// Default fallback
is_nightly_version(version)
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -256,7 +288,6 @@ pub struct BrowserRelease {
pub version: String,
pub date: String,
pub is_prerelease: bool,
pub download_url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -278,7 +309,6 @@ pub struct ApiClient {
github_api_base: String,
chromium_api_base: String,
tor_archive_base: String,
mozilla_download_base: String,
}
impl ApiClient {
@@ -291,7 +321,6 @@ impl ApiClient {
chromium_api_base: "https://commondatastorage.googleapis.com/chromium-browser-snapshots"
.to_string(),
tor_archive_base: "https://archive.torproject.org/tor-package-archive/torbrowser".to_string(),
mozilla_download_base: "https://download.mozilla.org".to_string(),
}
}
@@ -302,7 +331,6 @@ impl ApiClient {
github_api_base: String,
chromium_api_base: String,
tor_archive_base: String,
mozilla_download_base: String,
) -> Self {
Self {
client: Client::new(),
@@ -311,7 +339,6 @@ impl ApiClient {
github_api_base,
chromium_api_base,
tor_archive_base,
mozilla_download_base,
}
}
@@ -449,11 +476,7 @@ impl ApiClient {
BrowserRelease {
version: version.clone(),
date: "".to_string(), // Cache doesn't store dates
is_prerelease: is_nightly_version(&version),
download_url: Some(format!(
"{}/?product=firefox-{}&os=osx&lang=en-US",
self.mozilla_download_base, version
)),
is_prerelease: is_browser_version_nightly("firefox", &version, None),
}
})
.collect(),
@@ -489,10 +512,6 @@ impl ApiClient {
version: release.version.clone(),
date: release.date,
is_prerelease: !is_stable,
download_url: Some(format!(
"{}/?product=firefox-{}&os=osx&lang=en-US",
self.mozilla_download_base, release.version
)),
})
} else {
None
@@ -534,11 +553,7 @@ impl ApiClient {
BrowserRelease {
version: version.clone(),
date: "".to_string(), // Cache doesn't store dates
is_prerelease: is_nightly_version(&version),
download_url: Some(format!(
"{}/?product=devedition-{}&os=osx&lang=en-US",
self.mozilla_download_base, version
)),
is_prerelease: is_browser_version_nightly("firefox-developer", &version, None),
}
})
.collect(),
@@ -580,10 +595,6 @@ impl ApiClient {
version: release.version.clone(),
date: release.date,
is_prerelease: !is_stable,
download_url: Some(format!(
"{}/?product=devedition-{}&os=osx&lang=en-US",
self.mozilla_download_base, release.version
)),
})
} else {
None
@@ -685,7 +696,8 @@ impl ApiClient {
// Check for twilight updates and mark alpha releases
for release in &mut releases {
// Use browser-specific alpha detection for Zen Browser - only "twilight" is nightly
release.is_nightly = is_zen_nightly_version(&release.tag_name);
release.is_nightly =
is_browser_version_nightly("zen", &release.tag_name, Some(&release.name));
// Check for twilight update if this is a twilight release
if release.tag_name.to_lowercase() == "twilight" {
@@ -749,9 +761,9 @@ impl ApiClient {
let has_compatible_asset = Self::has_compatible_brave_asset(&release.assets, &os, &arch);
if has_compatible_asset {
// Set is_nightly based on the release name
// Stable releases start with "Release", everything else is nightly
release.is_nightly = !release.name.starts_with("Release");
// Use the centralized nightly detection function
release.is_nightly =
is_browser_version_nightly("brave", &release.tag_name, Some(&release.name));
Some(release)
} else {
None
@@ -877,7 +889,6 @@ impl ApiClient {
version: version.clone(),
date: "".to_string(), // Cache doesn't store dates
is_prerelease: false, // Chromium versions are generally stable builds
download_url: None,
}
})
.collect(),
@@ -914,7 +925,6 @@ impl ApiClient {
version: version.clone(),
date: "".to_string(),
is_prerelease: false,
download_url: None,
})
.collect(),
)
@@ -934,11 +944,7 @@ impl ApiClient {
BrowserRelease {
version: version.clone(),
date: "".to_string(), // Cache doesn't store dates
is_prerelease: is_nightly_version(&version),
download_url: Some(format!(
"{}/{version}/tor-browser-macos-{version}.dmg",
self.tor_archive_base
)),
is_prerelease: is_browser_version_nightly("tor-browser", &version, None),
}
})
.collect(),
@@ -1013,10 +1019,6 @@ impl ApiClient {
version: version.clone(),
date: "".to_string(), // TOR archive doesn't provide structured dates
is_prerelease: false, // Assume all archived versions are stable
download_url: Some(format!(
"{}/{version}/tor-browser-macos-{version}.dmg",
self.tor_archive_base
)),
}
})
.collect(),
@@ -1065,13 +1067,11 @@ impl ApiClient {
struct TwilightInfo {
file_size: u64,
last_updated: u64,
download_url: String,
}
let current_info = TwilightInfo {
file_size: asset.size,
last_updated: Self::get_current_timestamp(),
download_url: asset.browser_download_url.clone(),
};
if !twilight_cache_file.exists() {
@@ -1137,7 +1137,6 @@ mod tests {
base_url.clone(), // github_api_base
base_url.clone(), // chromium_api_base
base_url.clone(), // tor_archive_base
base_url.clone(), // mozilla_download_base
)
}
@@ -1317,12 +1316,6 @@ mod tests {
let releases = result.unwrap();
assert!(!releases.is_empty());
assert_eq!(releases[0].version, "139.0");
assert!(releases[0].download_url.is_some());
assert!(releases[0]
.download_url
.as_ref()
.unwrap()
.contains(&server.uri()));
}
#[tokio::test]
@@ -1365,12 +1358,6 @@ mod tests {
let releases = result.unwrap();
assert!(!releases.is_empty());
assert_eq!(releases[0].version, "140.0b1");
assert!(releases[0].download_url.is_some());
assert!(releases[0]
.download_url
.as_ref()
.unwrap()
.contains(&server.uri()));
}
#[tokio::test]
@@ -1615,12 +1602,6 @@ mod tests {
let releases = result.unwrap();
assert!(!releases.is_empty());
assert_eq!(releases[0].version, "14.0.4");
assert!(releases[0].download_url.is_some());
assert!(releases[0]
.download_url
.as_ref()
.unwrap()
.contains(&server.uri()));
}
#[tokio::test]
@@ -1693,13 +1674,13 @@ mod tests {
#[test]
fn test_is_zen_nightly_version() {
// Only "twilight" should be considered nightly for Zen Browser
assert!(is_zen_nightly_version("twilight"));
assert!(is_zen_nightly_version("TWILIGHT")); // Case insensitive
assert!(is_browser_version_nightly("zen", "twilight", None));
assert!(is_browser_version_nightly("zen", "TWILIGHT", None)); // Case insensitive
// Versions with "b" should NOT be considered nightly for Zen Browser
assert!(!is_zen_nightly_version("1.12.8b"));
assert!(!is_zen_nightly_version("1.0.0b1"));
assert!(!is_zen_nightly_version("2.0.0"));
assert!(!is_browser_version_nightly("zen", "1.12.8b", None));
assert!(!is_browser_version_nightly("zen", "1.0.0b1", None));
assert!(!is_browser_version_nightly("zen", "2.0.0", None));
}
#[tokio::test]
+3 -50
View File
@@ -227,45 +227,7 @@ impl AppAutoUpdater {
/// Get the appropriate download URL for the current platform
fn get_download_url_for_platform(&self, assets: &[AppReleaseAsset]) -> Option<String> {
println!("Looking for macOS universal binary assets");
for asset in assets {
println!("Found asset: {}", asset.name);
}
// Priority 1: Look for universal macOS DMG (preferred)
for asset in assets {
if asset.name.contains(".dmg")
&& (asset.name.contains("universal")
|| asset.name.contains("Universal")
|| asset.name.contains("_universal.dmg")
|| asset.name.contains("-universal.dmg")
|| asset.name.contains("_universal_")
|| asset.name.contains("-universal-"))
{
println!("Found universal binary: {}", asset.name);
return Some(asset.browser_download_url.clone());
}
}
// Priority 2: Look for generic macOS DMG without architecture specification
// This would be the case for universal binaries that don't explicitly mention "universal"
for asset in assets {
if asset.name.contains(".dmg")
&& (asset.name.to_lowercase().contains("macos")
|| asset.name.to_lowercase().contains("darwin"))
&& !asset.name.contains("x64")
&& !asset.name.contains("x86_64")
&& !asset.name.contains("x86-64")
&& !asset.name.contains("aarch64")
&& !asset.name.contains("arm64")
&& !asset.name.contains(".app.tar.gz")
{
println!("Found generic macOS DMG (likely universal): {}", asset.name);
return Some(asset.browser_download_url.clone());
}
}
// Priority 3: Fallback to current architecture-specific binary for backward compatibility
// Priority 1: Get architecture-specific binary for backward compatibility
let arch = if cfg!(target_arch = "aarch64") {
"aarch64"
} else if cfg!(target_arch = "x86_64") {
@@ -313,7 +275,7 @@ impl AppAutoUpdater {
}
}
// Priority 4: Final fallback to any macOS DMG
// Priority 2: Fallback to any macOS DMG
for asset in assets {
if asset.name.contains(".dmg")
&& (asset.name.to_lowercase().contains("macos")
@@ -688,20 +650,11 @@ mod tests {
browser_download_url: "https://example.com/aarch64.dmg".to_string(),
size: 12345,
},
AppReleaseAsset {
name: "Donut.Browser_0.1.0_universal.dmg".to_string(),
browser_download_url: "https://example.com/universal.dmg".to_string(),
size: 12345,
},
];
let url = updater.get_download_url_for_platform(&assets);
assert!(url.is_some());
// Should prefer universal binary over architecture-specific ones
let url = url.unwrap();
assert_eq!(url, "https://example.com/universal.dmg");
// Test with generic macOS DMG (no architecture specified)
let generic_assets = vec![AppReleaseAsset {
name: "Donut.Browser_0.1.0_macos.dmg".to_string(),
@@ -713,7 +666,7 @@ mod tests {
assert!(generic_url.is_some());
assert_eq!(generic_url.unwrap(), "https://example.com/macos.dmg");
// Test fallback to architecture-specific when no universal is available
// Test architecture-specific DMG
let arch_specific_assets = vec![
AppReleaseAsset {
name: "Donut.Browser_0.1.0_x64.dmg".to_string(),
+3 -6
View File
@@ -346,12 +346,9 @@ impl AutoUpdater {
// Helper methods
fn is_nightly_version(&self, version: &str) -> bool {
version.contains("alpha")
|| version.contains("beta")
|| version.contains("rc")
|| version.contains("a")
|| version.contains("b")
|| version.contains("dev")
// Use the centralized nightly detection function
// Since we don't have browser context here, use the general fallback
crate::api_client::is_nightly_version(version)
}
fn is_version_newer(&self, version1: &str, version2: &str) -> bool {
-8
View File
@@ -2377,14 +2377,6 @@ pub fn get_downloaded_browser_versions(browser_str: String) -> Result<Vec<String
Ok(registry.get_downloaded_versions(&browser_str))
}
#[tauri::command]
pub fn cleanup_unused_binaries() -> Result<Vec<String>, String> {
let browser_runner = BrowserRunner::new();
browser_runner
.cleanup_unused_binaries_internal()
.map_err(|e| format!("Failed to cleanup unused binaries: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
+52 -37
View File
@@ -122,7 +122,7 @@ impl BrowserVersionService {
.map(|version| {
BrowserVersionInfo {
version: version.clone(),
is_prerelease: crate::api_client::is_nightly_version(&version),
is_prerelease: crate::api_client::is_browser_version_nightly(browser, &version, None),
date: "".to_string(), // Cache doesn't store dates
}
})
@@ -240,7 +240,9 @@ impl BrowserVersionService {
} else {
BrowserVersionInfo {
version: version.clone(),
is_prerelease: crate::api_client::is_nightly_version(&version),
is_prerelease: crate::api_client::is_browser_version_nightly(
"firefox", &version, None,
),
date: "".to_string(),
}
}
@@ -261,7 +263,11 @@ impl BrowserVersionService {
} else {
BrowserVersionInfo {
version: version.clone(),
is_prerelease: crate::api_client::is_nightly_version(&version),
is_prerelease: crate::api_client::is_browser_version_nightly(
"firefox-developer",
&version,
None,
),
date: "".to_string(),
}
}
@@ -303,7 +309,7 @@ impl BrowserVersionService {
} else {
BrowserVersionInfo {
version: version.clone(),
is_prerelease: false, // Zen Browser releases are usually stable
is_prerelease: crate::api_client::is_browser_version_nightly("zen", &version, None),
date: "".to_string(),
}
}
@@ -324,7 +330,9 @@ impl BrowserVersionService {
} else {
BrowserVersionInfo {
version: version.clone(),
is_prerelease: version.contains("beta") || version.contains("dev"),
is_prerelease: crate::api_client::is_browser_version_nightly(
"brave", &version, None,
),
date: "".to_string(),
}
}
@@ -360,7 +368,11 @@ impl BrowserVersionService {
if let Some(release) = releases.iter().find(|r| r.version == version) {
BrowserVersionInfo {
version: release.version.clone(),
is_prerelease: crate::api_client::is_nightly_version(&version),
is_prerelease: crate::api_client::is_browser_version_nightly(
"tor-browser",
&release.version,
None,
),
date: release.date.clone(),
}
} else {
@@ -423,11 +435,16 @@ impl BrowserVersionService {
match browser {
"firefox" => {
let os_param = match (&os[..], &arch[..]) {
("windows", _) => "win64",
("linux", "x64") => "linux64",
("linux", "arm64") => "linux64-aarch64",
("macos", _) => "osx",
let (platform_path, filename, is_archive) = match (&os[..], &arch[..]) {
("windows", "x64") => ("win64", format!("Firefox Setup {version}.exe"), false),
("windows", "arm64") => (
"win64-aarch64",
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),
("macos", _) => ("mac", format!("Firefox {version}.dmg"), true),
_ => {
return Err(
format!("Unsupported platform/architecture for Firefox: {os}/{arch}").into(),
@@ -435,27 +452,25 @@ impl BrowserVersionService {
}
};
let (filename, is_archive) = match os.as_str() {
"windows" => (format!("firefox-{version}.exe"), false),
"linux" => (format!("firefox-{version}.tar.xz"), true),
"macos" => (format!("firefox-{version}.dmg"), true),
_ => return Err(format!("Unsupported platform for Firefox: {os}").into()),
};
Ok(DownloadInfo {
url: format!(
"https://download.mozilla.org/?product=firefox-{version}&os={os_param}&lang=en-US"
"https://download-installer.cdn.mozilla.net/pub/firefox/releases/{version}/{platform_path}/en-US/{filename}"
),
filename,
is_archive,
})
}
"firefox-developer" => {
let os_param = match (&os[..], &arch[..]) {
("windows", _) => "win64",
("linux", "x64") => "linux64",
("linux", "arm64") => "linux64-aarch64",
("macos", _) => "osx",
let (platform_path, filename, is_archive) = match (&os[..], &arch[..]) {
("windows", "x64") => ("win64", format!("Firefox Setup {version}.exe"), false),
("windows", "arm64") => (
"win64-aarch64",
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),
("macos", _) => ("mac", format!("Firefox {version}.dmg"), true),
_ => {
return Err(
format!("Unsupported platform/architecture for Firefox Developer: {os}/{arch}")
@@ -464,16 +479,9 @@ impl BrowserVersionService {
}
};
let (filename, is_archive) = match os.as_str() {
"windows" => (format!("firefox-developer-{version}.exe"), false),
"linux" => (format!("firefox-developer-{version}.tar.xz"), true),
"macos" => (format!("firefox-developer-{version}.dmg"), true),
_ => return Err(format!("Unsupported platform for Firefox Developer: {os}").into()),
};
Ok(DownloadInfo {
url: format!(
"https://download.mozilla.org/?product=firefox-devedition-{version}&os={os_param}&lang=en-US"
"https://download-installer.cdn.mozilla.net/pub/devedition/releases/{version}/{platform_path}/en-US/{filename}"
),
filename,
is_archive,
@@ -826,7 +834,6 @@ mod tests {
base_url.clone(), // github_api_base
base_url.clone(), // chromium_api_base
base_url.clone(), // tor_archive_base
base_url.clone(), // mozilla_download_base
)
}
@@ -1468,16 +1475,24 @@ mod tests {
// Test Firefox
let firefox_info = service.get_download_info("firefox", "139.0").unwrap();
assert_eq!(firefox_info.filename, "firefox-139.0.dmg");
assert!(firefox_info.url.contains("firefox-139.0"));
assert_eq!(firefox_info.filename, "Firefox 139.0.dmg");
assert!(firefox_info
.url
.contains("download-installer.cdn.mozilla.net"));
assert!(firefox_info.url.contains("/pub/firefox/releases/139.0/"));
assert!(firefox_info.is_archive);
// Test Firefox Developer
let firefox_dev_info = service
.get_download_info("firefox-developer", "139.0b1")
.unwrap();
assert_eq!(firefox_dev_info.filename, "firefox-developer-139.0b1.dmg");
assert!(firefox_dev_info.url.contains("devedition-139.0b1"));
assert_eq!(firefox_dev_info.filename, "Firefox 139.0b1.dmg");
assert!(firefox_dev_info
.url
.contains("download-installer.cdn.mozilla.net"));
assert!(firefox_dev_info
.url
.contains("/pub/devedition/releases/139.0b1/"));
assert!(firefox_dev_info.is_archive);
// Test Mullvad Browser
-1
View File
@@ -459,7 +459,6 @@ mod tests {
base_url.clone(), // github_api_base
base_url.clone(), // chromium_api_base
base_url.clone(), // tor_archive_base
base_url.clone(), // mozilla_download_base
)
}
+542 -135
View File
@@ -34,8 +34,15 @@ impl Extractor {
};
let _ = app_handle.emit("download-progress", &progress);
println!(
"Starting extraction of {} for browser {}",
archive_path.display(),
browser_type.as_str()
);
// Try to detect the actual file type by reading the file header
let actual_format = self.detect_file_format(archive_path)?;
println!("Detected format: {actual_format}");
match actual_format.as_str() {
"dmg" => {
@@ -88,6 +95,14 @@ impl Extractor {
use std::fs::File;
use std::io::Read;
// First check file extension for DMG files since they're common on macOS
// and can have misleading magic numbers
if let Some(ext) = file_path.extension().and_then(|ext| ext.to_str()) {
if ext.to_lowercase() == "dmg" {
return Ok("dmg".to_string());
}
}
let mut file = File::open(file_path)?;
let mut buffer = [0u8; 12]; // Read first 12 bytes for magic number detection
file.read_exact(&mut buffer)?;
@@ -179,6 +194,12 @@ impl Extractor {
dmg_path: &Path,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
println!(
"Extracting DMG: {} to {}",
dmg_path.display(),
dest_dir.display()
);
// Create a temporary mount point
let mount_point = std::env::temp_dir().join(format!(
"donut_mount_{}",
@@ -189,6 +210,8 @@ impl Extractor {
));
create_dir_all(&mount_point)?;
println!("Created mount point: {}", mount_point.display());
// Mount the DMG
let output = Command::new("hdiutil")
.args([
@@ -201,42 +224,109 @@ impl Extractor {
.output()?;
if !output.status.success() {
return Err(
format!(
"Failed to mount DMG: {}",
String::from_utf8_lossy(&output.stderr)
)
.into(),
);
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
println!("Failed to mount DMG. stdout: {stdout}, stderr: {stderr}");
// Clean up mount point before returning error
let _ = fs::remove_dir_all(&mount_point);
return Err(format!("Failed to mount DMG: {stderr}").into());
}
// Find the .app directory in the mount point
let app_entry = fs::read_dir(&mount_point)?
.filter_map(Result::ok)
.find(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
.ok_or("No .app found in DMG")?;
println!("Successfully mounted DMG");
// List the contents for debugging
println!("Mount point contents:");
if let Ok(entries) = fs::read_dir(&mount_point) {
for entry in entries.flatten() {
let path = entry.path();
println!(
" - {} ({})",
path.display(),
if path.is_dir() { "dir" } else { "file" }
);
}
}
// Find the .app directory in the mount point with enhanced search
let app_result = self.find_app_in_directory(&mount_point).await;
let app_entry = match app_result {
Ok(app_path) => app_path,
Err(e) => {
println!("Failed to find .app in mount point: {e}");
// Enhanced debugging - look for any interesting files/directories
if let Ok(entries) = fs::read_dir(&mount_point) {
println!("Detailed mount point analysis:");
for entry in entries.flatten() {
let path = entry.path();
let metadata = fs::metadata(&path);
println!(
" - {} ({}) - {:?}",
path.display(),
if path.is_dir() { "dir" } else { "file" },
metadata.map(|m| m.len()).unwrap_or(0)
);
// If it's a directory, look one level deep
if path.is_dir() {
if let Ok(sub_entries) = fs::read_dir(&path) {
for sub_entry in sub_entries.flatten().take(5) {
// Limit to first 5 items
let sub_path = sub_entry.path();
println!(
" - {} ({})",
sub_path.display(),
if sub_path.is_dir() { "dir" } else { "file" }
);
}
}
}
}
}
// Try to unmount before returning error
let _ = Command::new("hdiutil")
.args(["detach", "-force", mount_point.to_str().unwrap()])
.output();
let _ = fs::remove_dir_all(&mount_point);
return Err("No .app found after extraction".into());
}
};
println!("Found .app bundle: {}", app_entry.display());
// Copy the .app to the destination
let app_path = dest_dir.join(app_entry.file_name());
let app_path = dest_dir.join(app_entry.file_name().unwrap());
println!("Copying .app to: {}", app_path.display());
let output = Command::new("cp")
.args([
"-R",
app_entry.path().to_str().unwrap(),
app_entry.to_str().unwrap(),
app_path.to_str().unwrap(),
])
.output()?;
if !output.status.success() {
return Err(
format!(
"Failed to copy app: {}",
String::from_utf8_lossy(&output.stderr)
)
.into(),
);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Failed to copy app: {stderr}");
// Unmount before returning error
let _ = Command::new("hdiutil")
.args(["detach", "-force", mount_point.to_str().unwrap()])
.output();
let _ = fs::remove_dir_all(&mount_point);
return Err(format!("Failed to copy app: {stderr}").into());
}
println!("Successfully copied .app bundle");
// Remove quarantine attributes
let _ = Command::new("xattr")
.args(["-dr", "com.apple.quarantine", app_path.to_str().unwrap()])
@@ -246,29 +336,19 @@ impl Extractor {
.args(["-cr", app_path.to_str().unwrap()])
.output();
// Try to unmount the DMG with retries
let mut retry_count = 0;
let max_retries = 3;
let mut unmounted = false;
println!("Removed quarantine attributes");
while retry_count < max_retries && !unmounted {
// Wait a bit before trying to unmount
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
// Unmount the DMG
let output = Command::new("hdiutil")
.args(["detach", mount_point.to_str().unwrap()])
.output()?;
let output = Command::new("hdiutil")
.args(["detach", mount_point.to_str().unwrap()])
.output()?;
if output.status.success() {
unmounted = true;
} else if retry_count == max_retries - 1 {
// Force unmount on last retry
let _ = Command::new("hdiutil")
.args(["detach", "-force", mount_point.to_str().unwrap()])
.output();
unmounted = true; // Consider it unmounted even if force fails
}
retry_count += 1;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Warning: Failed to unmount DMG: {stderr}");
// Don't fail if unmount fails - the extraction was successful
} else {
println!("Successfully unmounted DMG");
}
// Clean up mount point directory
@@ -277,6 +357,79 @@ impl Extractor {
Ok(app_path)
}
#[cfg(target_os = "macos")]
async fn find_app_in_directory(
&self,
dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
self.find_app_recursive(dir, 0).await
}
#[cfg(target_os = "macos")]
async fn find_app_recursive(
&self,
dir: &Path,
depth: usize,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
// Limit search depth to avoid infinite loops
if depth > 4 {
return Err("Maximum search depth reached".into());
}
if let Ok(entries) = fs::read_dir(dir) {
let mut subdirs = Vec::new();
let mut hidden_subdirs = Vec::new();
// First pass: look for .app bundles directly
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(extension) = path.extension() {
if extension == "app" {
println!("Found .app bundle at depth {}: {}", depth, path.display());
return Ok(path);
}
}
// Collect subdirectories for second pass
let filename = path.file_name().unwrap_or_default().to_string_lossy();
if filename.starts_with('.') {
// Hidden directories - search these with lower priority
hidden_subdirs.push(path);
} else {
// Regular directories - search these first
subdirs.push(path);
}
}
}
// Second pass: search regular subdirectories first
for subdir in subdirs {
// Skip common directories that are unlikely to contain .app files
let dirname = subdir.file_name().unwrap_or_default().to_string_lossy();
if matches!(
dirname.as_ref(),
"Documents" | "Downloads" | "Desktop" | "Library" | "System" | "tmp" | "var"
) {
continue;
}
if let Ok(result) = Box::pin(self.find_app_recursive(&subdir, depth + 1)).await {
return Ok(result);
}
}
// Third pass: search hidden directories if nothing found in regular ones
for hidden_dir in hidden_subdirs {
if let Ok(result) = Box::pin(self.find_app_recursive(&hidden_dir, depth + 1)).await {
return Ok(result);
}
}
}
Err(format!("No .app found in directory: {}", dir.display()).into())
}
pub async fn extract_zip(
&self,
zip_path: &Path,
@@ -608,34 +761,65 @@ impl Extractor {
&self,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
// First, try to find any .app file in the destination directory
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 == "app") {
return Ok(path);
}
// For Chromium, check subdirectories (chrome-mac folder)
if path.is_dir() {
if let Ok(sub_entries) = fs::read_dir(&path) {
for sub_entry in sub_entries.flatten() {
let sub_path = sub_entry.path();
if sub_path.extension().is_some_and(|ext| ext == "app") {
// Move the app to the root destination directory
let target_path = dest_dir.join(sub_path.file_name().unwrap());
fs::rename(&sub_path, &target_path)?;
println!("Searching for .app bundle in: {}", dest_dir.display());
// Clean up the now-empty subdirectory
let _ = fs::remove_dir_all(&path);
return Ok(target_path);
// Use the enhanced recursive search
match self.find_app_in_directory(dest_dir).await {
Ok(app_path) => {
// Check if the app is in a subdirectory and move it to the root if needed
let app_parent = app_path.parent().unwrap();
if app_parent != dest_dir {
println!(
"Found .app in subdirectory, moving to root: {} -> {}",
app_path.display(),
dest_dir.display()
);
let target_path = dest_dir.join(app_path.file_name().unwrap());
// Move the app to the root destination directory
fs::rename(&app_path, &target_path)?;
// Try to clean up the now-empty subdirectory (ignore errors)
if let Some(parent_dir) = app_path.parent() {
if parent_dir != dest_dir {
let _ = fs::remove_dir_all(parent_dir);
}
}
println!("Successfully moved .app to: {}", target_path.display());
Ok(target_path)
} else {
println!("Found .app at root level: {}", app_path.display());
Ok(app_path)
}
}
Err(e) => {
println!("Failed to find .app bundle: {e}");
// List contents for debugging
if let Ok(entries) = fs::read_dir(dest_dir) {
println!("Destination directory contents:");
for entry in entries.flatten() {
let path = entry.path();
let metadata = if path.is_dir() { "dir" } else { "file" };
println!(" - {} ({})", path.display(), metadata);
// If it's a directory, also list its contents
if path.is_dir() {
if let Ok(sub_entries) = fs::read_dir(&path) {
for sub_entry in sub_entries.flatten() {
let sub_path = sub_entry.path();
let sub_metadata = if sub_path.is_dir() { "dir" } else { "file" };
println!(" - {} ({})", sub_path.display(), sub_metadata);
}
}
}
}
}
Err("No .app found after extraction".into())
}
}
Err("No .app found after extraction".into())
}
#[cfg(target_os = "windows")]
@@ -904,7 +1088,8 @@ impl Extractor {
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::fs::{create_dir_all, File};
use std::io::Write;
use tempfile::TempDir;
#[test]
@@ -915,50 +1100,81 @@ mod tests {
#[test]
fn test_unsupported_archive_format() {
let _ = Extractor::new();
let extractor = Extractor::new();
let temp_dir = TempDir::new().unwrap();
let fake_archive = temp_dir.path().join("test.rar");
File::create(&fake_archive).unwrap();
// Create a mock app handle (this won't work in real tests without Tauri runtime)
// For now, we'll just test the logic without the actual extraction
// Create a file with invalid header
let mut file = File::create(&fake_archive).unwrap();
file.write_all(b"invalid content").unwrap();
// Test that unsupported formats return an error
let extension = fake_archive
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
assert_eq!(extension, "rar");
// We know this would fail with "Unsupported archive format: rar"
// Test format detection
let result = extractor.detect_file_format(&fake_archive);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "unknown");
}
#[test]
fn test_dmg_path_validation() {
let temp_dir = TempDir::new().unwrap();
let dmg_path = temp_dir.path().join("test.dmg");
// Test that we can identify DMG files correctly
let extension = dmg_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
assert_eq!(extension, "dmg");
}
#[test]
fn test_zip_path_validation() {
fn test_format_detection_zip() {
let extractor = Extractor::new();
let temp_dir = TempDir::new().unwrap();
let zip_path = temp_dir.path().join("test.zip");
// Test that we can identify ZIP files correctly
let extension = zip_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
// Create a file with ZIP magic number
let mut file = File::create(&zip_path).unwrap();
file.write_all(&[0x50, 0x4B, 0x03, 0x04]).unwrap(); // ZIP magic
file.write_all(&[0; 8]).unwrap(); // padding
assert_eq!(extension, "zip");
let result = extractor.detect_file_format(&zip_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "zip");
}
#[test]
fn test_format_detection_dmg_by_extension() {
let extractor = Extractor::new();
let temp_dir = TempDir::new().unwrap();
let dmg_path = temp_dir.path().join("test.dmg");
// Create a file (magic number won't match, but extension will)
let mut file = File::create(&dmg_path).unwrap();
file.write_all(b"fake dmg content").unwrap();
let result = extractor.detect_file_format(&dmg_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "dmg");
}
#[test]
fn test_format_detection_exe() {
let extractor = Extractor::new();
let temp_dir = TempDir::new().unwrap();
let exe_path = temp_dir.path().join("test.exe");
// Create a file with PE header
let mut file = File::create(&exe_path).unwrap();
file.write_all(&[0x4D, 0x5A]).unwrap(); // PE magic
file.write_all(&[0; 10]).unwrap(); // padding
let result = extractor.detect_file_format(&exe_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "exe");
}
#[test]
fn test_format_detection_tar_gz() {
let extractor = Extractor::new();
let temp_dir = TempDir::new().unwrap();
let tar_gz_path = temp_dir.path().join("test.tar.gz");
// Create a file with gzip magic
let mut file = File::create(&tar_gz_path).unwrap();
file.write_all(&[0x1F, 0x8B, 0x08]).unwrap(); // gzip magic
file.write_all(&[0; 9]).unwrap(); // padding
let result = extractor.detect_file_format(&tar_gz_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "tar.gz");
}
#[test]
@@ -987,56 +1203,247 @@ mod tests {
assert!(mount_point2.to_string_lossy().contains("donut_mount_"));
}
#[test]
fn test_app_path_detection() {
#[tokio::test]
#[cfg(target_os = "macos")]
async fn test_find_app_at_root_level() {
let extractor = Extractor::new();
let temp_dir = TempDir::new().unwrap();
// Create a fake .app directory
let app_dir = temp_dir.path().join("TestApp.app");
std::fs::create_dir_all(&app_dir).unwrap();
// Create a Firefox.app directory
let firefox_app = temp_dir.path().join("Firefox.app");
create_dir_all(&firefox_app).unwrap();
// Test finding .app directories
let entries: Vec<_> = fs::read_dir(temp_dir.path())
.unwrap()
.filter_map(Result::ok)
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
.collect();
// Create the standard macOS app structure
let contents_dir = firefox_app.join("Contents");
let macos_dir = contents_dir.join("MacOS");
create_dir_all(&macos_dir).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].file_name(), "TestApp.app");
// Create the executable
let executable = macos_dir.join("firefox");
File::create(&executable).unwrap();
// Test finding the app
let result = extractor.find_app_in_directory(temp_dir.path()).await;
assert!(result.is_ok());
let found_app = result.unwrap();
assert_eq!(found_app.file_name().unwrap(), "Firefox.app");
assert!(found_app.exists());
}
#[test]
fn test_nested_app_detection() {
#[tokio::test]
#[cfg(target_os = "macos")]
async fn test_find_app_in_subdirectory() {
let extractor = Extractor::new();
let temp_dir = TempDir::new().unwrap();
// Create a nested structure like Chromium
let chrome_dir = temp_dir.path().join("chrome-mac");
std::fs::create_dir_all(&chrome_dir).unwrap();
// Create a nested structure like some browsers have
let subdir = temp_dir.path().join("chrome-mac");
create_dir_all(&subdir).unwrap();
let app_dir = chrome_dir.join("Chromium.app");
std::fs::create_dir_all(&app_dir).unwrap();
// Create a Brave Browser.app directory
let brave_app = subdir.join("Brave Browser.app");
create_dir_all(&brave_app).unwrap();
// Test finding nested .app directories
let mut found_app = false;
// Create the standard macOS app structure
let contents_dir = brave_app.join("Contents");
let macos_dir = contents_dir.join("MacOS");
create_dir_all(&macos_dir).unwrap();
if let Ok(entries) = fs::read_dir(temp_dir.path()) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Ok(sub_entries) = fs::read_dir(&path) {
for sub_entry in sub_entries.flatten() {
let sub_path = sub_entry.path();
if sub_path.extension().is_some_and(|ext| ext == "app") {
found_app = true;
break;
}
}
}
}
}
// Create the executable
let executable = macos_dir.join("Brave Browser");
File::create(&executable).unwrap();
// Test finding the app
let result = extractor.find_app_in_directory(temp_dir.path()).await;
assert!(result.is_ok());
let found_app = result.unwrap();
assert_eq!(found_app.file_name().unwrap(), "Brave Browser.app");
assert!(found_app.exists());
}
#[tokio::test]
#[cfg(target_os = "macos")]
async fn test_find_app_multiple_levels_deep() {
let extractor = Extractor::new();
let temp_dir = TempDir::new().unwrap();
// Create a deeply nested structure
let level1 = temp_dir.path().join("level1");
let level2 = level1.join("level2");
create_dir_all(&level2).unwrap();
// Create a Mullvad Browser.app directory
let mullvad_app = level2.join("Mullvad Browser.app");
create_dir_all(&mullvad_app).unwrap();
// Create the standard macOS app structure
let contents_dir = mullvad_app.join("Contents");
let macos_dir = contents_dir.join("MacOS");
create_dir_all(&macos_dir).unwrap();
// Create the executable
let executable = macos_dir.join("firefox");
File::create(&executable).unwrap();
// Test finding the app
let result = extractor.find_app_in_directory(temp_dir.path()).await;
assert!(result.is_ok());
let found_app = result.unwrap();
assert_eq!(found_app.file_name().unwrap(), "Mullvad Browser.app");
assert!(found_app.exists());
}
#[tokio::test]
#[cfg(target_os = "macos")]
async fn test_find_app_no_app_found() {
let extractor = Extractor::new();
let temp_dir = TempDir::new().unwrap();
// Create some files and directories that are NOT .app bundles
let regular_dir = temp_dir.path().join("regular_directory");
create_dir_all(&regular_dir).unwrap();
let regular_file = temp_dir.path().join("regular_file.txt");
File::create(&regular_file).unwrap();
// Create a directory that looks like an app but isn't (wrong extension)
let fake_app = temp_dir.path().join("NotAnApp.app-backup");
create_dir_all(&fake_app).unwrap();
// Test that no app is found
let result = extractor.find_app_in_directory(temp_dir.path()).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("No .app found"));
}
#[tokio::test]
#[cfg(target_os = "macos")]
async fn test_find_app_recursive_depth_limit() {
let extractor = Extractor::new();
let temp_dir = TempDir::new().unwrap();
// Create a very deep nested structure (deeper than our limit of 4)
let mut current_path = temp_dir.path().to_path_buf();
for i in 0..6 {
current_path = current_path.join(format!("level{i}"));
create_dir_all(&current_path).unwrap();
}
assert!(found_app);
// Create an app at the deepest level
let deep_app = current_path.join("Deep.app");
create_dir_all(&deep_app).unwrap();
// Test that the app is NOT found due to depth limit
let result = extractor.find_app_in_directory(temp_dir.path()).await;
assert!(result.is_err());
}
#[tokio::test]
#[cfg(target_os = "macos")]
async fn test_find_macos_app_and_move_from_subdir() {
let extractor = Extractor::new();
let temp_dir = TempDir::new().unwrap();
// Create a nested structure where the app is in a subdirectory
let subdir = temp_dir.path().join("extracted_content");
create_dir_all(&subdir).unwrap();
// Create a Tor Browser.app directory in the subdirectory
let tor_app = subdir.join("Tor Browser.app");
create_dir_all(&tor_app).unwrap();
// Create the standard macOS app structure
let contents_dir = tor_app.join("Contents");
let macos_dir = contents_dir.join("MacOS");
create_dir_all(&macos_dir).unwrap();
// Create the executable
let executable = macos_dir.join("firefox");
File::create(&executable).unwrap();
// Test finding and moving the app
let result = extractor.find_macos_app(temp_dir.path()).await;
assert!(result.is_ok());
let found_app = result.unwrap();
assert_eq!(found_app.file_name().unwrap(), "Tor Browser.app");
// Verify the app was moved to the root level
assert_eq!(found_app.parent().unwrap(), temp_dir.path());
assert!(found_app.exists());
// Verify the original subdirectory structure was cleaned up
assert!(!subdir.exists() || fs::read_dir(&subdir).unwrap().count() == 0);
}
#[tokio::test]
#[cfg(target_os = "macos")]
async fn test_multiple_apps_found_returns_first() {
let extractor = Extractor::new();
let temp_dir = TempDir::new().unwrap();
// Create multiple .app directories
let firefox_app = temp_dir.path().join("Firefox.app");
create_dir_all(&firefox_app).unwrap();
let chrome_app = temp_dir.path().join("Chrome.app");
create_dir_all(&chrome_app).unwrap();
// Test that we find one of them (implementation should be consistent)
let result = extractor.find_app_in_directory(temp_dir.path()).await;
assert!(result.is_ok());
let found_app = result.unwrap();
let app_name = found_app.file_name().unwrap().to_str().unwrap();
assert!(app_name == "Firefox.app" || app_name == "Chrome.app");
}
#[test]
fn test_browser_specific_app_names() {
// Test that we can identify common browser app names correctly
let common_browser_apps = [
"Firefox.app",
"Firefox Developer Edition.app",
"Brave Browser.app",
"Mullvad Browser.app",
"Tor Browser.app",
"Zen Browser.app",
"Chromium.app",
"Google Chrome.app",
];
for app_name in &common_browser_apps {
let path = std::path::Path::new(app_name);
let extension = path.extension().and_then(|ext| ext.to_str());
assert_eq!(extension, Some("app"), "Failed for {app_name}");
}
}
#[test]
fn test_edge_cases_in_path_handling() {
let temp_dir = TempDir::new().unwrap();
// Test paths with spaces and special characters
let problematic_names = [
"Firefox Developer Edition.app",
"Brave Browser.app",
"App with (parentheses).app",
"App-with-dashes.app",
"App_with_underscores.app",
];
for app_name in &problematic_names {
let app_path = temp_dir.path().join(app_name);
create_dir_all(&app_path).unwrap();
// Verify we can detect the .app extension correctly
assert!(app_path.extension().is_some_and(|ext| ext == "app"));
// Verify file_name extraction works
assert_eq!(app_path.file_name().unwrap().to_str().unwrap(), *app_name);
}
}
}
+15 -10
View File
@@ -16,6 +16,7 @@ mod default_browser;
mod download;
mod downloaded_browsers;
mod extraction;
mod profile_importer;
mod proxy_manager;
mod settings_manager;
mod theme_detector;
@@ -24,17 +25,17 @@ mod version_updater;
extern crate lazy_static;
use browser_runner::{
check_browser_exists, check_browser_status, cleanup_unused_binaries, create_browser_profile_new,
delete_profile, download_browser, fetch_browser_versions_cached_first,
fetch_browser_versions_with_count, fetch_browser_versions_with_count_cached_first,
get_downloaded_browser_versions, get_supported_browsers, is_browser_supported_on_platform,
kill_browser_profile, launch_browser_profile, list_browser_profiles, rename_profile,
update_profile_proxy, update_profile_version,
check_browser_exists, check_browser_status, create_browser_profile_new, delete_profile,
download_browser, fetch_browser_versions_cached_first, fetch_browser_versions_with_count,
fetch_browser_versions_with_count_cached_first, get_downloaded_browser_versions,
get_supported_browsers, is_browser_supported_on_platform, kill_browser_profile,
launch_browser_profile, list_browser_profiles, rename_profile, update_profile_proxy,
update_profile_version,
};
use settings_manager::{
clear_all_version_cache, get_app_settings, get_table_sorting_settings, save_app_settings,
save_table_sorting_settings, should_show_settings_on_startup,
clear_all_version_cache_and_refetch, get_app_settings, get_table_sorting_settings,
save_app_settings, save_table_sorting_settings, should_show_settings_on_startup,
};
use default_browser::{
@@ -55,6 +56,8 @@ use app_auto_updater::{
check_for_app_updates, check_for_app_updates_manual, download_and_install_app_update,
};
use profile_importer::{detect_existing_profiles, import_browser_profile};
use theme_detector::get_system_theme;
// Trait to extend WebviewWindow with transparent titlebar functionality
@@ -168,6 +171,7 @@ pub fn run() {
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
// Create the main window programmatically
#[allow(unused_variables)]
@@ -272,7 +276,6 @@ pub fn run() {
download_browser,
delete_profile,
check_browser_exists,
cleanup_unused_binaries,
create_browser_profile_new,
list_browser_profiles,
launch_browser_profile,
@@ -290,7 +293,7 @@ pub fn run() {
should_show_settings_on_startup,
get_table_sorting_settings,
save_table_sorting_settings,
clear_all_version_cache,
clear_all_version_cache_and_refetch,
is_default_browser,
open_url_with_profile,
set_as_default_browser,
@@ -309,6 +312,8 @@ pub fn run() {
check_for_app_updates_manual,
download_and_install_app_update,
get_system_theme,
detect_existing_profiles,
import_browser_profile,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
+661
View File
@@ -0,0 +1,661 @@
use directories::BaseDirs;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs::{self, create_dir_all};
use std::path::{Path, PathBuf};
use crate::browser::BrowserType;
use crate::browser_runner::BrowserRunner;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DetectedProfile {
pub browser: String,
pub name: String,
pub path: String,
pub description: String,
}
pub struct ProfileImporter {
base_dirs: BaseDirs,
browser_runner: BrowserRunner,
}
impl ProfileImporter {
pub fn new() -> Self {
Self {
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
browser_runner: BrowserRunner::new(),
}
}
/// Detect existing browser profiles on the system
pub fn detect_existing_profiles(
&self,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut detected_profiles = Vec::new();
// Detect Firefox profiles
detected_profiles.extend(self.detect_firefox_profiles()?);
// Detect Chrome profiles
detected_profiles.extend(self.detect_chrome_profiles()?);
// Detect Brave profiles
detected_profiles.extend(self.detect_brave_profiles()?);
// Detect Firefox Developer Edition profiles
detected_profiles.extend(self.detect_firefox_developer_profiles()?);
// Detect Chromium profiles
detected_profiles.extend(self.detect_chromium_profiles()?);
// Detect Mullvad Browser profiles
detected_profiles.extend(self.detect_mullvad_browser_profiles()?);
// Detect Zen Browser profiles
detected_profiles.extend(self.detect_zen_browser_profiles()?);
// Remove duplicates based on path
let mut seen_paths = HashSet::new();
let unique_profiles: Vec<DetectedProfile> = detected_profiles
.into_iter()
.filter(|profile| seen_paths.insert(profile.path.clone()))
.collect();
Ok(unique_profiles)
}
/// Detect Firefox profiles
fn detect_firefox_profiles(&self) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
let firefox_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/Firefox/Profiles");
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?);
}
#[cfg(target_os = "windows")]
{
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")?);
}
}
#[cfg(target_os = "linux")]
{
let firefox_dir = self.base_dirs.home_dir().join(".mozilla/firefox");
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?);
}
Ok(profiles)
}
/// Detect Firefox Developer Edition profiles
fn detect_firefox_developer_profiles(
&self,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
// Firefox Developer Edition on macOS uses separate profile directories
let firefox_dev_alt_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/Firefox Developer Edition/Profiles");
// Only scan the dedicated dev edition directory if it exists, otherwise skip to avoid duplicates
if firefox_dev_alt_dir.exists() {
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dev_alt_dir, "firefox-developer")?);
}
}
#[cfg(target_os = "windows")]
{
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")?);
}
}
}
#[cfg(target_os = "linux")]
{
// Firefox Developer Edition on Linux uses separate directories
let firefox_dev_dir = self
.base_dirs
.home_dir()
.join(".mozilla/firefox-dev-edition");
if firefox_dev_dir.exists() {
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dev_dir, "firefox-developer")?);
}
}
Ok(profiles)
}
/// Detect Chrome profiles
fn detect_chrome_profiles(&self) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
let chrome_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/Google/Chrome");
profiles.extend(self.scan_chrome_profiles_dir(&chrome_dir, "chromium")?);
}
#[cfg(target_os = "windows")]
{
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")?);
}
}
#[cfg(target_os = "linux")]
{
let chrome_dir = self.base_dirs.home_dir().join(".config/google-chrome");
profiles.extend(self.scan_chrome_profiles_dir(&chrome_dir, "chromium")?);
}
Ok(profiles)
}
/// Detect Chromium profiles
fn detect_chromium_profiles(&self) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
let chromium_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/Chromium");
profiles.extend(self.scan_chrome_profiles_dir(&chromium_dir, "chromium")?);
}
#[cfg(target_os = "windows")]
{
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")?);
}
}
#[cfg(target_os = "linux")]
{
let chromium_dir = self.base_dirs.home_dir().join(".config/chromium");
profiles.extend(self.scan_chrome_profiles_dir(&chromium_dir, "chromium")?);
}
Ok(profiles)
}
/// Detect Brave profiles
fn detect_brave_profiles(&self) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
let brave_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/BraveSoftware/Brave-Browser");
profiles.extend(self.scan_chrome_profiles_dir(&brave_dir, "brave")?);
}
#[cfg(target_os = "windows")]
{
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")?);
}
}
#[cfg(target_os = "linux")]
{
let brave_dir = self
.base_dirs
.home_dir()
.join(".config/BraveSoftware/Brave-Browser");
profiles.extend(self.scan_chrome_profiles_dir(&brave_dir, "brave")?);
}
Ok(profiles)
}
/// Detect Mullvad Browser profiles
fn detect_mullvad_browser_profiles(
&self,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
let mullvad_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/MullvadBrowser/Profiles");
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
}
#[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")?);
}
}
#[cfg(target_os = "linux")]
{
let mullvad_dir = self.base_dirs.home_dir().join(".mullvad-browser");
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
}
Ok(profiles)
}
/// Detect Zen Browser profiles
fn detect_zen_browser_profiles(
&self,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
#[cfg(target_os = "macos")]
{
let zen_dir = self
.base_dirs
.home_dir()
.join("Library/Application Support/Zen/Profiles");
profiles.extend(self.scan_firefox_profiles_dir(&zen_dir, "zen")?);
}
#[cfg(target_os = "windows")]
{
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")?);
}
}
#[cfg(target_os = "linux")]
{
let zen_dir = self.base_dirs.home_dir().join(".zen");
profiles.extend(self.scan_firefox_profiles_dir(&zen_dir, "zen")?);
}
Ok(profiles)
}
/// Scan Firefox-style profiles directory
fn scan_firefox_profiles_dir(
&self,
profiles_dir: &Path,
browser_type: &str,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
if !profiles_dir.exists() {
return Ok(profiles);
}
// Read profiles.ini file if it exists
let profiles_ini = profiles_dir
.parent()
.unwrap_or(profiles_dir)
.join("profiles.ini");
if profiles_ini.exists() {
if let Ok(content) = fs::read_to_string(&profiles_ini) {
profiles.extend(self.parse_firefox_profiles_ini(&content, profiles_dir, browser_type)?);
}
}
// Also scan directory for any profile folders not in profiles.ini
if let Ok(entries) = fs::read_dir(profiles_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let prefs_file = path.join("prefs.js");
if prefs_file.exists() {
let profile_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("Unknown Profile");
// Check if this profile was already found in profiles.ini
let already_added = profiles.iter().any(|p| p.path == path.to_string_lossy());
if !already_added {
profiles.push(DetectedProfile {
browser: browser_type.to_string(),
name: format!(
"{} Profile - {}",
self.get_browser_display_name(browser_type),
profile_name
),
path: path.to_string_lossy().to_string(),
description: format!("Profile folder: {profile_name}"),
});
}
}
}
}
}
Ok(profiles)
}
/// Parse Firefox profiles.ini file
fn parse_firefox_profiles_ini(
&self,
content: &str,
profiles_dir: &Path,
browser_type: &str,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
let mut current_section = String::new();
let mut profile_name = String::new();
let mut profile_path = String::new();
let mut is_relative = true;
for line in content.lines() {
let line = line.trim();
if line.starts_with('[') && line.ends_with(']') {
// Save previous profile if complete
if !current_section.is_empty()
&& current_section.starts_with("Profile")
&& !profile_path.is_empty()
{
let full_path = if is_relative {
profiles_dir.join(&profile_path)
} else {
PathBuf::from(&profile_path)
};
if full_path.exists() {
let display_name = if profile_name.is_empty() {
format!("{} Profile", self.get_browser_display_name(browser_type))
} else {
format!(
"{} - {}",
self.get_browser_display_name(browser_type),
profile_name
)
};
profiles.push(DetectedProfile {
browser: browser_type.to_string(),
name: display_name,
path: full_path.to_string_lossy().to_string(),
description: format!("Profile: {profile_name}"),
});
}
}
// Start new section
current_section = line[1..line.len() - 1].to_string();
profile_name.clear();
profile_path.clear();
is_relative = true;
} else if line.contains('=') {
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts.len() == 2 {
let key = parts[0].trim();
let value = parts[1].trim();
match key {
"Name" => profile_name = value.to_string(),
"Path" => profile_path = value.to_string(),
"IsRelative" => is_relative = value == "1",
_ => {}
}
}
}
}
// Handle last profile
if !current_section.is_empty()
&& current_section.starts_with("Profile")
&& !profile_path.is_empty()
{
let full_path = if is_relative {
profiles_dir.join(&profile_path)
} else {
PathBuf::from(&profile_path)
};
if full_path.exists() {
let display_name = if profile_name.is_empty() {
format!("{} Profile", self.get_browser_display_name(browser_type))
} else {
format!(
"{} - {}",
self.get_browser_display_name(browser_type),
profile_name
)
};
profiles.push(DetectedProfile {
browser: browser_type.to_string(),
name: display_name,
path: full_path.to_string_lossy().to_string(),
description: format!("Profile: {profile_name}"),
});
}
}
Ok(profiles)
}
/// Scan Chrome-style profiles directory
fn scan_chrome_profiles_dir(
&self,
browser_dir: &Path,
browser_type: &str,
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
let mut profiles = Vec::new();
if !browser_dir.exists() {
return Ok(profiles);
}
// Check for Default profile
let default_profile = browser_dir.join("Default");
if default_profile.exists() && default_profile.join("Preferences").exists() {
profiles.push(DetectedProfile {
browser: browser_type.to_string(),
name: format!(
"{} - Default Profile",
self.get_browser_display_name(browser_type)
),
path: default_profile.to_string_lossy().to_string(),
description: "Default profile".to_string(),
});
}
// Check for Profile X directories
if let Ok(entries) = fs::read_dir(browser_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if dir_name.starts_with("Profile ") && path.join("Preferences").exists() {
let profile_number = &dir_name[8..]; // Remove "Profile " prefix
profiles.push(DetectedProfile {
browser: browser_type.to_string(),
name: format!(
"{} - Profile {}",
self.get_browser_display_name(browser_type),
profile_number
),
path: path.to_string_lossy().to_string(),
description: format!("Profile {profile_number}"),
});
}
}
}
}
Ok(profiles)
}
/// Get browser display name
fn get_browser_display_name(&self, browser_type: &str) -> &str {
match browser_type {
"firefox" => "Firefox",
"firefox-developer" => "Firefox Developer",
"chromium" => "Chrome/Chromium",
"brave" => "Brave",
"mullvad-browser" => "Mullvad Browser",
"zen" => "Zen Browser",
"tor-browser" => "Tor Browser",
_ => "Unknown Browser",
}
}
/// Import a profile from an existing browser profile
pub fn import_profile(
&self,
source_path: &str,
browser_type: &str,
new_profile_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Validate that source path exists
let source_path = Path::new(source_path);
if !source_path.exists() {
return Err("Source profile path does not exist".into());
}
// Validate browser type
let _browser_type = BrowserType::from_str(browser_type)
.map_err(|_| format!("Invalid browser type: {browser_type}"))?;
// Check if a profile with this name already exists
let existing_profiles = self.browser_runner.list_profiles()?;
if existing_profiles
.iter()
.any(|p| p.name.to_lowercase() == new_profile_name.to_lowercase())
{
return Err(format!("Profile with name '{new_profile_name}' already exists").into());
}
// Create the new profile directory
let snake_case_name = new_profile_name.to_lowercase().replace(' ', "_");
let profiles_dir = self.browser_runner.get_profiles_dir();
let new_profile_path = profiles_dir.join(&snake_case_name);
create_dir_all(&new_profile_path)?;
// Copy all files from source to destination
Self::copy_directory_recursive(source_path, &new_profile_path)?;
// Create the profile metadata without overwriting the imported data
// We need to find a suitable version for this browser type
let available_versions = self.get_default_version_for_browser(browser_type)?;
let profile = crate::browser_runner::BrowserProfile {
name: new_profile_name.to_string(),
browser: browser_type.to_string(),
version: available_versions,
profile_path: new_profile_path.to_string_lossy().to_string(),
proxy: None,
process_id: None,
last_launch: None,
};
// Save the profile metadata
self.browser_runner.save_profile(&profile)?;
println!(
"Successfully imported profile '{}' from '{}'",
new_profile_name,
source_path.display()
);
Ok(())
}
/// Get a default version for a browser type
fn get_default_version_for_browser(
&self,
browser_type: &str,
) -> Result<String, Box<dyn std::error::Error>> {
// Try to get a downloaded version first, fallback to a reasonable default
let registry =
crate::downloaded_browsers::DownloadedBrowsersRegistry::load().unwrap_or_default();
let downloaded_versions = registry.get_downloaded_versions(browser_type);
if let Some(version) = downloaded_versions.first() {
return Ok(version.clone());
}
// If no downloaded versions, return a sensible default
match browser_type {
"firefox" => Ok("latest".to_string()),
"firefox-developer" => Ok("latest".to_string()),
"chromium" => Ok("latest".to_string()),
"brave" => Ok("latest".to_string()),
"zen" => Ok("latest".to_string()),
"mullvad-browser" => Ok("13.5.16".to_string()), // Mullvad Browser common version
"tor-browser" => Ok("latest".to_string()),
_ => Ok("latest".to_string()),
}
}
/// Recursively copy directory contents
fn copy_directory_recursive(
source: &Path,
destination: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
if !destination.exists() {
create_dir_all(destination)?;
}
for entry in fs::read_dir(source)? {
let entry = entry?;
let source_path = entry.path();
let dest_path = destination.join(entry.file_name());
if source_path.is_dir() {
Self::copy_directory_recursive(&source_path, &dest_path)?;
} else {
fs::copy(&source_path, &dest_path)?;
}
}
Ok(())
}
}
// Tauri commands
#[tauri::command]
pub async fn detect_existing_profiles() -> Result<Vec<DetectedProfile>, String> {
let importer = ProfileImporter::new();
importer
.detect_existing_profiles()
.map_err(|e| format!("Failed to detect existing profiles: {e}"))
}
#[tauri::command]
pub async fn import_browser_profile(
source_path: String,
browser_type: String,
new_profile_name: String,
) -> Result<(), String> {
let importer = ProfileImporter::new();
importer
.import_profile(&source_path, &browser_type, &new_profile_name)
.map_err(|e| format!("Failed to import profile: {e}"))
}
+25 -2
View File
@@ -4,6 +4,7 @@ use std::fs::{self, create_dir_all};
use std::path::PathBuf;
use crate::api_client::ApiClient;
use crate::browser_version_service::BrowserVersionService;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TableSortingSettings {
@@ -215,9 +216,31 @@ pub async fn save_table_sorting_settings(sorting: TableSortingSettings) -> Resul
}
#[tauri::command]
pub async fn clear_all_version_cache() -> Result<(), String> {
pub async fn clear_all_version_cache_and_refetch() -> Result<(), String> {
let api_client = ApiClient::new();
// Clear all cache first
api_client
.clear_all_cache()
.map_err(|e| format!("Failed to clear version cache: {e}"))
.map_err(|e| format!("Failed to clear version cache: {e}"))?;
// Trigger auto-fetch for all supported browsers
let service = BrowserVersionService::new();
let supported_browsers = service.get_supported_browsers();
for browser in supported_browsers {
// Start background fetch for each browser (don't wait for completion)
let service_clone = BrowserVersionService::new();
let browser_clone = browser.clone();
tokio::spawn(async move {
if let Err(e) = service_clone
.fetch_browser_versions_detailed(&browser_clone, false)
.await
{
eprintln!("Background version fetch failed for {browser_clone}: {e}");
}
});
}
Ok(())
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut Browser",
"version": "0.2.5",
"version": "0.3.1",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm dev",
+40 -9
View File
@@ -2,12 +2,19 @@
import { ChangeVersionDialog } from "@/components/change-version-dialog";
import { CreateProfileDialog } from "@/components/create-profile-dialog";
import { ImportProfileDialog } from "@/components/import-profile-dialog";
import { ProfilesDataTable } from "@/components/profile-data-table";
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
import { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
import { SettingsDialog } from "@/components/settings-dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
@@ -20,7 +27,8 @@ import type { BrowserProfile, ProxySettings } from "@/types";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useRef, useState } from "react";
import { GoGear, GoPlus } from "react-icons/go";
import { FaDownload } from "react-icons/fa";
import { GoGear, GoKebabHorizontal, GoPlus } from "react-icons/go";
type BrowserTypeString =
| "mullvad-browser"
@@ -43,6 +51,7 @@ export default function Home() {
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false);
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false);
const [pendingUrls, setPendingUrls] = useState<PendingUrl[]>([]);
const [currentProfileForProxy, setCurrentProfileForProxy] =
useState<BrowserProfile | null>(null);
@@ -407,21 +416,35 @@ export default function Home() {
<div className="flex justify-between items-center">
<CardTitle>Profiles</CardTitle>
<div className="flex gap-2 items-center">
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="sm"
variant="outline"
className="flex gap-2 items-center"
>
<GoKebabHorizontal className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setSettingsDialogOpen(true);
}}
className="flex gap-2 items-center"
>
<GoGear className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Settings</TooltipContent>
</Tooltip>
<GoGear className="mr-2 w-4 h-4" />
Settings
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setImportProfileDialogOpen(true);
}}
>
<FaDownload className="mr-2 w-4 h-4" />
Import Profile
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -489,6 +512,14 @@ export default function Home() {
onVersionChanged={() => void loadProfiles()}
/>
<ImportProfileDialog
isOpen={importProfileDialogOpen}
onClose={() => {
setImportProfileDialogOpen(false);
}}
onImportComplete={() => void loadProfiles()}
/>
{pendingUrls.map((pendingUrl) => (
<ProfileSelectorDialog
key={pendingUrl.id}
+482
View File
@@ -0,0 +1,482 @@
"use client";
import { LoadingButton } from "@/components/loading-button";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useBrowserSupport } from "@/hooks/use-browser-support";
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
import type { DetectedProfile } from "@/types";
import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";
import { useEffect, useState } from "react";
import { FaFolder } from "react-icons/fa";
import { toast } from "sonner";
interface ImportProfileDialogProps {
isOpen: boolean;
onClose: () => void;
onImportComplete?: () => void;
}
export function ImportProfileDialog({
isOpen,
onClose,
onImportComplete,
}: ImportProfileDialogProps) {
const [detectedProfiles, setDetectedProfiles] = useState<DetectedProfile[]>(
[],
);
const [isLoading, setIsLoading] = useState(false);
const [isImporting, setIsImporting] = useState(false);
const [importMode, setImportMode] = useState<"auto-detect" | "manual">(
"auto-detect",
);
// Auto-detect state
const [selectedDetectedProfile, setSelectedDetectedProfile] = useState<
string | null
>(null);
const [autoDetectProfileName, setAutoDetectProfileName] = useState("");
// Manual import state
const [manualBrowserType, setManualBrowserType] = useState<string | null>(
null,
);
const [manualProfilePath, setManualProfilePath] = useState("");
const [manualProfileName, setManualProfileName] = useState("");
const { supportedBrowsers, isLoading: isLoadingSupport } =
useBrowserSupport();
useEffect(() => {
if (isOpen) {
void loadDetectedProfiles();
}
}, [isOpen]);
const loadDetectedProfiles = async () => {
setIsLoading(true);
try {
const profiles = await invoke<DetectedProfile[]>(
"detect_existing_profiles",
);
setDetectedProfiles(profiles);
// Auto-switch to manual mode if no profiles detected
if (profiles.length === 0) {
setImportMode("manual");
} else {
// Auto-select first profile if available
setSelectedDetectedProfile(profiles[0].path);
// Generate default name from the detected profile
const profile = profiles[0];
const browserName = getBrowserDisplayName(profile.browser);
const defaultName = `Imported ${browserName} Profile`;
setAutoDetectProfileName(defaultName);
}
} catch (error) {
console.error("Failed to detect existing profiles:", error);
toast.error("Failed to detect existing browser profiles");
} finally {
setIsLoading(false);
}
};
const handleBrowseFolder = async () => {
try {
const selected = await open({
directory: true,
multiple: false,
title: "Select Browser Profile Folder",
});
if (selected && typeof selected === "string") {
setManualProfilePath(selected);
}
} catch (error) {
console.error("Failed to open folder dialog:", error);
toast.error("Failed to open folder dialog");
}
};
const handleAutoDetectImport = async () => {
if (!selectedDetectedProfile || !autoDetectProfileName.trim()) {
toast.error("Please select a profile and provide a name");
return;
}
const profile = detectedProfiles.find(
(p) => p.path === selectedDetectedProfile,
);
if (!profile) {
toast.error("Selected profile not found");
return;
}
setIsImporting(true);
try {
await invoke("import_browser_profile", {
sourcePath: profile.path,
browserType: profile.browser,
newProfileName: autoDetectProfileName.trim(),
});
toast.success(
`Successfully imported profile "${autoDetectProfileName.trim()}"`,
);
if (onImportComplete) {
onImportComplete();
}
onClose();
} catch (error) {
console.error("Failed to import profile:", error);
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(`Failed to import profile: ${errorMessage}`);
} finally {
setIsImporting(false);
}
};
const handleManualImport = async () => {
if (
!manualBrowserType ||
!manualProfilePath.trim() ||
!manualProfileName.trim()
) {
toast.error("Please fill in all fields");
return;
}
setIsImporting(true);
try {
await invoke("import_browser_profile", {
sourcePath: manualProfilePath.trim(),
browserType: manualBrowserType,
newProfileName: manualProfileName.trim(),
});
toast.success(
`Successfully imported profile "${manualProfileName.trim()}"`,
);
if (onImportComplete) {
onImportComplete();
}
onClose();
} catch (error) {
console.error("Failed to import profile:", error);
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(`Failed to import profile: ${errorMessage}`);
} finally {
setIsImporting(false);
}
};
const handleClose = () => {
setSelectedDetectedProfile(null);
setAutoDetectProfileName("");
setManualBrowserType(null);
setManualProfilePath("");
setManualProfileName("");
// Only reset to auto-detect if there are profiles available
if (detectedProfiles.length > 0) {
setImportMode("auto-detect");
} else {
setImportMode("manual");
}
onClose();
};
// Update auto-detect profile name when selection changes
useEffect(() => {
if (selectedDetectedProfile) {
const profile = detectedProfiles.find(
(p) => p.path === selectedDetectedProfile,
);
if (profile) {
const browserName = getBrowserDisplayName(profile.browser);
const defaultName = `Imported ${browserName} Profile`;
setAutoDetectProfileName(defaultName);
}
}
}, [selectedDetectedProfile, detectedProfiles]);
const selectedProfile = detectedProfiles.find(
(p) => p.path === selectedDetectedProfile,
);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[80vh] my-8 flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>Import Browser Profile</DialogTitle>
</DialogHeader>
<div className="overflow-y-auto flex-1 space-y-6 min-h-0">
{/* Mode Selection */}
<div className="flex gap-2">
<Button
variant={importMode === "auto-detect" ? "default" : "outline"}
onClick={() => {
setImportMode("auto-detect");
}}
className="flex-1"
disabled={isLoading}
>
Auto-Detect
</Button>
<Button
variant={importMode === "manual" ? "default" : "outline"}
onClick={() => {
setImportMode("manual");
}}
className="flex-1"
disabled={isLoading}
>
Manual Import
</Button>
</div>
{/* Auto-Detect Mode */}
{importMode === "auto-detect" && (
<div className="space-y-4">
<h3 className="text-lg font-medium">Detected Browser Profiles</h3>
{isLoading ? (
<div className="py-8 text-center">
<p className="text-muted-foreground">
Scanning for browser profiles...
</p>
</div>
) : detectedProfiles.length === 0 ? (
<div className="py-8 text-center">
<p className="text-muted-foreground">
No browser profiles found on your system.
</p>
<p className="mt-2 text-sm text-muted-foreground">
Try the manual import option if you have profiles in custom
locations.
</p>
</div>
) : (
<div className="space-y-4">
<div>
<Label htmlFor="detected-profile-select" className="mb-2">
Select Profile:
</Label>
<Select
value={selectedDetectedProfile ?? undefined}
onValueChange={(value) => {
setSelectedDetectedProfile(value);
}}
>
<SelectTrigger id="detected-profile-select">
<SelectValue placeholder="Choose a detected profile" />
</SelectTrigger>
<SelectContent>
{detectedProfiles.map((profile) => {
const IconComponent = getBrowserIcon(profile.browser);
return (
<SelectItem key={profile.path} value={profile.path}>
<div className="flex gap-2 items-center">
{IconComponent && (
<IconComponent className="w-4 h-4" />
)}
<div className="flex flex-col">
<span className="font-medium">
{profile.name}
</span>
<span className="text-xs text-muted-foreground">
{profile.description}
</span>
</div>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
{selectedProfile && (
<div className="p-3 rounded-lg bg-muted">
<p className="text-sm">
<span className="font-medium">Path:</span>{" "}
{selectedProfile.path}
</p>
<p className="text-sm">
<span className="font-medium">Browser:</span>{" "}
{getBrowserDisplayName(selectedProfile.browser)}
</p>
</div>
)}
<div>
<Label htmlFor="auto-profile-name" className="mb-2">
New Profile Name:
</Label>
<Input
id="auto-profile-name"
value={autoDetectProfileName}
onChange={(e) => {
setAutoDetectProfileName(e.target.value);
}}
placeholder="Enter a name for the imported profile"
/>
</div>
</div>
)}
</div>
)}
{/* Manual Import Mode */}
{importMode === "manual" && (
<div className="space-y-4">
<h3 className="text-lg font-medium">Manual Profile Import</h3>
<div className="space-y-4">
<div>
<Label htmlFor="manual-browser-select" className="mb-2">
Browser Type:
</Label>
<Select
value={manualBrowserType ?? undefined}
onValueChange={(value) => {
setManualBrowserType(value);
}}
disabled={isLoadingSupport}
>
<SelectTrigger id="manual-browser-select">
<SelectValue
placeholder={
isLoadingSupport
? "Loading browsers..."
: "Select browser type"
}
/>
</SelectTrigger>
<SelectContent>
{supportedBrowsers.map((browser) => {
const IconComponent = getBrowserIcon(browser);
return (
<SelectItem key={browser} value={browser}>
<div className="flex gap-2 items-center">
{IconComponent && (
<IconComponent className="w-4 h-4" />
)}
<span>{getBrowserDisplayName(browser)}</span>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="manual-profile-path" className="mb-2">
Profile Folder Path:
</Label>
<div className="flex gap-2">
<Input
id="manual-profile-path"
value={manualProfilePath}
onChange={(e) => {
setManualProfilePath(e.target.value);
}}
placeholder="Enter the full path to the profile folder"
/>
<Button
variant="outline"
size="icon"
onClick={() => void handleBrowseFolder()}
title="Browse for folder"
>
<FaFolder className="w-4 h-4" />
</Button>
</div>
<p className="mt-2 text-xs text-muted-foreground">
Example paths:
<br />
macOS: ~/Library/Application
Support/Firefox/Profiles/xxx.default
<br />
Windows: %APPDATA%\Mozilla\Firefox\Profiles\xxx.default
<br />
Linux: ~/.mozilla/firefox/xxx.default
</p>
</div>
<div>
<Label htmlFor="manual-profile-name" className="mb-2">
New Profile Name:
</Label>
<Input
id="manual-profile-name"
value={manualProfileName}
onChange={(e) => {
setManualProfileName(e.target.value);
}}
placeholder="Enter a name for the imported profile"
/>
</div>
</div>
</div>
)}
</div>
<DialogFooter className="flex-shrink-0">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
{importMode === "auto-detect" ? (
<LoadingButton
isLoading={isImporting}
onClick={() => {
void handleAutoDetectImport();
}}
disabled={
!selectedDetectedProfile ||
!autoDetectProfileName.trim() ||
isLoading
}
>
Import Detected Profile
</LoadingButton>
) : (
<LoadingButton
isLoading={isImporting}
onClick={() => {
void handleManualImport();
}}
disabled={
!manualBrowserType ||
!manualProfilePath.trim() ||
!manualProfileName.trim()
}
>
Import Manual Profile
</LoadingButton>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+10 -45
View File
@@ -19,6 +19,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { showSuccessToast } from "@/lib/toast-utils";
import { invoke } from "@tauri-apps/api/core";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
@@ -56,7 +57,6 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
const [isSaving, setIsSaving] = useState(false);
const [isSettingDefault, setIsSettingDefault] = useState(false);
const [isClearingCache, setIsClearingCache] = useState(false);
const [isCleaningBinaries, setIsCleaningBinaries] = useState(false);
const { setTheme } = useTheme();
@@ -114,9 +114,12 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
const handleClearCache = async () => {
setIsClearingCache(true);
try {
await invoke("clear_all_version_cache");
// Optionally show a success message
console.log("Cache cleared successfully");
await invoke("clear_all_version_cache_and_refetch");
showSuccessToast("Cache cleared successfully", {
description:
"All browser version cache has been cleared and browsers are being refreshed",
duration: 4000,
});
} catch (error) {
console.error("Failed to clear cache:", error);
} finally {
@@ -124,31 +127,10 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
}
};
const handleCleanupBinaries = async () => {
setIsCleaningBinaries(true);
try {
const cleanedUp = await invoke<string[]>("cleanup_unused_binaries");
if (cleanedUp.length > 0) {
console.log(
`Cleaned up ${cleanedUp.length} unused binaries:`,
cleanedUp,
);
// You could show a toast with the results
} else {
console.log("No unused binaries to clean up");
}
} catch (error) {
console.error("Failed to cleanup unused binaries:", error);
} finally {
setIsCleaningBinaries(false);
}
};
const handleSave = async () => {
setIsSaving(true);
try {
await invoke("save_app_settings", { settings });
// Apply theme change immediately
setTheme(settings.theme);
setOriginalSettings(settings);
onClose();
@@ -318,26 +300,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
</LoadingButton>
<p className="text-xs text-muted-foreground">
Clear all cached browser version data. This will force a fresh
download of version information on the next app restart or manual
refresh.
</p>
<LoadingButton
isLoading={isCleaningBinaries}
onClick={() => {
void handleCleanupBinaries();
}}
variant="outline"
className="w-full"
>
Clean Up Unused Binaries
</LoadingButton>
<p className="text-xs text-muted-foreground">
Manually remove browser binaries that are not used by any profile.
This can help free up disk space. Note: This will run
automatically when the setting above is enabled.
Clear all cached browser version data and refresh all browser
versions from their sources. This will force a fresh download of
version information for all browsers.
</p>
</div>
</div>
+7
View File
@@ -20,6 +20,13 @@ export interface BrowserProfile {
last_launch?: number;
}
export interface DetectedProfile {
browser: string;
name: string;
path: string;
description: string;
}
export interface AppUpdateInfo {
current_version: string;
new_version: string;