mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-11 17:27:54 +02:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb84068d30 | |||
| 5024eab062 | |||
| 8137f9bf8d | |||
| e2547c6ec7 | |||
| d8d59d2bd5 | |||
| b84350eb13 | |||
| 383cef916c | |||
| 743bc059be | |||
| c46f54536b | |||
| 6cbc8627a1 | |||
| a4f4cc2f27 | |||
| 21c4d0a8ab | |||
| 9335149153 | |||
| 6711659231 | |||
| 8a592e3d7d | |||
| beea23307b | |||
| 19b66d006d | |||
| c7a36f6cd0 | |||
| 4aaf2eecbc | |||
| f750e64b81 | |||
| 16fd3e3c5e |
@@ -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
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
pkg_target: "latest-linux-x64"
|
||||
nodecar_script: "build:linux-x64"
|
||||
- platform: "ubuntu-22.04"
|
||||
- platform: "ubuntu-22.04-arm"
|
||||
args: "--target aarch64-unknown-linux-gnu"
|
||||
arch: "aarch64"
|
||||
target: "aarch64-unknown-linux-gnu"
|
||||
@@ -101,29 +101,10 @@ jobs:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies (Ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.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 pkg-config
|
||||
# Install cross-compilation tools for ARM64
|
||||
if [[ "${{ matrix.arch }}" == "aarch64" ]]; then
|
||||
sudo dpkg --add-architecture arm64
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev:arm64 libgtk-3-dev:arm64 libayatana-appindicator3-dev:arm64 librsvg2-dev:arm64
|
||||
sudo apt-get install -y libglib2.0-dev:arm64 libcairo2-dev:arm64 libpango1.0-dev:arm64 libgdk-pixbuf2.0-dev:arm64 libatk1.0-dev:arm64
|
||||
fi
|
||||
|
||||
- name: Setup cross-compilation environment (Ubuntu ARM64 only)
|
||||
if: matrix.platform == 'ubuntu-22.04' && matrix.arch == 'aarch64'
|
||||
run: |
|
||||
echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
|
||||
echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> $GITHUB_ENV
|
||||
echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar" >> $GITHUB_ENV
|
||||
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_SYSROOT_DIR=/usr" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV
|
||||
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
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
pkg_target: "latest-linux-x64"
|
||||
nodecar_script: "build:linux-x64"
|
||||
- platform: "ubuntu-22.04"
|
||||
- platform: "ubuntu-22.04-arm"
|
||||
args: "--target aarch64-unknown-linux-gnu"
|
||||
arch: "aarch64"
|
||||
target: "aarch64-unknown-linux-gnu"
|
||||
@@ -87,29 +87,10 @@ jobs:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies (Ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.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 pkg-config
|
||||
# Install cross-compilation tools for ARM64
|
||||
if [[ "${{ matrix.arch }}" == "aarch64" ]]; then
|
||||
sudo dpkg --add-architecture arm64
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev:arm64 libgtk-3-dev:arm64 libayatana-appindicator3-dev:arm64 librsvg2-dev:arm64
|
||||
sudo apt-get install -y libglib2.0-dev:arm64 libcairo2-dev:arm64 libpango1.0-dev:arm64 libgdk-pixbuf2.0-dev:arm64 libatk1.0-dev:arm64
|
||||
fi
|
||||
|
||||
- name: Setup cross-compilation environment (Ubuntu ARM64 only)
|
||||
if: matrix.platform == 'ubuntu-22.04' && matrix.arch == 'aarch64'
|
||||
run: |
|
||||
echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
|
||||
echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> $GITHUB_ENV
|
||||
echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar" >> $GITHUB_ENV
|
||||
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_SYSROOT_DIR=/usr" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV
|
||||
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
|
||||
|
||||
Vendored
+2
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"appimage",
|
||||
"appindicator",
|
||||
"applescript",
|
||||
"autoconfig",
|
||||
@@ -44,6 +45,7 @@
|
||||
"plasmohq",
|
||||
"propertylist",
|
||||
"reqwest",
|
||||
"ridedott",
|
||||
"rlib",
|
||||
"rustc",
|
||||
"SARIF",
|
||||
|
||||
@@ -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
|
||||
@@ -1,16 +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>
|
||||
|
||||

|
||||
<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. 🍩**
|
||||
## Donut Browser
|
||||
|
||||
[](https://github.com/zhom/donutbrowser/releases/latest)
|
||||
[](https://github.com/zhom/donutbrowser/issues)
|
||||
[](https://github.com/zhom/donutbrowser/blob/main/LICENSE)
|
||||
[](https://github.com/zhom/donutbrowser/stargazers)
|
||||
> A free and open source browser orchestrator built with [Tauri](https://v2.tauri.app/).
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@@ -45,7 +66,13 @@ Have questions or want to contribute? We'd love to hear from you!
|
||||
|
||||
## Star History
|
||||
|
||||
[](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
|
||||
|
||||
|
||||
@@ -20,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",
|
||||
|
||||
+2
-2
@@ -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",
|
||||
@@ -56,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",
|
||||
|
||||
Generated
+15
-15
@@ -103,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
|
||||
@@ -119,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)
|
||||
@@ -151,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
|
||||
@@ -173,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
|
||||
@@ -1498,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==}
|
||||
@@ -4715,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
|
||||
|
||||
@@ -4872,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)
|
||||
@@ -4880,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
|
||||
|
||||
@@ -6878,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
|
||||
@@ -7032,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
|
||||
|
||||
Generated
+14
-98
@@ -405,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"
|
||||
@@ -993,7 +993,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donutbrowser"
|
||||
version = "0.2.5"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -1858,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",
|
||||
@@ -1879,7 +1879,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry 0.4.0",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4114,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",
|
||||
@@ -4363,7 +4363,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-registry 0.5.2",
|
||||
"windows-registry",
|
||||
"windows-result",
|
||||
]
|
||||
|
||||
@@ -4835,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",
|
||||
@@ -5388,7 +5388,7 @@ dependencies = [
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings 0.4.2",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5440,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"
|
||||
@@ -5459,7 +5448,7 @@ checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings 0.4.2",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5471,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"
|
||||
@@ -5540,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"
|
||||
@@ -5593,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"
|
||||
@@ -5611,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"
|
||||
@@ -5629,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"
|
||||
@@ -5659,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"
|
||||
@@ -5677,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"
|
||||
@@ -5695,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"
|
||||
@@ -5713,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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
+49
-68
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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(®ular_dir).unwrap();
|
||||
|
||||
let regular_file = temp_dir.path().join("regular_file.txt");
|
||||
File::create(®ular_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(¤t_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-10
@@ -25,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::{
|
||||
@@ -276,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,
|
||||
@@ -294,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,
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user