Compare commits

...

23 Commits

Author SHA1 Message Date
zhom 8005ec90b6 refactor: improve auto-delete and auto-install browser logic 2025-06-19 03:36:06 +04:00
zhom cdf30b7baa Merge pull request #28 from zhom/contributors-readme-action-OBsPbmEa9K
docs(contributor): contributors readme action update
2025-06-18 02:20:40 +00:00
github-actions[bot] fadef414fe docs(contributor): contrib-readme-action has updated readme 2025-06-18 02:17:09 +00:00
zhom e1c55233f7 chore: fix permissions for contributors workflow 2025-06-18 06:14:36 +04:00
zhom 801a2b5732 docs: rename agent instructions doc 2025-06-18 06:11:38 +04:00
zhom abe5c691ce docs: stale issue workflow 2025-06-18 06:08:15 +04:00
zhom 2f9a17c6e0 docs: automatically add contributors to readme 2025-06-18 05:38:38 +04:00
zhom fcdb80f75a docs: github newcomer greetings 2025-06-18 05:34:53 +04:00
zhom 7568e7998d chore : version bump 2025-06-18 03:00:45 +04:00
zhom e0f4f93c30 fix: don't create unique temp dir for every cli call 2025-06-18 02:59:11 +04:00
zhom d142b7f79b style: don't show release notes 2025-06-18 02:39:40 +04:00
zhom dc5553a5d3 chore: version bump 2025-06-18 01:30:02 +04:00
zhom 07445ff95b build: add content read permissions for linting workflows 2025-06-18 01:26:34 +04:00
zhom 6ecbc39e46 build: pin action versions 2025-06-18 01:25:21 +04:00
zhom 67849c00d5 refactor: use tmp for temp dirs and add more robust error handling for updateProxyConfig 2025-06-18 01:19:10 +04:00
zhom bdf71e4ef8 build: revert dependabot automerge workflow 2025-06-18 00:57:40 +04:00
zhom 2d2ebba40e build: assign read permission to all actions without one 2025-06-18 00:17:58 +04:00
zhom 2caac5bf4c build: pin action versions 2025-06-18 00:12:26 +04:00
zhom a816fbb140 chore: version bump 2025-06-17 19:24:17 +04:00
zhom c954668ed1 fix: launch chromium proxy directly instead of pac file 2025-06-17 19:22:18 +04:00
zhom 2db27b5ffd refactor: only use is_browser_version_nightly for release checks 2025-06-17 17:52:28 +04:00
zhom 845e9f28ad fix: don't let the user create profile or change version if latest version not downloaded 2025-06-17 16:29:42 +04:00
zhom ee8c6dcc85 chore: add checks for unused ts exports 2025-06-17 16:13:17 +04:00
44 changed files with 1340 additions and 1019 deletions
+5 -5
View File
@@ -29,13 +29,13 @@ jobs:
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
node-version-file: .node-version
cache: "pnpm"
@@ -44,7 +44,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@b1e4dc3db58c9601794e22a9f6d28d45461b9dbf #v3.29.0
with:
queries: security-extended
languages: ${{ matrix.language }}
@@ -56,6 +56,6 @@ jobs:
pnpm run build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@b1e4dc3db58c9601794e22a9f6d28d45461b9dbf #v3.29.0
with:
category: "/language:${{matrix.language}}"
+21
View File
@@ -0,0 +1,21 @@
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
contrib-readme-job:
runs-on: ubuntu-latest
name: Automatically update the contributors list in the README
permissions:
contents: write
pull-requests: write
steps:
- name: Contribute List
uses: akhilmhdh/contributors-readme-action@1ff4c56187458b34cd602aee93e897344ce34bfc #v2.3.10
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,82 @@
name: Dependabot Automerge
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
pull-requests: write
contents: write
checks: read
jobs:
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
permissions:
contents: read
lint-rust:
name: Lint Rust
if: ${{ github.actor == 'dependabot[bot]' }}
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
permissions:
contents: read
codeql:
name: CodeQL
uses: ./.github/workflows/codeql.yml
secrets: inherit
permissions:
security-events: write
contents: read
packages: read
actions: read
spellcheck:
name: Spell Check
uses: ./.github/workflows/spellcheck.yml
secrets: inherit
permissions:
contents: read
dependabot-automerge:
name: Dependabot Automerge
if: ${{ github.actor == 'dependabot[bot]' }}
needs: [security-scan, lint-js, lint-rust, codeql, spellcheck]
runs-on: ubuntu-latest
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b #v2.4.0
with:
compat-lookup: true
github-token: "${{ secrets.SECRET_DEPENDABOT_GITHUB_TOKEN }}"
- name: Auto-merge minor and patch updates
uses: ridedott/merge-me-action@338053c6f9b9311a6be80208f6f0723981e40627 #v2.10.122
with:
GITHUB_TOKEN: ${{ secrets.SECRET_DEPENDABOT_GITHUB_TOKEN }}
PRESET: DEPENDABOT_MINOR
MERGE_METHOD: SQUASH
timeout-minutes: 10
+16
View File
@@ -0,0 +1,16 @@
name: Greetings
on: [pull_request_target, issues]
jobs:
greeting:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/first-interaction@34f15e814fe48ac9312ccf29db4e74fa767cbab7 #v1.3.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: "Thank you for your first issue! If it's a feature request, please make sure it's clear what you want, why you want it, and how important it is to you. If you posted a bug report, please make sure it includes as much detail as possible. Thank you ❤️"
pr-message: "Welcome to the community and thank you for your first contribution ❤️ A human will review your PR shortly. Make sure that the pipelines are green, so that the PR is considered ready for a review and could be merged."
+6 -3
View File
@@ -16,6 +16,9 @@ on:
- ".github/workflows/lint-rs.yml"
- ".github/workflows/osv.yml"
permissions:
contents: read
jobs:
build:
strategy:
@@ -31,13 +34,13 @@ jobs:
run: git config --global core.autocrlf false
- name: Checkout repository code
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
node-version-file: .node-version
cache: "pnpm"
+8 -4
View File
@@ -24,6 +24,9 @@ on:
- "tsconfig.json"
- "biome.json"
permissions:
contents: read
jobs:
build:
strategy:
@@ -39,20 +42,21 @@ jobs:
run: git config --global core.autocrlf false
- name: Checkout repository code
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
node-version-file: .node-version
cache: "pnpm"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b #master
with:
toolchain: stable
components: rustfmt, clippy
- name: Install cargo-audit
+4
View File
@@ -16,11 +16,15 @@ jobs:
name: Lint JavaScript/TypeScript
uses: ./.github/workflows/lint-js.yml
secrets: inherit
permissions:
contents: read
lint-rust:
name: Lint Rust
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
permissions:
contents: read
security-scan:
name: Security Vulnerability Scan
+14 -7
View File
@@ -31,11 +31,15 @@ jobs:
name: Lint JavaScript/TypeScript
uses: ./.github/workflows/lint-js.yml
secrets: inherit
permissions:
contents: read
lint-rust:
name: Lint Rust
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
permissions:
contents: read
codeql:
name: CodeQL
@@ -51,6 +55,8 @@ jobs:
name: Spell Check
uses: ./.github/workflows/spellcheck.yml
secrets: inherit
permissions:
contents: read
release:
needs: [security-scan, lint-js, lint-rust, codeql, spellcheck]
@@ -99,19 +105,20 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
node-version-file: .node-version
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b #master
with:
toolchain: stable
targets: ${{ matrix.target }}
- name: Install dependencies (Ubuntu only)
@@ -121,7 +128,7 @@ jobs:
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev pkg-config xdg-utils
- name: Rust cache
uses: swatinem/rust-cache@v2
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 #v2.7.8
with:
workdir: ./src-tauri
@@ -153,7 +160,7 @@ jobs:
run: pnpm build
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
uses: tauri-apps/tauri-action@42e9df6c59070d114bf90dcd3943a1b8f138b113 #v0.5.20
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REF_NAME: ${{ github.ref_name }}
@@ -166,7 +173,7 @@ jobs:
args: ${{ matrix.args }}
- name: Commit CHANGELOG.md
uses: stefanzweifel/git-auto-commit-action@v6
uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 #v6.0.1
with:
branch: main
commit_message: "docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]"
+13 -6
View File
@@ -30,11 +30,15 @@ jobs:
name: Lint JavaScript/TypeScript
uses: ./.github/workflows/lint-js.yml
secrets: inherit
permissions:
contents: read
lint-rust:
name: Lint Rust
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
permissions:
contents: read
codeql:
name: CodeQL
@@ -50,6 +54,8 @@ jobs:
name: Spell Check
uses: ./.github/workflows/spellcheck.yml
secrets: inherit
permissions:
contents: read
rolling-release:
needs: [security-scan, lint-js, lint-rust, codeql, spellcheck]
@@ -98,19 +104,20 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0
with:
node-version-file: .node-version
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b #master
with:
toolchain: stable
targets: ${{ matrix.target }}
- name: Install dependencies (Ubuntu only)
@@ -120,7 +127,7 @@ jobs:
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev pkg-config xdg-utils
- name: Rust cache
uses: swatinem/rust-cache@v2
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 #v2.7.8
with:
workdir: ./src-tauri
@@ -161,7 +168,7 @@ jobs:
echo "Generated timestamp: ${TIMESTAMP}-${COMMIT_HASH}"
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
uses: tauri-apps/tauri-action@42e9df6c59070d114bf90dcd3943a1b8f138b113 #v0.5.20
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_TAG: "nightly-${{ steps.timestamp.outputs.timestamp }}"
+2 -2
View File
@@ -21,6 +21,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Spell Check Repo
uses: crate-ci/typos@v1.33.1
uses: crate-ci/typos@b1ae8d918b6e85bd611117d3d9a3be4f903ee5e4 #v1.33.1
+21
View File
@@ -0,0 +1,21 @@
name: Mark stale issues and pull requests
on:
schedule:
- cron: "35 23 * * *"
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue has been inactive for 60 days. Please respond to keep it open."
stale-pr-message: "This pull request has been inactive for 60 days. Please respond to keep it open."
stale-issue-label: "stale"
stale-pr-label: "stale"
+3
View File
@@ -1,6 +1,7 @@
{
"cSpell.words": [
"ahooks",
"akhilmhdh",
"appimage",
"appindicator",
"applescript",
@@ -38,12 +39,14 @@
"libpango",
"librsvg",
"libwebkit",
"libxdo",
"mountpoint",
"msvc",
"msys",
"Mullvad",
"mullvadbrowser",
"nodecar",
"nodemon",
"ntlm",
"objc",
"orhun",
+18
View File
@@ -83,6 +83,24 @@ Have questions or want to contribute? We'd love to hear from you!
</picture>
</a>
## Contributors
<!-- readme: collaborators,contributors -start -->
<table>
<tbody>
<tr>
<td align="center">
<a href="https://github.com/zhom">
<img src="https://avatars.githubusercontent.com/u/2717306?v=4" width="100;" alt="zhom"/>
<br />
<sub><b>zhom</b></sub>
</a>
</td>
</tr>
<tbody>
</table>
<!-- readme: collaborators,contributors -end -->
## Contact
Have an urgent question or want to report a security vulnerability? Send an email to contact at donutbrowser dot com and we'll get back to you as fast as possible.
+4
View File
@@ -28,8 +28,12 @@
"get-port": "^7.1.0",
"nodemon": "^3.1.10",
"proxy-chain": "^2.5.9",
"tmp": "^0.2.3",
"ts-node": "^10.9.2",
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.0"
},
"devDependencies": {
"@types/tmp": "^0.2.6"
}
}
+12 -11
View File
@@ -1,8 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import tmp from "tmp";
// Define the proxy configuration type
export interface ProxyConfig {
id: string;
upstreamUrl: string;
@@ -12,10 +11,8 @@ export interface ProxyConfig {
pid?: number;
}
// Path to store proxy configurations
const STORAGE_DIR = path.join(os.tmpdir(), "donutbrowser", "proxies");
const STORAGE_DIR = path.join(tmp.tmpdir, "donutbrowser", "proxies");
// Ensure storage directory exists
if (!fs.existsSync(STORAGE_DIR)) {
fs.mkdirSync(STORAGE_DIR, { recursive: true });
}
@@ -88,7 +85,7 @@ export function listProxyConfigs(): ProxyConfig[] {
try {
const content = fs.readFileSync(
path.join(STORAGE_DIR, file),
"utf-8"
"utf-8",
);
return JSON.parse(content) as ProxyConfig;
} catch (error) {
@@ -111,14 +108,18 @@ export function listProxyConfigs(): ProxyConfig[] {
export function updateProxyConfig(config: ProxyConfig): boolean {
const filePath = path.join(STORAGE_DIR, `${config.id}.json`);
if (!fs.existsSync(filePath)) {
return false;
}
try {
fs.readFileSync(filePath, "utf-8");
fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
console.error(
`Config ${config.id} was deleted while the app was running`,
);
return false;
}
console.error(`Error updating proxy config ${config.id}:`, error);
return false;
}
@@ -135,7 +136,7 @@ export function isProcessRunning(pid: number): boolean {
// but checks if it exists
process.kill(pid, 0);
return true;
} catch (error) {
} catch {
return false;
}
}
+3 -1
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.5.1",
"version": "0.5.4",
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
@@ -20,6 +20,7 @@
"format:js": "biome check src/ --fix",
"format": "pnpm format:js && pnpm format:rust",
"cargo": "cd src-tauri && cargo",
"unused-exports:js": "ts-unused-exports tsconfig.json",
"check-unused-commands": "cd src-tauri && cargo run --bin check_unused_commands"
},
"dependencies": {
@@ -71,6 +72,7 @@
"husky": "^9.1.7",
"lint-staged": "^16.1.1",
"tailwindcss": "^4.1.10",
"ts-unused-exports": "^11.0.1",
"tw-animate-css": "^1.3.4",
"typescript": "~5.8.3",
"typescript-eslint": "^8.34.0"
+33
View File
@@ -147,6 +147,9 @@ importers:
tailwindcss:
specifier: ^4.1.10
version: 4.1.10
ts-unused-exports:
specifier: ^11.0.1
version: 11.0.1(typescript@5.8.3)
tw-animate-css:
specifier: ^1.3.4
version: 1.3.4
@@ -180,6 +183,9 @@ importers:
proxy-chain:
specifier: ^2.5.9
version: 2.5.9
tmp:
specifier: ^0.2.3
version: 0.2.3
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@24.0.1)(typescript@5.8.3)
@@ -189,6 +195,10 @@ importers:
typescript-eslint:
specifier: ^8.34.0
version: 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
devDependencies:
'@types/tmp':
specifier: ^0.2.6
version: 0.2.6
packages:
@@ -1539,6 +1549,9 @@ packages:
'@types/react@19.1.8':
resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==}
'@types/tmp@0.2.6':
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
'@typescript-eslint/eslint-plugin@8.34.0':
resolution: {integrity: sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3406,6 +3419,10 @@ packages:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'}
tmp@0.2.3:
resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==}
engines: {node: '>=14.14'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -3437,6 +3454,12 @@ packages:
'@swc/wasm':
optional: true
ts-unused-exports@11.0.1:
resolution: {integrity: sha512-b1uIe0B8YfNZjeb+bx62LrB6qaO4CHT8SqMVBkwbwLj7Nh0xQ4J8uV0dS9E6AABId0U4LQ+3yB/HXZBMslGn2A==}
hasBin: true
peerDependencies:
typescript: '>=3.8.3'
tsconfig-paths@3.15.0:
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
@@ -4816,6 +4839,8 @@ snapshots:
dependencies:
csstype: 3.1.3
'@types/tmp@0.2.6': {}
'@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@@ -6992,6 +7017,8 @@ snapshots:
fdir: 6.4.5(picomatch@4.0.2)
picomatch: 4.0.2
tmp@0.2.3: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -7022,6 +7049,12 @@ snapshots:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
ts-unused-exports@11.0.1(typescript@5.8.3):
dependencies:
chalk: 4.1.2
tsconfig-paths: 3.15.0
typescript: 5.8.3
tsconfig-paths@3.15.0:
dependencies:
'@types/json5': 0.0.29
+1 -1
View File
@@ -993,7 +993,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.5.1"
version = "0.5.4"
dependencies = [
"async-trait",
"base64 0.22.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.5.1"
version = "0.5.4"
description = "Simple Yet Powerful Browser Orchestrator"
authors = ["zhom@github"]
edition = "2021"
+81 -9
View File
@@ -637,15 +637,39 @@ impl ApiClient {
"{}/repos/mullvad/mullvad-browser/releases?per_page=100",
self.github_api_base
);
let releases = self
let response = self
.client
.get(url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
.send()
.await?
.json::<Vec<GithubRelease>>()
.await?;
if !response.status().is_success() {
return Err(format!("GitHub API returned status: {}", response.status()).into());
}
// Get the response text first for better error reporting
let response_text = response.text().await?;
// Try to parse the JSON with better error handling
let releases: Vec<GithubRelease> = match serde_json::from_str(&response_text) {
Ok(releases) => releases,
Err(e) => {
eprintln!("Failed to parse GitHub API response for Mullvad releases:");
eprintln!("Error: {e}");
eprintln!(
"Response text (first 500 chars): {}",
if response_text.len() > 500 {
&response_text[..500]
} else {
&response_text
}
);
return Err(format!("Failed to parse GitHub API response: {e}").into());
}
};
let mut releases: Vec<GithubRelease> = releases
.into_iter()
.map(|mut release| {
@@ -683,15 +707,39 @@ impl ApiClient {
"{}/repos/zen-browser/desktop/releases?per_page=100",
self.github_api_base
);
let mut releases = self
let response = self
.client
.get(url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
.send()
.await?
.json::<Vec<GithubRelease>>()
.await?;
if !response.status().is_success() {
return Err(format!("GitHub API returned status: {}", response.status()).into());
}
// Get the response text first for better error reporting
let response_text = response.text().await?;
// Try to parse the JSON with better error handling
let mut releases: Vec<GithubRelease> = match serde_json::from_str(&response_text) {
Ok(releases) => releases,
Err(e) => {
eprintln!("Failed to parse GitHub API response for Zen releases:");
eprintln!("Error: {e}");
eprintln!(
"Response text (first 500 chars): {}",
if response_text.len() > 500 {
&response_text[..500]
} else {
&response_text
}
);
return Err(format!("Failed to parse GitHub API response: {e}").into());
}
};
// Check for twilight updates and mark alpha releases
for release in &mut releases {
// Use browser-specific alpha detection for Zen Browser - only "twilight" is nightly
@@ -740,15 +788,39 @@ impl ApiClient {
"{}/repos/brave/brave-browser/releases?per_page=100",
self.github_api_base
);
let releases = self
let response = self
.client
.get(url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
.send()
.await?
.json::<Vec<GithubRelease>>()
.await?;
if !response.status().is_success() {
return Err(format!("GitHub API returned status: {}", response.status()).into());
}
// Get the response text first for better error reporting
let response_text = response.text().await?;
// Try to parse the JSON with better error handling
let releases: Vec<GithubRelease> = match serde_json::from_str(&response_text) {
Ok(releases) => releases,
Err(e) => {
eprintln!("Failed to parse GitHub API response for Brave releases:");
eprintln!("Error: {e}");
eprintln!(
"Response text (first 500 chars): {}",
if response_text.len() > 500 {
&response_text[..500]
} else {
&response_text
}
);
return Err(format!("Failed to parse GitHub API response: {e}").into());
}
};
// Get platform info to filter appropriate releases
let (os, arch) = Self::get_platform_info();
+75 -59
View File
@@ -1,3 +1,4 @@
use crate::api_client::is_browser_version_nightly;
use crate::browser_runner::{BrowserProfile, BrowserRunner};
use crate::browser_version_service::{BrowserVersionInfo, BrowserVersionService};
use crate::settings_manager::SettingsManager;
@@ -101,16 +102,19 @@ impl AutoUpdater {
// Apply chromium threshold logic
if browser == "chromium" {
// For chromium, only show notifications if there are 50+ new versions
// Count how many versions are newer than the current profile version
let newer_versions_count = versions
.iter()
.filter(|v| self.is_version_newer(&v.version, &profile.version))
.count();
let current_version = &profile.version.parse::<u32>().unwrap();
let new_version = &update.new_version.parse::<u32>().unwrap();
if newer_versions_count >= 50 {
let result = new_version - current_version;
println!(
"Current version: {current_version}, New version: {new_version}, Result: {result}"
);
if result > 50 {
notifications.push(update);
} else {
println!("Skipping chromium update notification: only {newer_versions_count} new versions (need 50+)");
println!(
"Skipping chromium update notification: only {result} new versions (need 50+)"
);
}
} else {
notifications.push(update);
@@ -123,6 +127,8 @@ impl AutoUpdater {
}
pub async fn check_for_updates_with_progress(&self, app_handle: &tauri::AppHandle) {
println!("Starting auto-update check with progress...");
// Check for browser updates and trigger auto-downloads
match self.check_for_updates().await {
Ok(update_notifications) => {
@@ -139,22 +145,63 @@ impl AutoUpdater {
notification.browser, notification.new_version
);
// Emit a custom event to trigger auto-download
let auto_update_event = serde_json::json!({
"browser": notification.browser,
"new_version": notification.new_version,
"notification_id": notification.id,
"affected_profiles": notification.affected_profiles
});
// Clone app_handle for the async task
let app_handle_clone = app_handle.clone();
let browser = notification.browser.clone();
let new_version = notification.new_version.clone();
let notification_id = notification.id.clone();
let affected_profiles = notification.affected_profiles.clone();
if let Err(e) = app_handle.emit("browser-auto-update-available", &auto_update_event) {
eprintln!(
"Failed to emit auto-update event for {}: {e}",
notification.browser
);
} else {
println!("Emitted auto-update event for {}", notification.browser);
}
// Spawn async task to handle the download and auto-update
tokio::spawn(async move {
// First, check if browser already exists
match crate::browser_runner::is_browser_downloaded(
browser.clone(),
new_version.clone(),
) {
true => {
println!("Browser {browser} {new_version} already downloaded, proceeding to auto-update profiles");
// Browser already exists, go straight to profile update
match crate::auto_updater::complete_browser_update_with_auto_update(
browser.clone(),
new_version.clone(),
)
.await
{
Ok(updated_profiles) => {
println!(
"Auto-update completed for {} profiles: {:?}",
updated_profiles.len(),
updated_profiles
);
}
Err(e) => {
eprintln!("Failed to complete auto-update for {browser}: {e}");
}
}
}
false => {
println!("Downloading browser {browser} version {new_version}...");
// Emit the auto-update event to trigger frontend handling
let auto_update_event = serde_json::json!({
"browser": browser,
"new_version": new_version,
"notification_id": notification_id,
"affected_profiles": affected_profiles
});
if let Err(e) =
app_handle_clone.emit("browser-auto-update-available", &auto_update_event)
{
eprintln!("Failed to emit auto-update event for {browser}: {e}");
} else {
println!("Emitted auto-update event for {browser}");
}
}
}
});
}
} else {
println!("No browser updates needed");
@@ -173,16 +220,15 @@ impl AutoUpdater {
available_versions: &[BrowserVersionInfo],
) -> Result<Option<UpdateNotification>, Box<dyn std::error::Error + Send + Sync>> {
let current_version = &profile.version;
let is_current_stable = !self.is_nightly_version(current_version);
let is_current_nightly = is_browser_version_nightly(&profile.browser, current_version, None);
// Find the best available update
let best_update = available_versions
.iter()
.filter(|v| {
// Only consider versions newer than current
self.is_version_newer(&v.version, current_version) &&
// Respect version type preference
is_current_stable != v.is_prerelease
self.is_version_newer(&v.version, current_version)
&& is_browser_version_nightly(&profile.browser, &v.version, None) == is_current_nightly
})
.max_by(|a, b| self.compare_versions(&a.version, &b.version));
@@ -302,16 +348,9 @@ 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}");
}
// Always perform cleanup after auto-update - don't fail the update if cleanup fails
if let Err(e) = self.cleanup_unused_binaries_internal() {
eprintln!("Warning: Failed to cleanup unused binaries after auto-update: {e}");
}
Ok(updated_profiles)
@@ -367,14 +406,6 @@ impl AutoUpdater {
Ok(())
}
// Helper methods
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 {
self.compare_versions(version1, version2) == std::cmp::Ordering::Greater
}
@@ -502,21 +533,6 @@ mod tests {
}
}
#[test]
fn test_is_nightly_version() {
let updater = AutoUpdater::new();
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_nightly_version("1.0.0"));
assert!(!updater.is_nightly_version("1.2.3"));
}
#[test]
fn test_compare_versions() {
let updater = AutoUpdater::new();
+39 -11
View File
@@ -1,6 +1,4 @@
use base64::{engine::general_purpose, Engine as _};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -56,7 +54,7 @@ pub trait Browser: Send + Sync {
fn create_launch_args(
&self,
profile_path: &str,
_proxy_settings: Option<&ProxySettings>,
proxy_settings: Option<&ProxySettings>,
url: Option<String>,
) -> Result<Vec<String>, Box<dyn std::error::Error>>;
fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool;
@@ -639,14 +637,10 @@ impl Browser for ChromiumBrowser {
// Add proxy configuration if provided
if let Some(proxy) = proxy_settings {
if proxy.enabled {
let pac_path = Path::new(profile_path).join("proxy.pac");
if pac_path.exists() {
let pac_content = fs::read(&pac_path)?;
let pac_base64 = general_purpose::STANDARD.encode(&pac_content);
args.push(format!(
"--proxy-pac-url=data:application/x-javascript-config;base64,{pac_base64}"
));
}
args.push(format!(
"--proxy-server=http://{}:{}",
proxy.host, proxy.port
));
}
}
@@ -726,6 +720,24 @@ pub struct GithubRelease {
pub is_nightly: bool,
#[serde(default)]
pub prerelease: bool,
#[serde(default)]
pub draft: bool,
#[serde(default)]
pub body: Option<String>,
#[serde(default)]
pub html_url: Option<String>,
#[serde(default)]
pub id: Option<u64>,
#[serde(default)]
pub node_id: Option<String>,
#[serde(default)]
pub target_commitish: Option<String>,
#[serde(default)]
pub created_at: Option<String>,
#[serde(default)]
pub tarball_url: Option<String>,
#[serde(default)]
pub zipball_url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -734,6 +746,22 @@ pub struct GithubAsset {
pub browser_download_url: String,
#[serde(default)]
pub size: u64,
#[serde(default)]
pub download_count: Option<u64>,
#[serde(default)]
pub id: Option<u64>,
#[serde(default)]
pub node_id: Option<String>,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub content_type: Option<String>,
#[serde(default)]
pub state: Option<String>,
#[serde(default)]
pub created_at: Option<String>,
#[serde(default)]
pub updated_at: Option<String>,
}
#[cfg(test)]
+404 -201
View File
@@ -1058,6 +1058,8 @@ impl BrowserRunner {
release_type: &str,
proxy: Option<ProxySettings>,
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
println!("Attempting to create profile: {name}");
// Check if a profile with this name already exists (case insensitive)
let existing_profiles = self.list_profiles()?;
if existing_profiles
@@ -1068,10 +1070,16 @@ impl BrowserRunner {
}
let snake_case_name = name.to_lowercase().replace(" ", "_");
let profiles_dir = self.get_profiles_dir();
let profile_file = profiles_dir.join(format!("{snake_case_name}.json"));
let profile_path = profiles_dir.join(&snake_case_name);
// Double-check that the profile file doesn't exist
if profile_file.exists() {
return Err(format!("Profile file for '{name}' already exists").into());
}
// Create profile directory
let mut profile_path = self.get_profiles_dir();
profile_path.push(&snake_case_name);
create_dir_all(&profile_path)?;
let profile = BrowserProfile {
@@ -1088,6 +1096,13 @@ impl BrowserRunner {
// Save profile info
self.save_profile(&profile)?;
// Verify the profile was saved correctly
if !profile_file.exists() {
return Err(format!("Failed to create profile file for '{name}'").into());
}
println!("Profile '{name}' created successfully");
// Create user.js with common Firefox preferences and apply proxy settings if provided
if let Some(proxy_settings) = &proxy {
self.apply_proxy_settings_to_profile(&profile_path, proxy_settings, None)?;
@@ -1264,14 +1279,8 @@ impl BrowserRunner {
// Save the updated profile
self.save_profile(&profile)?;
// Check if auto-delete of unused binaries is enabled
let settings_manager = crate::settings_manager::SettingsManager::new();
if let Ok(settings) = settings_manager.load_settings() {
if settings.auto_delete_unused_binaries {
// Perform cleanup in the background
let _ = self.cleanup_unused_binaries_internal();
}
}
// Always perform cleanup after profile version update to remove unused binaries
let _ = self.cleanup_unused_binaries_internal();
Ok(profile)
}
@@ -1454,6 +1463,7 @@ impl BrowserRunner {
&self,
profile: &BrowserProfile,
url: Option<String>,
local_proxy_settings: Option<&ProxySettings>,
) -> Result<BrowserProfile, Box<dyn std::error::Error + Send + Sync>> {
// Create browser instance
let browser_type = BrowserType::from_str(&profile.browser)
@@ -1465,6 +1475,7 @@ impl BrowserRunner {
browser_dir.push(&profile.browser);
browser_dir.push(&profile.version);
println!("Browser directory: {browser_dir:?}");
let executable_path = browser
.get_executable_path(&browser_dir)
.expect("Failed to get executable path");
@@ -1475,9 +1486,16 @@ impl BrowserRunner {
// Continue anyway, the error might not be critical
}
// Get launch arguments (proxy settings will be handled later if needed)
// For Chromium browsers, use local proxy settings if available
// For Firefox browsers, continue using original proxy settings (handled via PAC files)
let proxy_for_launch_args = match browser_type {
BrowserType::Chromium | BrowserType::Brave => local_proxy_settings.or(profile.proxy.as_ref()),
_ => profile.proxy.as_ref(),
};
// Get launch arguments
let browser_args = browser
.create_launch_args(&profile.profile_path, None, url)
.create_launch_args(&profile.profile_path, proxy_for_launch_args, url)
.expect("Failed to create launch arguments");
// Launch browser using platform-specific method
@@ -1605,6 +1623,7 @@ impl BrowserRunner {
app_handle: tauri::AppHandle,
profile: &BrowserProfile,
url: &str,
_internal_proxy_settings: Option<&ProxySettings>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Use the comprehensive browser status check
let is_running = self.check_browser_status(app_handle, profile).await?;
@@ -1735,6 +1754,7 @@ impl BrowserRunner {
app_handle: tauri::AppHandle,
profile: &BrowserProfile,
url: Option<String>,
internal_proxy_settings: Option<&ProxySettings>,
) -> Result<BrowserProfile, Box<dyn std::error::Error + Send + Sync>> {
// Get the most up-to-date profile data
let profiles = self.list_profiles().expect("Failed to list profiles");
@@ -1779,7 +1799,12 @@ impl BrowserRunner {
}
}
match self
.open_url_in_existing_browser(app_handle, &final_profile, url_ref)
.open_url_in_existing_browser(
app_handle,
&final_profile,
url_ref,
internal_proxy_settings,
)
.await
{
Ok(()) => {
@@ -1804,7 +1829,7 @@ impl BrowserRunner {
final_profile.browser
);
// Fallback to launching a new instance for other browsers
self.launch_browser(&final_profile, url).await
self.launch_browser(&final_profile, url, internal_proxy_settings).await
}
}
}
@@ -1812,7 +1837,9 @@ impl BrowserRunner {
} else {
// This case shouldn't happen since we checked is_some() above, but handle it gracefully
println!("URL was unexpectedly None, launching new browser instance");
self.launch_browser(&final_profile, url).await
self
.launch_browser(&final_profile, url, internal_proxy_settings)
.await
}
} else {
// Browser is not running or no URL provided, launch new instance
@@ -1821,7 +1848,9 @@ impl BrowserRunner {
} else {
println!("Launching new browser instance - no URL provided");
}
self.launch_browser(&final_profile, url).await
self
.launch_browser(&final_profile, url, internal_proxy_settings)
.await
}
}
@@ -1891,33 +1920,53 @@ impl BrowserRunner {
}
pub fn delete_profile(&self, profile_name: &str) -> Result<(), Box<dyn std::error::Error>> {
let profiles_dir = self.get_profiles_dir();
let profile_file = profiles_dir.join(format!(
"{}.json",
profile_name.to_lowercase().replace(" ", "_")
));
let profile_path = profiles_dir.join(profile_name.to_lowercase().replace(" ", "_"));
println!("Attempting to delete profile: {profile_name}");
// Delete profile directory
let profiles_dir = self.get_profiles_dir();
let snake_case_name = profile_name.to_lowercase().replace(" ", "_");
let profile_file = profiles_dir.join(format!("{snake_case_name}.json"));
let profile_path = profiles_dir.join(&snake_case_name);
// Verify the profile exists before attempting to delete
if !profile_file.exists() {
return Err(format!("Profile '{profile_name}' not found").into());
}
// Read the profile to check if browser is running
if let Ok(content) = fs::read_to_string(&profile_file) {
if let Ok(profile) = serde_json::from_str::<BrowserProfile>(&content) {
if profile.process_id.is_some() {
return Err(
"Cannot delete profile while browser is running. Please stop the browser first.".into(),
);
}
}
}
// Delete profile directory first (if it exists)
if profile_path.exists() {
fs::remove_dir_all(profile_path)?
println!("Deleting profile directory: {}", profile_path.display());
fs::remove_dir_all(&profile_path)?;
println!("Profile directory deleted successfully");
}
// Delete profile JSON file
if profile_file.exists() {
fs::remove_file(profile_file)?
println!("Deleting profile file: {}", profile_file.display());
fs::remove_file(&profile_file)?;
println!("Profile file deleted successfully");
}
// Check if auto-delete of unused binaries is enabled
let settings_manager = crate::settings_manager::SettingsManager::new();
if let Ok(settings) = settings_manager.load_settings() {
if settings.auto_delete_unused_binaries {
// Perform cleanup in the background after profile deletion
// Ignore errors since this is not critical for profile deletion
if let Err(e) = self.cleanup_unused_binaries_internal() {
println!("Warning: Failed to cleanup unused binaries: {e}");
}
}
// Verify deletion was successful
if profile_file.exists() || profile_path.exists() {
return Err(format!("Failed to completely delete profile '{profile_name}'").into());
}
println!("Profile '{profile_name}' deleted successfully");
// Always perform cleanup after profile deletion to remove unused binaries
if let Err(e) = self.cleanup_unused_binaries_internal() {
println!("Warning: Failed to cleanup unused binaries: {e}");
}
Ok(())
@@ -2209,6 +2258,292 @@ impl BrowserRunner {
Ok(())
}
/// Check if browser binaries exist for all profiles and return missing binaries
pub async fn check_missing_binaries(
&self,
) -> Result<Vec<(String, String, String)>, Box<dyn std::error::Error + Send + Sync>> {
// Get all profiles
let profiles = self
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let mut missing_binaries = Vec::new();
for profile in profiles {
let browser_type = match BrowserType::from_str(&profile.browser) {
Ok(bt) => bt,
Err(_) => {
println!(
"Warning: Invalid browser type '{}' for profile '{}'",
profile.browser, profile.name
);
continue;
}
};
let browser = create_browser(browser_type.clone());
let binaries_dir = self.get_binaries_dir();
println!(
"binaries_dir: {binaries_dir:?} for profile: {}",
profile.name
);
// Check if the version is downloaded
if !browser.is_version_downloaded(&profile.version, &binaries_dir) {
missing_binaries.push((profile.name, profile.browser, profile.version));
}
}
Ok(missing_binaries)
}
/// Automatically download missing binaries for all profiles
pub async fn ensure_all_binaries_exist(
&self,
app_handle: &tauri::AppHandle,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
// First, clean up any stale registry entries
if let Ok(mut registry) = DownloadedBrowsersRegistry::load() {
if let Ok(cleaned_up) = registry.verify_and_cleanup_stale_entries(self) {
if !cleaned_up.is_empty() {
println!(
"Cleaned up {} stale registry entries: {}",
cleaned_up.len(),
cleaned_up.join(", ")
);
}
}
}
let missing_binaries = self.check_missing_binaries().await?;
let mut downloaded = Vec::new();
for (profile_name, browser, version) in missing_binaries {
println!("Downloading missing binary for profile '{profile_name}': {browser} {version}");
match self
.download_browser_impl(app_handle.clone(), browser.clone(), version.clone())
.await
{
Ok(_) => {
downloaded.push(format!(
"{browser} {version} (for profile '{profile_name}')"
));
}
Err(e) => {
eprintln!("Failed to download {browser} {version} for profile '{profile_name}': {e}");
}
}
}
Ok(downloaded)
}
pub async fn download_browser_impl(
&self,
app_handle: tauri::AppHandle,
browser_str: String,
version: String,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let browser_type =
BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?;
let browser = create_browser(browser_type.clone());
// Load registry and check if already downloaded
let mut registry = DownloadedBrowsersRegistry::load()
.map_err(|e| format!("Failed to load browser registry: {e}"))?;
// Check if registry thinks it's downloaded, but also verify files actually exist
if registry.is_browser_downloaded(&browser_str, &version) {
let binaries_dir = self.get_binaries_dir();
let actually_exists = browser.is_version_downloaded(&version, &binaries_dir);
if actually_exists {
return Ok(version);
} else {
// Registry says it's downloaded but files don't exist - clean up registry
println!("Registry indicates {browser_str} {version} is downloaded, but files are missing. Cleaning up registry entry.");
registry.remove_browser(&browser_str, &version);
registry
.save()
.map_err(|e| format!("Failed to save cleaned registry: {e}"))?;
}
}
// Check if browser is supported on current platform before attempting download
let version_service = BrowserVersionService::new();
if !version_service
.is_browser_supported(&browser_str)
.unwrap_or(false)
{
return Err(
format!(
"Browser '{}' is not supported on your platform ({} {}). Supported browsers: {}",
browser_str,
std::env::consts::OS,
std::env::consts::ARCH,
version_service.get_supported_browsers().join(", ")
)
.into(),
);
}
let download_info = version_service
.get_download_info(&browser_str, &version)
.map_err(|e| format!("Failed to get download info: {e}"))?;
// Create browser directory
let mut browser_dir = self.get_binaries_dir();
browser_dir.push(browser_type.as_str());
browser_dir.push(&version);
// Clean up any failed previous download
if let Err(e) = registry.cleanup_failed_download(&browser_str, &version) {
println!("Warning: Failed to cleanup previous download: {e}");
}
create_dir_all(&browser_dir).map_err(|e| format!("Failed to create browser directory: {e}"))?;
// Mark download as started in registry
registry.mark_download_started(&browser_str, &version, browser_dir.clone());
registry
.save()
.map_err(|e| format!("Failed to save registry: {e}"))?;
// Use the new download module
let downloader = Downloader::new();
let download_path = match downloader
.download_browser(
&app_handle,
browser_type.clone(),
&version,
&download_info,
&browser_dir,
)
.await
{
Ok(path) => path,
Err(e) => {
// Clean up failed download
let _ = registry.cleanup_failed_download(&browser_str, &version);
let _ = registry.save();
return Err(format!("Failed to download browser: {e}").into());
}
};
// Use the new extraction module
if download_info.is_archive {
let extractor = Extractor::new();
match extractor
.extract_browser(
&app_handle,
browser_type.clone(),
&version,
&download_path,
&browser_dir,
)
.await
{
Ok(_) => {
// Clean up the downloaded archive
if let Err(e) = std::fs::remove_file(&download_path) {
println!("Warning: Could not delete archive file: {e}");
}
}
Err(e) => {
// Clean up failed download
let _ = registry.cleanup_failed_download(&browser_str, &version);
let _ = registry.save();
return Err(format!("Failed to extract browser: {e}").into());
}
}
// Give filesystem a moment to settle after extraction
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
// Emit verification progress
let progress = DownloadProgress {
browser: browser_str.clone(),
version: version.clone(),
downloaded_bytes: 0,
total_bytes: None,
percentage: 100.0,
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: "verifying".to_string(),
};
let _ = app_handle.emit("download-progress", &progress);
// Verify the browser was downloaded correctly
println!("Verifying download for browser: {browser_str}, version: {version}");
// Use the browser's own verification method
let binaries_dir = self.get_binaries_dir();
if !browser.is_version_downloaded(&version, &binaries_dir) {
let _ = registry.cleanup_failed_download(&browser_str, &version);
let _ = registry.save();
return Err("Browser download completed but verification failed".into());
}
// Mark download as completed in registry
let actual_version = if browser_str == "chromium" {
Some(version.clone())
} else {
None
};
registry
.mark_download_completed_with_actual_version(&browser_str, &version, actual_version)
.map_err(|e| format!("Failed to mark download as completed: {e}"))?;
registry
.save()
.map_err(|e| format!("Failed to save registry: {e}"))?;
// Emit completion
let progress = DownloadProgress {
browser: browser_str.clone(),
version: version.clone(),
downloaded_bytes: 0,
total_bytes: None,
percentage: 100.0,
speed_bytes_per_sec: 0.0,
eta_seconds: Some(0.0),
stage: "completed".to_string(),
};
let _ = app_handle.emit("download-progress", &progress);
Ok(version)
}
/// Check if a browser version is downloaded
pub fn is_browser_downloaded(&self, browser_str: &str, version: &str) -> bool {
// Always check if files actually exist on disk
let browser_type = match BrowserType::from_str(browser_str) {
Ok(bt) => bt,
Err(_) => {
println!("Invalid browser type: {browser_str}");
return false;
}
};
let browser = create_browser(browser_type.clone());
let binaries_dir = self.get_binaries_dir();
let files_exist = browser.is_version_downloaded(version, &binaries_dir);
// If files don't exist but registry thinks they do, clean up the registry
if !files_exist {
if let Ok(mut registry) = DownloadedBrowsersRegistry::load() {
if registry.is_browser_downloaded(browser_str, version) {
println!("Cleaning up stale registry entry for {browser_str} {version}");
registry.remove_browser(browser_str, version);
let _ = registry.save(); // Don't fail if save fails, just log
}
}
}
files_exist
}
}
#[tauri::command]
@@ -2241,6 +2576,9 @@ pub async fn launch_browser_profile(
) -> Result<BrowserProfile, String> {
let browser_runner = BrowserRunner::new();
// Store the internal proxy settings for passing to launch_browser
let mut internal_proxy_settings: Option<ProxySettings> = None;
// If the profile has proxy settings, we need to start the proxy first
// and update the profile with proxy settings before launching
let profile_for_launch = profile.clone();
@@ -2254,14 +2592,17 @@ pub async fn launch_browser_profile(
.start_proxy(app_handle.clone(), proxy, temp_pid, Some(&profile.name))
.await
{
Ok(internal_proxy_settings) => {
Ok(internal_proxy) => {
let browser_runner = BrowserRunner::new();
let profiles_dir = browser_runner.get_profiles_dir();
let profile_path = profiles_dir.join(profile.name.to_lowercase().replace(" ", "_"));
// Store the internal proxy settings for later use
internal_proxy_settings = Some(internal_proxy.clone());
// Apply the proxy settings with the internal proxy to the profile directory
browser_runner
.apply_proxy_settings_to_profile(&profile_path, proxy, Some(&internal_proxy_settings))
.apply_proxy_settings_to_profile(&profile_path, proxy, Some(&internal_proxy))
.map_err(|e| format!("Failed to update profile proxy: {e}"))?;
println!("Successfully started proxy for profile: {}", profile.name);
@@ -2287,7 +2628,7 @@ pub async fn launch_browser_profile(
// Launch browser or open URL in existing instance
let updated_profile = browser_runner
.launch_or_open_url(app_handle.clone(), &profile_for_launch, url)
.launch_or_open_url(app_handle.clone(), &profile_for_launch, url, internal_proxy_settings.as_ref())
.await
.map_err(|e| {
// Check if this is an architecture compatibility issue
@@ -2467,174 +2808,16 @@ pub async fn download_browser(
version: String,
) -> Result<String, String> {
let browser_runner = BrowserRunner::new();
let browser_type =
BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?;
let browser = create_browser(browser_type.clone());
// Load registry and check if already downloaded
let mut registry = DownloadedBrowsersRegistry::load()
.map_err(|e| format!("Failed to load browser registry: {e}"))?;
if registry.is_browser_downloaded(&browser_str, &version) {
return Ok(version);
}
// Check if browser is supported on current platform before attempting download
let version_service = BrowserVersionService::new();
if !version_service
.is_browser_supported(&browser_str)
.unwrap_or(false)
{
return Err(format!(
"Browser '{}' is not supported on your platform ({} {}). Supported browsers: {}",
browser_str,
std::env::consts::OS,
std::env::consts::ARCH,
version_service.get_supported_browsers().join(", ")
));
}
let download_info = version_service
.get_download_info(&browser_str, &version)
.map_err(|e| format!("Failed to get download info: {e}"))?;
// Create browser directory
let mut browser_dir = browser_runner.get_binaries_dir();
browser_dir.push(browser_type.as_str());
browser_dir.push(&version);
// Clean up any failed previous download
if let Err(e) = registry.cleanup_failed_download(&browser_str, &version) {
println!("Warning: Failed to cleanup previous download: {e}");
}
create_dir_all(&browser_dir).map_err(|e| format!("Failed to create browser directory: {e}"))?;
// Mark download as started in registry
registry.mark_download_started(&browser_str, &version, browser_dir.clone());
registry
.save()
.map_err(|e| format!("Failed to save registry: {e}"))?;
// Use the new download module
let downloader = Downloader::new();
let download_path = match downloader
.download_browser(
&app_handle,
browser_type.clone(),
&version,
&download_info,
&browser_dir,
)
browser_runner
.download_browser_impl(app_handle, browser_str, version)
.await
{
Ok(path) => path,
Err(e) => {
// Clean up failed download
let _ = registry.cleanup_failed_download(&browser_str, &version);
let _ = registry.save();
return Err(format!("Failed to download browser: {e}"));
}
};
// Use the new extraction module
if download_info.is_archive {
let extractor = Extractor::new();
match extractor
.extract_browser(
&app_handle,
browser_type.clone(),
&version,
&download_path,
&browser_dir,
)
.await
{
Ok(_) => {
// Clean up the downloaded archive
if let Err(e) = std::fs::remove_file(&download_path) {
println!("Warning: Could not delete archive file: {e}");
}
}
Err(e) => {
// Clean up failed download
let _ = registry.cleanup_failed_download(&browser_str, &version);
let _ = registry.save();
return Err(format!("Failed to extract browser: {e}"));
}
}
// Give filesystem a moment to settle after extraction
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
// Emit verification progress
let progress = DownloadProgress {
browser: browser_str.clone(),
version: version.clone(),
downloaded_bytes: 0,
total_bytes: None,
percentage: 100.0,
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: "verifying".to_string(),
};
let _ = app_handle.emit("download-progress", &progress);
// Verify the browser was downloaded correctly
println!("Verifying download for browser: {browser_str}, version: {version}");
// Use the browser's own verification method
let binaries_dir = browser_runner.get_binaries_dir();
if !browser.is_version_downloaded(&version, &binaries_dir) {
let _ = registry.cleanup_failed_download(&browser_str, &version);
let _ = registry.save();
return Err("Browser download completed but verification failed".to_string());
}
// Mark download as completed in registry
let actual_version = if browser_str == "chromium" {
Some(version.clone())
} else {
None
};
registry
.mark_download_completed_with_actual_version(&browser_str, &version, actual_version)
.map_err(|e| format!("Failed to mark download as completed: {e}"))?;
registry
.save()
.map_err(|e| format!("Failed to save registry: {e}"))?;
// Emit completion
let progress = DownloadProgress {
browser: browser_str.clone(),
version: version.clone(),
downloaded_bytes: 0,
total_bytes: None,
percentage: 100.0,
speed_bytes_per_sec: 0.0,
eta_seconds: Some(0.0),
stage: "completed".to_string(),
};
let _ = app_handle.emit("download-progress", &progress);
Ok(version)
.map_err(|e| format!("Failed to download browser: {e}"))
}
#[tauri::command]
pub fn is_browser_downloaded(browser_str: String, version: String) -> bool {
if let Ok(registry) = DownloadedBrowsersRegistry::load() {
if registry.is_browser_downloaded(&browser_str, &version) {
return true;
}
}
let browser_type = BrowserType::from_str(&browser_str).expect("Invalid browser type");
let browser_runner = BrowserRunner::new();
let browser = create_browser(browser_type.clone());
let binaries_dir = browser_runner.get_binaries_dir();
browser.is_version_downloaded(&version, &binaries_dir)
browser_runner.is_browser_downloaded(&browser_str, &version)
}
#[tauri::command]
@@ -2700,7 +2883,27 @@ pub async fn get_browser_release_types(
service
.get_browser_release_types(&browser_str)
.await
.map_err(|e| format!("Failed to get browser release types: {e}"))
.map_err(|e| format!("Failed to get release types: {e}"))
}
#[tauri::command]
pub async fn check_missing_binaries() -> Result<Vec<(String, String, String)>, String> {
let browser_runner = BrowserRunner::new();
browser_runner
.check_missing_binaries()
.await
.map_err(|e| format!("Failed to check missing binaries: {e}"))
}
#[tauri::command]
pub async fn ensure_all_binaries_exist(
app_handle: tauri::AppHandle,
) -> Result<Vec<String>, String> {
let browser_runner = BrowserRunner::new();
browser_runner
.ensure_all_binaries_exist(&app_handle)
.await
.map_err(|e| format!("Failed to ensure all binaries exist: {e}"))
}
#[cfg(test)]
+2 -2
View File
@@ -535,7 +535,7 @@ pub async fn open_url_with_profile(
// Use launch_or_open_url which handles both launching new instances and opening in existing ones
runner
.launch_or_open_url(app_handle, &profile, Some(url.clone()))
.launch_or_open_url(app_handle, &profile, Some(url.clone()), None)
.await
.map_err(|e| {
println!("Failed to open URL with profile '{profile_name}': {e}");
@@ -612,7 +612,7 @@ pub async fn smart_open_url(
// Try to open the URL with this running profile
match runner
.launch_or_open_url(app_handle.clone(), profile, Some(url.clone()))
.launch_or_open_url(app_handle.clone(), profile, Some(url.clone()), None)
.await
{
Ok(_) => {
+30 -8
View File
@@ -79,15 +79,29 @@ impl Downloader {
}
BrowserType::Zen => {
// For Zen, verify the asset exists and handle different naming patterns
let releases = self
.api_client
.fetch_zen_releases_with_caching(true)
.await?;
let releases = match self.api_client.fetch_zen_releases_with_caching(true).await {
Ok(releases) => releases,
Err(e) => {
eprintln!("Failed to fetch Zen releases: {e}");
return Err(format!("Failed to fetch Zen releases from GitHub API: {e}. This might be due to GitHub API rate limiting or network issues. Please try again later.").into());
}
};
let release = releases
.iter()
.find(|r| r.tag_name == version)
.ok_or(format!("Zen version {version} not found"))?;
.ok_or_else(|| {
format!(
"Zen version {} not found. Available versions: {}",
version,
releases
.iter()
.take(5)
.map(|r| r.tag_name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
})?;
// Get platform and architecture info
let (os, arch) = Self::get_platform_info();
@@ -95,9 +109,17 @@ impl Downloader {
// Find the appropriate asset
let asset_url = self
.find_zen_asset(&release.assets, &os, &arch)
.ok_or(format!(
"No compatible asset found for Zen version {version} on {os}/{arch}"
))?;
.ok_or_else(|| {
let available_assets: Vec<&str> =
release.assets.iter().map(|a| a.name.as_str()).collect();
format!(
"No compatible asset found for Zen version {} on {}/{}. Available assets: {}",
version,
os,
arch,
available_assets.join(", ")
)
})?;
Ok(asset_url)
}
+61 -1
View File
@@ -189,8 +189,22 @@ impl DownloadedBrowsersRegistry {
let mut to_remove = Vec::new();
for (browser, versions) in &self.browsers {
for (version, info) in versions {
// Only remove verified downloads that are not used by any active profile
if info.verified && !active_set.contains(&(browser.clone(), version.clone())) {
to_remove.push((browser.clone(), version.clone()));
// Double-check that this browser+version is truly not in use
// by looking for exact matches in the active profiles
let is_in_use = active_profiles
.iter()
.any(|(active_browser, active_version)| {
active_browser == browser && active_version == version
});
if !is_in_use {
to_remove.push((browser.clone(), version.clone()));
println!("Marking for removal: {browser} {version} (not used by any profile)");
} else {
println!("Keeping: {browser} {version} (in use by profile)");
}
}
}
}
@@ -201,9 +215,16 @@ impl DownloadedBrowsersRegistry {
eprintln!("Failed to cleanup unused binary {browser}:{version}: {e}");
} else {
cleaned_up.push(format!("{browser} {version}"));
println!("Successfully removed unused binary: {browser} {version}");
}
}
if cleaned_up.is_empty() {
println!("No unused binaries found to clean up");
} else {
println!("Cleaned up {} unused binaries", cleaned_up.len());
}
Ok(cleaned_up)
}
@@ -217,6 +238,45 @@ impl DownloadedBrowsersRegistry {
.map(|profile| (profile.browser.clone(), profile.version.clone()))
.collect()
}
/// Verify that all registered browsers actually exist on disk and clean up stale entries
pub fn verify_and_cleanup_stale_entries(
&mut self,
browser_runner: &crate::browser_runner::BrowserRunner,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
use crate::browser::{create_browser, BrowserType};
let mut cleaned_up = Vec::new();
let binaries_dir = browser_runner.get_binaries_dir();
let browsers_to_check: Vec<(String, String)> = self
.browsers
.iter()
.flat_map(|(browser, versions)| {
versions
.keys()
.map(|version| (browser.clone(), version.clone()))
})
.collect();
for (browser_str, version) in browsers_to_check {
if let Ok(browser_type) = BrowserType::from_str(&browser_str) {
let browser = create_browser(browser_type);
if !browser.is_version_downloaded(&version, &binaries_dir) {
// Files don't exist, remove from registry
if let Some(_removed) = self.remove_browser(&browser_str, &version) {
cleaned_up.push(format!("{browser_str} {version}"));
println!("Removed stale registry entry for {browser_str} {version}");
}
}
}
}
if !cleaned_up.is_empty() {
self.save()?;
}
Ok(cleaned_up)
}
}
#[cfg(test)]
+8 -6
View File
@@ -26,12 +26,12 @@ mod version_updater;
extern crate lazy_static;
use browser_runner::{
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_browser_release_types,
get_downloaded_browser_versions, get_supported_browsers, is_browser_supported_on_platform,
kill_browser_profile, launch_browser_profile, list_browser_profiles, rename_profile,
update_profile_proxy, update_profile_version,
check_browser_exists, check_browser_status, check_missing_binaries, create_browser_profile_new,
delete_profile, download_browser, ensure_all_binaries_exist, fetch_browser_versions_cached_first,
fetch_browser_versions_with_count, fetch_browser_versions_with_count_cached_first,
get_browser_release_types, 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::{
@@ -374,6 +374,8 @@ pub fn run() {
get_system_theme,
detect_existing_profiles,
import_browser_profile,
check_missing_binaries,
ensure_all_binaries_exist,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
-3
View File
@@ -29,8 +29,6 @@ pub struct AppSettings {
pub show_settings_on_startup: bool,
#[serde(default = "default_theme")]
pub theme: String, // "light", "dark", or "system"
#[serde(default)]
pub auto_delete_unused_binaries: bool,
}
fn default_theme() -> String {
@@ -43,7 +41,6 @@ impl Default for AppSettings {
set_as_default_browser: false,
show_settings_on_startup: true,
theme: "system".to_string(),
auto_delete_unused_binaries: true,
}
}
}
+57 -98
View File
@@ -263,149 +263,108 @@ impl VersionUpdater {
&self,
app_handle: &tauri::AppHandle,
) -> Result<Vec<BackgroundUpdateResult>, Box<dyn std::error::Error + Send + Sync>> {
println!("Starting background version update for all browsers");
let all_browsers = [
"firefox",
"firefox-developer",
"mullvad-browser",
"zen",
"brave",
"chromium",
"tor-browser",
];
// Filter browsers to only include those supported on the current platform
let browsers: Vec<&str> = all_browsers
.iter()
.filter(|browser| {
self
.version_service
.is_browser_supported(browser)
.unwrap_or(false)
})
.copied()
.collect();
let total_browsers = browsers.len();
let supported_browsers = self.version_service.get_supported_browsers();
let total_browsers = supported_browsers.len();
let mut results = Vec::new();
let mut total_new_versions = 0;
println!(
"Updating {} supported browsers (filtered from {} total)",
browsers.len(),
all_browsers.len()
);
// Emit start event
let progress = VersionUpdateProgress {
current_browser: "".to_string(),
// Emit initial progress
let initial_progress = VersionUpdateProgress {
current_browser: String::new(),
total_browsers,
completed_browsers: 0,
new_versions_found: 0,
browser_new_versions: 0,
status: "updating".to_string(),
};
if let Err(e) = app_handle.emit("version-update-progress", &progress) {
eprintln!("Failed to emit start progress: {e}");
} else {
println!("Emitted start progress event");
if let Err(e) = app_handle.emit("version-update-progress", &initial_progress) {
eprintln!("Failed to emit initial progress: {e}");
}
for (index, browser) in browsers.iter().enumerate() {
println!(
"Processing browser {} ({}/{}): {}",
browser,
index + 1,
total_browsers,
browser
);
for (index, browser) in supported_browsers.iter().enumerate() {
println!("Updating browser versions for: {browser}");
// Emit progress for current browser
// Emit progress update for current browser
let progress = VersionUpdateProgress {
current_browser: browser.to_string(),
current_browser: browser.clone(),
total_browsers,
completed_browsers: index,
new_versions_found: total_new_versions,
browser_new_versions: 0,
status: "updating".to_string(),
};
if let Err(e) = app_handle.emit("version-update-progress", &progress) {
eprintln!("Failed to emit progress for {browser}: {e}");
} else {
println!("Emitted progress event for browser: {browser}");
}
if !self.version_service.should_update_cache(browser) {
println!("Skipping {browser} - cache is still fresh");
let browser_result = BackgroundUpdateResult {
browser: browser.to_string(),
new_versions_count: 0,
total_versions_count: 0,
updated_successfully: true,
error: None,
};
results.push(browser_result);
continue;
}
println!("Fetching new versions for browser: {browser}");
let result = self.update_browser_versions(browser).await;
match result {
Ok(new_count) => {
total_new_versions += new_count;
let browser_result = BackgroundUpdateResult {
browser: browser.to_string(),
new_versions_count: new_count,
total_versions_count: 0, // We'll update this if needed
match self.update_browser_versions(browser).await {
Ok(new_versions_count) => {
results.push(BackgroundUpdateResult {
browser: browser.clone(),
new_versions_count,
total_versions_count: 0, // We don't track total for background updates
updated_successfully: true,
error: None,
};
results.push(browser_result);
});
println!("Found {new_count} new versions for {browser}");
total_new_versions += new_versions_count;
// Emit progress update with new versions found
let progress = VersionUpdateProgress {
current_browser: browser.clone(),
total_browsers,
completed_browsers: index,
new_versions_found: total_new_versions,
browser_new_versions: new_versions_count,
status: "updating".to_string(),
};
if let Err(e) = app_handle.emit("version-update-progress", &progress) {
eprintln!("Failed to emit progress with versions for {browser}: {e}");
}
}
Err(e) => {
eprintln!("Failed to update versions for {browser}: {e}");
let browser_result = BackgroundUpdateResult {
browser: browser.to_string(),
results.push(BackgroundUpdateResult {
browser: browser.clone(),
new_versions_count: 0,
total_versions_count: 0,
updated_successfully: false,
error: Some(e.to_string()),
};
results.push(browser_result);
});
}
}
// Small delay between browsers to avoid overwhelming APIs
tokio::time::sleep(Duration::from_millis(200)).await;
}
self
.auto_updater
.check_for_updates_with_progress(app_handle)
.await;
// Emit completion event
let progress = VersionUpdateProgress {
current_browser: "".to_string(),
// Emit completion
let final_progress = VersionUpdateProgress {
current_browser: String::new(),
total_browsers,
completed_browsers: total_browsers,
new_versions_found: total_new_versions,
browser_new_versions: 0,
status: "completed".to_string(),
};
if let Err(e) = app_handle.emit("version-update-progress", &progress) {
if let Err(e) = app_handle.emit("version-update-progress", &final_progress) {
eprintln!("Failed to emit completion progress: {e}");
} else {
println!("Emitted completion progress event");
}
println!("Version update completed. Found {total_new_versions} new versions total");
// After all version updates are complete, trigger auto-update check
if total_new_versions > 0 {
println!(
"Found {total_new_versions} new versions across all browsers. Checking for auto-updates..."
);
// Trigger auto-update check which will automatically download browsers
self
.auto_updater
.check_for_updates_with_progress(app_handle)
.await;
} else {
println!("No new versions found, skipping auto-update check");
}
Ok(results)
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut Browser",
"version": "0.5.1",
"version": "0.5.4",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm dev",
+74 -2
View File
@@ -76,12 +76,59 @@ export default function Home() {
"list_browser_profiles",
);
setProfiles(profileList);
// Check for missing binaries after loading profiles
await checkMissingBinaries();
} catch (err: unknown) {
console.error("Failed to load profiles:", err);
setError(`Failed to load profiles: ${JSON.stringify(err)}`);
}
}, []);
// Check for missing binaries and offer to download them
const checkMissingBinaries = useCallback(async () => {
try {
const missingBinaries = await invoke<[string, string, string][]>(
"check_missing_binaries",
);
if (missingBinaries.length > 0) {
console.log("Found missing binaries:", missingBinaries);
// Show a toast notification about missing binaries and auto-download them
const missingList = missingBinaries
.map(
([profileName, browser, version]) =>
`${browser} ${version} (for ${profileName})`,
)
.join(", ");
console.log(`Downloading missing binaries: ${missingList}`);
try {
const downloaded = await invoke<string[]>(
"ensure_all_binaries_exist",
);
if (downloaded.length > 0) {
console.log(
"Successfully downloaded missing binaries:",
downloaded,
);
}
} catch (downloadError) {
console.error("Failed to download missing binaries:", downloadError);
setError(
`Failed to download missing binaries: ${JSON.stringify(
downloadError,
)}`,
);
}
}
} catch (err: unknown) {
console.error("Failed to check missing binaries:", err);
}
}, []);
// Version updater for handling version fetching progress events and auto-updates
useVersionUpdater();
@@ -99,11 +146,12 @@ export default function Home() {
// Check for updates after loading profiles
await checkForUpdates();
await checkMissingBinaries();
} catch (err: unknown) {
console.error("Failed to load profiles:", err);
setError(`Failed to load profiles: ${JSON.stringify(err)}`);
}
}, [checkForUpdates]);
}, [checkForUpdates, checkMissingBinaries]);
useAppUpdateNotifications();
@@ -439,12 +487,36 @@ export default function Home() {
const handleDeleteProfile = useCallback(
async (profile: BrowserProfile) => {
setError(null);
console.log("Attempting to delete profile:", profile.name);
try {
// First check if the browser is running for this profile
const isRunning = await invoke<boolean>("check_browser_status", {
profile,
});
if (isRunning) {
setError(
"Cannot delete profile while browser is running. Please stop the browser first.",
);
return;
}
// Attempt to delete the profile
await invoke("delete_profile", { profileName: profile.name });
console.log("Profile deletion command completed successfully");
// Give a small delay to ensure file system operations complete
await new Promise((resolve) => setTimeout(resolve, 100));
// Reload profiles to ensure UI is updated
await loadProfiles();
console.log("Profile deleted and profiles reloaded successfully");
} catch (err: unknown) {
console.error("Failed to delete profile:", err);
setError(`Failed to delete profile: ${JSON.stringify(err)}`);
const errorMessage = err instanceof Error ? err.message : String(err);
setError(`Failed to delete profile: ${errorMessage}`);
}
},
[loadProfiles],
+11 -26
View File
@@ -35,20 +35,20 @@ export function AppUpdateToast({
};
return (
<div className="flex items-start w-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 shadow-lg max-w-md">
<div className="flex items-start p-4 w-full max-w-md 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">
{isUpdating ? (
<LuRefreshCw className="h-5 w-5 text-blue-500 animate-spin flex-shrink-0" />
<LuRefreshCw className="flex-shrink-0 w-5 h-5 text-blue-500 animate-spin" />
) : (
<FaDownload className="h-5 w-5 text-blue-500 flex-shrink-0" />
<FaDownload className="flex-shrink-0 w-5 h-5 text-blue-500" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="flex gap-2 justify-between items-start">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-semibold text-foreground text-sm">
<div className="flex gap-2 items-center">
<span className="text-sm font-semibold text-foreground">
Donut Browser Update Available
</span>
<Badge
@@ -69,9 +69,9 @@ export function AppUpdateToast({
variant="ghost"
size="sm"
onClick={onDismiss}
className="h-6 w-6 p-0 shrink-0"
className="p-0 w-6 h-6 shrink-0"
>
<FaTimes className="h-3 w-3" />
<FaTimes className="w-3 h-3" />
</Button>
)}
</div>
@@ -83,13 +83,13 @@ export function AppUpdateToast({
)}
{!isUpdating && (
<div className="flex items-center gap-2 mt-3">
<div className="flex gap-2 items-center mt-3">
<Button
onClick={() => void handleUpdateClick()}
size="sm"
className="flex items-center gap-2 text-xs"
className="flex gap-2 items-center text-xs"
>
<FaDownload className="h-3 w-3" />
<FaDownload className="w-3 h-3" />
Update Now
</Button>
<Button
@@ -102,21 +102,6 @@ export function AppUpdateToast({
</Button>
</div>
)}
{updateInfo.release_notes && !isUpdating && (
<div className="mt-2">
<details className="text-xs">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
Release Notes
</summary>
<div className="mt-1 text-muted-foreground whitespace-pre-wrap max-h-32 overflow-y-auto">
{updateInfo.release_notes.length > 200
? `${updateInfo.release_notes.substring(0, 200)}...`
: updateInfo.release_notes}
</div>
</details>
</div>
)}
</div>
</div>
);
+78 -14
View File
@@ -164,13 +164,60 @@ export function ChangeVersionDialog({
</div>
</div>
{!releaseTypes.stable || !releaseTypes.nightly ? (
{!releaseTypes.stable && !releaseTypes.nightly ? (
<Alert>
<AlertDescription>
Only {profile.release_type} releases are available for{" "}
No releases are available for{" "}
{getBrowserDisplayName(profile.browser)}.
</AlertDescription>
</Alert>
) : !releaseTypes.stable || !releaseTypes.nightly ? (
<div className="space-y-4">
<Alert>
<AlertDescription>
Only {profile.release_type} releases are available for{" "}
{getBrowserDisplayName(profile.browser)}.
</AlertDescription>
</Alert>
<div className="grid gap-2">
<Label>New Release Type</Label>
{isLoadingReleaseTypes ? (
<div className="text-sm text-muted-foreground">
Loading release types...
</div>
) : (
<div className="space-y-4">
{selectedReleaseType &&
selectedReleaseType !== profile.release_type &&
selectedVersion &&
!isVersionDownloaded(selectedVersion) && (
<Alert>
<AlertDescription>
You must download{" "}
{getBrowserDisplayName(profile.browser)}{" "}
{selectedVersion} before switching to this release
type. Use the download button above to get the
latest version.
</AlertDescription>
</Alert>
)}
<ReleaseTypeSelector
selectedReleaseType={selectedReleaseType}
onReleaseTypeSelect={setSelectedReleaseType}
availableReleaseTypes={releaseTypes}
browser={profile.browser}
isDownloading={isDownloading}
onDownload={() => {
void handleDownload();
}}
placeholder="Select release type..."
downloadedVersions={downloadedVersions}
/>
</div>
)}
</div>
</div>
) : (
<div className="grid gap-2">
<Label>New Release Type</Label>
@@ -179,18 +226,35 @@ export function ChangeVersionDialog({
Loading release types...
</div>
) : (
<ReleaseTypeSelector
selectedReleaseType={selectedReleaseType}
onReleaseTypeSelect={setSelectedReleaseType}
availableReleaseTypes={releaseTypes}
browser={profile.browser}
isDownloading={isDownloading}
onDownload={() => {
void handleDownload();
}}
placeholder="Select release type..."
downloadedVersions={downloadedVersions}
/>
<div className="space-y-4">
{selectedReleaseType &&
selectedReleaseType !== profile.release_type &&
selectedVersion &&
!isVersionDownloaded(selectedVersion) && (
<Alert>
<AlertDescription>
You must download{" "}
{getBrowserDisplayName(profile.browser)}{" "}
{selectedVersion} before switching to this release
type. Use the download button above to get the latest
version.
</AlertDescription>
</Alert>
)}
<ReleaseTypeSelector
selectedReleaseType={selectedReleaseType}
onReleaseTypeSelect={setSelectedReleaseType}
availableReleaseTypes={releaseTypes}
browser={profile.browser}
isDownloading={isDownloading}
onDownload={() => {
void handleDownload();
}}
placeholder="Select release type..."
downloadedVersions={downloadedVersions}
/>
</div>
)}
</div>
)}
+35 -22
View File
@@ -93,6 +93,7 @@ export function CreateProfileDialog({
isDownloading,
downloadedVersions,
loadDownloadedVersions,
isVersionDownloaded,
} = useBrowserDownload();
const {
@@ -280,6 +281,7 @@ export function CreateProfileDialog({
selectedBrowser &&
selectedReleaseType &&
selectedVersion &&
isVersionDownloaded(selectedVersion) &&
(!proxyEnabled || isProxyDisabled || (proxyHost && proxyPort)) &&
!nameError;
@@ -370,37 +372,48 @@ export function CreateProfileDialog({
</Select>
</div>
{selectedBrowser &&
(!releaseTypes.stable || !releaseTypes.nightly) ? (
<Alert>
<AlertDescription>
Only {(releaseTypes.stable && "Stable") ?? "Nightly"} releases
are available for {getBrowserDisplayName(selectedBrowser)}.
</AlertDescription>
</Alert>
) : (
{selectedBrowser ? (
<div className="grid gap-2">
<Label>Release Type</Label>
{isLoadingReleaseTypes ? (
<div className="text-sm text-muted-foreground">
Loading release types...
</div>
) : Object.keys(releaseTypes).length === 0 ? (
<Alert>
<AlertDescription>
No releases are available for{" "}
{getBrowserDisplayName(selectedBrowser)}.
</AlertDescription>
</Alert>
) : (
<ReleaseTypeSelector
selectedReleaseType={selectedReleaseType}
onReleaseTypeSelect={setSelectedReleaseType}
availableReleaseTypes={releaseTypes}
browser={selectedBrowser ?? ""}
isDownloading={isDownloading}
onDownload={() => {
void handleDownload();
}}
placeholder="Select release type..."
downloadedVersions={downloadedVersions}
/>
<div className="space-y-4">
{(!releaseTypes.stable || !releaseTypes.nightly) && (
<Alert>
<AlertDescription>
Only {(releaseTypes.stable && "Stable") ?? "Nightly"}{" "}
releases are available for{" "}
{getBrowserDisplayName(selectedBrowser)}.
</AlertDescription>
</Alert>
)}
<ReleaseTypeSelector
selectedReleaseType={selectedReleaseType}
onReleaseTypeSelect={setSelectedReleaseType}
availableReleaseTypes={releaseTypes}
browser={selectedBrowser}
isDownloading={isDownloading}
onDownload={() => {
void handleDownload();
}}
placeholder="Select release type..."
downloadedVersions={downloadedVersions}
/>
</div>
)}
</div>
)}
) : null}
{/* Proxy Settings */}
<div className="grid gap-4 pt-4 border-t">
+102 -68
View File
@@ -55,6 +55,16 @@ export function ReleaseTypeSelector({
: []),
];
// Only show dropdown if there are multiple release types available
const showDropdown = releaseOptions.length > 1;
// If only one release type is available, auto-select it
if (!showDropdown && releaseOptions.length === 1 && !selectedReleaseType) {
setTimeout(() => {
onReleaseTypeSelect(releaseOptions[0].type);
}, 0);
}
const selectedDisplayText = selectedReleaseType
? selectedReleaseType === "stable"
? "Stable"
@@ -73,75 +83,99 @@ export function ReleaseTypeSelector({
return (
<div className="space-y-4">
<Popover open={popoverOpen} onOpenChange={setPopoverOpen} modal={true}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={popoverOpen}
className="justify-between w-full"
>
{selectedDisplayText}
<LuChevronsUpDown className="ml-2 w-4 h-4 opacity-50 shrink-0" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandEmpty>No release types available.</CommandEmpty>
<CommandList>
<CommandGroup>
{releaseOptions.map((option) => {
const isDownloaded = downloadedVersions.includes(
option.version,
);
return (
<CommandItem
key={option.type}
value={option.type}
onSelect={(currentValue) => {
const selectedType = currentValue as
| "stable"
| "nightly";
onReleaseTypeSelect(
selectedType === selectedReleaseType
? null
: selectedType,
);
setPopoverOpen(false);
}}
>
<LuCheck
className={cn(
"mr-2 h-4 w-4",
selectedReleaseType === option.type
? "opacity-100"
: "opacity-0",
)}
/>
<div className="flex gap-2 items-center">
<span className="capitalize">{option.type}</span>
{option.type === "nightly" && (
<Badge variant="secondary" className="text-xs">
Nightly
{showDropdown ? (
<Popover open={popoverOpen} onOpenChange={setPopoverOpen} modal={true}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={popoverOpen}
className="justify-between w-full"
>
{selectedDisplayText}
<LuChevronsUpDown className="ml-2 w-4 h-4 opacity-50 shrink-0" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandEmpty>No release types available.</CommandEmpty>
<CommandList>
<CommandGroup>
{releaseOptions.map((option) => {
const isDownloaded = downloadedVersions.includes(
option.version,
);
return (
<CommandItem
key={option.type}
value={option.type}
onSelect={(currentValue) => {
const selectedType = currentValue as
| "stable"
| "nightly";
onReleaseTypeSelect(
selectedType === selectedReleaseType
? null
: selectedType,
);
setPopoverOpen(false);
}}
>
<LuCheck
className={cn(
"mr-2 h-4 w-4",
selectedReleaseType === option.type
? "opacity-100"
: "opacity-0",
)}
/>
<div className="flex gap-2 items-center">
<span className="capitalize">{option.type}</span>
{option.type === "nightly" && (
<Badge variant="secondary" className="text-xs">
Nightly
</Badge>
)}
<Badge variant="outline" className="text-xs">
{option.version}
</Badge>
)}
<Badge variant="outline" className="text-xs">
{option.version}
</Badge>
{isDownloaded && (
<Badge variant="default" className="text-xs">
Downloaded
</Badge>
)}
</div>
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{isDownloaded && (
<Badge variant="default" className="text-xs">
Downloaded
</Badge>
)}
</div>
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
// Show a simple display when only one release type is available
releaseOptions.length === 1 && (
<div className="flex gap-2 items-center justify-center p-3 border rounded-md bg-muted/50">
<span className="capitalize text-sm font-medium">
{releaseOptions[0].type}
</span>
{releaseOptions[0].type === "nightly" && (
<Badge variant="secondary" className="text-xs">
Nightly
</Badge>
)}
<Badge variant="outline" className="text-xs">
{releaseOptions[0].version}
</Badge>
{downloadedVersions.includes(releaseOptions[0].version) && (
<Badge variant="default" className="text-xs">
Downloaded
</Badge>
)}
</div>
)
)}
{showDownloadButton &&
selectedReleaseType &&
+1 -33
View File
@@ -31,7 +31,6 @@ interface AppSettings {
set_as_default_browser: boolean;
show_settings_on_startup: boolean;
theme: string;
auto_delete_unused_binaries: boolean;
}
interface PermissionInfo {
@@ -50,13 +49,11 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
set_as_default_browser: false,
show_settings_on_startup: true,
theme: "system",
auto_delete_unused_binaries: true,
});
const [originalSettings, setOriginalSettings] = useState<AppSettings>({
set_as_default_browser: false,
show_settings_on_startup: true,
theme: "system",
auto_delete_unused_binaries: true,
});
const [isDefaultBrowser, setIsDefaultBrowser] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@@ -287,9 +284,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
const hasChanges =
settings.show_settings_on_startup !==
originalSettings.show_settings_on_startup ||
settings.theme !== originalSettings.theme ||
settings.auto_delete_unused_binaries !==
originalSettings.auto_delete_unused_binaries;
settings.theme !== originalSettings.theme;
return (
<Dialog open={isOpen} onOpenChange={onClose}>
@@ -358,33 +353,6 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
</p>
</div>
{/* Auto-Update Section */}
<div className="space-y-4">
<Label className="text-base font-medium">Auto-Updates</Label>
<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. Unused
binaries will be automatically deleted to save disk space.
</p>
</div>
{/* Startup Behavior Section */}
<div className="space-y-4">
<Label className="text-base font-medium">Startup Behavior</Label>
-107
View File
@@ -1,107 +0,0 @@
"use client";
/* eslint-disable @typescript-eslint/no-misused-promises */
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { getBrowserDisplayName } from "@/lib/browser-utils";
import React from "react";
import { FaDownload, FaTimes } from "react-icons/fa";
interface UpdateNotification {
id: string;
browser: string;
current_version: string;
new_version: string;
affected_profiles: string[];
is_stable_update: boolean;
timestamp: number;
}
interface UpdateNotificationProps {
notification: UpdateNotification;
onUpdate: (browser: string, newVersion: string) => Promise<void>;
onDismiss: (notificationId: string) => Promise<void>;
isUpdating?: boolean;
}
export function UpdateNotificationComponent({
notification,
onUpdate,
onDismiss,
isUpdating = false,
}: UpdateNotificationProps) {
const browserDisplayName = getBrowserDisplayName(notification.browser);
const profileText =
notification.affected_profiles.length === 1
? `profile "${notification.affected_profiles[0]}"`
: `${notification.affected_profiles.length} profiles`;
const handleUpdateClick = async () => {
// Dismiss the notification immediately to close the modal
await onDismiss(notification.id);
// Then start the update process
await onUpdate(notification.browser, notification.new_version);
};
return (
<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 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" : "Nightly"}
</Badge>
</div>
<div className="text-sm text-muted-foreground">
Update {profileText} from {notification.current_version} to{" "}
<span className="font-medium">{notification.new_version}</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={async () => {
await onDismiss(notification.id);
}}
className="p-0 w-6 h-6 shrink-0"
>
<FaTimes className="w-3 h-3" />
</Button>
</div>
<div className="flex gap-2 items-center">
<Button
onClick={handleUpdateClick}
disabled={isUpdating}
size="sm"
className="flex gap-2 items-center"
>
<FaDownload className="w-3 h-3" />
Update
</Button>
<Button
variant="outline"
onClick={async () => {
await onDismiss(notification.id);
}}
size="sm"
>
Later
</Button>
</div>
{notification.affected_profiles.length > 1 && (
<div className="text-xs text-muted-foreground">
Affected profiles: {notification.affected_profiles.join(", ")}
</div>
)}
</div>
);
}
-156
View File
@@ -1,156 +0,0 @@
"use client";
import { LoadingButton } from "@/components/loading-button";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { useState } from "react";
import { LuDownload } from "react-icons/lu";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
import { ScrollArea } from "./ui/scroll-area";
interface GithubRelease {
tag_name: string;
assets: {
name: string;
browser_download_url: string;
hash?: string;
}[];
published_at: string;
is_nightly: boolean;
}
interface VersionSelectorProps {
selectedVersion: string | null;
onVersionSelect: (version: string | null) => void;
availableVersions: GithubRelease[];
downloadedVersions: string[];
isDownloading: boolean;
onDownload: () => void;
placeholder?: string;
showDownloadButton?: boolean;
}
export function VersionSelector({
selectedVersion,
onVersionSelect,
availableVersions,
downloadedVersions,
isDownloading,
onDownload,
placeholder = "Select version...",
showDownloadButton = true,
}: VersionSelectorProps) {
const [versionPopoverOpen, setVersionPopoverOpen] = useState(false);
const isVersionDownloaded = selectedVersion
? downloadedVersions.includes(selectedVersion)
: false;
return (
<div className="space-y-4">
<Popover
open={versionPopoverOpen}
onOpenChange={setVersionPopoverOpen}
modal={true}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={versionPopoverOpen}
className="justify-between w-full"
>
{selectedVersion ?? placeholder}
<LuChevronsUpDown className="ml-2 w-4 h-4 opacity-50 shrink-0" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandInput placeholder="Search versions..." />
<CommandEmpty>No versions found.</CommandEmpty>
<CommandList>
<ScrollArea
className={
"[&>[data-radix-scroll-area-viewport]]:max-h-[200px]"
}
>
<CommandGroup>
{availableVersions.map((version) => {
const isDownloaded = downloadedVersions.includes(
version.tag_name,
);
return (
<CommandItem
key={version.tag_name}
value={version.tag_name}
onSelect={(currentValue) => {
onVersionSelect(
currentValue === selectedVersion
? null
: currentValue,
);
setVersionPopoverOpen(false);
}}
>
<LuCheck
className={cn(
"mr-2 h-4 w-4",
selectedVersion === version.tag_name
? "opacity-100"
: "opacity-0",
)}
/>
<div className="flex gap-2 items-center">
<span>{version.tag_name}</span>
{version.is_nightly && (
<Badge variant="secondary" className="text-xs">
Nightly
</Badge>
)}
{isDownloaded && (
<Badge variant="default" className="text-xs">
Downloaded
</Badge>
)}
</div>
</CommandItem>
);
})}
</CommandGroup>
</ScrollArea>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* Download Button */}
{showDownloadButton && selectedVersion && !isVersionDownloaded && (
<LoadingButton
isLoading={isDownloading}
onClick={() => {
onDownload();
}}
variant="outline"
className="w-full"
>
<LuDownload className="mr-2 w-4 h-4" />
{isDownloading ? "Downloading..." : "Download Browser"}
</LoadingButton>
)}
</div>
);
}
-6
View File
@@ -1,12 +1,6 @@
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);
+1 -1
View File
@@ -20,7 +20,7 @@ const loadMacOSPermissions = async () => {
export type PermissionType = "microphone" | "camera";
export interface UsePermissionsReturn {
interface UsePermissionsReturn {
requestPermission: (type: PermissionType) => Promise<void>;
isMicrophoneAccessGranted: boolean;
isCameraAccessGranted: boolean;
-11
View File
@@ -46,14 +46,3 @@ export function getBrowserIcon(browserType: string) {
return null;
}
}
/**
* Format browser name by capitalizing words and joining with spaces
* (fallback method for simple transformations)
*/
export function formatBrowserName(browserType: string): string {
return browserType
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
+13 -114
View File
@@ -2,27 +2,26 @@ import { UnifiedToast } from "@/components/custom-toast";
import React from "react";
import { toast as sonnerToast } from "sonner";
// Define toast types locally
export interface BaseToastProps {
interface BaseToastProps {
id?: string;
title: string;
description?: string;
duration?: number;
}
export interface LoadingToastProps extends BaseToastProps {
interface LoadingToastProps extends BaseToastProps {
type: "loading";
}
export interface SuccessToastProps extends BaseToastProps {
interface SuccessToastProps extends BaseToastProps {
type: "success";
}
export interface ErrorToastProps extends BaseToastProps {
interface ErrorToastProps extends BaseToastProps {
type: "error";
}
export interface DownloadToastProps extends BaseToastProps {
interface DownloadToastProps extends BaseToastProps {
type: "download";
stage?:
| "downloading"
@@ -37,7 +36,7 @@ export interface DownloadToastProps extends BaseToastProps {
};
}
export interface VersionUpdateToastProps extends BaseToastProps {
interface VersionUpdateToastProps extends BaseToastProps {
type: "version-update";
progress?: {
current: number;
@@ -47,39 +46,23 @@ export interface VersionUpdateToastProps extends BaseToastProps {
};
}
export interface FetchingToastProps extends BaseToastProps {
type: "fetching";
browserName?: string;
}
export interface TwilightUpdateToastProps extends BaseToastProps {
type: "twilight-update";
browserName?: string;
hasUpdate?: boolean;
}
export type ToastProps =
| LoadingToastProps
type ToastProps =
| SuccessToastProps
| ErrorToastProps
| DownloadToastProps
| VersionUpdateToastProps
| FetchingToastProps
| TwilightUpdateToastProps;
| LoadingToastProps
| VersionUpdateToastProps;
// Unified toast function
export function showToast(props: ToastProps & { id?: string }) {
const toastId = props.id ?? `toast-${props.type}-${Date.now()}`;
// Improved duration logic - make toasts disappear more quickly
let duration: number;
if (props.duration !== undefined) {
duration = props.duration;
} else {
switch (props.type) {
case "loading":
case "fetching":
duration = 10000; // 10 seconds instead of infinite
duration = 10000;
break;
case "download":
// Only keep infinite for active downloading, others get shorter durations
@@ -91,18 +74,15 @@ export function showToast(props: ToastProps & { id?: string }) {
duration = 20000;
}
break;
case "version-update":
duration = 15000;
break;
case "twilight-update":
duration = 10000;
break;
case "success":
duration = 3000;
break;
case "error":
duration = 10000;
break;
case "version-update":
duration = 15000;
break;
default:
duration = 5000;
}
@@ -152,22 +132,6 @@ export function showToast(props: ToastProps & { id?: string }) {
return toastId;
}
// Convenience functions for common use cases
export function showLoadingToast(
title: string,
options?: {
id?: string;
description?: string;
duration?: number;
},
) {
return showToast({
type: "loading",
title,
...options,
});
}
export function showDownloadToast(
browserName: string,
version: string,
@@ -206,45 +170,6 @@ export function showDownloadToast(
});
}
export function showVersionUpdateToast(
title: string,
options?: {
id?: string;
description?: string;
progress?: {
current: number;
total: number;
found: number;
current_browser?: string;
};
duration?: number;
},
) {
return showToast({
type: "version-update",
title,
...options,
});
}
export function showFetchingToast(
browserName: string,
options?: {
id?: string;
description?: string;
duration?: number;
},
) {
return showToast({
type: "fetching",
title: `Checking for new ${browserName} versions...`,
description:
options?.description ?? "Fetching latest release information...",
browserName,
...options,
});
}
export function showSuccessToast(
title: string,
options?: {
@@ -275,25 +200,6 @@ export function showErrorToast(
});
}
export function showTwilightUpdateToast(
browserName: string,
options?: {
id?: string;
description?: string;
hasUpdate?: boolean;
duration?: number;
},
) {
return showToast({
type: "twilight-update",
title: options?.hasUpdate
? `${browserName} twilight update available`
: `Checking for ${browserName} twilight updates...`,
browserName,
...options,
});
}
export function showAutoUpdateToast(
browserName: string,
version: string,
@@ -314,17 +220,10 @@ export function showAutoUpdateToast(
});
}
// Generic helper for dismissing toasts
export function dismissToast(id: string) {
sonnerToast.dismiss(id);
}
// Dismiss all toasts
export function dismissAllToasts() {
sonnerToast.dismiss();
}
// Add a specific function for unified version update progress
export function showUnifiedVersionUpdateToast(
title: string,
options?: {
-19
View File
@@ -43,22 +43,3 @@ export interface AppUpdateInfo {
is_nightly: boolean;
published_at: string;
}
export interface AppVersionInfo {
version: string;
is_nightly: boolean;
}
export type PermissionType = "microphone" | "camera" | "location";
export type PermissionStatus =
| "granted"
| "denied"
| "not_determined"
| "restricted";
export interface PermissionInfo {
permission_type: PermissionType;
status: PermissionStatus;
description: string;
}