mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-05-03 17:15:12 +02:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0ca14c184 | |||
| eea94ad360 | |||
| 2b678ed04d | |||
| dff201ddec | |||
| 743ad59348 | |||
| d43e9ef21b | |||
| 7515cbacd6 | |||
| f41172e822 | |||
| 25ce691bbc | |||
| b945ee7088 | |||
| eb3589b4c0 | |||
| b71b9a00ca | |||
| 6535b37c98 | |||
| bc72a837e2 | |||
| fb84068d30 | |||
| 5024eab062 | |||
| 8137f9bf8d | |||
| e2547c6ec7 | |||
| d8d59d2bd5 | |||
| b84350eb13 | |||
| 383cef916c | |||
| 743bc059be | |||
| c46f54536b | |||
| 6cbc8627a1 | |||
| a4f4cc2f27 | |||
| 21c4d0a8ab | |||
| 9335149153 | |||
| 6711659231 | |||
| 8a592e3d7d | |||
| beea23307b | |||
| 19b66d006d | |||
| c7a36f6cd0 | |||
| 7404cb3ff8 | |||
| ee91445fe1 | |||
| 77d53c7f32 | |||
| a21f22a916 | |||
| 4aaf2eecbc | |||
| f750e64b81 | |||
| 16fd3e3c5e | |||
| 93eae1d77f | |||
| 82615c24bd | |||
| 0769106a51 | |||
| 18aa3cb87b | |||
| f066105c0f | |||
| 9d8b3629f6 | |||
| 353e149886 | |||
| 2258edbb18 | |||
| 69fff99bbe | |||
| a277b7d8a2 | |||
| 7af3f7e86a | |||
| de9c47241a | |||
| 5747729e30 | |||
| f71bda0fbf | |||
| 67bfb17e5a | |||
| bb2eb65c1e | |||
| f397568785 | |||
| 0da34f04cb | |||
| 6836d73ffa | |||
| 63b890d47f | |||
| aeb6a08fc8 | |||
| c698fff101 | |||
| ccfd1f81f6 | |||
| 4c42099661 | |||
| 4c4aa10d8c | |||
| a1a6ef63e4 | |||
| bd7b9f1d9f | |||
| f93b5daa9b | |||
| f7f45bdc90 | |||
| 48067ee3a7 | |||
| 87bd75aa21 | |||
| cf443061b6 | |||
| ca662d91a1 | |||
| 2963dbc0f9 | |||
| 225ed05d08 | |||
| 97de246ac6 | |||
| b00f62ebec | |||
| 2025a2a690 | |||
| 2f1faa02e4 |
@@ -0,0 +1,6 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
Don't leave comments that don't add value
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
Do not duplicate code unless you have a very good reason to do so. It is important that the same logic is not duplicated multiple times
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
After your changes, instead of running specific tests or linting specific files, run "pnpm format && pnpm lint && pnpm test"
|
||||
@@ -1,14 +1,14 @@
|
||||
# ✨ Pull Request
|
||||
|
||||
### 📓 Referenced Issue
|
||||
## 📓 Referenced Issue
|
||||
|
||||
<!-- Please link the related issue. Use # before the issue number and use the verbs 'fixes', 'resolves' to auto-link it, for eg, Fixes: #<issue-number> -->
|
||||
|
||||
### ℹ️ About the PR
|
||||
## ℹ️ About the PR
|
||||
|
||||
<!-- Please provide a description of your solution if it is not clear in the related issue or if the PR has a breaking change. If there is an interesting topic to discuss or you have questions or there is an issue with Tauri, Rust, or another library that you have used. -->
|
||||
|
||||
### 🔄 Type of Change
|
||||
## 🔄 Type of Change
|
||||
|
||||
<!-- Mark the relevant option with an "x". -->
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
- [ ] 🧹 Code cleanup/refactoring
|
||||
- [ ] ⚡ Performance improvement
|
||||
|
||||
### 🖼️ Testing Scenarios / Screenshots
|
||||
## 🖼️ Testing Scenarios / Screenshots
|
||||
|
||||
<!-- Please include screenshots or gif to showcase the final output. Also, try to explain the testing you did to validate your change. -->
|
||||
|
||||
### ✅ Checklist
|
||||
## ✅ Checklist
|
||||
|
||||
<!-- Mark completed items with an "x". -->
|
||||
|
||||
@@ -36,11 +36,11 @@
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
- [ ] Any dependent changes have been merged and published
|
||||
|
||||
### 🧪 How Has This Been Tested?
|
||||
## 🧪 How Has This Been Tested?
|
||||
|
||||
<!-- Please describe the tests that you ran to verify your changes. -->
|
||||
|
||||
### 📱 Platform Testing
|
||||
## 📱 Platform Testing
|
||||
|
||||
<!-- Which platforms have you tested on? -->
|
||||
|
||||
@@ -49,6 +49,6 @@
|
||||
- [ ] Windows (if applicable)
|
||||
- [ ] Linux (if applicable)
|
||||
|
||||
### 📋 Additional Notes
|
||||
## 📋 Additional Notes
|
||||
|
||||
<!-- Any additional information that reviewers should know about this PR. -->
|
||||
|
||||
+46
-4
@@ -1,28 +1,70 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Enable version updates for Node.js dependencies
|
||||
# Frontend dependencies (root package.json)
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "saturday"
|
||||
time: "09:00"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
groups:
|
||||
all:
|
||||
frontend-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
ignore:
|
||||
- dependency-name: "eslint"
|
||||
versions: ">= 9"
|
||||
commit-message:
|
||||
prefix: "deps"
|
||||
include: "scope"
|
||||
|
||||
# Enable version updates for rust
|
||||
# Nodecar dependencies
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/nodecar"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "saturday"
|
||||
time: "09:00"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
groups:
|
||||
nodecar-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
commit-message:
|
||||
prefix: "deps(nodecar)"
|
||||
include: "scope"
|
||||
|
||||
# Rust dependencies
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/src-tauri"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "saturday"
|
||||
time: "09:00"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
groups:
|
||||
all:
|
||||
rust-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
commit-message:
|
||||
prefix: "deps(rust)"
|
||||
include: "scope"
|
||||
|
||||
# GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "saturday"
|
||||
time: "09:00"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
include: "scope"
|
||||
|
||||
@@ -1,21 +1,60 @@
|
||||
# Automatically squashes and merges Dependabot dependency upgrades if tests pass
|
||||
name: Dependabot Automerge
|
||||
|
||||
name: Dependabot Auto-merge
|
||||
|
||||
on: pull_request_target
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
checks: read
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
security-scan:
|
||||
name: Security Vulnerability Scan
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@e69cc6c86b31f1e7e23935bbe7031b50e51082de" # v2.0.2
|
||||
with:
|
||||
scan-args: |-
|
||||
-r
|
||||
--skip-git
|
||||
--lockfile=pnpm-lock.yaml
|
||||
--lockfile=src-tauri/Cargo.lock
|
||||
--lockfile=nodecar/pnpm-lock.yaml
|
||||
./
|
||||
permissions:
|
||||
security-events: write
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
lint-js:
|
||||
name: Lint JavaScript/TypeScript
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
uses: ./.github/workflows/lint-js.yml
|
||||
secrets: inherit
|
||||
|
||||
lint-rust:
|
||||
name: Lint Rust
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
uses: ./.github/workflows/lint-rs.yml
|
||||
secrets: inherit
|
||||
|
||||
dependabot-automerge:
|
||||
name: Dependabot Automerge
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
needs: [security-scan, lint-js, lint-rust]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch Dependabot metadata
|
||||
id: dependabot-metadata
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v2
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Auto-merge minor and patch updates
|
||||
uses: ridedott/merge-me-action@v2
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.SECRET_DEPENDABOT_GITHUB_TOKEN }}
|
||||
PRESET: DEPENDABOT_MINOR
|
||||
MERGE_METHOD: SQUASH
|
||||
timeout-minutes: 10
|
||||
|
||||
@@ -13,6 +13,8 @@ on:
|
||||
paths-ignore:
|
||||
- "src-tauri/**"
|
||||
- "README.md"
|
||||
- ".github/workflows/lint-rs.yml"
|
||||
- ".github/workflows/osv.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -12,11 +12,17 @@ on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "src/**"
|
||||
- "nodecar/**"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
- "yarn.lock"
|
||||
- "pnpm-lock.yaml"
|
||||
- "yarn.lock"
|
||||
- "README.md"
|
||||
- ".github/workflows/lint-js.yml"
|
||||
- ".github/workflows/osv.yml"
|
||||
- "next.config.js"
|
||||
- "tailwind.config.js"
|
||||
- "tsconfig.json"
|
||||
- "biome.json"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -49,6 +55,9 @@ jobs:
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Install cargo-audit
|
||||
run: cargo install cargo-audit
|
||||
|
||||
- name: Install dependencies (Ubuntu only)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
@@ -61,7 +70,7 @@ jobs:
|
||||
- name: Install nodecar dependencies
|
||||
working-directory: ./nodecar
|
||||
run: |
|
||||
pnpm install --ignore-workspace --frozen-lockfile
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build nodecar binary
|
||||
shell: bash
|
||||
@@ -70,7 +79,7 @@ jobs:
|
||||
if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then
|
||||
pnpm run build:linux-x64
|
||||
elif [[ "${{ matrix.os }}" == "macos-latest" ]]; then
|
||||
pnpm run build:aarch64
|
||||
pnpm run build:mac-aarch64
|
||||
elif [[ "${{ matrix.os }}" == "windows-latest" ]]; then
|
||||
pnpm run build:win-x64
|
||||
fi
|
||||
@@ -101,3 +110,7 @@ jobs:
|
||||
- name: Run Rust unit tests
|
||||
run: cargo test
|
||||
working-directory: src-tauri
|
||||
|
||||
- name: Run cargo audit security check
|
||||
run: cargo audit
|
||||
working-directory: src-tauri
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# A sample workflow which sets up periodic OSV-Scanner scanning for vulnerabilities,
|
||||
# in addition to a PR check which fails if new vulnerabilities are introduced.
|
||||
#
|
||||
# For more examples and options, including how to ignore specific vulnerabilities,
|
||||
# see https://google.github.io/osv-scanner/github-action/
|
||||
|
||||
# Security vulnerability scanning for Donut Browser
|
||||
# Scans dependencies in package managers (npm/pnpm, Cargo) for known vulnerabilities
|
||||
# Runs on schedule and when dependencies change
|
||||
|
||||
name: Security Vulnerability Scan
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "src-tauri/Cargo.toml"
|
||||
- "src-tauri/Cargo.lock"
|
||||
- "nodecar/package.json"
|
||||
- "nodecar/pnpm-lock.yaml"
|
||||
- ".github/workflows/osv.yml"
|
||||
merge_group:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
# Run weekly on Tuesdays at 2:20 PM UTC
|
||||
- cron: "20 14 * * 2"
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "src-tauri/Cargo.toml"
|
||||
- "src-tauri/Cargo.lock"
|
||||
- "nodecar/package.json"
|
||||
- "nodecar/pnpm-lock.yaml"
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
scan-scheduled:
|
||||
name: Scheduled Security Scan
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
|
||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@e69cc6c86b31f1e7e23935bbe7031b50e51082de" # v2.0.2
|
||||
with:
|
||||
scan-args: |-
|
||||
-r
|
||||
--skip-git
|
||||
--lockfile=pnpm-lock.yaml
|
||||
--lockfile=src-tauri/Cargo.lock
|
||||
--lockfile=nodecar/pnpm-lock.yaml
|
||||
./
|
||||
|
||||
scan-pr:
|
||||
name: PR Security Scan
|
||||
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@e69cc6c86b31f1e7e23935bbe7031b50e51082de" # v2.0.2
|
||||
with:
|
||||
scan-args: |-
|
||||
-r
|
||||
--skip-git
|
||||
--lockfile=pnpm-lock.yaml
|
||||
--lockfile=src-tauri/Cargo.lock
|
||||
--lockfile=nodecar/pnpm-lock.yaml
|
||||
./
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Pull Request Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
merge_group:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
lint-js:
|
||||
name: Lint JavaScript/TypeScript
|
||||
uses: ./.github/workflows/lint-js.yml
|
||||
secrets: inherit
|
||||
|
||||
lint-rust:
|
||||
name: Lint Rust
|
||||
uses: ./.github/workflows/lint-rs.yml
|
||||
secrets: inherit
|
||||
|
||||
security-scan:
|
||||
name: Security Vulnerability Scan
|
||||
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@e69cc6c86b31f1e7e23935bbe7031b50e51082de" # v2.0.2
|
||||
with:
|
||||
scan-args: |-
|
||||
-r
|
||||
--skip-git
|
||||
--lockfile=pnpm-lock.yaml
|
||||
--lockfile=nodecar/pnpm-lock.yaml
|
||||
--lockfile=src-tauri/Cargo.lock
|
||||
./
|
||||
|
||||
pr-status:
|
||||
name: PR Status Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-js, lint-rust, security-scan]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check all jobs succeeded
|
||||
run: |
|
||||
if [[ "${{ needs.lint-js.result }}" != "success" || "${{ needs.lint-rust.result }}" != "success" || "${{ needs.security-scan.result }}" != "success" ]]; then
|
||||
echo "One or more checks failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "All checks passed!"
|
||||
@@ -11,6 +11,22 @@ env:
|
||||
STABLE_RELEASE: "true"
|
||||
|
||||
jobs:
|
||||
security-scan:
|
||||
name: Security Vulnerability Scan
|
||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@e69cc6c86b31f1e7e23935bbe7031b50e51082de" # v2.0.2
|
||||
with:
|
||||
scan-args: |-
|
||||
-r
|
||||
--skip-git
|
||||
--lockfile=pnpm-lock.yaml
|
||||
--lockfile=src-tauri/Cargo.lock
|
||||
--lockfile=nodecar/pnpm-lock.yaml
|
||||
./
|
||||
permissions:
|
||||
security-events: write
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
lint-js:
|
||||
name: Lint JavaScript/TypeScript
|
||||
uses: ./.github/workflows/lint-js.yml
|
||||
@@ -22,7 +38,7 @@ jobs:
|
||||
secrets: inherit
|
||||
|
||||
release:
|
||||
needs: [lint-js, lint-rust]
|
||||
needs: [security-scan, lint-js, lint-rust]
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
@@ -34,26 +50,26 @@ jobs:
|
||||
arch: "aarch64"
|
||||
target: "aarch64-apple-darwin"
|
||||
pkg_target: "latest-macos-arm64"
|
||||
nodecar_script: "build:aarch64"
|
||||
nodecar_script: "build:mac-aarch64"
|
||||
- platform: "macos-latest"
|
||||
args: "--target x86_64-apple-darwin"
|
||||
arch: "x86_64"
|
||||
target: "x86_64-apple-darwin"
|
||||
pkg_target: "latest-macos-x64"
|
||||
nodecar_script: "build:x86_64"
|
||||
nodecar_script: "build:mac-x86_64"
|
||||
- platform: "ubuntu-22.04"
|
||||
args: "--target x86_64-unknown-linux-gnu"
|
||||
arch: "x86_64"
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
pkg_target: "latest-linux-x64"
|
||||
nodecar_script: "build:linux-x64"
|
||||
- platform: "ubuntu-22.04-arm"
|
||||
args: "--target aarch64-unknown-linux-gnu"
|
||||
arch: "aarch64"
|
||||
target: "aarch64-unknown-linux-gnu"
|
||||
pkg_target: "latest-linux-arm64"
|
||||
nodecar_script: "build:linux-arm64"
|
||||
# Future platforms can be added here:
|
||||
# - platform: "ubuntu-20.04"
|
||||
# args: "--target x86_64-unknown-linux-gnu"
|
||||
# arch: "x86_64"
|
||||
# target: "x86_64-unknown-linux-gnu"
|
||||
# pkg_target: "latest-linux-x64"
|
||||
# nodecar_script: "build:linux-x64"
|
||||
# - platform: "ubuntu-20.04"
|
||||
# args: "--target aarch64-unknown-linux-gnu"
|
||||
# arch: "aarch64"
|
||||
# target: "aarch64-unknown-linux-gnu"
|
||||
# pkg_target: "latest-linux-arm64"
|
||||
# nodecar_script: "build:linux-arm64"
|
||||
# - platform: "windows-latest"
|
||||
# args: "--target x86_64-pc-windows-msvc"
|
||||
# arch: "x86_64"
|
||||
@@ -85,10 +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
|
||||
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
|
||||
@@ -101,7 +117,7 @@ jobs:
|
||||
- name: Install nodecar dependencies
|
||||
working-directory: ./nodecar
|
||||
run: |
|
||||
pnpm install --ignore-workspace --frozen-lockfile
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build nodecar sidecar
|
||||
shell: bash
|
||||
|
||||
@@ -10,6 +10,22 @@ env:
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
|
||||
jobs:
|
||||
security-scan:
|
||||
name: Security Vulnerability Scan
|
||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@e69cc6c86b31f1e7e23935bbe7031b50e51082de" # v2.0.2
|
||||
with:
|
||||
scan-args: |-
|
||||
-r
|
||||
--skip-git
|
||||
--lockfile=pnpm-lock.yaml
|
||||
--lockfile=src-tauri/Cargo.lock
|
||||
--lockfile=nodecar/pnpm-lock.yaml
|
||||
./
|
||||
permissions:
|
||||
security-events: write
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
lint-js:
|
||||
name: Lint JavaScript/TypeScript
|
||||
uses: ./.github/workflows/lint-js.yml
|
||||
@@ -21,7 +37,7 @@ jobs:
|
||||
secrets: inherit
|
||||
|
||||
rolling-release:
|
||||
needs: [lint-js, lint-rust]
|
||||
needs: [security-scan, lint-js, lint-rust]
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
@@ -33,13 +49,25 @@ jobs:
|
||||
arch: "aarch64"
|
||||
target: "aarch64-apple-darwin"
|
||||
pkg_target: "latest-macos-arm64"
|
||||
nodecar_script: "build:aarch64"
|
||||
nodecar_script: "build:mac-aarch64"
|
||||
- platform: "macos-latest"
|
||||
args: "--target x86_64-apple-darwin"
|
||||
arch: "x86_64"
|
||||
target: "x86_64-apple-darwin"
|
||||
pkg_target: "latest-macos-x64"
|
||||
nodecar_script: "build:x86_64"
|
||||
nodecar_script: "build:mac-x86_64"
|
||||
- platform: "ubuntu-22.04"
|
||||
args: "--target x86_64-unknown-linux-gnu"
|
||||
arch: "x86_64"
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
pkg_target: "latest-linux-x64"
|
||||
nodecar_script: "build:linux-x64"
|
||||
- platform: "ubuntu-22.04-arm"
|
||||
args: "--target aarch64-unknown-linux-gnu"
|
||||
arch: "aarch64"
|
||||
target: "aarch64-unknown-linux-gnu"
|
||||
pkg_target: "latest-linux-arm64"
|
||||
nodecar_script: "build:linux-arm64"
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
@@ -58,6 +86,12 @@ jobs:
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies (Ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev pkg-config xdg-utils
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -69,7 +103,7 @@ jobs:
|
||||
- name: Install nodecar dependencies
|
||||
working-directory: ./nodecar
|
||||
run: |
|
||||
pnpm install --ignore-workspace --frozen-lockfile
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build nodecar sidecar
|
||||
shell: bash
|
||||
@@ -81,7 +115,11 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p src-tauri/binaries
|
||||
cp nodecar/dist/nodecar src-tauri/binaries/nodecar-${{ matrix.target }}
|
||||
if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then
|
||||
cp nodecar/dist/nodecar.exe src-tauri/binaries/nodecar-${{ matrix.target }}.exe
|
||||
else
|
||||
cp nodecar/dist/nodecar src-tauri/binaries/nodecar-${{ matrix.target }}
|
||||
fi
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm build
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# npm/yarn lock files (project uses pnpm only)
|
||||
**/package-lock.json
|
||||
**/yarn.lock
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
@@ -42,4 +46,7 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# eslint
|
||||
.eslintcache
|
||||
|
||||
!**/.gitkeep
|
||||
+1
-1
@@ -1 +1 @@
|
||||
pnpm lint-staged
|
||||
pnpm exec lint-staged
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
22
|
||||
23
|
||||
|
||||
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"biomejs.biome",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"usernamehw.errorlens",
|
||||
"heybourn.headwind",
|
||||
"yoavbls.pretty-ts-errors",
|
||||
"rust-lang.rust-analyzer",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
Vendored
+51
-1
@@ -1,23 +1,73 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"appimage",
|
||||
"appindicator",
|
||||
"applescript",
|
||||
"autoconfig",
|
||||
"autologin",
|
||||
"biomejs",
|
||||
"cdylib",
|
||||
"CFURL",
|
||||
"checkin",
|
||||
"clippy",
|
||||
"codegen",
|
||||
"devedition",
|
||||
"donutbrowser",
|
||||
"dpkg",
|
||||
"dtolnay",
|
||||
"elif",
|
||||
"esbuild",
|
||||
"eslintcache",
|
||||
"frontmost",
|
||||
"gifs",
|
||||
"gsettings",
|
||||
"icns",
|
||||
"idletime",
|
||||
"KHTML",
|
||||
"launchservices",
|
||||
"libatk",
|
||||
"libayatana",
|
||||
"libcairo",
|
||||
"libgdk",
|
||||
"libglib",
|
||||
"libpango",
|
||||
"librsvg",
|
||||
"libwebkit",
|
||||
"mountpoint",
|
||||
"msvc",
|
||||
"msys",
|
||||
"Mullvad",
|
||||
"nodecar",
|
||||
"ntlm",
|
||||
"objc",
|
||||
"osascript",
|
||||
"pixbuf",
|
||||
"plasmohq",
|
||||
"propertylist",
|
||||
"reqwest",
|
||||
"ridedott",
|
||||
"rlib",
|
||||
"rustc",
|
||||
"SARIF",
|
||||
"serde",
|
||||
"shadcn",
|
||||
"signon",
|
||||
"sonner",
|
||||
"sspi",
|
||||
"staticlib",
|
||||
"subdirs",
|
||||
"swatinem",
|
||||
"sysinfo",
|
||||
"systempreferences",
|
||||
"turbopack"
|
||||
"tauri",
|
||||
"titlebar",
|
||||
"Torbrowser",
|
||||
"turbopack",
|
||||
"unlisten",
|
||||
"unrs",
|
||||
"vercel",
|
||||
"wiremock",
|
||||
"xattr",
|
||||
"zhom"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# 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
|
||||
- Do not duplicate code unless you have a very good reason to do so. It is important that the same logic is not duplicated multiple times
|
||||
+1
-1
@@ -50,7 +50,7 @@ After having the above dependencies installed, proceed through the following ste
|
||||
|
||||
```bash
|
||||
cd nodecar
|
||||
pnpm install --ignore-workspace --frozen-lockfile
|
||||
pnpm install --frozen-lockfile
|
||||
cd ..
|
||||
```
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/zhom/donutbrowser/releases/latest" target="_blank"><img alt="GitHub release" src="https://img.shields.io/github/v/release/zhom/donutbrowser">
|
||||
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/releases/latest" target="_blank"><img alt="GitHub release" src="https://img.shields.io/github/v/release/zhom/donutbrowser">
|
||||
</a>
|
||||
<a href="https://github.com/zhom/donutbrowser/issues" target="_blank">
|
||||
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/issues" target="_blank">
|
||||
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="PRs Welcome">
|
||||
</a>
|
||||
<a href="https://github.com/zhom/donutbrowser/blob/main/LICENSE" target="_blank">
|
||||
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/blob/main/LICENSE" target="_blank">
|
||||
<img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg" alt="License">
|
||||
</a>
|
||||
<a href="https://github.com/zhom/donutbrowser/stargazers" target="_blank">
|
||||
<a 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>
|
||||
@@ -25,17 +25,26 @@
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
- Automatic updates both for browsers and for the app itself
|
||||
- Set Donut Browser as your default browser to control in which profile to open links
|
||||
|
||||
## Download
|
||||
|
||||
> As of right now, the app is not signed by Apple. You need to have Gatekeeper disabled to run it. The app automatically checks for updates on each launch.
|
||||
> For Linux, .deb and .rpm packages are available as well as standalone .AppImage files.
|
||||
|
||||
The app can be downloaded from the [releases page](https://github.com/zhom/donutbrowser/releases/latest).
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
- ✅ **macOS** (Intel & Apple Silicon)
|
||||
- ✅ **Linux** (x64 & arm64)
|
||||
- 🔄 **Windows** (Planned)
|
||||
- 🔄 **Linux** (Planned)
|
||||
|
||||
## Development
|
||||
|
||||
@@ -54,6 +63,16 @@ Have questions or want to contribute? We'd love to hear from you!
|
||||
- **Issues**: [GitHub Issues](https://github.com/zhom/donutbrowser/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://github.com/zhom/donutbrowser/discussions)
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/#zhom/donutbrowser&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=zhom/donutbrowser&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Contact
|
||||
|
||||
Have an urgent question or want to report a security vulnerability? Send an email to contact at donutbrowser dot com and we'll get back to you as fast as possible.
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
Thanks for helping make Donut Browser safe for everyone! ❤️
|
||||
|
||||
We take the security of Donut Browser seriously. If you believe you have found a security vulnerability in Donut Browser, please report it to us through coordinated disclosure.
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
|
||||
|
||||
Instead, please send an email to **contact at donutbrowser dot com** with the subject line "Security Vulnerability Report".
|
||||
|
||||
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
|
||||
|
||||
- The type of issue (e.g., buffer overflow, injection attack, privilege escalation, or cross-site scripting)
|
||||
- Full paths of source file(s) related to the manifestation of the issue
|
||||
- The location of the affected source code (tag/branch/commit or direct URL)
|
||||
- Any special configuration required to reproduce the issue
|
||||
- Step-by-step instructions to reproduce the issue
|
||||
- Proof-of-concept or exploit code (if possible)
|
||||
- Impact of the issue, including how an attacker might exploit the issue
|
||||
- Your assessment of the severity level
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
## What to Expect
|
||||
|
||||
- **Response Time**: We will acknowledge receipt of your vulnerability report within 72 hours.
|
||||
- **Investigation**: We will investigate the issue and provide you with updates on our progress.
|
||||
- **Resolution**: We aim to resolve critical security issues as fast as possible, but no longer than in 30 days after the initial report.
|
||||
- **Disclosure**: We will coordinate with you on the timing of any public disclosure.
|
||||
|
||||
## Contact
|
||||
|
||||
For urgent security matters, please contact us at **contact at donutbrowser dot com**.
|
||||
|
||||
For general questions about this security policy, you can also reach out through:
|
||||
|
||||
- [GitHub Issues](https://github.com/zhom/donutbrowser/issues) (for non-security questions only)
|
||||
- [GitHub Discussions](https://github.com/zhom/donutbrowser/discussions)
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
@@ -18,4 +18,4 @@
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Determine file extension based on platform
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" || "$OSTYPE" == "cygwin" ]]; then
|
||||
EXT=".exe"
|
||||
else
|
||||
EXT=""
|
||||
fi
|
||||
|
||||
# If architecture provided in the command line, use it to rename the binary in TARGET_TRIPLE
|
||||
if [ -n "$1" ]; then
|
||||
TARGET_TRIPLE="$1"
|
||||
else
|
||||
RUST_INFO=$(rustc -vV)
|
||||
TARGET_TRIPLE=$(echo "$RUST_INFO" | grep -o 'host: [^ ]*' | cut -d' ' -f2)
|
||||
fi
|
||||
|
||||
# Check if target triple was found
|
||||
if [ -z "$TARGET_TRIPLE" ]; then
|
||||
echo "Failed to determine platform target triple" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy the file
|
||||
cp "dist/nodecar${EXT}" "../src-tauri/binaries/nodecar-${TARGET_TRIPLE}${EXT}"
|
||||
+12
-12
@@ -7,26 +7,26 @@
|
||||
"watch": "nodemon --exec ts-node --esm ./src/index.ts --watch src",
|
||||
"dev": "node --loader ts-node/esm ./src/index.ts",
|
||||
"start": "node --loader ts-node/esm ./src/index.ts",
|
||||
"build": "tsc && pkg ./dist/index.js --targets latest-macos-arm64 --output dist/nodecar",
|
||||
"build:aarch64": "tsc && pkg ./dist/index.js --targets latest-macos-arm64 --output dist/nodecar",
|
||||
"build:x86_64": "tsc && pkg ./dist/index.js --targets latest-macos-x64 --output dist/nodecar",
|
||||
"build:linux-x64": "tsc && pkg ./dist/index.js --targets latest-linux-x64 --output dist/nodecar",
|
||||
"build:linux-arm64": "tsc && pkg ./dist/index.js --targets latest-linux-arm64 --output dist/nodecar",
|
||||
"build:win-x64": "tsc && pkg ./dist/index.js --targets latest-win-x64 --output dist/nodecar",
|
||||
"build:win-arm64": "tsc && pkg ./dist/index.js --targets latest-win-arm64 --output dist/nodecar"
|
||||
"rename-binary": "sh ./copy-binary.sh",
|
||||
"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: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",
|
||||
"build:win-arm64": "tsc && pkg ./dist/index.js --targets latest-win-arm64 --output dist/nodecar && pnpm rename-binary"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "AGPL-3.0",
|
||||
"packageManager": "pnpm@10.6.1",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.15.17",
|
||||
"@yao-pkg/pkg": "^6.4.1",
|
||||
"commander": "^13.1.0",
|
||||
"@types/node": "^22.15.30",
|
||||
"@yao-pkg/pkg": "^6.5.1",
|
||||
"commander": "^14.0.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"get-port": "^7.1.0",
|
||||
"nodemon": "^3.1.10",
|
||||
"proxy-chain": "^2.5.8",
|
||||
"proxy-chain": "^2.5.9",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
||||
Generated
-1304
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
|
||||
const ext = process.platform === "win32" ? ".exe" : "";
|
||||
|
||||
const rustInfo = execSync("rustc -vV");
|
||||
const targetTriple = /host: (\S+)/g.exec(rustInfo)[1];
|
||||
if (!targetTriple) {
|
||||
console.error("Failed to determine platform target triple");
|
||||
}
|
||||
fs.renameSync(
|
||||
`dist/nodecar${ext}`,
|
||||
`../src-tauri/binaries/nodecar-${targetTriple}${ext}`
|
||||
);
|
||||
@@ -48,8 +48,8 @@ program
|
||||
ignoreProxyCertificate: options.ignoreCertificate,
|
||||
});
|
||||
console.log(JSON.stringify(config));
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to start proxy: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
console.error(`Failed to start proxy: ${JSON.stringify(error)}`);
|
||||
}
|
||||
} else if (action === "stop") {
|
||||
if (options.id) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spawn } from "child_process";
|
||||
import path from "path";
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import getPort from "get-port";
|
||||
import {
|
||||
type ProxyConfig,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
|
||||
// Define the proxy configuration type
|
||||
export interface ProxyConfig {
|
||||
|
||||
+25
-20
@@ -2,22 +2,25 @@
|
||||
"name": "donutbrowser",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"version": "0.2.4",
|
||||
"version": "0.3.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"test": "pnpm test:rust",
|
||||
"test:rust": "cd src-tauri && cargo test",
|
||||
"lint": "pnpm lint:js && pnpm lint:rust",
|
||||
"lint:js": "biome check src/ && tsc --noEmit && next lint",
|
||||
"lint:rust": "cd src-tauri && cargo clippy --all-targets --all-features -- -D warnings -D clippy::all && cargo fmt --all",
|
||||
"tauri": "tauri",
|
||||
"shadcn:add": "pnpm dlx shadcn@latest add",
|
||||
"prepare": "husky",
|
||||
"prepare": "husky && husky install",
|
||||
"format:rust": "cd src-tauri && cargo clippy --fix --allow-dirty --all-targets --all-features -- -D warnings -D clippy::all && cargo fmt --all",
|
||||
"format:js": "biome check src/ --fix",
|
||||
"format": "pnpm format:js && pnpm format:rust",
|
||||
"cargo": "cd src-tauri && cargo"
|
||||
"cargo": "cd src-tauri && cargo",
|
||||
"check-unused-commands": "cd src-tauri && cargo run --bin check_unused_commands"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
@@ -32,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",
|
||||
@@ -42,36 +46,37 @@
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"sonner": "^2.0.3",
|
||||
"sonner": "^2.0.5",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@next/eslint-plugin-next": "^15.3.2",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@next/eslint-plugin-next": "^15.3.3",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@tauri-apps/cli": "^2.5.0",
|
||||
"@types/node": "^22.15.21",
|
||||
"@types/react": "^19.1.5",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-next": "^15.3.2",
|
||||
"@types/node": "^22.15.30",
|
||||
"@types/react": "^19.1.6",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
||||
"@typescript-eslint/parser": "^8.33.1",
|
||||
"@vitejs/plugin-react": "^4.5.1",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-next": "^15.3.3",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.0",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"tw-animate-css": "^1.3.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.32.1"
|
||||
"typescript-eslint": "^8.33.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
|
||||
"packageManager": "pnpm@10.11.1",
|
||||
"lint-staged": {
|
||||
"src/**/*.{js,jsx,ts,tsx,json,css,md}": [
|
||||
"biome check --fix"
|
||||
"biome check --fix",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"src-tauri/**/*.rs": [
|
||||
"cd src-tauri && cargo fmt --all",
|
||||
|
||||
Generated
+1251
-363
File diff suppressed because it is too large
Load Diff
+6
-2
@@ -1,5 +1,9 @@
|
||||
packages:
|
||||
- "nodecar"
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- '@biomejs/biome'
|
||||
- '@tailwindcss/oxide'
|
||||
- "@biomejs/biome"
|
||||
- "@tailwindcss/oxide"
|
||||
- esbuild
|
||||
- sharp
|
||||
- unrs-resolver
|
||||
|
||||
Generated
+128
-104
@@ -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"
|
||||
@@ -458,9 +476,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.1.9"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
|
||||
checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -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.4"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -971,6 +1001,8 @@ dependencies = [
|
||||
"directories",
|
||||
"futures-util",
|
||||
"lazy_static",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -978,6 +1010,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-shell",
|
||||
@@ -1825,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",
|
||||
@@ -1846,7 +1879,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry 0.4.0",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2597,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",
|
||||
]
|
||||
|
||||
@@ -2608,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",
|
||||
@@ -3280,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"
|
||||
@@ -3300,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"
|
||||
@@ -3318,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"
|
||||
@@ -3393,9 +3455,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.18"
|
||||
version = "0.12.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5"
|
||||
checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -3436,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"
|
||||
@@ -4027,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",
|
||||
@@ -4276,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"
|
||||
@@ -4578,6 +4683,7 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -4729,9 +4835,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.4"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e"
|
||||
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bytes",
|
||||
@@ -5282,7 +5388,7 @@ dependencies = [
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings 0.4.2",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5334,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"
|
||||
@@ -5353,7 +5448,7 @@ checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings 0.4.2",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5365,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"
|
||||
@@ -5434,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"
|
||||
@@ -5487,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"
|
||||
@@ -5505,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"
|
||||
@@ -5523,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"
|
||||
@@ -5553,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"
|
||||
@@ -5571,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"
|
||||
@@ -5589,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"
|
||||
@@ -5607,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"
|
||||
@@ -5793,6 +5815,7 @@ dependencies = [
|
||||
"ordered-stream",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -6004,6 +6027,7 @@ dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"url",
|
||||
"winnow 0.7.10",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "donutbrowser"
|
||||
version = "0.2.4"
|
||||
description = "Browser Orchestrator"
|
||||
version = "0.3.2"
|
||||
description = "Simple Yet Powerful Browser Orchestrator"
|
||||
authors = ["zhom@github"]
|
||||
edition = "2021"
|
||||
default-run = "donutbrowser"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -25,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"] }
|
||||
@@ -37,6 +39,8 @@ futures-util = "0.3"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation="0.10"
|
||||
objc2 = "0.6.1"
|
||||
objc2-app-kit = { version = "0.3.1", features = ["NSWindow"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.13.0"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.4</string>
|
||||
<string>0.3.2</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:event:default",
|
||||
"core:window:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"opener:default",
|
||||
"fs:default",
|
||||
"shell:allow-execute",
|
||||
@@ -13,6 +18,8 @@
|
||||
"shell:allow-open",
|
||||
"shell:allow-spawn",
|
||||
"shell:allow-stdin-write",
|
||||
"deep-link:default"
|
||||
"deep-link:default",
|
||||
"dialog:default",
|
||||
"dialog:allow-open"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Donut Browser
|
||||
Comment=Simple Yet Powerful Browser Orchestrator
|
||||
Exec=donutbrowser %u
|
||||
Icon=donutbrowser
|
||||
StartupNotify=true
|
||||
NoDisplay=false
|
||||
Categories=Network;WebBrowser;Productivity;
|
||||
MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml;
|
||||
StartupWMClass=donutbrowser
|
||||
Keywords=browser;web;internet;productivity;
|
||||
+174
-116
@@ -224,15 +224,46 @@ pub fn sort_github_releases(releases: &mut [GithubRelease]) {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn is_alpha_version(version: &str) -> bool {
|
||||
pub fn is_nightly_version(version: &str) -> bool {
|
||||
let version_comp = VersionComponent::parse(version);
|
||||
version_comp.pre_release.is_some()
|
||||
}
|
||||
|
||||
// Browser-specific alpha version detection for Zen Browser
|
||||
pub fn is_zen_alpha_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 {
|
||||
true
|
||||
}
|
||||
}
|
||||
"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 +287,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 +308,6 @@ pub struct ApiClient {
|
||||
github_api_base: String,
|
||||
chromium_api_base: String,
|
||||
tor_archive_base: String,
|
||||
mozilla_download_base: String,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
@@ -291,7 +320,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 +330,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 +338,6 @@ impl ApiClient {
|
||||
github_api_base,
|
||||
chromium_api_base,
|
||||
tor_archive_base,
|
||||
mozilla_download_base,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,11 +475,7 @@ impl ApiClient {
|
||||
BrowserRelease {
|
||||
version: version.clone(),
|
||||
date: "".to_string(), // Cache doesn't store dates
|
||||
is_prerelease: is_alpha_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(),
|
||||
@@ -467,7 +489,7 @@ impl ApiClient {
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("User-Agent", "donutbrowser")
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -489,10 +511,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 +552,7 @@ impl ApiClient {
|
||||
BrowserRelease {
|
||||
version: version.clone(),
|
||||
date: "".to_string(), // Cache doesn't store dates
|
||||
is_prerelease: is_alpha_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(),
|
||||
@@ -552,7 +566,7 @@ impl ApiClient {
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("User-Agent", "donutbrowser")
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -580,10 +594,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
|
||||
@@ -624,13 +634,13 @@ impl ApiClient {
|
||||
|
||||
println!("Fetching Mullvad releases from GitHub API...");
|
||||
let url = format!(
|
||||
"{}/repos/mullvad/mullvad-browser/releases",
|
||||
"{}/repos/mullvad/mullvad-browser/releases?per_page=100",
|
||||
self.github_api_base
|
||||
);
|
||||
let releases = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("User-Agent", "donutbrowser")
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
.send()
|
||||
.await?
|
||||
.json::<Vec<GithubRelease>>()
|
||||
@@ -639,7 +649,7 @@ impl ApiClient {
|
||||
let mut releases: Vec<GithubRelease> = releases
|
||||
.into_iter()
|
||||
.map(|mut release| {
|
||||
release.is_alpha = release.prerelease;
|
||||
release.is_nightly = release.prerelease;
|
||||
release
|
||||
})
|
||||
.collect();
|
||||
@@ -670,13 +680,13 @@ impl ApiClient {
|
||||
|
||||
println!("Fetching Zen releases from GitHub API...");
|
||||
let url = format!(
|
||||
"{}/repos/zen-browser/desktop/releases",
|
||||
"{}/repos/zen-browser/desktop/releases?per_page=100",
|
||||
self.github_api_base
|
||||
);
|
||||
let mut releases = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("User-Agent", "donutbrowser")
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
.send()
|
||||
.await?
|
||||
.json::<Vec<GithubRelease>>()
|
||||
@@ -684,8 +694,9 @@ impl ApiClient {
|
||||
|
||||
// Check for twilight updates and mark alpha releases
|
||||
for release in &mut releases {
|
||||
// Use browser-specific alpha detection for Zen Browser
|
||||
release.is_alpha = is_zen_alpha_version(&release.tag_name) || release.prerelease;
|
||||
// Use browser-specific alpha detection for Zen Browser - only "twilight" is nightly
|
||||
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" {
|
||||
@@ -726,32 +737,32 @@ impl ApiClient {
|
||||
|
||||
println!("Fetching Brave releases from GitHub API...");
|
||||
let url = format!(
|
||||
"{}/repos/brave/brave-browser/releases",
|
||||
"{}/repos/brave/brave-browser/releases?per_page=100",
|
||||
self.github_api_base
|
||||
);
|
||||
let releases = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("User-Agent", "donutbrowser")
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
.send()
|
||||
.await?
|
||||
.json::<Vec<GithubRelease>>()
|
||||
.await?;
|
||||
|
||||
// Filter releases that have universal macOS DMG assets
|
||||
// Get platform info to filter appropriate releases
|
||||
let (os, arch) = Self::get_platform_info();
|
||||
|
||||
// Filter releases that have assets compatible with the current platform
|
||||
let mut filtered_releases: Vec<GithubRelease> = releases
|
||||
.into_iter()
|
||||
.filter_map(|mut release| {
|
||||
// Check if this release has a universal DMG asset
|
||||
let has_universal_dmg = release
|
||||
.assets
|
||||
.iter()
|
||||
.any(|asset| asset.name.contains(".dmg") && asset.name.contains("universal"));
|
||||
// Check if this release has compatible assets for the current platform
|
||||
let has_compatible_asset = Self::has_compatible_brave_asset(&release.assets, &os, &arch);
|
||||
|
||||
if has_universal_dmg {
|
||||
// Set is_alpha based on the release name
|
||||
// Nightly releases contain "Nightly", stable contain "Release"
|
||||
release.is_alpha = release.name.to_lowercase().contains("nightly");
|
||||
if has_compatible_asset {
|
||||
// Use the centralized nightly detection function
|
||||
release.is_nightly =
|
||||
is_browser_version_nightly("brave", &release.tag_name, Some(&release.name));
|
||||
Some(release)
|
||||
} else {
|
||||
None
|
||||
@@ -772,6 +783,67 @@ impl ApiClient {
|
||||
Ok(filtered_releases)
|
||||
}
|
||||
|
||||
/// Check if a Brave release has compatible assets for the given platform and architecture
|
||||
fn has_compatible_brave_asset(
|
||||
assets: &[crate::browser::GithubAsset],
|
||||
os: &str,
|
||||
arch: &str,
|
||||
) -> bool {
|
||||
match os {
|
||||
"windows" => {
|
||||
// For Windows, look for standalone setup EXE (not the auto-updater one)
|
||||
assets.iter().any(|asset| {
|
||||
let name = asset.name.to_lowercase();
|
||||
name.contains("standalone") && name.ends_with(".exe") && !name.contains("silent")
|
||||
}) || assets.iter().any(|asset| asset.name.ends_with(".exe"))
|
||||
}
|
||||
"macos" => {
|
||||
// For macOS, prefer universal DMG
|
||||
assets.iter().any(|asset| {
|
||||
let name = asset.name.to_lowercase();
|
||||
name.contains("universal") && name.ends_with(".dmg")
|
||||
}) || assets.iter().any(|asset| asset.name.ends_with(".dmg"))
|
||||
}
|
||||
"linux" => {
|
||||
// For Linux, be strict about architecture matching - only allow assets that explicitly match the current architecture
|
||||
let arch_pattern = if arch == "arm64" { "arm64" } else { "amd64" };
|
||||
|
||||
if assets.iter().any(|asset| {
|
||||
let name = asset.name.to_lowercase();
|
||||
name.contains("linux") && name.contains(arch_pattern) && name.ends_with(".zip")
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get platform and architecture information
|
||||
fn get_platform_info() -> (String, String) {
|
||||
let os = if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
let arch = if cfg!(target_arch = "x86_64") {
|
||||
"x64"
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
"arm64"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
(os.to_string(), arch.to_string())
|
||||
}
|
||||
|
||||
pub async fn fetch_chromium_latest_version(
|
||||
&self,
|
||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
@@ -785,7 +857,7 @@ impl ApiClient {
|
||||
let version = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("User-Agent", "donutbrowser")
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
@@ -811,7 +883,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(),
|
||||
@@ -848,7 +919,6 @@ impl ApiClient {
|
||||
version: version.clone(),
|
||||
date: "".to_string(),
|
||||
is_prerelease: false,
|
||||
download_url: None,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
@@ -868,11 +938,7 @@ impl ApiClient {
|
||||
BrowserRelease {
|
||||
version: version.clone(),
|
||||
date: "".to_string(), // Cache doesn't store dates
|
||||
is_prerelease: false, // Assume all archived versions are stable
|
||||
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(),
|
||||
@@ -885,7 +951,7 @@ impl ApiClient {
|
||||
let html = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("User-Agent", "donutbrowser")
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
@@ -947,10 +1013,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(),
|
||||
@@ -965,7 +1027,7 @@ impl ApiClient {
|
||||
let html = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("User-Agent", "donutbrowser")
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
@@ -999,13 +1061,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() {
|
||||
@@ -1032,12 +1092,31 @@ impl ApiClient {
|
||||
|
||||
Ok(false) // No update detected
|
||||
}
|
||||
|
||||
pub fn clear_all_cache(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let cache_dir = Self::get_cache_dir()?;
|
||||
|
||||
if cache_dir.exists() {
|
||||
// Remove all cache files
|
||||
for entry in fs::read_dir(&cache_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
fs::remove_file(&path)?;
|
||||
println!("Removed cache file: {path:?}");
|
||||
}
|
||||
}
|
||||
println!("All version cache cleared successfully");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wiremock::matchers::{header, method, path};
|
||||
use wiremock::matchers::{method, path, query_param};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
async fn setup_mock_server() -> MockServer {
|
||||
@@ -1052,7 +1131,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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1215,7 +1293,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/firefox.json"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -1226,16 +1303,13 @@ mod tests {
|
||||
|
||||
let result = client.fetch_firefox_releases_with_caching(true).await;
|
||||
|
||||
if let Err(e) = &result {
|
||||
println!("Firefox API test error: {e}");
|
||||
}
|
||||
assert!(result.is_ok());
|
||||
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]
|
||||
@@ -1259,7 +1333,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/devedition.json"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -1272,16 +1345,13 @@ mod tests {
|
||||
.fetch_firefox_developer_releases_with_caching(true)
|
||||
.await;
|
||||
|
||||
if let Err(e) = &result {
|
||||
println!("Firefox Developer API test error: {e}");
|
||||
}
|
||||
assert!(result.is_ok());
|
||||
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]
|
||||
@@ -1307,7 +1377,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/mullvad/mullvad-browser/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -1322,7 +1392,7 @@ mod tests {
|
||||
let releases = result.unwrap();
|
||||
assert!(!releases.is_empty());
|
||||
assert_eq!(releases[0].tag_name, "14.5a6");
|
||||
assert!(releases[0].is_alpha);
|
||||
assert!(releases[0].is_nightly);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1348,7 +1418,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/zen-browser/desktop/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -1388,7 +1458,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/brave/brave-browser/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -1399,11 +1469,14 @@ mod tests {
|
||||
|
||||
let result = client.fetch_brave_releases_with_caching(true).await;
|
||||
|
||||
if let Err(e) = &result {
|
||||
println!("Brave API test error: {e}");
|
||||
}
|
||||
assert!(result.is_ok());
|
||||
let releases = result.unwrap();
|
||||
assert!(!releases.is_empty());
|
||||
assert_eq!(releases[0].tag_name, "v1.81.9");
|
||||
assert!(!releases[0].is_alpha);
|
||||
assert!(releases[0].is_nightly);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1419,7 +1492,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!("/{arch}/LAST_CHANGE")))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string("1465660")
|
||||
@@ -1448,7 +1520,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!("/{arch}/LAST_CHANGE")))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string("1465660")
|
||||
@@ -1491,7 +1562,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_html)
|
||||
@@ -1502,7 +1572,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/14.0.4/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(version_html)
|
||||
@@ -1513,7 +1582,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/14.0.3/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(version_html.replace("14.0.4", "14.0.3"))
|
||||
@@ -1528,12 +1596,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]
|
||||
@@ -1551,7 +1613,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/14.0.4/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(version_html)
|
||||
@@ -1581,7 +1642,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/14.0.5/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(version_html)
|
||||
@@ -1597,24 +1657,24 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_alpha_version() {
|
||||
assert!(is_alpha_version("1.2.3a1"));
|
||||
assert!(is_alpha_version("137.0b5"));
|
||||
assert!(is_alpha_version("140.0rc1"));
|
||||
assert!(!is_alpha_version("139.0"));
|
||||
assert!(!is_alpha_version("1.2.3"));
|
||||
fn test_is_nightly_version() {
|
||||
assert!(is_nightly_version("1.2.3a1"));
|
||||
assert!(is_nightly_version("137.0b5"));
|
||||
assert!(is_nightly_version("140.0rc1"));
|
||||
assert!(!is_nightly_version("139.0"));
|
||||
assert!(!is_nightly_version("1.2.3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_zen_alpha_version() {
|
||||
// Only "twilight" should be considered alpha for Zen Browser
|
||||
assert!(is_zen_alpha_version("twilight"));
|
||||
assert!(is_zen_alpha_version("TWILIGHT")); // Case insensitive
|
||||
fn test_is_zen_nightly_version() {
|
||||
// Only "twilight" should be considered nightly for Zen Browser
|
||||
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 alpha for Zen Browser
|
||||
assert!(!is_zen_alpha_version("1.12.8b"));
|
||||
assert!(!is_zen_alpha_version("1.0.0b1"));
|
||||
assert!(!is_zen_alpha_version("2.0.0"));
|
||||
// Versions with "b" should NOT be considered nightly for Zen Browser
|
||||
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]
|
||||
@@ -1624,7 +1684,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/firefox.json"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(ResponseTemplate::new(404))
|
||||
.mount(&server)
|
||||
.await;
|
||||
@@ -1640,7 +1699,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/firefox.json"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string("invalid json")
|
||||
@@ -1660,7 +1718,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/zen-browser/desktop/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(ResponseTemplate::new(429).insert_header("retry-after", "60"))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
@@ -156,7 +156,7 @@ impl AppAutoUpdater {
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("User-Agent", "donutbrowser")
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -227,6 +227,7 @@ impl AppAutoUpdater {
|
||||
|
||||
/// Get the appropriate download URL for the current platform
|
||||
fn get_download_url_for_platform(&self, assets: &[AppReleaseAsset]) -> Option<String> {
|
||||
// Priority 1: Get architecture-specific binary for backward compatibility
|
||||
let arch = if cfg!(target_arch = "aarch64") {
|
||||
"aarch64"
|
||||
} else if cfg!(target_arch = "x86_64") {
|
||||
@@ -235,12 +236,9 @@ impl AppAutoUpdater {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
println!("Looking for assets with architecture: {arch}");
|
||||
for asset in assets {
|
||||
println!("Found asset: {}", asset.name);
|
||||
}
|
||||
println!("Falling back to architecture-specific search for: {arch}");
|
||||
|
||||
// Priority 1: Look for exact architecture match in DMG
|
||||
// Look for exact architecture match in DMG
|
||||
for asset in assets {
|
||||
if asset.name.contains(".dmg")
|
||||
&& (asset.name.contains(&format!("_{arch}.dmg"))
|
||||
@@ -253,7 +251,7 @@ impl AppAutoUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Look for x86_64 variations if we're looking for x64
|
||||
// Look for x86_64 variations if we're looking for x64
|
||||
if arch == "x64" {
|
||||
for asset in assets {
|
||||
if asset.name.contains(".dmg")
|
||||
@@ -265,7 +263,7 @@ impl AppAutoUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Look for arm64 variations if we're looking for aarch64
|
||||
// Look for arm64 variations if we're looking for aarch64
|
||||
if arch == "aarch64" {
|
||||
for asset in assets {
|
||||
if asset.name.contains(".dmg")
|
||||
@@ -277,7 +275,7 @@ impl AppAutoUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 4: 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")
|
||||
@@ -356,7 +354,7 @@ impl AppAutoUpdater {
|
||||
let response = self
|
||||
.client
|
||||
.get(download_url)
|
||||
.header("User-Agent", "donutbrowser")
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -390,7 +388,16 @@ impl AppAutoUpdater {
|
||||
.unwrap_or("");
|
||||
|
||||
match extension {
|
||||
"dmg" => extractor.extract_dmg(archive_path, dest_dir).await,
|
||||
"dmg" => {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
extractor.extract_dmg(archive_path, dest_dir).await
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
Err("DMG extraction is only supported on macOS".into())
|
||||
}
|
||||
}
|
||||
"zip" => extractor.extract_zip(archive_path, dest_dir).await,
|
||||
_ => Err(format!("Unsupported archive format: {extension}").into()),
|
||||
}
|
||||
@@ -535,14 +542,6 @@ pub async fn download_and_install_app_update(
|
||||
.map_err(|e| format!("Failed to install app update: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_app_version_info() -> Result<(String, bool), String> {
|
||||
Ok((
|
||||
AppAutoUpdater::get_current_version(),
|
||||
AppAutoUpdater::is_nightly_build(),
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_for_app_updates_manual() -> Result<Option<AppUpdateInfo>, String> {
|
||||
println!("Manual app update check triggered");
|
||||
@@ -656,9 +655,36 @@ mod tests {
|
||||
let url = updater.get_download_url_for_platform(&assets);
|
||||
assert!(url.is_some());
|
||||
|
||||
// The exact URL depends on the target architecture
|
||||
let url = url.unwrap();
|
||||
assert!(url.contains(".dmg"));
|
||||
// Test with generic macOS DMG (no architecture specified)
|
||||
let generic_assets = vec![AppReleaseAsset {
|
||||
name: "Donut.Browser_0.1.0_macos.dmg".to_string(),
|
||||
browser_download_url: "https://example.com/macos.dmg".to_string(),
|
||||
size: 12345,
|
||||
}];
|
||||
|
||||
let generic_url = updater.get_download_url_for_platform(&generic_assets);
|
||||
assert!(generic_url.is_some());
|
||||
assert_eq!(generic_url.unwrap(), "https://example.com/macos.dmg");
|
||||
|
||||
// Test architecture-specific DMG
|
||||
let arch_specific_assets = vec![
|
||||
AppReleaseAsset {
|
||||
name: "Donut.Browser_0.1.0_x64.dmg".to_string(),
|
||||
browser_download_url: "https://example.com/x64.dmg".to_string(),
|
||||
size: 12345,
|
||||
},
|
||||
AppReleaseAsset {
|
||||
name: "Donut.Browser_0.1.0_aarch64.dmg".to_string(),
|
||||
browser_download_url: "https://example.com/aarch64.dmg".to_string(),
|
||||
size: 12345,
|
||||
},
|
||||
];
|
||||
|
||||
let arch_url = updater.get_download_url_for_platform(&arch_specific_assets);
|
||||
assert!(arch_url.is_some());
|
||||
// The exact URL depends on the target architecture, but should be one of the available ones
|
||||
let arch_url = arch_url.unwrap();
|
||||
assert!(arch_url.contains(".dmg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -112,7 +112,7 @@ impl AutoUpdater {
|
||||
available_versions: &[BrowserVersionInfo],
|
||||
) -> Result<Option<UpdateNotification>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let current_version = &profile.version;
|
||||
let is_current_stable = !self.is_alpha_version(current_version);
|
||||
let is_current_stable = !self.is_nightly_version(current_version);
|
||||
|
||||
// Find the best available update
|
||||
let best_update = available_versions
|
||||
@@ -218,40 +218,6 @@ impl AutoUpdater {
|
||||
Ok(state.auto_update_downloads.contains(&download_key))
|
||||
}
|
||||
|
||||
/// Start browser update process
|
||||
pub async fn start_browser_update(
|
||||
&self,
|
||||
browser: &str,
|
||||
new_version: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Add browser to disabled list to prevent conflicts during update
|
||||
let mut state = self.load_auto_update_state()?;
|
||||
state.disabled_browsers.insert(browser.to_string());
|
||||
|
||||
// Mark this download as auto-update for toast suppression
|
||||
let download_key = format!("{browser}-{new_version}");
|
||||
state.auto_update_downloads.insert(download_key);
|
||||
|
||||
self.save_auto_update_state(&state)?;
|
||||
|
||||
// The actual download will be triggered by the frontend
|
||||
// This function now just marks the browser as updating to prevent conflicts
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Complete browser update process
|
||||
pub async fn complete_browser_update(
|
||||
&self,
|
||||
browser: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Remove browser from disabled list
|
||||
let mut state = self.load_auto_update_state()?;
|
||||
state.disabled_browsers.remove(browser);
|
||||
self.save_auto_update_state(&state)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Automatically update all affected profile versions after browser download
|
||||
pub async fn auto_update_profile_versions(
|
||||
&self,
|
||||
@@ -312,9 +278,51 @@ impl AutoUpdater {
|
||||
state.auto_update_downloads.remove(&download_key);
|
||||
self.save_auto_update_state(&state)?;
|
||||
|
||||
// Check if auto-delete of unused binaries is enabled and perform cleanup
|
||||
let settings = self
|
||||
.settings_manager
|
||||
.load_settings()
|
||||
.map_err(|e| format!("Failed to load settings: {e}"))?;
|
||||
if settings.auto_delete_unused_binaries {
|
||||
// Perform cleanup in the background - don't fail the update if cleanup fails
|
||||
if let Err(e) = self.cleanup_unused_binaries_internal() {
|
||||
eprintln!("Warning: Failed to cleanup unused binaries after auto-update: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(updated_profiles)
|
||||
}
|
||||
|
||||
/// Internal method to cleanup unused binaries (used by auto-cleanup)
|
||||
fn cleanup_unused_binaries_internal(
|
||||
&self,
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Load current profiles
|
||||
let profiles = self
|
||||
.browser_runner
|
||||
.list_profiles()
|
||||
.map_err(|e| format!("Failed to load profiles: {e}"))?;
|
||||
|
||||
// Load registry
|
||||
let mut registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::load()
|
||||
.map_err(|e| format!("Failed to load browser registry: {e}"))?;
|
||||
|
||||
// Get active browser versions
|
||||
let active_versions = registry.get_active_browser_versions(&profiles);
|
||||
|
||||
// Cleanup unused binaries
|
||||
let cleaned_up = registry
|
||||
.cleanup_unused_binaries(&active_versions)
|
||||
.map_err(|e| format!("Failed to cleanup unused binaries: {e}"))?;
|
||||
|
||||
// Save updated registry
|
||||
registry
|
||||
.save()
|
||||
.map_err(|e| format!("Failed to save registry: {e}"))?;
|
||||
|
||||
Ok(cleaned_up)
|
||||
}
|
||||
|
||||
/// Check if browser is disabled due to ongoing update
|
||||
pub fn is_browser_disabled(
|
||||
&self,
|
||||
@@ -337,13 +345,10 @@ impl AutoUpdater {
|
||||
|
||||
// Helper methods
|
||||
|
||||
fn is_alpha_version(&self, version: &str) -> bool {
|
||||
version.contains("alpha")
|
||||
|| version.contains("beta")
|
||||
|| version.contains("rc")
|
||||
|| version.contains("a")
|
||||
|| version.contains("b")
|
||||
|| version.contains("dev")
|
||||
fn is_nightly_version(&self, version: &str) -> bool {
|
||||
// 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 {
|
||||
@@ -414,24 +419,6 @@ pub async fn check_for_browser_updates() -> Result<Vec<UpdateNotification>, Stri
|
||||
Ok(grouped)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_browser_update(browser: String, new_version: String) -> Result<(), String> {
|
||||
let updater = AutoUpdater::new();
|
||||
updater
|
||||
.start_browser_update(&browser, &new_version)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to start browser update: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn complete_browser_update(browser: String) -> Result<(), String> {
|
||||
let updater = AutoUpdater::new();
|
||||
updater
|
||||
.complete_browser_update(&browser)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to complete browser update: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn is_browser_disabled_for_update(browser: String) -> Result<bool, String> {
|
||||
let updater = AutoUpdater::new();
|
||||
@@ -509,18 +496,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_alpha_version() {
|
||||
fn test_is_nightly_version() {
|
||||
let updater = AutoUpdater::new();
|
||||
|
||||
assert!(updater.is_alpha_version("1.0.0-alpha"));
|
||||
assert!(updater.is_alpha_version("1.0.0-beta"));
|
||||
assert!(updater.is_alpha_version("1.0.0-rc"));
|
||||
assert!(updater.is_alpha_version("1.0.0a1"));
|
||||
assert!(updater.is_alpha_version("1.0.0b1"));
|
||||
assert!(updater.is_alpha_version("1.0.0-dev"));
|
||||
assert!(updater.is_nightly_version("1.0.0-alpha"));
|
||||
assert!(updater.is_nightly_version("1.0.0-beta"));
|
||||
assert!(updater.is_nightly_version("1.0.0-rc"));
|
||||
assert!(updater.is_nightly_version("1.0.0a1"));
|
||||
assert!(updater.is_nightly_version("1.0.0b1"));
|
||||
assert!(updater.is_nightly_version("1.0.0-dev"));
|
||||
|
||||
assert!(!updater.is_alpha_version("1.0.0"));
|
||||
assert!(!updater.is_alpha_version("1.2.3"));
|
||||
assert!(!updater.is_nightly_version("1.0.0"));
|
||||
assert!(!updater.is_nightly_version("1.2.3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+526
-136
@@ -50,7 +50,6 @@ impl BrowserType {
|
||||
}
|
||||
|
||||
pub trait Browser: Send + Sync {
|
||||
fn browser_type(&self) -> BrowserType;
|
||||
fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>>;
|
||||
fn create_launch_args(
|
||||
&self,
|
||||
@@ -59,24 +58,17 @@ pub trait Browser: Send + Sync {
|
||||
url: Option<String>,
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error>>;
|
||||
fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool;
|
||||
fn prepare_executable(&self, executable_path: &Path) -> Result<(), Box<dyn std::error::Error>>;
|
||||
}
|
||||
|
||||
pub struct FirefoxBrowser {
|
||||
browser_type: BrowserType,
|
||||
}
|
||||
// Platform-specific modules
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos {
|
||||
use super::*;
|
||||
|
||||
impl FirefoxBrowser {
|
||||
pub fn new(browser_type: BrowserType) -> Self {
|
||||
Self { browser_type }
|
||||
}
|
||||
}
|
||||
|
||||
impl Browser for FirefoxBrowser {
|
||||
fn browser_type(&self) -> BrowserType {
|
||||
self.browser_type.clone()
|
||||
}
|
||||
|
||||
fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
pub fn get_firefox_executable_path(
|
||||
install_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// Find the .app directory
|
||||
let app_path = std::fs::read_dir(install_dir)?
|
||||
.filter_map(Result::ok)
|
||||
@@ -106,6 +98,441 @@ impl Browser for FirefoxBrowser {
|
||||
Ok(executable_path)
|
||||
}
|
||||
|
||||
pub fn get_chromium_executable_path(
|
||||
install_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// Find the .app directory
|
||||
let app_path = std::fs::read_dir(install_dir)?
|
||||
.filter_map(Result::ok)
|
||||
.find(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
|
||||
.ok_or("Browser app not found")?;
|
||||
|
||||
// Construct the browser executable path
|
||||
let mut executable_dir = app_path.path();
|
||||
executable_dir.push("Contents");
|
||||
executable_dir.push("MacOS");
|
||||
|
||||
// Find the first executable in the MacOS directory
|
||||
let executable_path = std::fs::read_dir(&executable_dir)?
|
||||
.filter_map(Result::ok)
|
||||
.find(|entry| {
|
||||
let binding = entry.file_name();
|
||||
let name = binding.to_string_lossy();
|
||||
name.contains("Chromium") || name.contains("Brave") || name.contains("Google Chrome")
|
||||
})
|
||||
.map(|entry| entry.path())
|
||||
.ok_or("No executable found in MacOS directory")?;
|
||||
|
||||
Ok(executable_path)
|
||||
}
|
||||
|
||||
pub fn is_firefox_version_downloaded(install_dir: &Path) -> bool {
|
||||
// On macOS, check for .app files
|
||||
if let Ok(entries) = std::fs::read_dir(install_dir) {
|
||||
for entry in entries.flatten() {
|
||||
if entry.path().extension().is_some_and(|ext| ext == "app") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_chromium_version_downloaded(install_dir: &Path) -> bool {
|
||||
// On macOS, check for .app files
|
||||
if let Ok(entries) = std::fs::read_dir(install_dir) {
|
||||
for entry in entries.flatten() {
|
||||
if entry.path().extension().is_some_and(|ext| ext == "app") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn prepare_executable(_executable_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// On macOS, no special preparation needed
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux {
|
||||
use super::*;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
pub fn get_firefox_executable_path(
|
||||
install_dir: &Path,
|
||||
browser_type: &BrowserType,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// Expected structure: install_dir/<browser>/<binary>
|
||||
let browser_subdir = install_dir.join(browser_type.as_str());
|
||||
|
||||
// Try firefox first (preferred), then firefox-bin
|
||||
let possible_executables = match browser_type {
|
||||
BrowserType::Firefox | BrowserType::FirefoxDeveloper => {
|
||||
vec![
|
||||
browser_subdir.join("firefox"),
|
||||
browser_subdir.join("firefox-bin"),
|
||||
]
|
||||
}
|
||||
BrowserType::MullvadBrowser => {
|
||||
vec![
|
||||
browser_subdir.join("firefox"),
|
||||
browser_subdir.join("mullvad-browser"),
|
||||
browser_subdir.join("firefox-bin"),
|
||||
]
|
||||
}
|
||||
BrowserType::Zen => {
|
||||
vec![browser_subdir.join("zen"), browser_subdir.join("zen-bin")]
|
||||
}
|
||||
BrowserType::TorBrowser => {
|
||||
vec![
|
||||
browser_subdir.join("firefox"),
|
||||
browser_subdir.join("tor-browser"),
|
||||
browser_subdir.join("firefox-bin"),
|
||||
]
|
||||
}
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
for executable_path in &possible_executables {
|
||||
if executable_path.exists() && executable_path.is_file() {
|
||||
return Ok(executable_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Err(
|
||||
format!(
|
||||
"Firefox executable not found in {}/{}",
|
||||
install_dir.display(),
|
||||
browser_type.as_str()
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_chromium_executable_path(
|
||||
install_dir: &Path,
|
||||
browser_type: &BrowserType,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// Expected structure: install_dir/<browser>/<binary>
|
||||
let browser_subdir = install_dir.join(browser_type.as_str());
|
||||
|
||||
let possible_executables = match browser_type {
|
||||
BrowserType::Chromium => vec![
|
||||
browser_subdir.join("chromium"),
|
||||
browser_subdir.join("chrome"),
|
||||
],
|
||||
BrowserType::Brave => vec![
|
||||
browser_subdir.join("brave"),
|
||||
browser_subdir.join("brave-browser"),
|
||||
],
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
for executable_path in &possible_executables {
|
||||
if executable_path.exists() && executable_path.is_file() {
|
||||
return Ok(executable_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Err(
|
||||
format!(
|
||||
"Chromium executable not found in {}/{}",
|
||||
install_dir.display(),
|
||||
browser_type.as_str()
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_firefox_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool {
|
||||
// Expected structure: install_dir/<browser>/<binary>
|
||||
let browser_subdir = install_dir.join(browser_type.as_str());
|
||||
|
||||
if !browser_subdir.exists() || !browser_subdir.is_dir() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let possible_executables = match browser_type {
|
||||
BrowserType::Firefox | BrowserType::FirefoxDeveloper => {
|
||||
vec![
|
||||
browser_subdir.join("firefox-bin"),
|
||||
browser_subdir.join("firefox"),
|
||||
]
|
||||
}
|
||||
BrowserType::MullvadBrowser => {
|
||||
vec![
|
||||
browser_subdir.join("mullvad-browser"),
|
||||
browser_subdir.join("firefox-bin"),
|
||||
browser_subdir.join("firefox"),
|
||||
]
|
||||
}
|
||||
BrowserType::Zen => {
|
||||
vec![browser_subdir.join("zen"), browser_subdir.join("zen-bin")]
|
||||
}
|
||||
BrowserType::TorBrowser => {
|
||||
vec![
|
||||
browser_subdir.join("tor-browser"),
|
||||
browser_subdir.join("firefox-bin"),
|
||||
browser_subdir.join("firefox"),
|
||||
]
|
||||
}
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
for exe_path in &possible_executables {
|
||||
if exe_path.exists() && exe_path.is_file() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool {
|
||||
// Expected structure: install_dir/<browser>/<binary>
|
||||
let browser_subdir = install_dir.join(browser_type.as_str());
|
||||
|
||||
if !browser_subdir.exists() || !browser_subdir.is_dir() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let possible_executables = match browser_type {
|
||||
BrowserType::Chromium => vec![
|
||||
browser_subdir.join("chromium"),
|
||||
browser_subdir.join("chrome"),
|
||||
],
|
||||
BrowserType::Brave => vec![
|
||||
browser_subdir.join("brave"),
|
||||
browser_subdir.join("brave-browser"),
|
||||
browser_subdir.join("brave-browser-nightly"),
|
||||
browser_subdir.join("brave-browser-beta"),
|
||||
],
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
for exe_path in &possible_executables {
|
||||
if exe_path.exists() && exe_path.is_file() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn prepare_executable(executable_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// On Linux, ensure the executable has proper permissions
|
||||
println!("Setting execute permissions for: {:?}", executable_path);
|
||||
|
||||
let metadata = std::fs::metadata(executable_path)?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
// Add execute permissions for owner, group, and others
|
||||
let mode = permissions.mode();
|
||||
permissions.set_mode(mode | 0o755);
|
||||
|
||||
std::fs::set_permissions(executable_path, permissions)?;
|
||||
|
||||
println!(
|
||||
"Execute permissions set successfully for: {:?}",
|
||||
executable_path
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows {
|
||||
use super::*;
|
||||
|
||||
pub fn get_firefox_executable_path(
|
||||
install_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// On Windows, look for firefox.exe
|
||||
let possible_paths = [
|
||||
install_dir.join("firefox.exe"),
|
||||
install_dir.join("firefox").join("firefox.exe"),
|
||||
install_dir.join("bin").join("firefox.exe"),
|
||||
];
|
||||
|
||||
for path in &possible_paths {
|
||||
if path.exists() && path.is_file() {
|
||||
return Ok(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Look for any .exe file that might be the browser
|
||||
if let Ok(entries) = std::fs::read_dir(install_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|ext| ext == "exe") {
|
||||
let name = path.file_stem().unwrap_or_default().to_string_lossy();
|
||||
if name.starts_with("firefox")
|
||||
|| name.starts_with("mullvad")
|
||||
|| name.starts_with("zen")
|
||||
|| name.starts_with("tor")
|
||||
|| name.contains("browser")
|
||||
{
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Firefox executable not found in Windows installation directory".into())
|
||||
}
|
||||
|
||||
pub fn get_chromium_executable_path(
|
||||
install_dir: &Path,
|
||||
browser_type: &BrowserType,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// On Windows, look for .exe files
|
||||
let possible_paths = match browser_type {
|
||||
BrowserType::Chromium => vec![
|
||||
install_dir.join("chromium.exe"),
|
||||
install_dir.join("chrome.exe"),
|
||||
install_dir.join("chromium-browser.exe"),
|
||||
install_dir.join("bin").join("chromium.exe"),
|
||||
],
|
||||
BrowserType::Brave => vec![
|
||||
install_dir.join("brave.exe"),
|
||||
install_dir.join("brave-browser.exe"),
|
||||
install_dir.join("bin").join("brave.exe"),
|
||||
],
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
for path in &possible_paths {
|
||||
if path.exists() && path.is_file() {
|
||||
return Ok(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Look for any .exe file that might be the browser
|
||||
if let Ok(entries) = std::fs::read_dir(install_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|ext| ext == "exe") {
|
||||
let name = path.file_stem().unwrap_or_default().to_string_lossy();
|
||||
if name.contains("chromium") || name.contains("brave") || name.contains("chrome") {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Chromium/Brave executable not found in Windows installation directory".into())
|
||||
}
|
||||
|
||||
pub fn is_firefox_version_downloaded(install_dir: &Path) -> bool {
|
||||
// On Windows, check for .exe files
|
||||
let possible_executables = [
|
||||
install_dir.join("firefox.exe"),
|
||||
install_dir.join("firefox").join("firefox.exe"),
|
||||
install_dir.join("bin").join("firefox.exe"),
|
||||
];
|
||||
|
||||
for exe_path in &possible_executables {
|
||||
if exe_path.exists() && exe_path.is_file() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any .exe file that looks like a browser
|
||||
if let Ok(entries) = std::fs::read_dir(install_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().is_some_and(|ext| ext == "exe") {
|
||||
let name = path.file_stem().unwrap_or_default().to_string_lossy();
|
||||
if name.starts_with("firefox")
|
||||
|| name.starts_with("mullvad")
|
||||
|| name.starts_with("zen")
|
||||
|| name.starts_with("tor")
|
||||
|| name.contains("browser")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool {
|
||||
// On Windows, check for .exe files
|
||||
let possible_executables = match browser_type {
|
||||
BrowserType::Chromium => vec![
|
||||
install_dir.join("chromium.exe"),
|
||||
install_dir.join("chrome.exe"),
|
||||
install_dir.join("chromium-browser.exe"),
|
||||
install_dir.join("bin").join("chromium.exe"),
|
||||
],
|
||||
BrowserType::Brave => vec![
|
||||
install_dir.join("brave.exe"),
|
||||
install_dir.join("brave-browser.exe"),
|
||||
install_dir.join("bin").join("brave.exe"),
|
||||
],
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
for exe_path in &possible_executables {
|
||||
if exe_path.exists() && exe_path.is_file() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any .exe file that looks like the browser
|
||||
if let Ok(entries) = std::fs::read_dir(install_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().is_some_and(|ext| ext == "exe") {
|
||||
let name = path.file_stem().unwrap_or_default().to_string_lossy();
|
||||
if name.contains("chromium") || name.contains("brave") || name.contains("chrome") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn prepare_executable(_executable_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// On Windows, no special preparation needed
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FirefoxBrowser {
|
||||
browser_type: BrowserType,
|
||||
}
|
||||
|
||||
impl FirefoxBrowser {
|
||||
pub fn new(browser_type: BrowserType) -> Self {
|
||||
Self { browser_type }
|
||||
}
|
||||
}
|
||||
|
||||
impl Browser for FirefoxBrowser {
|
||||
fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos::get_firefox_executable_path(install_dir);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux::get_firefox_executable_path(install_dir, &self.browser_type);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::get_firefox_executable_path(install_dir);
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
Err("Unsupported platform".into())
|
||||
}
|
||||
|
||||
fn create_launch_args(
|
||||
&self,
|
||||
profile_path: &str,
|
||||
@@ -135,34 +562,52 @@ impl Browser for FirefoxBrowser {
|
||||
}
|
||||
|
||||
fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool {
|
||||
let browser_dir = binaries_dir
|
||||
.join(self.browser_type().as_str())
|
||||
.join(version);
|
||||
// Expected structure: binaries/<browser>/<version>
|
||||
let browser_dir = binaries_dir.join(self.browser_type.as_str()).join(version);
|
||||
|
||||
println!("Firefox browser checking version {version} in directory: {browser_dir:?}");
|
||||
|
||||
// Only check if directory exists and contains a .app file
|
||||
if browser_dir.exists() {
|
||||
println!("Directory exists, checking for .app files...");
|
||||
if let Ok(entries) = std::fs::read_dir(&browser_dir) {
|
||||
for entry in entries.flatten() {
|
||||
println!(" Found entry: {:?}", entry.path());
|
||||
if entry.path().extension().is_some_and(|ext| ext == "app") {
|
||||
println!(" Found .app file: {:?}", entry.path());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("No .app files found in directory");
|
||||
} else {
|
||||
if !browser_dir.exists() {
|
||||
println!("Directory does not exist: {browser_dir:?}");
|
||||
return false;
|
||||
}
|
||||
false
|
||||
|
||||
println!("Directory exists, checking for browser files...");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos::is_firefox_version_downloaded(&browser_dir);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux::is_firefox_version_downloaded(&browser_dir, &self.browser_type);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::is_firefox_version_downloaded(&browser_dir);
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
println!("Unsupported platform for browser verification");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_executable(&self, executable_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos::prepare_executable(executable_path);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux::prepare_executable(executable_path);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::prepare_executable(executable_path);
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
Err("Unsupported platform".into())
|
||||
}
|
||||
}
|
||||
|
||||
// Chromium-based browsers (Chromium, Brave)
|
||||
pub struct ChromiumBrowser {
|
||||
#[allow(dead_code)]
|
||||
browser_type: BrowserType,
|
||||
}
|
||||
|
||||
@@ -173,34 +618,18 @@ impl ChromiumBrowser {
|
||||
}
|
||||
|
||||
impl Browser for ChromiumBrowser {
|
||||
fn browser_type(&self) -> BrowserType {
|
||||
self.browser_type.clone()
|
||||
}
|
||||
|
||||
fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// Find the .app directory
|
||||
let app_path = std::fs::read_dir(install_dir)?
|
||||
.filter_map(Result::ok)
|
||||
.find(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
|
||||
.ok_or("Browser app not found")?;
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos::get_chromium_executable_path(install_dir);
|
||||
|
||||
// Construct the browser executable path
|
||||
let mut executable_dir = app_path.path();
|
||||
executable_dir.push("Contents");
|
||||
executable_dir.push("MacOS");
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux::get_chromium_executable_path(install_dir, &self.browser_type);
|
||||
|
||||
// Find the first executable in the MacOS directory
|
||||
let executable_path = std::fs::read_dir(&executable_dir)?
|
||||
.filter_map(Result::ok)
|
||||
.find(|entry| {
|
||||
let binding = entry.file_name();
|
||||
let name = binding.to_string_lossy();
|
||||
name.contains("Chromium") || name.contains("Brave") || name.contains("Google Chrome")
|
||||
})
|
||||
.map(|entry| entry.path())
|
||||
.ok_or("No executable found in MacOS directory")?;
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::get_chromium_executable_path(install_dir, &self.browser_type);
|
||||
|
||||
Ok(executable_path)
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
Err("Unsupported platform".into())
|
||||
}
|
||||
|
||||
fn create_launch_args(
|
||||
@@ -240,35 +669,46 @@ impl Browser for ChromiumBrowser {
|
||||
}
|
||||
|
||||
fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool {
|
||||
let browser_dir = binaries_dir
|
||||
.join(self.browser_type().as_str())
|
||||
.join(version);
|
||||
// Expected structure: binaries/<browser>/<version>
|
||||
let browser_dir = binaries_dir.join(self.browser_type.as_str()).join(version);
|
||||
|
||||
println!("Chromium browser checking version {version} in directory: {browser_dir:?}");
|
||||
|
||||
// Check if directory exists and contains at least one .app file
|
||||
if browser_dir.exists() {
|
||||
println!("Directory exists, checking for .app files...");
|
||||
if let Ok(entries) = std::fs::read_dir(&browser_dir) {
|
||||
for entry in entries.flatten() {
|
||||
println!(" Found entry: {:?}", entry.path());
|
||||
if entry.path().extension().is_some_and(|ext| ext == "app") {
|
||||
println!(" Found .app file: {:?}", entry.path());
|
||||
// Try to get the executable path as a final verification
|
||||
if self.get_executable_path(&browser_dir).is_ok() {
|
||||
println!(" Executable path verification successful");
|
||||
return true;
|
||||
} else {
|
||||
println!(" Executable path verification failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("No valid .app files found in directory");
|
||||
} else {
|
||||
if !browser_dir.exists() {
|
||||
println!("Directory does not exist: {browser_dir:?}");
|
||||
return false;
|
||||
}
|
||||
false
|
||||
|
||||
println!("Directory exists, checking for browser files...");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos::is_chromium_version_downloaded(&browser_dir);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux::is_chromium_version_downloaded(&browser_dir, &self.browser_type);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::is_chromium_version_downloaded(&browser_dir, &self.browser_type);
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
println!("Unsupported platform for browser verification");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_executable(&self, executable_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos::prepare_executable(executable_path);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux::prepare_executable(executable_path);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::prepare_executable(executable_path);
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
Err("Unsupported platform".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +734,7 @@ pub struct GithubRelease {
|
||||
#[serde(default)]
|
||||
pub published_at: String,
|
||||
#[serde(default)]
|
||||
pub is_alpha: bool,
|
||||
pub is_nightly: bool,
|
||||
#[serde(default)]
|
||||
pub prerelease: bool,
|
||||
}
|
||||
@@ -354,56 +794,6 @@ mod tests {
|
||||
assert!(BrowserType::from_str("Firefox").is_err()); // Case sensitive
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_firefox_browser_creation() {
|
||||
let browser = FirefoxBrowser::new(BrowserType::Firefox);
|
||||
assert_eq!(browser.browser_type(), BrowserType::Firefox);
|
||||
|
||||
let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser);
|
||||
assert_eq!(browser.browser_type(), BrowserType::MullvadBrowser);
|
||||
|
||||
let browser = FirefoxBrowser::new(BrowserType::TorBrowser);
|
||||
assert_eq!(browser.browser_type(), BrowserType::TorBrowser);
|
||||
|
||||
let browser = FirefoxBrowser::new(BrowserType::Zen);
|
||||
assert_eq!(browser.browser_type(), BrowserType::Zen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chromium_browser_creation() {
|
||||
let browser = ChromiumBrowser::new(BrowserType::Chromium);
|
||||
assert_eq!(browser.browser_type(), BrowserType::Chromium);
|
||||
|
||||
let browser = ChromiumBrowser::new(BrowserType::Brave);
|
||||
assert_eq!(browser.browser_type(), BrowserType::Brave);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_browser_factory() {
|
||||
// Test Firefox-based browsers
|
||||
let browser = create_browser(BrowserType::Firefox);
|
||||
assert_eq!(browser.browser_type(), BrowserType::Firefox);
|
||||
|
||||
let browser = create_browser(BrowserType::MullvadBrowser);
|
||||
assert_eq!(browser.browser_type(), BrowserType::MullvadBrowser);
|
||||
|
||||
let browser = create_browser(BrowserType::Zen);
|
||||
assert_eq!(browser.browser_type(), BrowserType::Zen);
|
||||
|
||||
let browser = create_browser(BrowserType::TorBrowser);
|
||||
assert_eq!(browser.browser_type(), BrowserType::TorBrowser);
|
||||
|
||||
let browser = create_browser(BrowserType::FirefoxDeveloper);
|
||||
assert_eq!(browser.browser_type(), BrowserType::FirefoxDeveloper);
|
||||
|
||||
// Test Chromium-based browsers
|
||||
let browser = create_browser(BrowserType::Chromium);
|
||||
assert_eq!(browser.browser_type(), BrowserType::Chromium);
|
||||
|
||||
let browser = create_browser(BrowserType::Brave);
|
||||
assert_eq!(browser.browser_type(), BrowserType::Brave);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_firefox_launch_args() {
|
||||
// Test regular Firefox (should not use -no-remote)
|
||||
@@ -509,7 +899,7 @@ mod tests {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let binaries_dir = temp_dir.path();
|
||||
|
||||
// Create a mock Firefox browser installation
|
||||
// Create a mock Firefox browser installation with new path structure: binaries/<browser>/<version>/
|
||||
let browser_dir = binaries_dir.join("firefox").join("139.0");
|
||||
fs::create_dir_all(&browser_dir).unwrap();
|
||||
|
||||
@@ -521,7 +911,7 @@ mod tests {
|
||||
assert!(browser.is_version_downloaded("139.0", binaries_dir));
|
||||
assert!(!browser.is_version_downloaded("140.0", binaries_dir));
|
||||
|
||||
// Test with Chromium browser
|
||||
// Test with Chromium browser with new path structure
|
||||
let chromium_dir = binaries_dir.join("chromium").join("1465660");
|
||||
fs::create_dir_all(&chromium_dir).unwrap();
|
||||
let chromium_app_dir = chromium_dir.join("Chromium.app");
|
||||
@@ -544,7 +934,7 @@ mod tests {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let binaries_dir = temp_dir.path();
|
||||
|
||||
// Create browser directory but no .app directory
|
||||
// Create browser directory but no .app directory with new path structure
|
||||
let browser_dir = binaries_dir.join("firefox").join("139.0");
|
||||
fs::create_dir_all(&browser_dir).unwrap();
|
||||
|
||||
|
||||
+953
-697
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,70 @@ impl BrowserVersionService {
|
||||
Self { api_client }
|
||||
}
|
||||
|
||||
/// Check if a browser is supported on the current platform and architecture
|
||||
pub fn is_browser_supported(
|
||||
&self,
|
||||
browser: &str,
|
||||
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let (os, arch) = Self::get_platform_info();
|
||||
|
||||
match browser {
|
||||
"firefox" | "firefox-developer" => Ok(true),
|
||||
"mullvad-browser" => {
|
||||
// Mullvad doesn't support ARM64 on Windows and Linux
|
||||
if arch == "arm64" && (os == "windows" || os == "linux") {
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
"zen" => {
|
||||
// Zen supports all platforms and architectures
|
||||
Ok(true)
|
||||
}
|
||||
"brave" => {
|
||||
// Brave supports all platforms and architectures
|
||||
Ok(true)
|
||||
}
|
||||
"chromium" => {
|
||||
// Chromium doesn't support ARM64 on Linux
|
||||
if arch == "arm64" && os == "linux" {
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
"tor-browser" => {
|
||||
// TOR Browser doesn't support ARM64 on Windows and Linux
|
||||
if arch == "arm64" && (os == "windows" || os == "linux") {
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
_ => Err(format!("Unknown browser: {browser}").into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get list of browsers supported on the current platform
|
||||
pub fn get_supported_browsers(&self) -> Vec<String> {
|
||||
let all_browsers = vec![
|
||||
"firefox",
|
||||
"firefox-developer",
|
||||
"mullvad-browser",
|
||||
"zen",
|
||||
"brave",
|
||||
"chromium",
|
||||
"tor-browser",
|
||||
];
|
||||
|
||||
all_browsers
|
||||
.into_iter()
|
||||
.filter(|browser| self.is_browser_supported(browser).unwrap_or(false))
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get cached browser versions immediately (returns None if no cache exists)
|
||||
pub fn get_cached_browser_versions(&self, browser: &str) -> Option<Vec<String>> {
|
||||
self.api_client.load_cached_versions(browser)
|
||||
@@ -58,7 +122,7 @@ impl BrowserVersionService {
|
||||
.map(|version| {
|
||||
BrowserVersionInfo {
|
||||
version: version.clone(),
|
||||
is_prerelease: crate::api_client::is_alpha_version(&version),
|
||||
is_prerelease: crate::api_client::is_browser_version_nightly(browser, &version, None),
|
||||
date: "".to_string(), // Cache doesn't store dates
|
||||
}
|
||||
})
|
||||
@@ -176,7 +240,9 @@ impl BrowserVersionService {
|
||||
} else {
|
||||
BrowserVersionInfo {
|
||||
version: version.clone(),
|
||||
is_prerelease: crate::api_client::is_alpha_version(&version),
|
||||
is_prerelease: crate::api_client::is_browser_version_nightly(
|
||||
"firefox", &version, None,
|
||||
),
|
||||
date: "".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -197,7 +263,11 @@ impl BrowserVersionService {
|
||||
} else {
|
||||
BrowserVersionInfo {
|
||||
version: version.clone(),
|
||||
is_prerelease: crate::api_client::is_alpha_version(&version),
|
||||
is_prerelease: crate::api_client::is_browser_version_nightly(
|
||||
"firefox-developer",
|
||||
&version,
|
||||
None,
|
||||
),
|
||||
date: "".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -212,7 +282,7 @@ impl BrowserVersionService {
|
||||
if let Some(release) = releases.iter().find(|r| r.tag_name == version) {
|
||||
BrowserVersionInfo {
|
||||
version: release.tag_name.clone(),
|
||||
is_prerelease: release.is_alpha,
|
||||
is_prerelease: release.is_nightly,
|
||||
date: release.published_at.clone(),
|
||||
}
|
||||
} else {
|
||||
@@ -233,13 +303,13 @@ impl BrowserVersionService {
|
||||
if let Some(release) = releases.iter().find(|r| r.tag_name == version) {
|
||||
BrowserVersionInfo {
|
||||
version: release.tag_name.clone(),
|
||||
is_prerelease: release.prerelease,
|
||||
is_prerelease: release.is_nightly,
|
||||
date: release.published_at.clone(),
|
||||
}
|
||||
} else {
|
||||
BrowserVersionInfo {
|
||||
version: version.clone(),
|
||||
is_prerelease: version.contains("alpha") || version.contains("beta"),
|
||||
is_prerelease: crate::api_client::is_browser_version_nightly("zen", &version, None),
|
||||
date: "".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -254,13 +324,15 @@ impl BrowserVersionService {
|
||||
if let Some(release) = releases.iter().find(|r| r.tag_name == version) {
|
||||
BrowserVersionInfo {
|
||||
version: release.tag_name.clone(),
|
||||
is_prerelease: release.prerelease,
|
||||
is_prerelease: release.is_nightly,
|
||||
date: release.published_at.clone(),
|
||||
}
|
||||
} 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(),
|
||||
}
|
||||
}
|
||||
@@ -281,7 +353,7 @@ impl BrowserVersionService {
|
||||
} else {
|
||||
BrowserVersionInfo {
|
||||
version: version.clone(),
|
||||
is_prerelease: false, // Chromium versions are usually stable
|
||||
is_prerelease: false, // Chromium usually stable releases
|
||||
date: "".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -296,13 +368,17 @@ impl BrowserVersionService {
|
||||
if let Some(release) = releases.iter().find(|r| r.version == version) {
|
||||
BrowserVersionInfo {
|
||||
version: release.version.clone(),
|
||||
is_prerelease: release.is_prerelease,
|
||||
is_prerelease: crate::api_client::is_browser_version_nightly(
|
||||
"tor-browser",
|
||||
&release.version,
|
||||
None,
|
||||
),
|
||||
date: release.date.clone(),
|
||||
}
|
||||
} else {
|
||||
BrowserVersionInfo {
|
||||
version: version.clone(),
|
||||
is_prerelease: version.contains("alpha") || version.contains("rc"),
|
||||
is_prerelease: false, // TOR Browser usually stable releases
|
||||
date: "".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -355,61 +431,256 @@ impl BrowserVersionService {
|
||||
browser: &str,
|
||||
version: &str,
|
||||
) -> Result<DownloadInfo, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let (os, arch) = Self::get_platform_info();
|
||||
|
||||
match browser {
|
||||
"firefox" => Ok(DownloadInfo {
|
||||
url: format!("https://download.mozilla.org/?product=firefox-{version}&os=osx&lang=en-US"),
|
||||
filename: format!("firefox-{version}.dmg"),
|
||||
is_archive: true,
|
||||
}),
|
||||
"firefox-developer" => Ok(DownloadInfo {
|
||||
url: format!("https://download.mozilla.org/?product=devedition-{version}&os=osx&lang=en-US"),
|
||||
filename: format!("firefox-developer-{version}.dmg"),
|
||||
is_archive: true,
|
||||
}),
|
||||
"mullvad-browser" => Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://github.com/mullvad/mullvad-browser/releases/download/{version}/mullvad-browser-macos-{version}.dmg"
|
||||
),
|
||||
filename: format!("mullvad-browser-{version}.dmg"),
|
||||
is_archive: true,
|
||||
}),
|
||||
"zen" => Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://github.com/zen-browser/desktop/releases/download/{version}/zen.macos-universal.dmg"
|
||||
),
|
||||
filename: format!("zen-{version}.dmg"),
|
||||
is_archive: true,
|
||||
}),
|
||||
"brave" => {
|
||||
// For Brave, we use a placeholder URL since we need to resolve the actual asset URL dynamically
|
||||
// The actual URL will be resolved in the download service using the GitHub API
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://github.com/brave/brave-browser/releases/download/{version}/Brave-Browser-universal.dmg"
|
||||
),
|
||||
filename: format!("brave-{version}.dmg"),
|
||||
is_archive: true,
|
||||
})
|
||||
}
|
||||
"chromium" => {
|
||||
let arch = if cfg!(target_arch = "aarch64") { "Mac_Arm" } else { "Mac" };
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://commondatastorage.googleapis.com/chromium-browser-snapshots/{arch}/{version}/chrome-mac.zip"
|
||||
),
|
||||
filename: format!("chromium-{version}.zip"),
|
||||
is_archive: true,
|
||||
})
|
||||
}
|
||||
"tor-browser" => Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://archive.torproject.org/tor-package-archive/torbrowser/{version}/tor-browser-macos-{version}.dmg"
|
||||
),
|
||||
filename: format!("tor-browser-{version}.dmg"),
|
||||
is_archive: true,
|
||||
}),
|
||||
_ => Err(format!("Unsupported browser: {browser}").into()),
|
||||
"firefox" => {
|
||||
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.xz"), true),
|
||||
("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.xz"), true),
|
||||
("macos", _) => ("mac", format!("Firefox {version}.dmg"), true),
|
||||
_ => {
|
||||
return Err(
|
||||
format!("Unsupported platform/architecture for Firefox: {os}/{arch}").into(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://download-installer.cdn.mozilla.net/pub/firefox/releases/{version}/{platform_path}/en-US/{filename}"
|
||||
),
|
||||
filename,
|
||||
is_archive,
|
||||
})
|
||||
}
|
||||
"firefox-developer" => {
|
||||
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.xz"), true),
|
||||
("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.xz"), true),
|
||||
("macos", _) => ("mac", format!("Firefox {version}.dmg"), true),
|
||||
_ => {
|
||||
return Err(
|
||||
format!("Unsupported platform/architecture for Firefox Developer: {os}/{arch}")
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://download-installer.cdn.mozilla.net/pub/devedition/releases/{version}/{platform_path}/en-US/{filename}"
|
||||
),
|
||||
filename,
|
||||
is_archive,
|
||||
})
|
||||
}
|
||||
"mullvad-browser" => {
|
||||
// Mullvad Browser doesn't support ARM64 on Windows and Linux
|
||||
if arch == "arm64" && (os == "windows" || os == "linux") {
|
||||
return Err(format!("Mullvad Browser doesn't support ARM64 on {os}").into());
|
||||
}
|
||||
|
||||
let (platform_str, filename, is_archive) = match os.as_str() {
|
||||
"windows" => {
|
||||
if arch == "arm64" {
|
||||
return Err("Mullvad Browser doesn't support ARM64 on Windows".into());
|
||||
}
|
||||
(
|
||||
"windows-x86_64",
|
||||
format!("mullvad-browser-windows-x86_64-{version}.exe"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
"linux" => {
|
||||
if arch == "arm64" {
|
||||
return Err("Mullvad Browser doesn't support ARM64 on Linux".into());
|
||||
}
|
||||
(
|
||||
"x86_64",
|
||||
format!("mullvad-browser-x86_64-{version}.tar.xz"),
|
||||
true,
|
||||
)
|
||||
}
|
||||
"macos" => (
|
||||
"macos",
|
||||
format!("mullvad-browser-macos-{version}.dmg"),
|
||||
true,
|
||||
),
|
||||
_ => return Err(format!("Unsupported platform for Mullvad Browser: {os}").into()),
|
||||
};
|
||||
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://github.com/mullvad/mullvad-browser/releases/download/{version}/mullvad-browser-{platform_str}-{version}{}",
|
||||
if os == "windows" { ".exe" } else if os == "linux" { ".tar.xz" } else { ".dmg" }
|
||||
),
|
||||
filename,
|
||||
is_archive,
|
||||
})
|
||||
}
|
||||
"zen" => {
|
||||
let (asset_name, filename, is_archive) = match (&os[..], &arch[..]) {
|
||||
("windows", "x64") => ("zen.installer.exe", format!("zen-{version}.exe"), false),
|
||||
("windows", "arm64") => (
|
||||
"zen.installer-arm64.exe",
|
||||
format!("zen-{version}-arm64.exe"),
|
||||
false,
|
||||
),
|
||||
("linux", "x64") => (
|
||||
"zen.linux-x86_64.tar.xz",
|
||||
format!("zen-{version}-x86_64.tar.xz"),
|
||||
true,
|
||||
),
|
||||
("linux", "arm64") => (
|
||||
"zen.linux-aarch64.tar.xz",
|
||||
format!("zen-{version}-aarch64.tar.xz"),
|
||||
true,
|
||||
),
|
||||
("macos", _) => (
|
||||
"zen.macos-universal.dmg",
|
||||
format!("zen-{version}.dmg"),
|
||||
true,
|
||||
),
|
||||
_ => {
|
||||
return Err(format!("Unsupported platform/architecture for Zen: {os}/{arch}").into())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://github.com/zen-browser/desktop/releases/download/{version}/{asset_name}"
|
||||
),
|
||||
filename,
|
||||
is_archive,
|
||||
})
|
||||
}
|
||||
"brave" => {
|
||||
let (filename, is_archive) = match (&os[..], &arch[..]) {
|
||||
("windows", _) => (format!("brave-{version}.exe"), false),
|
||||
("linux", "x64") => (format!("brave-browser-{version}-linux-amd64.zip"), true),
|
||||
("linux", "arm64") => (format!("brave-browser-{version}-linux-arm64.zip"), true),
|
||||
("macos", _) => ("Brave-Browser-universal.dmg".to_string(), true),
|
||||
_ => {
|
||||
return Err(format!("Unsupported platform/architecture for Brave: {os}/{arch}").into())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://github.com/brave/brave-browser/releases/download/{version}/{filename}"
|
||||
),
|
||||
filename,
|
||||
is_archive,
|
||||
})
|
||||
}
|
||||
"chromium" => {
|
||||
let platform_str = match (&os[..], &arch[..]) {
|
||||
("windows", "x64") => "Win_x64",
|
||||
("windows", "arm64") => "Win_Arm64",
|
||||
("linux", "x64") => "Linux_x64",
|
||||
("linux", "arm64") => return Err("Chromium doesn't support ARM64 on Linux".into()),
|
||||
("macos", "x64") => "Mac",
|
||||
("macos", "arm64") => "Mac_Arm",
|
||||
_ => {
|
||||
return Err(
|
||||
format!("Unsupported platform/architecture for Chromium: {os}/{arch}").into(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let (archive_name, filename) = match os.as_str() {
|
||||
"windows" => ("chrome-win.zip", format!("chromium-{version}-win.zip")),
|
||||
"linux" => ("chrome-linux.zip", format!("chromium-{version}-linux.zip")),
|
||||
"macos" => ("chrome-mac.zip", format!("chromium-{version}-mac.zip")),
|
||||
_ => return Err(format!("Unsupported platform for Chromium: {os}").into()),
|
||||
};
|
||||
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://commondatastorage.googleapis.com/chromium-browser-snapshots/{platform_str}/{version}/{archive_name}"
|
||||
),
|
||||
filename,
|
||||
is_archive: true,
|
||||
})
|
||||
}
|
||||
"tor-browser" => {
|
||||
// TOR Browser doesn't support ARM64 on Windows and Linux
|
||||
if arch == "arm64" && (os == "windows" || os == "linux") {
|
||||
return Err(format!("TOR Browser doesn't support ARM64 on {os}").into());
|
||||
}
|
||||
|
||||
let (platform_str, filename, is_archive) = match os.as_str() {
|
||||
"windows" => {
|
||||
if arch == "arm64" {
|
||||
return Err("TOR Browser doesn't support ARM64 on Windows".into());
|
||||
}
|
||||
(
|
||||
"windows-x86_64-portable",
|
||||
format!("tor-browser-windows-x86_64-portable-{version}.exe"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
"linux" => {
|
||||
if arch == "arm64" {
|
||||
return Err("TOR Browser doesn't support ARM64 on Linux".into());
|
||||
}
|
||||
(
|
||||
"linux-x86_64",
|
||||
format!("tor-browser-linux-x86_64-{version}.tar.xz"),
|
||||
true,
|
||||
)
|
||||
}
|
||||
"macos" => ("macos", format!("tor-browser-macos-{version}.dmg"), true),
|
||||
_ => return Err(format!("Unsupported platform for TOR Browser: {os}").into()),
|
||||
};
|
||||
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://archive.torproject.org/tor-package-archive/torbrowser/{version}/tor-browser-{platform_str}-{version}{}",
|
||||
if os == "windows" { ".exe" } else if os == "linux" { ".tar.xz" } else { ".dmg" }
|
||||
),
|
||||
filename,
|
||||
is_archive,
|
||||
})
|
||||
}
|
||||
_ => Err(format!("Unsupported browser: {browser}").into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get platform and architecture information
|
||||
fn get_platform_info() -> (String, String) {
|
||||
let os = if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
let arch = if cfg!(target_arch = "x86_64") {
|
||||
"x64"
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
"arm64"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
(os.to_string(), arch.to_string())
|
||||
}
|
||||
|
||||
// Private helper methods for each browser type
|
||||
@@ -546,7 +817,7 @@ impl BrowserVersionService {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wiremock::matchers::{header, method, path};
|
||||
use wiremock::matchers::{method, path, query_param};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
async fn setup_mock_server() -> MockServer {
|
||||
@@ -561,7 +832,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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -604,7 +874,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/firefox.json"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -640,7 +909,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/devedition.json"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -682,7 +950,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/mullvad/mullvad-browser/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -724,7 +992,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/zen-browser/desktop/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -737,21 +1005,31 @@ mod tests {
|
||||
async fn setup_brave_mocks(server: &MockServer) {
|
||||
let mock_response = r#"[
|
||||
{
|
||||
"tag_name": "v1.81.9",
|
||||
"name": "Brave Release 1.81.9",
|
||||
"tag_name": "v1.79.119",
|
||||
"name": "Release v1.79.119 (Chromium 137.0.7151.68)",
|
||||
"prerelease": false,
|
||||
"published_at": "2024-01-15T10:00:00Z",
|
||||
"assets": [
|
||||
{
|
||||
"name": "brave-v1.81.9-universal.dmg",
|
||||
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
|
||||
"name": "brave-v1.79.119-universal.dmg",
|
||||
"browser_download_url": "https://example.com/brave-1.79.119-universal.dmg",
|
||||
"size": 200000000
|
||||
},
|
||||
{
|
||||
"name": "brave-browser-1.79.119-linux-amd64.zip",
|
||||
"browser_download_url": "https://example.com/brave-browser-1.79.119-linux-amd64.zip",
|
||||
"size": 150000000
|
||||
},
|
||||
{
|
||||
"name": "brave-browser-1.79.119-linux-arm64.zip",
|
||||
"browser_download_url": "https://example.com/brave-browser-1.79.119-linux-arm64.zip",
|
||||
"size": 145000000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag_name": "v1.81.8",
|
||||
"name": "Brave Release 1.81.8",
|
||||
"name": "Nightly v1.81.8",
|
||||
"prerelease": false,
|
||||
"published_at": "2024-01-10T10:00:00Z",
|
||||
"assets": [
|
||||
@@ -766,7 +1044,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/brave/brave-browser/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -785,7 +1063,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!("/{arch}/LAST_CHANGE")))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string("1465660")
|
||||
@@ -833,7 +1110,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_html)
|
||||
@@ -844,7 +1120,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/14.0.4/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(version_html_144)
|
||||
@@ -855,7 +1130,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/14.0.3/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(version_html_143)
|
||||
@@ -866,7 +1140,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/14.0.2/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(version_html_142)
|
||||
@@ -878,7 +1151,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_browser_version_service_creation() {
|
||||
let _service = BrowserVersionService::new();
|
||||
let _ = BrowserVersionService::new();
|
||||
// Test passes if we can create the service without panicking
|
||||
}
|
||||
|
||||
@@ -1200,23 +1473,31 @@ 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
|
||||
let mullvad_info = service
|
||||
.get_download_info("mullvad-browser", "14.5a6")
|
||||
.unwrap();
|
||||
assert_eq!(mullvad_info.filename, "mullvad-browser-14.5a6.dmg");
|
||||
assert_eq!(mullvad_info.filename, "mullvad-browser-macos-14.5a6.dmg");
|
||||
assert!(mullvad_info.url.contains("mullvad-browser-macos-14.5a6"));
|
||||
assert!(mullvad_info.is_archive);
|
||||
|
||||
@@ -1228,20 +1509,20 @@ mod tests {
|
||||
|
||||
// Test Tor Browser
|
||||
let tor_info = service.get_download_info("tor-browser", "14.0.4").unwrap();
|
||||
assert_eq!(tor_info.filename, "tor-browser-14.0.4.dmg");
|
||||
assert_eq!(tor_info.filename, "tor-browser-macos-14.0.4.dmg");
|
||||
assert!(tor_info.url.contains("tor-browser-macos-14.0.4"));
|
||||
assert!(tor_info.is_archive);
|
||||
|
||||
// Test Chromium
|
||||
let chromium_info = service.get_download_info("chromium", "1465660").unwrap();
|
||||
assert_eq!(chromium_info.filename, "chromium-1465660.zip");
|
||||
assert_eq!(chromium_info.filename, "chromium-1465660-mac.zip");
|
||||
assert!(chromium_info.url.contains("chrome-mac.zip"));
|
||||
assert!(chromium_info.is_archive);
|
||||
|
||||
// Test Brave
|
||||
// Test Brave - Note: Brave uses dynamic URL resolution, so get_download_info provides a template URL
|
||||
let brave_info = service.get_download_info("brave", "v1.81.9").unwrap();
|
||||
assert_eq!(brave_info.filename, "brave-v1.81.9.dmg");
|
||||
assert!(brave_info.url.contains("Brave-Browser"));
|
||||
assert_eq!(brave_info.filename, "Brave-Browser-universal.dmg");
|
||||
assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/Brave-Browser-universal.dmg");
|
||||
assert!(brave_info.is_archive);
|
||||
|
||||
// Test unsupported browser
|
||||
|
||||
@@ -77,13 +77,139 @@ mod windows {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux {
|
||||
use std::process::Command;
|
||||
|
||||
const APP_DESKTOP_NAME: &str = "donutbrowser.desktop";
|
||||
|
||||
pub fn is_default_browser() -> Result<bool, String> {
|
||||
// Linux implementation would go here
|
||||
Err("Linux support not implemented yet".to_string())
|
||||
// Check if xdg-mime is available
|
||||
if !is_xdg_mime_available() {
|
||||
return Err("xdg-mime utility not found. Please install xdg-utils package.".to_string());
|
||||
}
|
||||
|
||||
let schemes = ["http", "https"];
|
||||
|
||||
for scheme in schemes {
|
||||
let mime_type = format!("x-scheme-handler/{}", scheme);
|
||||
|
||||
// Query the current default handler for this scheme
|
||||
let output = Command::new("xdg-mime")
|
||||
.args(["query", "default", &mime_type])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to query default handler for {}: {}", scheme, e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("xdg-mime query failed for {}: {}", scheme, stderr));
|
||||
}
|
||||
|
||||
let current_handler = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
|
||||
// Check if our app is the default handler
|
||||
if current_handler != APP_DESKTOP_NAME {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn set_as_default_browser() -> Result<(), String> {
|
||||
Err("Linux support not implemented yet".to_string())
|
||||
// Check if xdg-mime is available
|
||||
if !is_xdg_mime_available() {
|
||||
return Err("xdg-mime utility not found. Please install xdg-utils package.".to_string());
|
||||
}
|
||||
|
||||
// Check if the desktop file exists in common locations
|
||||
if !check_desktop_file_exists() {
|
||||
return Err(format!(
|
||||
"Desktop file '{}' not found in standard locations. Please ensure the application is properly installed. You can manually set Donut Browser as the default browser in your system settings.",
|
||||
APP_DESKTOP_NAME
|
||||
));
|
||||
}
|
||||
|
||||
let schemes = ["http", "https"];
|
||||
let mut all_succeeded = true;
|
||||
let mut error_messages = Vec::new();
|
||||
|
||||
for scheme in schemes {
|
||||
let mime_type = format!("x-scheme-handler/{}", scheme);
|
||||
|
||||
// Set our app as the default handler for this scheme
|
||||
let output = Command::new("xdg-mime")
|
||||
.args(["default", APP_DESKTOP_NAME, &mime_type])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to set default handler for {}: {}", scheme, e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
all_succeeded = false;
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
error_messages.push(format!("Failed to set default for {}: {}", scheme, stderr));
|
||||
}
|
||||
}
|
||||
|
||||
if !all_succeeded {
|
||||
return Err(format!(
|
||||
"Some xdg-mime commands failed:\n{}\n\nYou may need to:\n1. Run with appropriate permissions\n2. Manually set the default browser in your desktop environment settings\n3. Restart your desktop session",
|
||||
error_messages.join("\n")
|
||||
));
|
||||
}
|
||||
|
||||
// Give the system a moment to process the changes
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
|
||||
// Verify the changes took effect
|
||||
match is_default_browser() {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => {
|
||||
// This is the common case where commands succeed but verification fails
|
||||
Err(format!(
|
||||
"The xdg-mime commands completed successfully, but Donut Browser is not yet set as the default. This is common on some Linux distributions. Please try one of these options:\n\n1. Restart your desktop session and try again\n2. Log out and log back in\n3. Manually set Donut Browser as the default in your system settings:\n - GNOME: Settings > Default Applications > Web\n - KDE: System Settings > Applications > Default Applications > Web Browser\n - XFCE: Settings > Preferred Applications > Web Browser\n - Or run: xdg-settings set default-web-browser {}\n\nThe changes may take effect automatically after a desktop restart.",
|
||||
APP_DESKTOP_NAME
|
||||
))
|
||||
}
|
||||
Err(e) => Err(format!(
|
||||
"Set as default completed, but verification failed: {}. The changes may still be in effect after restarting your desktop session.",
|
||||
e
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_xdg_mime_available() -> bool {
|
||||
Command::new("which")
|
||||
.arg("xdg-mime")
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn check_desktop_file_exists() -> bool {
|
||||
let desktop_locations = [
|
||||
"~/.local/share/applications/",
|
||||
"/usr/share/applications/",
|
||||
"/usr/local/share/applications/",
|
||||
"/var/lib/flatpak/exports/share/applications/",
|
||||
"~/.local/share/flatpak/exports/share/applications/",
|
||||
];
|
||||
|
||||
for location in &desktop_locations {
|
||||
let path = if location.starts_with('~') {
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
location.replace('~', &home)
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
location.to_string()
|
||||
};
|
||||
|
||||
let full_path = format!("{}{}", path, APP_DESKTOP_NAME);
|
||||
if std::path::Path::new(&full_path).exists() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+204
-38
@@ -51,7 +51,7 @@ impl Downloader {
|
||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
match browser_type {
|
||||
BrowserType::Brave => {
|
||||
// For Brave, we need to find the actual macOS asset
|
||||
// For Brave, we need to find the actual platform-specific asset
|
||||
let releases = self
|
||||
.api_client
|
||||
.fetch_brave_releases_with_caching(true)
|
||||
@@ -65,19 +65,20 @@ impl Downloader {
|
||||
})
|
||||
.ok_or(format!("Brave version {version} not found"))?;
|
||||
|
||||
// Find the universal macOS DMG asset
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name.contains(".dmg") && asset.name.contains("universal"))
|
||||
// Get platform and architecture info
|
||||
let (os, arch) = Self::get_platform_info();
|
||||
|
||||
// Find the appropriate asset based on platform and architecture
|
||||
let asset_url = self
|
||||
.find_brave_asset(&release.assets, &os, &arch)
|
||||
.ok_or(format!(
|
||||
"No universal macOS DMG asset found for Brave version {version}"
|
||||
"No compatible asset found for Brave version {version} on {os}/{arch}"
|
||||
))?;
|
||||
|
||||
Ok(asset.browser_download_url.clone())
|
||||
Ok(asset_url)
|
||||
}
|
||||
BrowserType::Zen => {
|
||||
// For Zen, verify the asset exists
|
||||
// For Zen, verify the asset exists and handle different naming patterns
|
||||
let releases = self
|
||||
.api_client
|
||||
.fetch_zen_releases_with_caching(true)
|
||||
@@ -88,16 +89,17 @@ impl Downloader {
|
||||
.find(|r| r.tag_name == version)
|
||||
.ok_or(format!("Zen version {version} not found"))?;
|
||||
|
||||
// Find the macOS universal DMG asset
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == "zen.macos-universal.dmg")
|
||||
// Get platform and architecture info
|
||||
let (os, arch) = Self::get_platform_info();
|
||||
|
||||
// Find the appropriate asset
|
||||
let asset_url = self
|
||||
.find_zen_asset(&release.assets, &os, &arch)
|
||||
.ok_or(format!(
|
||||
"No macOS universal asset found for Zen version {version}"
|
||||
"No compatible asset found for Zen version {version} on {os}/{arch}"
|
||||
))?;
|
||||
|
||||
Ok(asset.browser_download_url.clone())
|
||||
Ok(asset_url)
|
||||
}
|
||||
BrowserType::MullvadBrowser => {
|
||||
// For Mullvad, verify the asset exists
|
||||
@@ -111,16 +113,17 @@ impl Downloader {
|
||||
.find(|r| r.tag_name == version)
|
||||
.ok_or(format!("Mullvad version {version} not found"))?;
|
||||
|
||||
// Find the macOS DMG asset
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name.contains(".dmg") && asset.name.contains("mac"))
|
||||
// Get platform and architecture info
|
||||
let (os, arch) = Self::get_platform_info();
|
||||
|
||||
// Find the appropriate asset
|
||||
let asset_url = self
|
||||
.find_mullvad_asset(&release.assets, &os, &arch)
|
||||
.ok_or(format!(
|
||||
"No macOS asset found for Mullvad version {version}"
|
||||
"No compatible asset found for Mullvad version {version} on {os}/{arch}"
|
||||
))?;
|
||||
|
||||
Ok(asset.browser_download_url.clone())
|
||||
Ok(asset_url)
|
||||
}
|
||||
_ => {
|
||||
// For other browsers, use the provided URL
|
||||
@@ -129,6 +132,173 @@ impl Downloader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get platform and architecture information
|
||||
fn get_platform_info() -> (String, String) {
|
||||
let os = if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
let arch = if cfg!(target_arch = "x86_64") {
|
||||
"x64"
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
"arm64"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
(os.to_string(), arch.to_string())
|
||||
}
|
||||
|
||||
/// Find the appropriate Brave asset for the current platform and architecture
|
||||
fn find_brave_asset(
|
||||
&self,
|
||||
assets: &[crate::browser::GithubAsset],
|
||||
os: &str,
|
||||
arch: &str,
|
||||
) -> Option<String> {
|
||||
// Brave asset naming patterns:
|
||||
// Windows: BraveBrowserStandaloneNightlySetup.exe, BraveBrowserStandaloneSilentNightlySetup.exe
|
||||
// macOS: Brave-Browser-Nightly-universal.dmg, Brave-Browser-Nightly-universal.pkg
|
||||
// Linux: brave-browser-1.79.119-linux-arm64.zip, brave-browser-1.79.119-linux-amd64.zip
|
||||
|
||||
let asset = match os {
|
||||
"windows" => {
|
||||
// For Windows, look for standalone setup EXE (not the auto-updater one)
|
||||
assets
|
||||
.iter()
|
||||
.find(|asset| {
|
||||
let name = asset.name.to_lowercase();
|
||||
name.contains("standalone") && name.ends_with(".exe") && !name.contains("silent")
|
||||
})
|
||||
.or_else(|| {
|
||||
// Fallback to any EXE if standalone not found
|
||||
assets.iter().find(|asset| asset.name.ends_with(".exe"))
|
||||
})
|
||||
}
|
||||
"macos" => {
|
||||
// For macOS, prefer universal DMG
|
||||
assets
|
||||
.iter()
|
||||
.find(|asset| {
|
||||
let name = asset.name.to_lowercase();
|
||||
name.contains("universal") && name.ends_with(".dmg")
|
||||
})
|
||||
.or_else(|| {
|
||||
// Fallback to any DMG
|
||||
assets.iter().find(|asset| asset.name.ends_with(".dmg"))
|
||||
})
|
||||
}
|
||||
"linux" => {
|
||||
// For Linux, be strict about architecture matching - same logic as has_compatible_brave_asset
|
||||
let arch_pattern = if arch == "arm64" { "arm64" } else { "amd64" };
|
||||
|
||||
assets.iter().find(|asset| {
|
||||
let name = asset.name.to_lowercase();
|
||||
name.contains("linux") && name.contains(arch_pattern) && name.ends_with(".zip")
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
asset.map(|a| a.browser_download_url.clone())
|
||||
}
|
||||
|
||||
/// Find the appropriate Zen asset for the current platform and architecture
|
||||
fn find_zen_asset(
|
||||
&self,
|
||||
assets: &[crate::browser::GithubAsset],
|
||||
os: &str,
|
||||
arch: &str,
|
||||
) -> Option<String> {
|
||||
// Zen asset naming patterns:
|
||||
// Windows: zen.installer.exe, zen.installer-arm64.exe
|
||||
// macOS: zen.macos-universal.dmg
|
||||
// Linux: zen.linux-x86_64.tar.xz, zen.linux-aarch64.tar.xz, zen-x86_64.AppImage, zen-aarch64.AppImage
|
||||
|
||||
let asset = match (os, arch) {
|
||||
("windows", "x64") => assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == "zen.installer.exe"),
|
||||
("windows", "arm64") => assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == "zen.installer-arm64.exe"),
|
||||
("macos", _) => assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == "zen.macos-universal.dmg"),
|
||||
("linux", "x64") => {
|
||||
// Prefer tar.xz, fallback to AppImage
|
||||
assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == "zen.linux-x86_64.tar.xz")
|
||||
.or_else(|| {
|
||||
assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == "zen-x86_64.AppImage")
|
||||
})
|
||||
}
|
||||
("linux", "arm64") => {
|
||||
// Prefer tar.xz, fallback to AppImage
|
||||
assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == "zen.linux-aarch64.tar.xz")
|
||||
.or_else(|| {
|
||||
assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == "zen-aarch64.AppImage")
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
asset.map(|a| a.browser_download_url.clone())
|
||||
}
|
||||
|
||||
/// Find the appropriate Mullvad asset for the current platform and architecture
|
||||
fn find_mullvad_asset(
|
||||
&self,
|
||||
assets: &[crate::browser::GithubAsset],
|
||||
os: &str,
|
||||
arch: &str,
|
||||
) -> Option<String> {
|
||||
// Mullvad asset naming patterns:
|
||||
// Windows: mullvad-browser-windows-x86_64-VERSION.exe
|
||||
// macOS: mullvad-browser-macos-VERSION.dmg
|
||||
// Linux: mullvad-browser-x86_64-VERSION.tar.xz
|
||||
|
||||
let asset = match (os, arch) {
|
||||
("windows", "x64") => assets.iter().find(|asset| {
|
||||
asset.name.contains("windows")
|
||||
&& asset.name.contains("x86_64")
|
||||
&& asset.name.ends_with(".exe")
|
||||
}),
|
||||
("windows", "arm64") => {
|
||||
// Mullvad doesn't support ARM64 on Windows
|
||||
None
|
||||
}
|
||||
("macos", _) => assets
|
||||
.iter()
|
||||
.find(|asset| asset.name.contains("macos") && asset.name.ends_with(".dmg")),
|
||||
("linux", "x64") => assets.iter().find(|asset| {
|
||||
asset.name.contains("x86_64")
|
||||
&& asset.name.ends_with(".tar.xz")
|
||||
&& !asset.name.contains("windows")
|
||||
}),
|
||||
("linux", "arm64") => {
|
||||
// Mullvad doesn't support ARM64 on Linux
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
asset.map(|a| a.browser_download_url.clone())
|
||||
}
|
||||
|
||||
pub async fn download_browser<R: tauri::Runtime>(
|
||||
&self,
|
||||
app_handle: &tauri::AppHandle<R>,
|
||||
@@ -170,7 +340,7 @@ impl Downloader {
|
||||
let response = self
|
||||
.client
|
||||
.get(&download_url)
|
||||
.header("User-Agent", "donutbrowser")
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -247,7 +417,7 @@ mod tests {
|
||||
use crate::browser_version_service::DownloadInfo;
|
||||
|
||||
use tempfile::TempDir;
|
||||
use wiremock::matchers::{header, method, path};
|
||||
use wiremock::matchers::{method, path, query_param};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
async fn setup_mock_server() -> MockServer {
|
||||
@@ -262,7 +432,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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -290,7 +459,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/brave/brave-browser/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -338,7 +507,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/zen-browser/desktop/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -386,7 +555,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/mullvad/mullvad-browser/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -497,7 +666,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/brave/brave-browser/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -547,7 +716,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/zen-browser/desktop/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -570,7 +739,7 @@ mod tests {
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("No macOS universal asset found"));
|
||||
.contains("No compatible asset found"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -589,7 +758,6 @@ mod tests {
|
||||
// Mock the download endpoint
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/test-download"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_bytes(test_content)
|
||||
@@ -640,7 +808,6 @@ mod tests {
|
||||
// Mock a 404 response
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/missing-file"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(ResponseTemplate::new(404))
|
||||
.mount(&server)
|
||||
.await;
|
||||
@@ -691,7 +858,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/mullvad/mullvad-browser/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -714,7 +881,7 @@ mod tests {
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("No macOS asset found"));
|
||||
.contains("No compatible asset found"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -741,7 +908,7 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/brave/brave-browser/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
@@ -780,7 +947,6 @@ mod tests {
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/chunked-download"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_bytes(test_content.clone())
|
||||
|
||||
@@ -175,6 +175,48 @@ impl DownloadedBrowsersRegistry {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find and remove unused browser binaries that are not referenced by any active profiles
|
||||
pub fn cleanup_unused_binaries(
|
||||
&mut self,
|
||||
active_profiles: &[(String, String)], // (browser, version) pairs
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
||||
let active_set: std::collections::HashSet<(String, String)> =
|
||||
active_profiles.iter().cloned().collect();
|
||||
let mut cleaned_up = Vec::new();
|
||||
|
||||
// Collect all downloaded browsers that are not in active profiles
|
||||
let mut to_remove = Vec::new();
|
||||
for (browser, versions) in &self.browsers {
|
||||
for (version, info) in versions {
|
||||
if info.verified && !active_set.contains(&(browser.clone(), version.clone())) {
|
||||
to_remove.push((browser.clone(), version.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove unused binaries
|
||||
for (browser, version) in to_remove {
|
||||
if let Err(e) = self.cleanup_failed_download(&browser, &version) {
|
||||
eprintln!("Failed to cleanup unused binary {browser}:{version}: {e}");
|
||||
} else {
|
||||
cleaned_up.push(format!("{browser} {version}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cleaned_up)
|
||||
}
|
||||
|
||||
/// Get all browsers and versions referenced by active profiles
|
||||
pub fn get_active_browser_versions(
|
||||
&self,
|
||||
profiles: &[crate::browser_runner::BrowserProfile],
|
||||
) -> Vec<(String, String)> {
|
||||
profiles
|
||||
.iter()
|
||||
.map(|profile| (profile.browser.clone(), profile.version.clone()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+1237
-153
File diff suppressed because it is too large
Load Diff
+248
-37
@@ -1,7 +1,6 @@
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{Emitter, Manager};
|
||||
use tauri::{Emitter, Manager, Runtime, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
// Store pending URLs that need to be handled when the window is ready
|
||||
@@ -17,25 +16,26 @@ mod default_browser;
|
||||
mod download;
|
||||
mod downloaded_browsers;
|
||||
mod extraction;
|
||||
mod profile_importer;
|
||||
mod proxy_manager;
|
||||
mod settings_manager;
|
||||
mod theme_detector;
|
||||
mod version_updater;
|
||||
|
||||
extern crate lazy_static;
|
||||
|
||||
use browser_runner::{
|
||||
check_browser_exists, check_browser_status, create_browser_profile, create_browser_profile_new,
|
||||
delete_profile, download_browser, fetch_browser_versions, fetch_browser_versions_cached_first,
|
||||
fetch_browser_versions_detailed, fetch_browser_versions_with_count,
|
||||
fetch_browser_versions_with_count_cached_first, get_cached_browser_versions_detailed,
|
||||
get_downloaded_browser_versions, get_saved_mullvad_releases, get_supported_browsers,
|
||||
is_browser_downloaded, kill_browser_profile, launch_browser_profile, list_browser_profiles,
|
||||
rename_profile, should_update_browser_cache, update_profile_proxy, update_profile_version,
|
||||
check_browser_exists, check_browser_status, 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::{
|
||||
disable_default_browser_prompt, get_app_settings, get_table_sorting_settings, save_app_settings,
|
||||
save_table_sorting_settings, should_show_settings_on_startup,
|
||||
clear_all_version_cache_and_refetch, get_app_settings, get_table_sorting_settings,
|
||||
save_app_settings, save_table_sorting_settings, should_show_settings_on_startup,
|
||||
};
|
||||
|
||||
use default_browser::{
|
||||
@@ -43,26 +43,66 @@ use default_browser::{
|
||||
};
|
||||
|
||||
use version_updater::{
|
||||
check_version_update_needed, force_version_update_check, get_version_update_status,
|
||||
get_version_updater, trigger_manual_version_update,
|
||||
get_version_update_status, get_version_updater, trigger_manual_version_update,
|
||||
};
|
||||
|
||||
use auto_updater::{
|
||||
check_for_browser_updates, complete_browser_update, complete_browser_update_with_auto_update,
|
||||
dismiss_update_notification, is_auto_update_download, is_browser_disabled_for_update,
|
||||
mark_auto_update_download, remove_auto_update_download, start_browser_update,
|
||||
check_for_browser_updates, complete_browser_update_with_auto_update, dismiss_update_notification,
|
||||
is_auto_update_download, is_browser_disabled_for_update, mark_auto_update_download,
|
||||
remove_auto_update_download,
|
||||
};
|
||||
|
||||
use app_auto_updater::{
|
||||
check_for_app_updates, check_for_app_updates_manual, download_and_install_app_update,
|
||||
get_app_version_info,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
fn greet() -> String {
|
||||
let now = SystemTime::now();
|
||||
let epoch_ms = now.duration_since(UNIX_EPOCH).unwrap().as_millis();
|
||||
format!("Hello world from Rust! Current epoch: {epoch_ms}")
|
||||
use profile_importer::{detect_existing_profiles, import_browser_profile};
|
||||
|
||||
use theme_detector::get_system_theme;
|
||||
|
||||
// Trait to extend WebviewWindow with transparent titlebar functionality
|
||||
pub trait WindowExt {
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_transparent_titlebar(&self, transparent: bool) -> Result<(), String>;
|
||||
}
|
||||
|
||||
impl<R: Runtime> WindowExt for WebviewWindow<R> {
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_transparent_titlebar(&self, transparent: bool) -> Result<(), String> {
|
||||
use objc2::rc::Retained;
|
||||
use objc2_app_kit::{NSWindow, NSWindowStyleMask, NSWindowTitleVisibility};
|
||||
|
||||
unsafe {
|
||||
let ns_window: Retained<NSWindow> =
|
||||
Retained::retain(self.ns_window().unwrap().cast()).unwrap();
|
||||
|
||||
if transparent {
|
||||
// Hide the title text
|
||||
ns_window.setTitleVisibility(NSWindowTitleVisibility(2)); // NSWindowTitleHidden
|
||||
|
||||
// Make titlebar transparent
|
||||
ns_window.setTitlebarAppearsTransparent(true);
|
||||
|
||||
// Set full size content view
|
||||
let current_mask = ns_window.styleMask();
|
||||
let new_mask = NSWindowStyleMask(current_mask.0 | (1 << 15)); // NSFullSizeContentViewWindowMask
|
||||
ns_window.setStyleMask(new_mask);
|
||||
} else {
|
||||
// Show the title text
|
||||
ns_window.setTitleVisibility(NSWindowTitleVisibility(0)); // NSWindowTitleVisible
|
||||
|
||||
// Make titlebar opaque
|
||||
ns_window.setTitlebarAppearsTransparent(false);
|
||||
|
||||
// Remove full size content view
|
||||
let current_mask = ns_window.styleMask();
|
||||
let new_mask = NSWindowStyleMask(current_mask.0 & !(1 << 15));
|
||||
ns_window.setStyleMask(new_mask);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -131,7 +171,27 @@ 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)]
|
||||
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
|
||||
.title("Donut Browser")
|
||||
.inner_size(900.0, 600.0)
|
||||
.resizable(false)
|
||||
.fullscreen(false);
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let window = win_builder.build().unwrap();
|
||||
|
||||
// Set transparent titlebar for macOS
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Err(e) = window.set_transparent_titlebar(true) {
|
||||
eprintln!("Failed to set transparent titlebar: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Set up deep link handler
|
||||
let handle = app.handle().clone();
|
||||
|
||||
@@ -211,25 +271,18 @@ pub fn run() {
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
greet,
|
||||
get_supported_browsers,
|
||||
is_browser_supported_on_platform,
|
||||
download_browser,
|
||||
delete_profile,
|
||||
is_browser_downloaded,
|
||||
check_browser_exists,
|
||||
create_browser_profile_new,
|
||||
create_browser_profile,
|
||||
list_browser_profiles,
|
||||
launch_browser_profile,
|
||||
fetch_browser_versions,
|
||||
fetch_browser_versions_detailed,
|
||||
fetch_browser_versions_with_count,
|
||||
fetch_browser_versions_cached_first,
|
||||
fetch_browser_versions_with_count_cached_first,
|
||||
get_cached_browser_versions_detailed,
|
||||
should_update_browser_cache,
|
||||
get_downloaded_browser_versions,
|
||||
get_saved_mullvad_releases,
|
||||
update_profile_proxy,
|
||||
update_profile_version,
|
||||
check_browser_status,
|
||||
@@ -238,22 +291,17 @@ pub fn run() {
|
||||
get_app_settings,
|
||||
save_app_settings,
|
||||
should_show_settings_on_startup,
|
||||
disable_default_browser_prompt,
|
||||
get_table_sorting_settings,
|
||||
save_table_sorting_settings,
|
||||
clear_all_version_cache_and_refetch,
|
||||
is_default_browser,
|
||||
open_url_with_profile,
|
||||
set_as_default_browser,
|
||||
smart_open_url,
|
||||
handle_url_open,
|
||||
check_and_handle_startup_url,
|
||||
trigger_manual_version_update,
|
||||
get_version_update_status,
|
||||
check_version_update_needed,
|
||||
force_version_update_check,
|
||||
check_for_browser_updates,
|
||||
start_browser_update,
|
||||
complete_browser_update,
|
||||
is_browser_disabled_for_update,
|
||||
dismiss_update_notification,
|
||||
complete_browser_update_with_auto_update,
|
||||
@@ -263,8 +311,171 @@ pub fn run() {
|
||||
check_for_app_updates,
|
||||
check_for_app_updates_manual,
|
||||
download_and_install_app_update,
|
||||
get_app_version_info,
|
||||
get_system_theme,
|
||||
detect_existing_profiles,
|
||||
import_browser_profile,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn test_no_unused_tauri_commands() {
|
||||
check_unused_commands(false); // Run in strict mode for CI
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unused_tauri_commands_detailed() {
|
||||
check_unused_commands(true); // Run in verbose mode for development
|
||||
}
|
||||
|
||||
fn check_unused_commands(verbose: bool) {
|
||||
// Extract command names from the generate_handler! macro in this file
|
||||
let lib_rs_content = fs::read_to_string("src/lib.rs").expect("Failed to read lib.rs");
|
||||
let commands = extract_tauri_commands(&lib_rs_content);
|
||||
|
||||
// Get all frontend files
|
||||
let frontend_files = get_frontend_files("../src");
|
||||
|
||||
// Check which commands are actually used
|
||||
let mut unused_commands = Vec::new();
|
||||
let mut used_commands = Vec::new();
|
||||
|
||||
for command in &commands {
|
||||
let mut is_used = false;
|
||||
|
||||
for file_content in &frontend_files {
|
||||
// More comprehensive search for command usage
|
||||
if is_command_used(file_content, command) {
|
||||
is_used = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if is_used {
|
||||
used_commands.push(command.clone());
|
||||
if verbose {
|
||||
println!("✅ {command}");
|
||||
}
|
||||
} else {
|
||||
unused_commands.push(command.clone());
|
||||
if verbose {
|
||||
println!("❌ {command} (UNUSED)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if verbose {
|
||||
println!("\n📊 Summary:");
|
||||
println!(" ✅ Used commands: {}", used_commands.len());
|
||||
println!(" ❌ Unused commands: {}", unused_commands.len());
|
||||
}
|
||||
|
||||
if !unused_commands.is_empty() {
|
||||
let message = format!(
|
||||
"Found {} unused Tauri commands: {}\n\nThese commands are exported in generate_handler! but not used in the frontend.\nConsider removing them or add them to the allowlist if they're used elsewhere.\n\nRun `pnpm check-unused-commands` for detailed analysis.",
|
||||
unused_commands.len(),
|
||||
unused_commands.join(", ")
|
||||
);
|
||||
|
||||
if verbose {
|
||||
println!("\n🚨 {message}");
|
||||
} else {
|
||||
panic!("{}", message);
|
||||
}
|
||||
} else if verbose {
|
||||
println!("\n🎉 All exported commands are being used!");
|
||||
} else {
|
||||
println!(
|
||||
"✅ All {} exported Tauri commands are being used in the frontend",
|
||||
commands.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_command_used(content: &str, command: &str) -> bool {
|
||||
// Check various patterns for invoke usage
|
||||
let patterns = vec![
|
||||
format!("invoke<{}>(\"{}\"", "", command), // invoke<Type>("command"
|
||||
format!("invoke(\"{}\"", command), // invoke("command"
|
||||
format!("invoke<{}>(\"{}\",", "", command), // invoke<Type>("command",
|
||||
format!("invoke(\"{}\",", command), // invoke("command",
|
||||
format!("\"{}\"", command), // Just the command name in quotes
|
||||
];
|
||||
|
||||
for pattern in patterns {
|
||||
if content.contains(&pattern) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for the command name appearing after "invoke" within a reasonable distance
|
||||
if let Some(invoke_pos) = content.find("invoke") {
|
||||
let after_invoke = &content[invoke_pos..];
|
||||
if let Some(cmd_pos) = after_invoke.find(&format!("\"{command}\"")) {
|
||||
// If the command appears within 100 characters of "invoke", consider it used
|
||||
if cmd_pos < 100 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn extract_tauri_commands(content: &str) -> Vec<String> {
|
||||
let mut commands = Vec::new();
|
||||
|
||||
// Find the generate_handler! macro
|
||||
if let Some(start) = content.find("tauri::generate_handler![") {
|
||||
if let Some(end) = content[start..].find("])") {
|
||||
let handler_content = &content[start + 25..start + end]; // Skip "tauri::generate_handler!["
|
||||
|
||||
// Extract command names
|
||||
for line in handler_content.lines() {
|
||||
let line = line.trim();
|
||||
if !line.is_empty() && !line.starts_with("//") {
|
||||
// Remove trailing comma and whitespace
|
||||
let command = line.trim_end_matches(',').trim();
|
||||
if !command.is_empty() {
|
||||
commands.push(command.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commands
|
||||
}
|
||||
|
||||
fn get_frontend_files(src_dir: &str) -> Vec<String> {
|
||||
let mut files_content = Vec::new();
|
||||
|
||||
if let Ok(entries) = fs::read_dir(src_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
// Recursively read subdirectories
|
||||
let subdir_files = get_frontend_files(&path.to_string_lossy());
|
||||
files_content.extend(subdir_files);
|
||||
} else if let Some(extension) = path.extension() {
|
||||
if matches!(
|
||||
extension.to_str(),
|
||||
Some("ts") | Some("tsx") | Some("js") | Some("jsx")
|
||||
) {
|
||||
if let Ok(content) = fs::read_to_string(&path) {
|
||||
files_content.push(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
files_content
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"))
|
||||
}
|
||||
@@ -3,6 +3,9 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fs::{self, create_dir_all};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::api_client::ApiClient;
|
||||
use crate::browser_version_service::BrowserVersionService;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct TableSortingSettings {
|
||||
pub column: String, // Column to sort by: "name", "browser", "status"
|
||||
@@ -28,6 +31,8 @@ pub struct AppSettings {
|
||||
pub theme: String, // "light", "dark", or "system"
|
||||
#[serde(default = "default_auto_updates_enabled")]
|
||||
pub auto_updates_enabled: bool,
|
||||
#[serde(default = "default_auto_delete_unused_binaries")]
|
||||
pub auto_delete_unused_binaries: bool,
|
||||
}
|
||||
|
||||
fn default_show_settings_on_startup() -> bool {
|
||||
@@ -42,6 +47,10 @@ fn default_auto_updates_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_auto_delete_unused_binaries() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for AppSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -49,6 +58,7 @@ impl Default for AppSettings {
|
||||
show_settings_on_startup: default_show_settings_on_startup(),
|
||||
theme: default_theme(),
|
||||
auto_updates_enabled: default_auto_updates_enabled(),
|
||||
auto_delete_unused_binaries: default_auto_delete_unused_binaries(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,13 +173,6 @@ impl SettingsManager {
|
||||
// 3. User hasn't explicitly disabled the default browser setting
|
||||
Ok(settings.show_settings_on_startup && !settings.set_as_default_browser)
|
||||
}
|
||||
|
||||
pub fn disable_default_browser_prompt(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut settings = self.load_settings()?;
|
||||
settings.show_settings_on_startup = false;
|
||||
self.save_settings(&settings)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -196,14 +199,6 @@ pub async fn should_show_settings_on_startup() -> Result<bool, String> {
|
||||
.map_err(|e| format!("Failed to check prompt setting: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn disable_default_browser_prompt() -> Result<(), String> {
|
||||
let manager = SettingsManager::new();
|
||||
manager
|
||||
.disable_default_browser_prompt()
|
||||
.map_err(|e| format!("Failed to disable prompt: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_table_sorting_settings() -> Result<TableSortingSettings, String> {
|
||||
let manager = SettingsManager::new();
|
||||
@@ -219,3 +214,33 @@ pub async fn save_table_sorting_settings(sorting: TableSortingSettings) -> Resul
|
||||
.save_table_sorting(&sorting)
|
||||
.map_err(|e| format!("Failed to save table sorting settings: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clear_all_version_cache_and_refetch() -> 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}"))?;
|
||||
|
||||
// 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(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,539 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct SystemTheme {
|
||||
pub theme: String, // "light", "dark", or "unknown"
|
||||
}
|
||||
|
||||
pub struct ThemeDetector;
|
||||
|
||||
impl ThemeDetector {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Detect the system theme preference
|
||||
pub fn detect_system_theme(&self) -> SystemTheme {
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux::detect_system_theme();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos::detect_system_theme();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::detect_system_theme();
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
|
||||
return SystemTheme {
|
||||
theme: "unknown".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux {
|
||||
use super::*;
|
||||
|
||||
pub fn detect_system_theme() -> SystemTheme {
|
||||
// Try multiple methods in order of preference
|
||||
|
||||
// 1. Try GNOME/GTK settings via gsettings
|
||||
if let Ok(theme) = detect_gnome_theme() {
|
||||
return SystemTheme { theme };
|
||||
}
|
||||
|
||||
// 2. Try KDE Plasma settings via kreadconfig5/kreadconfig6
|
||||
if let Ok(theme) = detect_kde_theme() {
|
||||
return SystemTheme { theme };
|
||||
}
|
||||
|
||||
// 3. Try XFCE settings via xfconf-query
|
||||
if let Ok(theme) = detect_xfce_theme() {
|
||||
return SystemTheme { theme };
|
||||
}
|
||||
|
||||
// 4. Try looking at current GTK theme name
|
||||
if let Ok(theme) = detect_gtk_theme() {
|
||||
return SystemTheme { theme };
|
||||
}
|
||||
|
||||
// 5. Try dconf directly (fallback for GNOME-based systems)
|
||||
if let Ok(theme) = detect_dconf_theme() {
|
||||
return SystemTheme { theme };
|
||||
}
|
||||
|
||||
// 6. Try environment variables
|
||||
if let Ok(theme) = detect_env_theme() {
|
||||
return SystemTheme { theme };
|
||||
}
|
||||
|
||||
// 7. Try freedesktop portal
|
||||
if let Ok(theme) = detect_portal_theme() {
|
||||
return SystemTheme { theme };
|
||||
}
|
||||
|
||||
// 8. Try looking at system color scheme files
|
||||
if let Ok(theme) = detect_system_files_theme() {
|
||||
return SystemTheme { theme };
|
||||
}
|
||||
|
||||
// Fallback to unknown
|
||||
SystemTheme {
|
||||
theme: "unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_gnome_theme() -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Check if gsettings is available
|
||||
if !is_command_available("gsettings") {
|
||||
return Err("gsettings not available".into());
|
||||
}
|
||||
|
||||
// Try GNOME color scheme first (modern way)
|
||||
if let Ok(output) = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.desktop.interface", "color-scheme"])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let scheme = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
match scheme.as_str() {
|
||||
"'prefer-dark'" => return Ok("dark".to_string()),
|
||||
"'prefer-light'" => return Ok("light".to_string()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to GTK theme name detection
|
||||
if let Ok(output) = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.desktop.interface", "gtk-theme"])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let theme_name = String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.trim_matches('\'')
|
||||
.to_lowercase();
|
||||
|
||||
if theme_name.contains("dark") || theme_name.contains("night") {
|
||||
return Ok("dark".to_string());
|
||||
} else if theme_name.contains("light") || theme_name.contains("adwaita") {
|
||||
return Ok("light".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not detect GNOME theme".into())
|
||||
}
|
||||
|
||||
fn detect_kde_theme() -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Try KDE Plasma 6 first
|
||||
if is_command_available("kreadconfig6") {
|
||||
if let Ok(output) = Command::new("kreadconfig6")
|
||||
.args([
|
||||
"--file",
|
||||
"kdeglobals",
|
||||
"--group",
|
||||
"KDE",
|
||||
"--key",
|
||||
"LookAndFeelPackage",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let theme = String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
if theme.contains("dark") || theme.contains("breezedark") {
|
||||
return Ok("dark".to_string());
|
||||
} else if theme.contains("light") || theme.contains("breeze") {
|
||||
return Ok("light".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try color scheme as well
|
||||
if let Ok(output) = Command::new("kreadconfig6")
|
||||
.args([
|
||||
"--file",
|
||||
"kdeglobals",
|
||||
"--group",
|
||||
"General",
|
||||
"--key",
|
||||
"ColorScheme",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let scheme = String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
if scheme.contains("dark") || scheme.contains("breezedark") {
|
||||
return Ok("dark".to_string());
|
||||
} else if scheme.contains("light") || scheme.contains("breeze") {
|
||||
return Ok("light".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try KDE Plasma 5 as fallback
|
||||
if is_command_available("kreadconfig5") {
|
||||
if let Ok(output) = Command::new("kreadconfig5")
|
||||
.args([
|
||||
"--file",
|
||||
"kdeglobals",
|
||||
"--group",
|
||||
"KDE",
|
||||
"--key",
|
||||
"LookAndFeelPackage",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let theme = String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
if theme.contains("dark") || theme.contains("breezedark") {
|
||||
return Ok("dark".to_string());
|
||||
} else if theme.contains("light") || theme.contains("breeze") {
|
||||
return Ok("light".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not detect KDE theme".into())
|
||||
}
|
||||
|
||||
fn detect_xfce_theme() -> Result<String, Box<dyn std::error::Error>> {
|
||||
if !is_command_available("xfconf-query") {
|
||||
return Err("xfconf-query not available".into());
|
||||
}
|
||||
|
||||
// Check XFCE theme
|
||||
if let Ok(output) = Command::new("xfconf-query")
|
||||
.args(["-c", "xsettings", "-p", "/Net/ThemeName"])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let theme = String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
if theme.contains("dark") || theme.contains("night") {
|
||||
return Ok("dark".to_string());
|
||||
} else if theme.contains("light") {
|
||||
return Ok("light".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check XFCE window manager theme as backup
|
||||
if let Ok(output) = Command::new("xfconf-query")
|
||||
.args(["-c", "xfwm4", "-p", "/general/theme"])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let theme = String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
if theme.contains("dark") || theme.contains("night") {
|
||||
return Ok("dark".to_string());
|
||||
} else if theme.contains("light") {
|
||||
return Ok("light".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not detect XFCE theme".into())
|
||||
}
|
||||
|
||||
fn detect_gtk_theme() -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Try to read GTK3 settings file
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
let gtk3_settings = std::path::Path::new(&home).join(".config/gtk-3.0/settings.ini");
|
||||
if gtk3_settings.exists() {
|
||||
if let Ok(content) = std::fs::read_to_string(gtk3_settings) {
|
||||
for line in content.lines() {
|
||||
if line.starts_with("gtk-theme-name=") {
|
||||
let theme_name = line.split('=').nth(1).unwrap_or("").trim().to_lowercase();
|
||||
if theme_name.contains("dark") || theme_name.contains("night") {
|
||||
return Ok("dark".to_string());
|
||||
} else if theme_name.contains("light") || theme_name.contains("adwaita") {
|
||||
return Ok("light".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try GTK4 settings
|
||||
let gtk4_settings = std::path::Path::new(&home).join(".config/gtk-4.0/settings.ini");
|
||||
if gtk4_settings.exists() {
|
||||
if let Ok(content) = std::fs::read_to_string(gtk4_settings) {
|
||||
for line in content.lines() {
|
||||
if line.starts_with("gtk-theme-name=") {
|
||||
let theme_name = line.split('=').nth(1).unwrap_or("").trim().to_lowercase();
|
||||
if theme_name.contains("dark") || theme_name.contains("night") {
|
||||
return Ok("dark".to_string());
|
||||
} else if theme_name.contains("light") || theme_name.contains("adwaita") {
|
||||
return Ok("light".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not detect GTK theme".into())
|
||||
}
|
||||
|
||||
fn detect_dconf_theme() -> Result<String, Box<dyn std::error::Error>> {
|
||||
if !is_command_available("dconf") {
|
||||
return Err("dconf not available".into());
|
||||
}
|
||||
|
||||
// Try reading color scheme directly from dconf
|
||||
if let Ok(output) = Command::new("dconf")
|
||||
.args(["read", "/org/gnome/desktop/interface/color-scheme"])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let scheme = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
match scheme.as_str() {
|
||||
"'prefer-dark'" => return Ok("dark".to_string()),
|
||||
"'prefer-light'" => return Ok("light".to_string()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try reading GTK theme from dconf
|
||||
if let Ok(output) = Command::new("dconf")
|
||||
.args(["read", "/org/gnome/desktop/interface/gtk-theme"])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let theme_name = String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.trim_matches('\'')
|
||||
.to_lowercase();
|
||||
|
||||
if theme_name.contains("dark") || theme_name.contains("night") {
|
||||
return Ok("dark".to_string());
|
||||
} else if theme_name.contains("light") || theme_name.contains("adwaita") {
|
||||
return Ok("light".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not detect dconf theme".into())
|
||||
}
|
||||
|
||||
fn detect_env_theme() -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Check common environment variables
|
||||
if let Ok(theme) = std::env::var("GTK_THEME") {
|
||||
let theme_lower = theme.to_lowercase();
|
||||
if theme_lower.contains("dark") || theme_lower.contains("night") {
|
||||
return Ok("dark".to_string());
|
||||
} else if theme_lower.contains("light") {
|
||||
return Ok("light".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(theme) = std::env::var("QT_STYLE_OVERRIDE") {
|
||||
let theme_lower = theme.to_lowercase();
|
||||
if theme_lower.contains("dark") || theme_lower.contains("night") {
|
||||
return Ok("dark".to_string());
|
||||
} else if theme_lower.contains("light") {
|
||||
return Ok("light".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not detect theme from environment".into())
|
||||
}
|
||||
|
||||
fn detect_portal_theme() -> Result<String, Box<dyn std::error::Error>> {
|
||||
if !is_command_available("busctl") {
|
||||
return Err("busctl not available".into());
|
||||
}
|
||||
|
||||
// Try to query the color scheme via org.freedesktop.portal.Settings
|
||||
if let Ok(output) = Command::new("busctl")
|
||||
.args([
|
||||
"--user",
|
||||
"call",
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"Read",
|
||||
"ss",
|
||||
"org.freedesktop.appearance",
|
||||
"color-scheme",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let response = String::from_utf8_lossy(&output.stdout);
|
||||
// Parse DBus response - look for preference values
|
||||
if response.contains(" 1 ") {
|
||||
return Ok("dark".to_string());
|
||||
} else if response.contains(" 2 ") {
|
||||
return Ok("light".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not detect portal theme".into())
|
||||
}
|
||||
|
||||
fn detect_system_files_theme() -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Check if we're in a dark terminal (heuristic)
|
||||
if let Ok(term) = std::env::var("TERM") {
|
||||
let term_lower = term.to_lowercase();
|
||||
if term_lower.contains("dark") || term_lower.contains("night") {
|
||||
return Ok("dark".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we can determine from desktop session
|
||||
if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") {
|
||||
let desktop_lower = desktop.to_lowercase();
|
||||
// Some desktops default to dark
|
||||
if desktop_lower.contains("i3") || desktop_lower.contains("sway") {
|
||||
// Window managers often use dark themes by default
|
||||
return Ok("dark".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not detect theme from system files".into())
|
||||
}
|
||||
|
||||
fn is_command_available(command: &str) -> bool {
|
||||
Command::new("which")
|
||||
.arg(command)
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos {
|
||||
use super::*;
|
||||
|
||||
pub fn detect_system_theme() -> SystemTheme {
|
||||
// macOS theme detection using osascript
|
||||
if let Ok(output) = Command::new("osascript")
|
||||
.args([
|
||||
"-e",
|
||||
"tell application \"System Events\" to tell appearance preferences to get dark mode",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let result = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let result = result.trim();
|
||||
match result {
|
||||
"true" => {
|
||||
return SystemTheme {
|
||||
theme: "dark".to_string(),
|
||||
}
|
||||
}
|
||||
"false" => {
|
||||
return SystemTheme {
|
||||
theme: "light".to_string(),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback method using defaults
|
||||
if let Ok(output) = Command::new("defaults")
|
||||
.args(["read", "-g", "AppleInterfaceStyle"])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let style = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let style = style.trim();
|
||||
if style.to_lowercase() == "dark" {
|
||||
return SystemTheme {
|
||||
theme: "dark".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to light if we can't determine
|
||||
SystemTheme {
|
||||
theme: "light".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows {
|
||||
use super::*;
|
||||
|
||||
pub fn detect_system_theme() -> SystemTheme {
|
||||
// Windows theme detection via registry
|
||||
// This is a simplified implementation - you might want to use winreg crate for better registry access
|
||||
if let Ok(output) = Command::new("reg")
|
||||
.args([
|
||||
"query",
|
||||
"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
"/v",
|
||||
"AppsUseLightTheme",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let result = String::from_utf8_lossy(&output.stdout);
|
||||
if result.contains("0x0") {
|
||||
return SystemTheme {
|
||||
theme: "dark".to_string(),
|
||||
};
|
||||
} else if result.contains("0x1") {
|
||||
return SystemTheme {
|
||||
theme: "light".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to light if we can't determine
|
||||
SystemTheme {
|
||||
theme: "light".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Command to expose this functionality to the frontend
|
||||
#[tauri::command]
|
||||
pub fn get_system_theme() -> SystemTheme {
|
||||
let detector = ThemeDetector::new();
|
||||
detector.detect_system_theme()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_theme_detector_creation() {
|
||||
let detector = ThemeDetector::new();
|
||||
let theme = detector.detect_system_theme();
|
||||
|
||||
// Should return a valid theme string
|
||||
assert!(matches!(theme.theme.as_str(), "light" | "dark" | "unknown"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_system_theme_command() {
|
||||
let theme = get_system_theme();
|
||||
assert!(matches!(theme.theme.as_str(), "light" | "dark" | "unknown"));
|
||||
}
|
||||
}
|
||||
@@ -448,22 +448,6 @@ pub async fn get_version_update_status() -> Result<(Option<u64>, u64), String> {
|
||||
Ok((last_update, time_until_next))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_version_update_needed() -> Result<bool, String> {
|
||||
Ok(VersionUpdater::should_run_background_update())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn force_version_update_check(_app_handle: tauri::AppHandle) -> Result<bool, String> {
|
||||
let updater = get_version_updater();
|
||||
let updater_guard = updater.lock().await;
|
||||
|
||||
match updater_guard.check_and_run_startup_update().await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) => Err(format!("Failed to run version update check: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
+22
-10
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Donut Browser",
|
||||
"version": "0.2.4",
|
||||
"version": "0.3.2",
|
||||
"identifier": "com.donutbrowser",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
@@ -10,15 +10,7 @@
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Donut Browser",
|
||||
"width": 900,
|
||||
"height": 600,
|
||||
"resizable": false,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
@@ -26,6 +18,7 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"category": "Productivity",
|
||||
"externalBin": ["binaries/nodecar"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
@@ -45,6 +38,25 @@
|
||||
"files": {
|
||||
"Info.plist": "Info.plist"
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["xdg-utils"],
|
||||
"files": {
|
||||
"/usr/share/applications/donutbrowser.desktop": "donutbrowser.desktop"
|
||||
}
|
||||
},
|
||||
"rpm": {
|
||||
"depends": ["xdg-utils"],
|
||||
"files": {
|
||||
"/usr/share/applications/donutbrowser.desktop": "donutbrowser.desktop"
|
||||
}
|
||||
},
|
||||
"appimage": {
|
||||
"files": {
|
||||
"usr/share/applications/donutbrowser.desktop": "donutbrowser.desktop"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
|
||||
@@ -4,6 +4,7 @@ import "@/styles/globals.css";
|
||||
import { CustomThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { WindowDragArea } from "@/components/window-drag-area";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -26,6 +27,7 @@ export default function RootLayout({
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<CustomThemeProvider>
|
||||
<WindowDragArea />
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
<Toaster />
|
||||
</CustomThemeProvider>
|
||||
|
||||
+52
-15
@@ -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,11 +51,13 @@ 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);
|
||||
const [currentProfileForVersionChange, setCurrentProfileForVersionChange] =
|
||||
useState<BrowserProfile | null>(null);
|
||||
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
|
||||
|
||||
// Simple profiles loader without updates check (for use as callback)
|
||||
const loadProfiles = useCallback(async () => {
|
||||
@@ -110,6 +120,9 @@ export default function Home() {
|
||||
}, [loadProfilesWithUpdateCheck, checkForUpdates]);
|
||||
|
||||
const checkStartupPrompt = async () => {
|
||||
// Only check once during app startup to prevent reopening after dismissing notifications
|
||||
if (hasCheckedStartupPrompt) return;
|
||||
|
||||
try {
|
||||
const shouldShow = await invoke<boolean>(
|
||||
"should_show_settings_on_startup",
|
||||
@@ -117,8 +130,10 @@ export default function Home() {
|
||||
if (shouldShow) {
|
||||
setSettingsDialogOpen(true);
|
||||
}
|
||||
setHasCheckedStartupPrompt(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to check startup prompt:", error);
|
||||
setHasCheckedStartupPrompt(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -394,28 +409,42 @@ export default function Home() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 gap-8 sm:p-12 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center w-full max-w-3xl">
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 gap-8 sm:p-12 font-[family-name:var(--font-geist-sans)] bg-white dark:bg-black">
|
||||
<main className="flex flex-col row-start-2 gap-8 items-center w-full max-w-3xl">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex justify-between items-center">
|
||||
<CardTitle>Profiles</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex gap-2 items-center">
|
||||
<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 items-center gap-2"
|
||||
>
|
||||
<GoGear className="h-4 w-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
|
||||
@@ -423,9 +452,9 @@ export default function Home() {
|
||||
onClick={() => {
|
||||
setCreateProfileDialogOpen(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<GoPlus className="h-4 w-4" />
|
||||
<GoPlus className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Create a new profile</TooltipContent>
|
||||
@@ -483,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}
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { VersionSelector } from "@/components/version-selector";
|
||||
import { useBrowserDownload } from "@/hooks/use-browser-download";
|
||||
import { useBrowserSupport } from "@/hooks/use-browser-support";
|
||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||
import type { BrowserProfile, ProxySettings } from "@/types";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -60,9 +62,6 @@ export function CreateProfileDialog({
|
||||
const [selectedBrowser, setSelectedBrowser] =
|
||||
useState<BrowserTypeString | null>("mullvad-browser");
|
||||
const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
|
||||
const [supportedBrowsers, setSupportedBrowsers] = useState<
|
||||
BrowserTypeString[]
|
||||
>([]);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [existingProfiles, setExistingProfiles] = useState<BrowserProfile[]>(
|
||||
[],
|
||||
@@ -84,13 +83,29 @@ export function CreateProfileDialog({
|
||||
isVersionDownloaded,
|
||||
} = useBrowserDownload();
|
||||
|
||||
const {
|
||||
supportedBrowsers,
|
||||
isLoading: isLoadingSupport,
|
||||
isBrowserSupported,
|
||||
} = useBrowserSupport();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
void loadSupportedBrowsers();
|
||||
void loadExistingProfiles();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (supportedBrowsers.length > 0) {
|
||||
// Set default browser to first supported browser
|
||||
if (supportedBrowsers.includes("mullvad-browser")) {
|
||||
setSelectedBrowser("mullvad-browser");
|
||||
} else if (supportedBrowsers.length > 0) {
|
||||
setSelectedBrowser(supportedBrowsers[0] as BrowserTypeString);
|
||||
}
|
||||
}
|
||||
}, [supportedBrowsers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && selectedBrowser) {
|
||||
// Reset selected version when browser changes
|
||||
@@ -105,7 +120,7 @@ export function CreateProfileDialog({
|
||||
if (availableVersions.length > 0 && selectedBrowser) {
|
||||
// Always reset version when browser changes or versions are loaded
|
||||
// Find the latest stable version (not alpha/beta)
|
||||
const stableVersions = availableVersions.filter((v) => !v.is_alpha);
|
||||
const stableVersions = availableVersions.filter((v) => !v.is_nightly);
|
||||
|
||||
if (stableVersions.length > 0) {
|
||||
// Select the first stable version (they're already sorted newest first)
|
||||
@@ -117,22 +132,6 @@ export function CreateProfileDialog({
|
||||
}
|
||||
}, [availableVersions, selectedBrowser]);
|
||||
|
||||
const loadSupportedBrowsers = async () => {
|
||||
try {
|
||||
const browsers = await invoke<BrowserTypeString[]>(
|
||||
"get_supported_browsers",
|
||||
);
|
||||
setSupportedBrowsers(browsers);
|
||||
if (browsers.includes("mullvad-browser")) {
|
||||
setSelectedBrowser("mullvad-browser");
|
||||
} else if (browsers.length > 0) {
|
||||
setSelectedBrowser(browsers[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load supported browsers:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadExistingProfiles = async () => {
|
||||
try {
|
||||
const profiles = await invoke<BrowserProfile[]>("list_browser_profiles");
|
||||
@@ -261,21 +260,58 @@ export function CreateProfileDialog({
|
||||
onValueChange={(value) => {
|
||||
setSelectedBrowser(value as BrowserTypeString);
|
||||
}}
|
||||
disabled={isLoadingSupport}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select browser" />
|
||||
<SelectValue
|
||||
placeholder={
|
||||
isLoadingSupport ? "Loading browsers..." : "Select browser"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{supportedBrowsers.map((browser) => (
|
||||
<SelectItem key={browser} value={browser}>
|
||||
{browser
|
||||
.split("-")
|
||||
.map(
|
||||
(word) => word.charAt(0).toUpperCase() + word.slice(1),
|
||||
)
|
||||
.join(" ")}
|
||||
</SelectItem>
|
||||
))}
|
||||
{(
|
||||
[
|
||||
"mullvad-browser",
|
||||
"firefox",
|
||||
"firefox-developer",
|
||||
"chromium",
|
||||
"brave",
|
||||
"zen",
|
||||
"tor-browser",
|
||||
] as BrowserTypeString[]
|
||||
).map((browser) => {
|
||||
const isSupported = isBrowserSupported(browser);
|
||||
const displayName = getBrowserDisplayName(browser);
|
||||
|
||||
if (!isSupported) {
|
||||
return (
|
||||
<Tooltip key={browser}>
|
||||
<TooltipTrigger asChild>
|
||||
<SelectItem
|
||||
value={browser}
|
||||
disabled={true}
|
||||
className="opacity-50"
|
||||
>
|
||||
{displayName} (Not supported)
|
||||
</SelectItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{displayName} is not supported on your current
|
||||
platform or architecture.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectItem key={browser} value={browser}>
|
||||
{displayName}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -116,31 +116,31 @@ type ToastProps =
|
||||
function getToastIcon(type: ToastProps["type"], stage?: string) {
|
||||
switch (type) {
|
||||
case "success":
|
||||
return <LuCheckCheck className="h-4 w-4 text-green-500 flex-shrink-0" />;
|
||||
return <LuCheckCheck className="flex-shrink-0 w-4 h-4 text-green-500" />;
|
||||
case "error":
|
||||
return <LuTriangleAlert className="h-4 w-4 text-red-500 flex-shrink-0" />;
|
||||
return <LuTriangleAlert className="flex-shrink-0 w-4 h-4 text-red-500" />;
|
||||
case "download":
|
||||
if (stage === "completed") {
|
||||
return (
|
||||
<LuCheckCheck className="h-4 w-4 text-green-500 flex-shrink-0" />
|
||||
<LuCheckCheck className="flex-shrink-0 w-4 h-4 text-green-500" />
|
||||
);
|
||||
}
|
||||
return <LuDownload className="h-4 w-4 text-blue-500 flex-shrink-0" />;
|
||||
return <LuDownload className="flex-shrink-0 w-4 h-4 text-blue-500" />;
|
||||
case "version-update":
|
||||
return (
|
||||
<LuRefreshCw className="h-4 w-4 text-blue-500 animate-spin flex-shrink-0" />
|
||||
<LuRefreshCw className="flex-shrink-0 w-4 h-4 text-blue-500 animate-spin" />
|
||||
);
|
||||
case "fetching":
|
||||
return (
|
||||
<LuRefreshCw className="h-4 w-4 text-blue-500 animate-spin flex-shrink-0" />
|
||||
<LuRefreshCw className="flex-shrink-0 w-4 h-4 text-blue-500 animate-spin" />
|
||||
);
|
||||
case "twilight-update":
|
||||
return (
|
||||
<LuRefreshCw className="h-4 w-4 text-purple-500 animate-spin flex-shrink-0" />
|
||||
<LuRefreshCw className="flex-shrink-0 w-4 h-4 text-purple-500 animate-spin" />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-500 border-t-transparent flex-shrink-0" />
|
||||
<div className="flex-shrink-0 w-4 h-4 rounded-full border-2 border-blue-500 animate-spin border-t-transparent" />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -151,10 +151,10 @@ export function UnifiedToast(props: ToastProps) {
|
||||
const progress = "progress" in props ? props.progress : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex items-start w-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-3 shadow-lg">
|
||||
<div className="flex items-start p-3 w-full bg-white rounded-lg border border-gray-200 shadow-lg dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="mr-3 mt-0.5">{getToastIcon(type, stage)}</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white leading-tight">
|
||||
<p className="text-sm font-medium leading-tight text-gray-900 dark:text-white">
|
||||
{title}
|
||||
</p>
|
||||
|
||||
@@ -165,7 +165,7 @@ export function UnifiedToast(props: ToastProps) {
|
||||
stage === "downloading" && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-xs text-gray-600 dark:text-gray-300 min-w-0 flex-1">
|
||||
<p className="flex-1 min-w-0 text-xs text-gray-600 dark:text-gray-300">
|
||||
{progress.percentage.toFixed(1)}%
|
||||
{progress.speed && ` • ${progress.speed} MB/s`}
|
||||
{progress.eta && ` • ${progress.eta} remaining`}
|
||||
@@ -195,7 +195,7 @@ export function UnifiedToast(props: ToastProps) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap shrink-0 w-8 text-right">
|
||||
<span className="w-8 text-xs text-right text-gray-500 whitespace-nowrap dark:text-gray-400 shrink-0">
|
||||
{progress.current}/{progress.total}
|
||||
</span>
|
||||
</div>
|
||||
@@ -211,7 +211,7 @@ export function UnifiedToast(props: ToastProps) {
|
||||
: "Checking for twilight updates..."}
|
||||
</p>
|
||||
{props.browserName && (
|
||||
<p className="text-xs text-purple-600 dark:text-purple-400 mt-1">
|
||||
<p className="mt-1 text-xs text-purple-600 dark:text-purple-400">
|
||||
{props.browserName} • Rolling Release
|
||||
</p>
|
||||
)}
|
||||
@@ -220,7 +220,7 @@ export function UnifiedToast(props: ToastProps) {
|
||||
|
||||
{/* Description */}
|
||||
{description && (
|
||||
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300 leading-tight">
|
||||
<p className="mt-1 text-xs leading-tight text-gray-600 dark:text-gray-300">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
@@ -235,7 +235,7 @@ export function UnifiedToast(props: ToastProps) {
|
||||
)}
|
||||
{stage === "verifying" && (
|
||||
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
||||
Verifying installation...
|
||||
Verifying browser files...
|
||||
</p>
|
||||
)}
|
||||
{stage === "downloading (twilight rolling release)" && (
|
||||
|
||||
@@ -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 Profile
|
||||
</LoadingButton>
|
||||
) : (
|
||||
<LoadingButton
|
||||
isLoading={isImporting}
|
||||
onClick={() => {
|
||||
void handleManualImport();
|
||||
}}
|
||||
disabled={
|
||||
!manualBrowserType ||
|
||||
!manualProfilePath.trim() ||
|
||||
!manualProfileName.trim()
|
||||
}
|
||||
>
|
||||
Import Profile
|
||||
</LoadingButton>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -28,6 +29,7 @@ interface AppSettings {
|
||||
show_settings_on_startup: boolean;
|
||||
theme: string;
|
||||
auto_updates_enabled: boolean;
|
||||
auto_delete_unused_binaries: boolean;
|
||||
}
|
||||
|
||||
interface SettingsDialogProps {
|
||||
@@ -41,17 +43,20 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
show_settings_on_startup: true,
|
||||
theme: "system",
|
||||
auto_updates_enabled: true,
|
||||
auto_delete_unused_binaries: true,
|
||||
});
|
||||
const [originalSettings, setOriginalSettings] = useState<AppSettings>({
|
||||
set_as_default_browser: false,
|
||||
show_settings_on_startup: true,
|
||||
theme: "system",
|
||||
auto_updates_enabled: true,
|
||||
auto_delete_unused_binaries: true,
|
||||
});
|
||||
const [isDefaultBrowser, setIsDefaultBrowser] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isSettingDefault, setIsSettingDefault] = useState(false);
|
||||
const [isClearingCache, setIsClearingCache] = useState(false);
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
@@ -106,11 +111,26 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearCache = async () => {
|
||||
setIsClearingCache(true);
|
||||
try {
|
||||
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 {
|
||||
setIsClearingCache(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await invoke("save_app_settings", { settings });
|
||||
// Apply theme change immediately
|
||||
setTheme(settings.theme);
|
||||
setOriginalSettings(settings);
|
||||
onClose();
|
||||
@@ -130,7 +150,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
settings.show_settings_on_startup !==
|
||||
originalSettings.show_settings_on_startup ||
|
||||
settings.theme !== originalSettings.theme ||
|
||||
settings.auto_updates_enabled !== originalSettings.auto_updates_enabled;
|
||||
settings.auto_updates_enabled !== originalSettings.auto_updates_enabled ||
|
||||
settings.auto_delete_unused_binaries !==
|
||||
originalSettings.auto_delete_unused_binaries;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
@@ -139,7 +161,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
<DialogTitle>Settings</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-6 py-4 overflow-y-auto flex-1 min-h-0">
|
||||
<div className="grid overflow-y-auto flex-1 gap-6 py-4 min-h-0">
|
||||
{/* Appearance Section */}
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Appearance</Label>
|
||||
@@ -172,7 +194,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
|
||||
{/* Default Browser Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label className="text-base font-medium">Default Browser</Label>
|
||||
<Badge variant={isDefaultBrowser ? "default" : "secondary"}>
|
||||
{isDefaultBrowser ? "Active" : "Inactive"}
|
||||
@@ -216,9 +238,26 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="auto-delete-binaries"
|
||||
checked={settings.auto_delete_unused_binaries}
|
||||
onCheckedChange={(checked) => {
|
||||
updateSetting(
|
||||
"auto_delete_unused_binaries",
|
||||
checked as boolean,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="auto-delete-binaries" className="text-sm">
|
||||
Automatically delete unused browser binaries
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When enabled, Donut Browser will check for browser updates and
|
||||
notify you when updates are available for your profiles.
|
||||
notify you when updates are available for your profiles. Unused
|
||||
binaries will be automatically deleted to save disk space.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -244,6 +283,28 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
starts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Advanced Section */}
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Advanced</Label>
|
||||
|
||||
<LoadingButton
|
||||
isLoading={isClearingCache}
|
||||
onClick={() => {
|
||||
void handleClearCache();
|
||||
}}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
Clear All Version Cache
|
||||
</LoadingButton>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
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>
|
||||
|
||||
<DialogFooter className="flex-shrink-0">
|
||||
|
||||
@@ -9,6 +9,10 @@ interface AppSettings {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
interface SystemTheme {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
interface CustomThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
@@ -24,6 +28,25 @@ function getSystemTheme(): string {
|
||||
return "light";
|
||||
}
|
||||
|
||||
// Function to get native system theme (fallback to CSS media query)
|
||||
async function getNativeSystemTheme(): Promise<string> {
|
||||
try {
|
||||
const systemTheme = await invoke<SystemTheme>("get_system_theme");
|
||||
if (systemTheme.theme === "dark" || systemTheme.theme === "light") {
|
||||
return systemTheme.theme;
|
||||
}
|
||||
// Fallback to CSS media query if native detection returns "unknown"
|
||||
return getSystemTheme();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"Failed to get native system theme, falling back to CSS media query:",
|
||||
error,
|
||||
);
|
||||
// Fallback to CSS media query
|
||||
return getSystemTheme();
|
||||
}
|
||||
}
|
||||
|
||||
export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [defaultTheme, setDefaultTheme] = useState<string>("system");
|
||||
@@ -41,7 +64,7 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||
} catch (error) {
|
||||
console.error("Failed to load theme settings:", error);
|
||||
// For first-time users, detect system preference and apply it
|
||||
const systemTheme = getSystemTheme();
|
||||
const systemTheme = await getNativeSystemTheme();
|
||||
console.log(
|
||||
"First-time user detected, applying system theme:",
|
||||
systemTheme,
|
||||
@@ -69,6 +92,50 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||
void loadTheme();
|
||||
}, []);
|
||||
|
||||
// Monitor system theme changes when using "system" theme
|
||||
useEffect(() => {
|
||||
if (!mounted || defaultTheme !== "system") {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkSystemTheme = async () => {
|
||||
try {
|
||||
const currentSystemTheme = await getNativeSystemTheme();
|
||||
// Force re-evaluation by toggling the theme
|
||||
const html = document.documentElement;
|
||||
const currentClass = html.className;
|
||||
|
||||
// Apply the system theme class
|
||||
if (currentSystemTheme === "dark") {
|
||||
if (!html.classList.contains("dark")) {
|
||||
html.classList.add("dark");
|
||||
html.classList.remove("light");
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!html.classList.contains("light") ||
|
||||
html.classList.contains("dark")
|
||||
) {
|
||||
html.classList.add("light");
|
||||
html.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to check system theme:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Check system theme every 2 seconds when using system theme
|
||||
const intervalId = setInterval(() => void checkSystemTheme(), 2000);
|
||||
|
||||
// Initial check
|
||||
void checkSystemTheme();
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [mounted, defaultTheme]);
|
||||
|
||||
if (isLoading) {
|
||||
// Use a consistent loading screen that doesn't depend on system theme during SSR
|
||||
// This prevents hydration mismatch by ensuring server and client render the same initially
|
||||
@@ -77,6 +144,7 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||
|
||||
// Only apply system theme detection after component is mounted (client-side only)
|
||||
if (mounted) {
|
||||
// Use CSS media query for loading screen since async call would complicate this
|
||||
const systemTheme = getSystemTheme();
|
||||
loadingBgColor = systemTheme === "dark" ? "bg-gray-900" : "bg-white";
|
||||
spinnerColor =
|
||||
@@ -85,10 +153,10 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed inset-0 ${loadingBgColor} flex items-center justify-center`}
|
||||
className={`flex fixed inset-0 justify-center items-center ${loadingBgColor}`}
|
||||
>
|
||||
<div
|
||||
className={`animate-spin rounded-full h-8 w-8 border-2 ${spinnerColor} border-t-transparent`}
|
||||
className={`w-8 h-8 rounded-full border-2 animate-spin ${spinnerColor} border-t-transparent`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,8 +15,15 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
zIndex: 99999,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
toastOptions={{
|
||||
style: {
|
||||
zIndex: 99999,
|
||||
pointerEvents: "auto",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -47,17 +47,17 @@ export function UpdateNotificationComponent({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4 max-w-md bg-background border border-border rounded-lg shadow-lg">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex flex-col gap-3 p-4 max-w-md rounded-lg border shadow-lg bg-background border-border">
|
||||
<div className="flex gap-2 justify-between items-start">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className="font-semibold text-foreground">
|
||||
{browserDisplayName} Update Available
|
||||
</span>
|
||||
<Badge
|
||||
variant={notification.is_stable_update ? "default" : "secondary"}
|
||||
>
|
||||
{notification.is_stable_update ? "Stable" : "Beta"}
|
||||
{notification.is_stable_update ? "Stable" : "Nightly"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
@@ -71,20 +71,20 @@ export function UpdateNotificationComponent({
|
||||
onClick={async () => {
|
||||
await onDismiss(notification.id);
|
||||
}}
|
||||
className="h-6 w-6 p-0 shrink-0"
|
||||
className="p-0 w-6 h-6 shrink-0"
|
||||
>
|
||||
<FaTimes className="h-3 w-3" />
|
||||
<FaTimes className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Button
|
||||
onClick={handleUpdateClick}
|
||||
disabled={isUpdating}
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<FaDownload className="h-3 w-3" />
|
||||
<FaDownload className="w-3 h-3" />
|
||||
Update
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -30,7 +30,7 @@ interface GithubRelease {
|
||||
hash?: string;
|
||||
}>;
|
||||
published_at: string;
|
||||
is_alpha: boolean;
|
||||
is_nightly: boolean;
|
||||
}
|
||||
|
||||
interface VersionSelectorProps {
|
||||
@@ -75,7 +75,7 @@ export function VersionSelector({
|
||||
className="justify-between w-full"
|
||||
>
|
||||
{selectedVersion ?? placeholder}
|
||||
<LuChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<LuChevronsUpDown className="ml-2 w-4 h-4 opacity-50 shrink-0" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
@@ -114,11 +114,11 @@ export function VersionSelector({
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<span>{version.tag_name}</span>
|
||||
{version.is_alpha && (
|
||||
{version.is_nightly && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Alpha
|
||||
Nightly
|
||||
</Badge>
|
||||
)}
|
||||
{isDownloaded && (
|
||||
@@ -147,7 +147,7 @@ export function VersionSelector({
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
<LuDownload className="mr-2 h-4 w-4" />
|
||||
<LuDownload className="mr-2 w-4 h-4" />
|
||||
{isDownloading ? "Downloading..." : "Download Browser"}
|
||||
</LoadingButton>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function WindowDragArea() {
|
||||
const [isMacOS, setIsMacOS] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if we're on macOS using user agent detection
|
||||
const checkPlatform = () => {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
setIsMacOS(userAgent.includes("mac"));
|
||||
};
|
||||
|
||||
checkPlatform();
|
||||
}, []);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
// Only handle left mouse button
|
||||
if (e.button !== 0) return;
|
||||
|
||||
// Start dragging asynchronously
|
||||
const startDrag = async () => {
|
||||
try {
|
||||
const window = getCurrentWindow();
|
||||
await window.startDragging();
|
||||
} catch (error) {
|
||||
console.error("Failed to start window dragging:", error);
|
||||
}
|
||||
};
|
||||
|
||||
void startDrag();
|
||||
};
|
||||
|
||||
// Only render on macOS
|
||||
if (!isMacOS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed top-0 right-0 left-0 z-50 h-8 cursor-move"
|
||||
style={{
|
||||
// Ensure it's above all other content
|
||||
zIndex: 9999,
|
||||
// Make it transparent but still capture mouse events
|
||||
backgroundColor: "transparent",
|
||||
// Prevent text selection during drag
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
// Prevent context menu
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -135,6 +135,10 @@ export function useAppUpdateNotifications() {
|
||||
id: "app-update",
|
||||
duration: Number.POSITIVE_INFINITY, // Persistent until user action
|
||||
position: "top-left",
|
||||
style: {
|
||||
zIndex: 99999, // Ensure app updates appear above dialogs
|
||||
pointerEvents: "auto", // Ensure app updates remain interactive
|
||||
},
|
||||
},
|
||||
);
|
||||
}, [
|
||||
|
||||
@@ -19,7 +19,7 @@ interface GithubRelease {
|
||||
hash?: string;
|
||||
}>;
|
||||
published_at: string;
|
||||
is_alpha: boolean;
|
||||
is_nightly: boolean;
|
||||
}
|
||||
|
||||
interface BrowserVersionInfo {
|
||||
@@ -231,7 +231,7 @@ export function useBrowserDownload() {
|
||||
tag_name: versionInfo.version,
|
||||
assets: [],
|
||||
published_at: versionInfo.date,
|
||||
is_alpha: versionInfo.is_prerelease,
|
||||
is_nightly: versionInfo.is_prerelease,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -272,7 +272,7 @@ export function useBrowserDownload() {
|
||||
tag_name: versionInfo.version,
|
||||
assets: [],
|
||||
published_at: versionInfo.date,
|
||||
is_alpha: versionInfo.is_prerelease,
|
||||
is_nightly: versionInfo.is_prerelease,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -325,6 +325,22 @@ export function useBrowserDownload() {
|
||||
setIsDownloading(true);
|
||||
|
||||
try {
|
||||
// Check browser compatibility before attempting download
|
||||
const isSupported = await invoke<boolean>(
|
||||
"is_browser_supported_on_platform",
|
||||
{ browserStr },
|
||||
);
|
||||
if (!isSupported) {
|
||||
const supportedBrowsers = await invoke<string[]>(
|
||||
"get_supported_browsers",
|
||||
);
|
||||
throw new Error(
|
||||
`${browserName} is not supported on your platform. Supported browsers: ${supportedBrowsers
|
||||
.map(getBrowserDisplayName)
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
await invoke("download_browser", { browserStr, version });
|
||||
await loadDownloadedVersions(browserStr);
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface BrowserSupportInfo {
|
||||
supportedBrowsers: string[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function useBrowserSupport() {
|
||||
const [supportedBrowsers, setSupportedBrowsers] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadSupportedBrowsers = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const browsers = await invoke<string[]>("get_supported_browsers");
|
||||
setSupportedBrowsers(browsers);
|
||||
} catch (err) {
|
||||
console.error("Failed to load supported browsers:", err);
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Failed to load supported browsers",
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void loadSupportedBrowsers();
|
||||
}, []);
|
||||
|
||||
const isBrowserSupported = (browser: string): boolean => {
|
||||
return supportedBrowsers.includes(browser);
|
||||
};
|
||||
|
||||
const checkBrowserSupport = async (browser: string): Promise<boolean> => {
|
||||
try {
|
||||
return await invoke<boolean>("is_browser_supported_on_platform", {
|
||||
browserStr: browser,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Failed to check support for browser ${browser}:`, err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
supportedBrowsers,
|
||||
isLoading,
|
||||
error,
|
||||
isBrowserSupported,
|
||||
checkBrowserSupport,
|
||||
};
|
||||
}
|
||||
@@ -232,8 +232,10 @@ export function useUpdateNotifications(
|
||||
id: notification.id,
|
||||
duration: Number.POSITIVE_INFINITY, // Persistent until user action
|
||||
position: "top-right",
|
||||
// Remove transparent styling to fix background issue
|
||||
style: undefined,
|
||||
style: {
|
||||
zIndex: 99999, // Ensure notifications appear above dialogs
|
||||
pointerEvents: "auto", // Ensure notifications remain interactive
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ export function showToast(props: ToastProps & { id?: string }) {
|
||||
border: "none",
|
||||
boxShadow: "none",
|
||||
padding: 0,
|
||||
zIndex: 99999,
|
||||
pointerEvents: "auto",
|
||||
},
|
||||
});
|
||||
} else if (props.type === "error") {
|
||||
@@ -127,6 +129,8 @@ export function showToast(props: ToastProps & { id?: string }) {
|
||||
border: "none",
|
||||
boxShadow: "none",
|
||||
padding: 0,
|
||||
zIndex: 99999,
|
||||
pointerEvents: "auto",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -138,6 +142,8 @@ export function showToast(props: ToastProps & { id?: string }) {
|
||||
border: "none",
|
||||
boxShadow: "none",
|
||||
padding: 0,
|
||||
zIndex: 99999,
|
||||
pointerEvents: "auto",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,3 +123,23 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure Sonner toasts appear above all dialogs and remain interactive */
|
||||
.toaster,
|
||||
[data-sonner-toaster] {
|
||||
z-index: 99999 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
[data-sonner-toast] {
|
||||
z-index: 99999 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Ensure toast buttons and interactive elements work */
|
||||
[data-sonner-toast] button,
|
||||
[data-sonner-toast] [role="button"],
|
||||
[data-sonner-toast] input,
|
||||
[data-sonner-toast] select {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
black: "#000000",
|
||||
},
|
||||
backgroundColor: {
|
||||
dark: "#000000",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user