Compare commits

...

56 Commits

Author SHA1 Message Date
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
zhom 08453fe9a6 build: update codeql permissions 2025-06-17 07:05:20 +04:00
zhom b486f00875 chore: version bump 2025-06-17 07:00:27 +04:00
zhom 703154b30f chore: linting 2025-06-17 07:00:18 +04:00
zhom 130f8b86d1 refactor: browser auto-update 2025-06-17 06:55:52 +04:00
zhom 607ed66e29 style: copy 2025-06-17 06:32:47 +04:00
zhom 9570b6d605 build: fix codeql permissions 2025-06-17 06:28:34 +04:00
zhom 2d92cbb0e5 refactor: fetch release information the same way for manual and automatic checks 2025-06-17 06:17:57 +04:00
zhom 251016609f refactor: change event emitting and remove sleep 2025-06-17 04:05:17 +04:00
zhom bddf796946 refactor: don't mark updates as automatic and fetch versions via version_updater only 2025-06-17 03:44:59 +04:00
zhom 8d793a6868 style: make release type selector behave the same in both creation and change modals 2025-06-17 03:42:47 +04:00
zhom 469f161293 refactor: add extra settings to not show update prompt 2025-06-17 03:24:10 +04:00
zhom 9756e64319 refactor: increase default update interval for firefox 2025-06-17 03:22:30 +04:00
zhom 800544ede9 refactor: supress all update prompts in the ui for firefox 2025-06-17 03:21:21 +04:00
zhom aa2228a8aa fix: show switch release option for correct browsers 2025-06-17 03:10:16 +04:00
zhom 432e5bff90 refactor: switch download mode from 0 to 2 on firefox 2025-06-17 03:06:50 +04:00
zhom f4b60eb6c7 chore: linting 2025-06-17 03:04:06 +04:00
zhom 30122c5781 reafctor: get cache releases first and don't return twilight in results 2025-06-17 03:00:09 +04:00
zhom b71d84fda4 refactor: improve signatures for public functions 2025-06-17 02:58:46 +04:00
zhom 859af72724 refactor: improve auto-update inside browser prevention 2025-06-17 02:57:30 +04:00
zhom 0360a89ceb style: remove 'enable automatic browser updates' setting 2025-06-17 02:46:33 +04:00
zhom cb6f744d6b style: only show the select menu if both stable and nightly releases are available 2025-06-17 02:42:03 +04:00
zhom 575d7f80b1 style: remove switch release option for zen and chromium 2025-06-17 02:40:48 +04:00
zhom d05b69ff3d build: require codeql and spellcheck to pass successfully before build starts 2025-06-16 23:28:53 +04:00
zhom 54abb11129 build: run spellcheck on build 2025-06-16 03:41:38 +04:00
zhom 04c690c750 build: run codeql before build 2025-06-16 03:30:13 +04:00
zhom 9a4be86e95 style: copy 2025-06-15 05:40:18 +04:00
zhom 6d013d86aa Merge pull request #26 from zhom/dependabot/cargo/src-tauri/rust-dependencies-f6a8cef228
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 2 updates
2025-06-14 18:26:30 +00:00
zhom 769fbf9d75 Merge pull request #25 from zhom/dependabot/github_actions/github-actions-eba975d771
ci(deps): bump stefanzweifel/git-auto-commit-action from 4 to 6 in the github-actions group
2025-06-14 18:26:09 +00:00
zhom 6e62abc601 chore: version bump 2025-06-14 22:22:17 +04:00
dependabot[bot] 8848fa8130 deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 2 updates: [serde_with](https://github.com/jonasbb/serde_with) and [serde_with_macros](https://github.com/jonasbb/serde_with).


Updates `serde_with` from 3.12.0 to 3.13.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.12.0...v3.13.0)

Updates `serde_with_macros` from 3.12.0 to 3.13.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.12.0...v3.13.0)

---
updated-dependencies:
- dependency-name: serde_with
  dependency-version: 3.13.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_with_macros
  dependency-version: 3.13.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-14 18:21:50 +00:00
zhom 1f0ecbe36e docs: use dark-themed preview for users with dark mode on 2025-06-14 22:21:26 +04:00
dependabot[bot] f83f2033fe ci(deps): bump stefanzweifel/git-auto-commit-action
Bumps the github-actions group with 1 update: [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action).


Updates `stefanzweifel/git-auto-commit-action` from 4 to 6
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v4...v6)

---
updated-dependencies:
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-14 18:02:10 +00:00
zhom 821cd4ea82 style: only allow user to switch between releases 2025-06-14 21:55:18 +04:00
zhom d3a63c37bf test: update mock 2025-06-14 19:20:51 +04:00
zhom 95cd2426c3 feat: automatically update browsers on new versions 2025-06-14 19:03:02 +04:00
zhom 5a3fb7b2b0 refactor: install chromium update after 50 new builds 2025-06-14 18:44:03 +04:00
zhom 767a0701ce refactor: don't check for nodecar dependency updates 2025-06-14 18:33:33 +04:00
zhom ec61d51c07 build: update changelog generation workflow 2025-06-14 17:33:38 +04:00
zhom 545c518a55 feat: don't spam update notification, show more concise toasts, don't fetch unsupported browser updates 2025-06-14 16:58:55 +04:00
zhom c99eee2c21 style: use proper zen icon 2025-06-14 16:08:26 +04:00
zhom 7f3683cc2e style: show 'not supported' for tor browser proxies 2025-06-14 15:57:33 +04:00
zhom baac3a533a refactor: fetch 100 latest app updates from github releases 2025-06-14 15:43:50 +04:00
zhom 5cd1774ffc chore: remove dependabot automerge workflow 2025-06-14 15:42:02 +04:00
zhom cb87641890 Merge pull request #23 from zhom/dependabot/cargo/src-tauri/rust-dependencies-af3af11ff5
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 17 updates
2025-06-14 11:34:34 +00:00
zhom 3df5ac671b Merge pull request #24 from zhom/dependabot/npm_and_yarn/frontend-dependencies-f9895d0c1f
deps(deps): bump the frontend-dependencies group with 3 updates
2025-06-14 11:34:05 +00:00
dependabot[bot] 390f79f97b deps(deps): bump the frontend-dependencies group with 3 updates
Bumps the frontend-dependencies group with 3 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [lint-staged](https://github.com/lint-staged/lint-staged) and [undici-types](https://github.com/nodejs/undici).


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

Updates `lint-staged` from 16.1.0 to 16.1.1
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v16.1.0...v16.1.1)

Updates `undici-types` from 6.21.0 to 7.8.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.21.0...v7.8.0)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
- dependency-name: lint-staged
  dependency-version: 16.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: undici-types
  dependency-version: 7.8.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-14 10:18:35 +00:00
dependabot[bot] c4dc2ed50c deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 17 updates:

| Package | From | To |
| --- | --- | --- |
| [reqwest](https://github.com/seanmonstar/reqwest) | `0.12.19` | `0.12.20` |
| [windows](https://github.com/microsoft/windows-rs) | `0.61.1` | `0.61.3` |
| [adler2](https://github.com/oyvindln/adler2) | `2.0.0` | `2.0.1` |
| [bytemuck](https://github.com/Lokathor/bytemuck) | `1.23.0` | `1.23.1` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.26` | `1.2.27` |
| [cfg-if](https://github.com/rust-lang/cfg-if) | `1.0.0` | `1.0.1` |
| [enumflags2](https://github.com/meithecatte/enumflags2) | `0.7.11` | `0.7.12` |
| [enumflags2_derive](https://github.com/meithecatte/enumflags2) | `0.7.11` | `0.7.12` |
| [hermit-abi](https://github.com/hermit-os/hermit-rs) | `0.5.1` | `0.5.2` |
| [libc](https://github.com/rust-lang/libc) | `0.2.172` | `0.2.173` |
| [memchr](https://github.com/BurntSushi/memchr) | `2.7.4` | `2.7.5` |
| [miniz_oxide](https://github.com/Frommi/miniz_oxide) | `0.8.8` | `0.8.9` |
| [plist](https://github.com/ebarnard/rust-plist) | `1.7.1` | `1.7.2` |
| [quick-xml](https://github.com/tafia/quick-xml) | `0.32.0` | `0.37.5` |
| redox_syscall | `0.5.12` | `0.5.13` |
| [rustc-demangle](https://github.com/rust-lang/rustc-demangle) | `0.1.24` | `0.1.25` |
| [windows-link](https://github.com/microsoft/windows-rs) | `0.1.1` | `0.1.3` |


Updates `reqwest` from 0.12.19 to 0.12.20
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.19...v0.12.20)

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

Updates `adler2` from 2.0.0 to 2.0.1
- [Changelog](https://github.com/oyvindln/adler2/blob/main/CHANGELOG.md)
- [Commits](https://github.com/oyvindln/adler2/commits)

Updates `bytemuck` from 1.23.0 to 1.23.1
- [Changelog](https://github.com/Lokathor/bytemuck/blob/main/changelog.md)
- [Commits](https://github.com/Lokathor/bytemuck/compare/v1.23.0...v1.23.1)

Updates `cc` from 1.2.26 to 1.2.27
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.26...cc-v1.2.27)

Updates `cfg-if` from 1.0.0 to 1.0.1
- [Release notes](https://github.com/rust-lang/cfg-if/releases)
- [Changelog](https://github.com/rust-lang/cfg-if/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cfg-if/compare/1.0.0...v1.0.1)

Updates `enumflags2` from 0.7.11 to 0.7.12
- [Release notes](https://github.com/meithecatte/enumflags2/releases)
- [Commits](https://github.com/meithecatte/enumflags2/compare/v0.7.11...v0.7.12)

Updates `enumflags2_derive` from 0.7.11 to 0.7.12
- [Release notes](https://github.com/meithecatte/enumflags2/releases)
- [Commits](https://github.com/meithecatte/enumflags2/compare/v0.7.11...v0.7.12)

Updates `hermit-abi` from 0.5.1 to 0.5.2
- [Release notes](https://github.com/hermit-os/hermit-rs/releases)
- [Commits](https://github.com/hermit-os/hermit-rs/compare/hermit-abi-0.5.1...hermit-abi-0.5.2)

Updates `libc` from 0.2.172 to 0.2.173
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.173/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.172...0.2.173)

Updates `memchr` from 2.7.4 to 2.7.5
- [Commits](https://github.com/BurntSushi/memchr/compare/2.7.4...2.7.5)

Updates `miniz_oxide` from 0.8.8 to 0.8.9
- [Changelog](https://github.com/Frommi/miniz_oxide/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Frommi/miniz_oxide/commits)

Updates `plist` from 1.7.1 to 1.7.2
- [Release notes](https://github.com/ebarnard/rust-plist/releases)
- [Commits](https://github.com/ebarnard/rust-plist/compare/v1.7.1...v1.7.2)

Updates `quick-xml` from 0.32.0 to 0.37.5
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.32.0...v0.37.5)

Updates `redox_syscall` from 0.5.12 to 0.5.13

Updates `rustc-demangle` from 0.1.24 to 0.1.25
- [Commits](https://github.com/rust-lang/rustc-demangle/commits)

Updates `windows-link` from 0.1.1 to 0.1.3
- [Release notes](https://github.com/microsoft/windows-rs/releases)
- [Commits](https://github.com/microsoft/windows-rs/commits)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-version: 0.12.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: windows
  dependency-version: 0.61.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: adler2
  dependency-version: 2.0.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: bytemuck
  dependency-version: 1.23.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.27
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cfg-if
  dependency-version: 1.0.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: enumflags2
  dependency-version: 0.7.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: enumflags2_derive
  dependency-version: 0.7.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: hermit-abi
  dependency-version: 0.5.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libc
  dependency-version: 0.2.173
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: memchr
  dependency-version: 2.7.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: miniz_oxide
  dependency-version: 0.8.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: plist
  dependency-version: 1.7.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: quick-xml
  dependency-version: 0.37.5
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: redox_syscall
  dependency-version: 0.5.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rustc-demangle
  dependency-version: 0.1.25
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: windows-link
  dependency-version: 0.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-14 09:44:26 +00:00
zhom 3b7315cc0d docs: add fossa badge 2025-06-14 03:42:52 +04:00
zhom bbd0f5df0c chore: version bump 2025-06-14 02:51:24 +04:00
zhom 8e7982bdf8 fix: properly handle urls added by system events 2025-06-14 02:11:40 +04:00
zhom 9ac662aee8 chore: use single-instance plugin as described by the docs 2025-06-13 21:40:50 +04:00
52 changed files with 2633 additions and 2288 deletions
-17
View File
@@ -20,23 +20,6 @@ updates:
prefix: "deps"
include: "scope"
# Nodecar dependencies
- package-ecosystem: "npm"
directory: "/nodecar"
schedule:
interval: "weekly"
day: "saturday"
time: "09:00"
allow:
- dependency-type: "all"
groups:
nodecar-dependencies:
patterns:
- "*"
commit-message:
prefix: "deps(nodecar)"
include: "scope"
# Rust dependencies
- package-ecosystem: "cargo"
directory: "/src-tauri"
-25
View File
@@ -1,25 +0,0 @@
name: Generate changelog
on:
release:
types: [created, edited]
jobs:
changelog:
name: Generate changelog
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate a changelog
uses: orhun/git-cliff-action@v4
id: git-cliff
with:
args: --verbose
env:
OUTPUT: CHANGELOG.md
- name: Print the changelog
run: cat "${{ steps.git-cliff.outputs.changelog }}"
+61
View File
@@ -0,0 +1,61 @@
name: "CodeQL"
on:
workflow_call:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
- cron: "16 13 * * 5"
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
security-events: write
packages: read
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up pnpm package manager
uses: pnpm/action-setup@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: "pnpm"
- name: Install dependencies from lockfile
run: pnpm install --frozen-lockfile
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
queries: security-extended
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- if: matrix.build-mode == 'manual'
shell: bash
run: |
pnpm run build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
@@ -1,60 +0,0 @@
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
lint-rust:
name: Lint Rust
if: ${{ github.actor == 'dependabot[bot]' }}
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
dependabot-automerge:
name: Dependabot Automerge
if: ${{ github.actor == 'dependabot[bot]' }}
needs: [security-scan, lint-js, lint-rust]
runs-on: ubuntu-latest
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Auto-merge minor and patch updates
uses: ridedott/merge-me-action@v2
with:
GITHUB_TOKEN: ${{ secrets.SECRET_DEPENDABOT_GITHUB_TOKEN }}
PRESET: DEPENDABOT_MINOR
MERGE_METHOD: SQUASH
timeout-minutes: 10
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- name: Set up pnpm package manager
uses: pnpm/action-setup@v4
- name: Set up Node.js v22
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: .node-version
+23 -1
View File
@@ -37,8 +37,23 @@ jobs:
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
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
release:
needs: [security-scan, lint-js, lint-rust]
needs: [security-scan, lint-js, lint-rust, codeql, spellcheck]
permissions:
contents: write
strategy:
@@ -149,3 +164,10 @@ jobs:
releaseDraft: false
prerelease: false
args: ${{ matrix.args }}
- name: Commit CHANGELOG.md
uses: stefanzweifel/git-auto-commit-action@v6
with:
branch: main
commit_message: "docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]"
file_pattern: CHANGELOG.md
+16 -1
View File
@@ -36,8 +36,23 @@ jobs:
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
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
rolling-release:
needs: [security-scan, lint-js, lint-rust]
needs: [security-scan, lint-js, lint-rust, codeql, spellcheck]
permissions:
contents: write
strategy:
+26
View File
@@ -0,0 +1,26 @@
name: Spell Check
permissions:
contents: read
on:
workflow_call:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
RUST_BACKTRACE: 1
CARGO_TERM_COLOR: always
CLICOLOR: 1
jobs:
spelling:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v4
- name: Spell Check Repo
uses: crate-ci/typos@v1.33.1
+6
View File
@@ -10,6 +10,7 @@
"cdylib",
"CFURL",
"checkin",
"CLICOLOR",
"clippy",
"cmdk",
"codegen",
@@ -41,6 +42,7 @@
"msvc",
"msys",
"Mullvad",
"mullvadbrowser",
"nodecar",
"ntlm",
"objc",
@@ -60,16 +62,20 @@
"sonner",
"sspi",
"staticlib",
"stefanzweifel",
"subdirs",
"swatinem",
"sysinfo",
"systempreferences",
"taskkill",
"tauri",
"titlebar",
"Torbrowser",
"turbopack",
"unlisten",
"unminimize",
"unrs",
"urlencoding",
"vercel",
"winreg",
"wiremock",
+1 -1
View File
@@ -23,6 +23,6 @@ Examples of unacceptable behavior by participants include:
## Enforcement
Violations of the Code of Conduct may be reported by pinging @zhom on Github. All reports will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Further details of specific enforcement policies may be posted separately.
Violations of the Code of Conduct may be reported to contact at donutbrowser dot com. All reports will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Further details of specific enforcement policies may be posted separately.
We hold the right and responsibility to remove comments or other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any members for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+8 -1
View File
@@ -17,6 +17,9 @@
<a href="https://app.codacy.com/gh/zhom/donutbrowser/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade">
<img src="https://app.codacy.com/project/badge/Grade/b9c9beafc92d4bc8bc7c5b42c6c4ba81"/>
</a>
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2Fzhom%2Fdonutbrowser?ref=badge_shield&issueType=security" alt="FOSSA Status">
<img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Fzhom%2Fdonutbrowser.svg?type=shield&issueType=security"/>
</a>
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/stargazers" target="_blank">
<img src="https://img.shields.io/github/stars/zhom/donutbrowser?style=social" alt="GitHub stars">
</a>
@@ -26,7 +29,11 @@
> A free and open source browser orchestrator built with [Tauri](https://v2.tauri.app/).
![Donut Browser Preview](assets/preview.png)
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/preview-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="assets/preview.png" />
<img alt="Preview" src="assets/preview.png" />
</picture>
## Features
Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 540 KiB

+2 -2
View File
@@ -21,7 +21,7 @@
"author": "",
"license": "AGPL-3.0",
"dependencies": {
"@types/node": "^22.15.30",
"@types/node": "^24.0.1",
"@yao-pkg/pkg": "^6.5.1",
"commander": "^14.0.0",
"dotenv": "^16.5.0",
@@ -30,6 +30,6 @@
"proxy-chain": "^2.5.9",
"ts-node": "^10.9.2",
"typescript": "^5.8.3",
"typescript-eslint": "^8.33.1"
"typescript-eslint": "^8.34.0"
}
}
+16 -13
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.4.0",
"version": "0.5.2",
"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": {
@@ -35,6 +36,7 @@
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-table": "^8.21.3",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-deep-link": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "~2.3.0",
"@tauri-apps/plugin-opener": "^2.2.7",
@@ -48,31 +50,32 @@
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"sonner": "^2.0.5",
"tailwind-merge": "^3.3.0",
"tailwind-merge": "^3.3.1",
"tauri-plugin-macos-permissions-api": "^2.3.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.28.0",
"@eslint/js": "^9.29.0",
"@next/eslint-plugin-next": "^15.3.3",
"@tailwindcss/postcss": "^4.1.8",
"@tailwindcss/postcss": "^4.1.10",
"@tauri-apps/cli": "^2.5.0",
"@types/node": "^22.15.30",
"@types/react": "^19.1.6",
"@types/node": "^24.0.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@typescript-eslint/eslint-plugin": "^8.33.1",
"@typescript-eslint/parser": "^8.33.1",
"@vitejs/plugin-react": "^4.5.1",
"eslint": "^9.28.0",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.34.0",
"@vitejs/plugin-react": "^4.5.2",
"eslint": "^9.29.0",
"eslint-config-next": "^15.3.3",
"eslint-plugin-react-hooks": "^5.2.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"tailwindcss": "^4.1.8",
"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.33.1"
"typescript-eslint": "^8.34.0"
},
"packageManager": "pnpm@10.11.1",
"lint-staged": {
+786 -721
View File
File diff suppressed because it is too large Load Diff
+95 -47
View File
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "adler2"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aes"
@@ -411,9 +411,9 @@ checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
[[package]]
name = "bytemuck"
version = "1.23.0"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c"
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
[[package]]
name = "byteorder"
@@ -518,9 +518,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.26"
version = "1.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
dependencies = [
"jobserver",
"libc",
@@ -556,9 +556,9 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "cfg_aliases"
@@ -993,7 +993,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.4.0"
version = "0.5.2"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -1018,6 +1018,7 @@ dependencies = [
"tauri-plugin-macos-permissions",
"tauri-plugin-opener",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tempfile",
"tokio",
"tokio-test",
@@ -1103,9 +1104,9 @@ checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "enumflags2"
version = "0.7.11"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
"enumflags2_derive",
"serde",
@@ -1113,9 +1114,9 @@ dependencies = [
[[package]]
name = "enumflags2_derive"
version = "0.7.11"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79"
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
@@ -1733,9 +1734,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
@@ -2274,9 +2275,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.172"
version = "0.2.173"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb"
[[package]]
name = "libloading"
@@ -2393,9 +2394,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "memchr"
version = "2.7.4"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "memoffset"
@@ -2424,9 +2425,9 @@ dependencies = [
[[package]]
name = "miniz_oxide"
version = "0.8.8"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
@@ -3163,9 +3164,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plist"
version = "1.7.1"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d"
checksum = "3d77244ce2d584cd84f6a15f86195b8c9b2a0dfbfd817c09e0464244091a58ed"
dependencies = [
"base64 0.22.1",
"indexmap 2.9.0",
@@ -3301,9 +3302,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.32.0"
version = "0.37.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
dependencies = [
"memchr",
]
@@ -3441,9 +3442,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "redox_syscall"
version = "0.5.12"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
dependencies = [
"bitflags 2.9.1",
]
@@ -3459,6 +3460,26 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "ref-cast"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "regex"
version = "1.11.1"
@@ -3490,9 +3511,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.12.19"
version = "0.12.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -3507,12 +3528,10 @@ dependencies = [
"hyper-rustls",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pki-types",
@@ -3585,9 +3604,9 @@ dependencies = [
[[package]]
name = "rustc-demangle"
version = "0.1.24"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
[[package]]
name = "rustc_version"
@@ -3689,6 +3708,18 @@ dependencies = [
"uuid",
]
[[package]]
name = "schemars"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "schemars_derive"
version = "0.8.22"
@@ -3847,15 +3878,16 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.12.0"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42"
dependencies = [
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.9.0",
"schemars 0.9.0",
"serde",
"serde_derive",
"serde_json",
@@ -3865,9 +3897,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.12.0"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77"
dependencies = [
"darling",
"proc-macro2",
@@ -4314,7 +4346,7 @@ dependencies = [
"glob",
"heck 0.5.0",
"json-patch",
"schemars",
"schemars 0.8.22",
"semver",
"serde",
"serde_json",
@@ -4374,7 +4406,7 @@ dependencies = [
"anyhow",
"glob",
"plist",
"schemars",
"schemars 0.8.22",
"serde",
"serde_json",
"tauri-utils",
@@ -4430,7 +4462,7 @@ dependencies = [
"dunce",
"glob",
"percent-encoding",
"schemars",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_repr",
@@ -4468,7 +4500,7 @@ dependencies = [
"objc2-app-kit",
"objc2-foundation 0.3.1",
"open",
"schemars",
"schemars 0.8.22",
"serde",
"serde_json",
"tauri",
@@ -4490,7 +4522,7 @@ dependencies = [
"open",
"os_pipe",
"regex",
"schemars",
"schemars 0.8.22",
"serde",
"serde_json",
"shared_child",
@@ -4500,6 +4532,22 @@ dependencies = [
"tokio",
]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97d0e07b40fb2eb13778e30778f5979347a2bf30e1b9d47f78ff7fe92d2e4b3d"
dependencies = [
"serde",
"serde_json",
"tauri",
"tauri-plugin-deep-link",
"thiserror 2.0.12",
"tracing",
"windows-sys 0.59.0",
"zbus",
]
[[package]]
name = "tauri-runtime"
version = "2.6.0"
@@ -4572,7 +4620,7 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
"schemars",
"schemars 0.8.22",
"semver",
"serde",
"serde-untagged",
@@ -5430,9 +5478,9 @@ dependencies = [
[[package]]
name = "windows"
version = "0.61.1"
version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections",
"windows-core",
@@ -5498,9 +5546,9 @@ dependencies = [
[[package]]
name = "windows-link"
version = "0.1.1"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-numerics"
+4 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.4.0"
version = "0.5.2"
description = "Simple Yet Powerful Browser Orchestrator"
authors = ["zhom@github"]
edition = "2021"
@@ -39,6 +39,9 @@ async-trait = "0.1"
futures-util = "0.3"
urlencoding = "2.1"
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation="0.10"
objc2 = "0.6.1"
+28
View File
@@ -26,5 +26,33 @@
<string>public.app-category.productivity</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2025 Donut Browser</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>HTML document</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.html</string>
<string>public.xhtml</string>
</array>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Web site URL</string>
<key>CFBundleURLSchemes</key>
<array>
<string>http</string>
<string>https</string>
</array>
</dict>
</array>
</dict>
</plist>
+4
View File
@@ -19,6 +19,10 @@
"shell:allow-spawn",
"shell:allow-stdin-write",
"deep-link:default",
"deep-link:allow-register",
"deep-link:allow-unregister",
"deep-link:allow-is-registered",
"deep-link:allow-get-current",
"dialog:default",
"dialog:allow-open",
"macos-permissions:default",
+2 -2
View File
@@ -1443,7 +1443,7 @@ mod tests {
let mock_response = r#"[
{
"tag_name": "v1.81.9",
"name": "Brave Release 1.81.9",
"name": "Release v1.81.9 (Chromium 137.0.7151.104)",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
@@ -1476,7 +1476,7 @@ mod tests {
let releases = result.unwrap();
assert!(!releases.is_empty());
assert_eq!(releases[0].tag_name, "v1.81.9");
assert!(releases[0].is_nightly);
assert!(!releases[0].is_nightly); // "Release v1.81.9 (Chromium 137.0.7151.104)" starts with "Release" so it should be stable
}
#[tokio::test]
+1 -1
View File
@@ -152,7 +152,7 @@ impl AppAutoUpdater {
async fn fetch_app_releases(
&self,
) -> Result<Vec<AppRelease>, Box<dyn std::error::Error + Send + Sync>> {
let url = "https://api.github.com/repos/zhom/donutbrowser/releases";
let url = "https://api.github.com/repos/zhom/donutbrowser/releases?per_page=100";
let response = self
.client
.get(url)
+128 -98
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;
@@ -5,6 +6,7 @@ use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
use tauri::Emitter;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UpdateNotification {
@@ -45,32 +47,32 @@ impl AutoUpdater {
pub async fn check_for_updates(
&self,
) -> Result<Vec<UpdateNotification>, Box<dyn std::error::Error + Send + Sync>> {
// Check if auto-updates are enabled
let settings = self
.settings_manager
.load_settings()
.map_err(|e| format!("Failed to load settings: {e}"))?;
if !settings.auto_updates_enabled {
return Ok(Vec::new());
}
let mut notifications = Vec::new();
let mut browser_versions: HashMap<String, Vec<BrowserVersionInfo>> = HashMap::new();
// Group profiles by browser
let profiles = self
.browser_runner
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let mut notifications = Vec::new();
let mut browser_versions: HashMap<String, Vec<BrowserVersionInfo>> = HashMap::new();
// Group profiles by browser type
let mut browser_profiles: HashMap<String, Vec<BrowserProfile>> = HashMap::new();
for profile in profiles {
// Only check supported browsers
if !self
.version_service
.is_browser_supported(&profile.browser)
.unwrap_or(false)
{
continue;
}
browser_profiles
.entry(profile.browser.clone())
.or_default()
.push(profile);
}
// Check each browser type
for (browser, profiles) in browser_profiles {
// Get cached versions first, then try to fetch if needed
let versions = if let Some(cached) = self
@@ -97,7 +99,26 @@ impl AutoUpdater {
// Check each profile for updates
for profile in profiles {
if let Some(update) = self.check_profile_update(&profile, &versions)? {
notifications.push(update);
// Apply chromium threshold logic
if browser == "chromium" {
// For chromium, only show notifications if there are 50+ new versions
let current_version = &profile.version.parse::<u32>().unwrap();
let new_version = &update.new_version.parse::<u32>().unwrap();
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 {result} new versions (need 50+)"
);
}
} else {
notifications.push(update);
}
}
}
}
@@ -105,6 +126,93 @@ impl AutoUpdater {
Ok(notifications)
}
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) => {
if !update_notifications.is_empty() {
println!(
"Found {} browser updates to auto-download",
update_notifications.len()
);
// Trigger automatic downloads for each update
for notification in update_notifications {
println!(
"Auto-downloading {} version {}",
notification.browser, notification.new_version
);
// 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();
// 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");
}
}
Err(e) => {
eprintln!("Failed to check for browser updates: {e}");
}
}
}
/// Check if a specific profile has an available update
fn check_profile_update(
&self,
@@ -112,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));
@@ -181,43 +288,6 @@ impl AutoUpdater {
result
}
/// Mark download as auto-update
pub fn mark_auto_update_download(
&self,
browser: &str,
version: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut state = self.load_auto_update_state()?;
let download_key = format!("{browser}-{version}");
state.auto_update_downloads.insert(download_key);
self.save_auto_update_state(&state)?;
Ok(())
}
/// Remove auto-update download tracking
pub fn remove_auto_update_download(
&self,
browser: &str,
version: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut state = self.load_auto_update_state()?;
let download_key = format!("{browser}-{version}");
state.auto_update_downloads.remove(&download_key);
self.save_auto_update_state(&state)?;
Ok(())
}
/// Check if download is marked as auto-update
pub fn is_auto_update_download(
&self,
browser: &str,
version: &str,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let state = self.load_auto_update_state()?;
let download_key = format!("{browser}-{version}");
Ok(state.auto_update_downloads.contains(&download_key))
}
/// Automatically update all affected profile versions after browser download
pub async fn auto_update_profile_versions(
&self,
@@ -343,14 +413,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
}
@@ -448,27 +510,9 @@ pub async fn complete_browser_update_with_auto_update(
}
#[tauri::command]
pub async fn mark_auto_update_download(browser: String, version: String) -> Result<(), String> {
pub async fn check_for_updates_with_progress(app_handle: tauri::AppHandle) {
let updater = AutoUpdater::new();
updater
.mark_auto_update_download(&browser, &version)
.map_err(|e| format!("Failed to mark auto-update download: {e}"))
}
#[tauri::command]
pub async fn remove_auto_update_download(browser: String, version: String) -> Result<(), String> {
let updater = AutoUpdater::new();
updater
.remove_auto_update_download(&browser, &version)
.map_err(|e| format!("Failed to remove auto-update download: {e}"))
}
#[tauri::command]
pub async fn is_auto_update_download(browser: String, version: String) -> Result<bool, String> {
let updater = AutoUpdater::new();
updater
.is_auto_update_download(&browser, &version)
.map_err(|e| format!("Failed to check auto-update download: {e}"))
updater.check_for_updates_with_progress(&app_handle).await;
}
#[cfg(test)]
@@ -484,6 +528,7 @@ mod tests {
process_id: None,
proxy: None,
last_launch: None,
release_type: "stable".to_string(),
}
}
@@ -495,21 +540,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();
+6 -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;
@@ -633,19 +631,16 @@ impl Browser for ChromiumBrowser {
"--disable-component-update".to_string(),
"--disable-background-timer-throttling".to_string(),
"--crash-server-url=".to_string(),
"--disable-updater".to_string(),
];
// 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
));
}
}
+119 -34
View File
@@ -27,6 +27,12 @@ pub struct BrowserProfile {
pub process_id: Option<u32>,
#[serde(default)]
pub last_launch: Option<u64>,
#[serde(default = "default_release_type")]
pub release_type: String, // "stable" or "nightly"
}
fn default_release_type() -> String {
"stable".to_string()
}
// Platform-specific modules
@@ -1049,6 +1055,7 @@ impl BrowserRunner {
name: &str,
browser: &str,
version: &str,
release_type: &str,
proxy: Option<ProxySettings>,
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
// Check if a profile with this name already exists (case insensitive)
@@ -1075,6 +1082,7 @@ impl BrowserRunner {
proxy: proxy.clone(),
process_id: None,
last_launch: None,
release_type: release_type.to_string(),
};
// Save profile info
@@ -1245,6 +1253,14 @@ impl BrowserRunner {
// Update version
profile.version = version.to_string();
// Update the release_type based on the version and browser
profile.release_type =
if crate::api_client::is_browser_version_nightly(&profile.browser, version, None) {
"nightly".to_string()
} else {
"stable".to_string()
};
// Save the updated profile
self.save_profile(&profile)?;
@@ -1261,9 +1277,13 @@ impl BrowserRunner {
}
/// Internal method to cleanup unused binaries (used by auto-cleanup)
fn cleanup_unused_binaries_internal(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
fn cleanup_unused_binaries_internal(
&self,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
// Load current profiles
let profiles = self.list_profiles()?;
let profiles = self
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
// Load registry
let mut registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::load()?;
@@ -1284,23 +1304,21 @@ impl BrowserRunner {
vec![
// Disable default browser check
"user_pref(\"browser.shell.checkDefaultBrowser\", false);".to_string(),
// Disable automatic updates
"user_pref(\"app.update.enabled\", false);".to_string(),
"user_pref(\"app.update.auto\", false);".to_string(),
"user_pref(\"app.update.mode\", 0);".to_string(),
"user_pref(\"app.update.mode\", 2);".to_string(),
"user_pref(\"app.update.promptWaitTime\", 0);".to_string(),
"user_pref(\"app.update.service.enabled\", false);".to_string(),
"user_pref(\"app.update.silent\", false);".to_string(),
// Disable update checking entirely
"user_pref(\"app.update.silent\", true);".to_string(),
"user_pref(\"app.update.checkInstallTime\", false);".to_string(),
"user_pref(\"app.update.url\", \"\");".to_string(),
"user_pref(\"app.update.url.manual\", \"\");".to_string(),
"user_pref(\"app.update.url.details\", \"\");".to_string(),
// Disable background update downloads
"user_pref(\"app.update.url.override\", \"\");".to_string(),
"user_pref(\"app.update.interval\", 9999999999);".to_string(),
"user_pref(\"app.update.background.interval\", 9999999999);".to_string(),
"user_pref(\"app.update.download.attemptOnce\", false);".to_string(),
"user_pref(\"app.update.idletime\", -1);".to_string(),
// Additional update-related preferences for completeness
"user_pref(\"security.tls.insecure_fallback_hosts\", \"\");".to_string(),
"user_pref(\"app.update.staging.enabled\", false);".to_string(),
]
}
@@ -1436,6 +1454,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)
@@ -1447,6 +1466,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");
@@ -1457,9 +1477,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
@@ -1587,6 +1614,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?;
@@ -1717,6 +1745,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");
@@ -1761,7 +1790,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(()) => {
@@ -1786,7 +1820,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
}
}
}
@@ -1794,7 +1828,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
@@ -1803,7 +1839,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
}
}
@@ -1895,7 +1933,10 @@ impl BrowserRunner {
if let Ok(settings) = settings_manager.load_settings() {
if settings.auto_delete_unused_binaries {
// Perform cleanup in the background after profile deletion
let _ = self.cleanup_unused_binaries_internal();
// 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}");
}
}
}
@@ -2195,11 +2236,12 @@ pub fn create_browser_profile(
name: String,
browser: String,
version: String,
release_type: String,
proxy: Option<ProxySettings>,
) -> Result<BrowserProfile, String> {
let browser_runner = BrowserRunner::new();
browser_runner
.create_profile(&name, &browser, &version, proxy)
.create_profile(&name, &browser, &version, &release_type, proxy)
.map_err(|e| format!("Failed to create profile: {e}"))
}
@@ -2219,6 +2261,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();
@@ -2232,14 +2277,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);
@@ -2265,7 +2313,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
@@ -2638,11 +2686,18 @@ pub fn create_browser_profile_new(
name: String,
browser_str: String,
version: String,
release_type: String,
proxy: Option<ProxySettings>,
) -> Result<BrowserProfile, String> {
let browser_type =
BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?;
create_browser_profile(name, browser_type.as_str().to_string(), version, proxy)
create_browser_profile(
name,
browser_type.as_str().to_string(),
version,
release_type,
proxy,
)
}
#[tauri::command]
@@ -2663,6 +2718,17 @@ pub fn get_downloaded_browser_versions(browser_str: String) -> Result<Vec<String
Ok(registry.get_downloaded_versions(&browser_str))
}
#[tauri::command]
pub async fn get_browser_release_types(
browser_str: String,
) -> Result<crate::browser_version_service::BrowserReleaseTypes, String> {
let service = BrowserVersionService::new();
service
.get_browser_release_types(&browser_str)
.await
.map_err(|e| format!("Failed to get browser release types: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -2708,7 +2774,7 @@ mod tests {
let (runner, _temp_dir) = create_test_browser_runner();
let profile = runner
.create_profile("Test Profile", "firefox", "139.0", None)
.create_profile("Test Profile", "firefox", "139.0", "stable", None)
.unwrap();
assert_eq!(profile.name, "Test Profile");
@@ -2736,6 +2802,7 @@ mod tests {
"Test Profile with Proxy",
"firefox",
"139.0",
"stable",
Some(proxy.clone()),
)
.unwrap();
@@ -2753,7 +2820,7 @@ mod tests {
let (runner, _temp_dir) = create_test_browser_runner();
let profile = runner
.create_profile("Test Save Load", "firefox", "139.0", None)
.create_profile("Test Save Load", "firefox", "139.0", "stable", None)
.unwrap();
// Save the profile
@@ -2773,7 +2840,7 @@ mod tests {
// Create profile
let _ = runner
.create_profile("Original Name", "firefox", "139.0", None)
.create_profile("Original Name", "firefox", "139.0", "stable", None)
.unwrap();
// Rename profile
@@ -2793,7 +2860,7 @@ mod tests {
// Create profile
let _ = runner
.create_profile("To Delete", "firefox", "139.0", None)
.create_profile("To Delete", "firefox", "139.0", "stable", None)
.unwrap();
// Verify profile exists
@@ -2814,7 +2881,13 @@ mod tests {
// Create profile with spaces and special characters
let profile = runner
.create_profile("Test Profile With Spaces", "firefox", "139.0", None)
.create_profile(
"Test Profile With Spaces",
"firefox",
"139.0",
"stable",
None,
)
.unwrap();
// Profile path should use snake_case
@@ -2827,13 +2900,13 @@ mod tests {
// Create multiple profiles
let _ = runner
.create_profile("Profile 1", "firefox", "139.0", None)
.create_profile("Profile 1", "firefox", "139.0", "stable", None)
.unwrap();
let _ = runner
.create_profile("Profile 2", "chromium", "1465660", None)
.create_profile("Profile 2", "chromium", "1465660", "stable", None)
.unwrap();
let _ = runner
.create_profile("Profile 3", "brave", "v1.81.9", None)
.create_profile("Profile 3", "brave", "v1.81.9", "stable", None)
.unwrap();
// List profiles
@@ -2852,10 +2925,10 @@ mod tests {
// Test that we can't rename to an existing profile name
let _ = runner
.create_profile("Profile 1", "firefox", "139.0", None)
.create_profile("Profile 1", "firefox", "139.0", "stable", None)
.unwrap();
let _ = runner
.create_profile("Profile 2", "firefox", "139.0", None)
.create_profile("Profile 2", "firefox", "139.0", "stable", None)
.unwrap();
// Try to rename profile2 to profile1's name (should fail)
@@ -2870,7 +2943,13 @@ mod tests {
// Create profile without proxy
let profile = runner
.create_profile("Test Firefox Prefs", "firefox", "139.0", None)
.create_profile(
"Test Firefox Preferences",
"firefox",
"139.0",
"stable",
None,
)
.unwrap();
// Check that user.js file was created with default browser preference
@@ -2896,7 +2975,13 @@ mod tests {
};
let profile_with_proxy = runner
.create_profile("Test Firefox Prefs Proxy", "firefox", "139.0", Some(proxy))
.create_profile(
"Test Firefox Preferences Proxy",
"firefox",
"139.0",
"stable",
Some(proxy),
)
.unwrap();
// Check that user.js file contains both proxy settings and default browser preference
+79 -2
View File
@@ -17,6 +17,12 @@ pub struct BrowserVersionsResult {
pub total_versions_count: usize,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrowserReleaseTypes {
pub stable: Option<String>,
pub nightly: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DownloadInfo {
pub url: String,
@@ -136,6 +142,67 @@ impl BrowserVersionService {
self.api_client.is_cache_expired(browser)
}
/// Get latest stable and nightly versions for a browser (cached first)
pub async fn get_browser_release_types(
&self,
browser: &str,
) -> Result<BrowserReleaseTypes, Box<dyn std::error::Error + Send + Sync>> {
// Try to get from cache first
if let Some(cached_versions) = self.get_cached_browser_versions_detailed(browser) {
// For Chromium, only return stable since all releases are stable
if browser == "chromium" {
let latest_stable = cached_versions.first().map(|v| v.version.clone());
return Ok(BrowserReleaseTypes {
stable: latest_stable,
nightly: None,
});
}
let latest_stable = cached_versions
.iter()
.find(|v| !v.is_prerelease)
.map(|v| v.version.clone());
let latest_nightly = cached_versions
.iter()
.find(|v| v.is_prerelease)
.map(|v| v.version.clone());
return Ok(BrowserReleaseTypes {
stable: latest_stable,
nightly: latest_nightly,
});
}
// Fallback to fetching if not cached
// For Chromium, only return stable since all releases are stable
if browser == "chromium" {
let detailed_versions = self.fetch_browser_versions_detailed(browser, false).await?;
let latest_stable = detailed_versions.first().map(|v| v.version.clone());
return Ok(BrowserReleaseTypes {
stable: latest_stable,
nightly: None,
});
}
let detailed_versions = self.fetch_browser_versions_detailed(browser, false).await?;
let latest_stable = detailed_versions
.iter()
.find(|v| !v.is_prerelease)
.map(|v| v.version.clone());
let latest_nightly = detailed_versions
.iter()
.find(|v| v.is_prerelease)
.map(|v| v.version.clone());
Ok(BrowserReleaseTypes {
stable: latest_stable,
nightly: latest_nightly,
})
}
/// Fetch browser versions with optional caching
pub async fn fetch_browser_versions(
&self,
@@ -299,6 +366,8 @@ impl BrowserVersionService {
let releases = self.fetch_zen_releases_detailed(true).await?;
merged_versions
.into_iter()
// Filter out twilight releases at the detailed level too
.filter(|version| version.to_lowercase() != "twilight")
.map(|version| {
if let Some(release) = releases.iter().find(|r| r.tag_name == version) {
BrowserVersionInfo {
@@ -385,7 +454,9 @@ impl BrowserVersionService {
})
.collect()
}
_ => return Err(format!("Unsupported browser: {browser}").into()),
_ => {
return Err(format!("Unsupported browser: {browser}").into());
}
};
Ok(detailed_info)
@@ -746,7 +817,13 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let releases = self.fetch_zen_releases_detailed(no_caching).await?;
Ok(releases.into_iter().map(|r| r.tag_name).collect())
Ok(
releases
.into_iter()
.filter(|r| r.tag_name.to_lowercase() != "twilight")
.map(|r| r.tag_name)
.collect(),
)
}
async fn fetch_zen_releases_detailed(
+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(_) => {
+5 -5
View File
@@ -27,7 +27,7 @@ impl DownloadedBrowsersRegistry {
Self::default()
}
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
pub fn load() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let registry_path = Self::get_registry_path()?;
if !registry_path.exists() {
@@ -39,7 +39,7 @@ impl DownloadedBrowsersRegistry {
Ok(registry)
}
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
pub fn save(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let registry_path = Self::get_registry_path()?;
// Ensure parent directory exists
@@ -52,7 +52,7 @@ impl DownloadedBrowsersRegistry {
Ok(())
}
fn get_registry_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
fn get_registry_path() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let mut path = base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
@@ -146,7 +146,7 @@ impl DownloadedBrowsersRegistry {
&mut self,
browser: &str,
version: &str,
) -> Result<(), Box<dyn std::error::Error>> {
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Some(info) = self.remove_browser(browser, version) {
// Clean up any files that might have been left behind
if info.file_path.exists() {
@@ -180,7 +180,7 @@ impl DownloadedBrowsersRegistry {
pub fn cleanup_unused_binaries(
&mut self,
active_profiles: &[(String, String)], // (browser, version) pairs
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let active_set: std::collections::HashSet<(String, String)> =
active_profiles.iter().cloned().collect();
let mut cleaned_up = Vec::new();
+96 -37
View File
@@ -1,4 +1,5 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use std::env;
use std::sync::Mutex;
use tauri::{Emitter, Manager, Runtime, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
use tauri_plugin_deep_link::DeepLinkExt;
@@ -27,10 +28,10 @@ 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_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,
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::{
@@ -48,8 +49,7 @@ use version_updater::{
use auto_updater::{
check_for_browser_updates, complete_browser_update_with_auto_update, dismiss_update_notification,
is_auto_update_download, is_browser_disabled_for_update, mark_auto_update_download,
remove_auto_update_download,
is_browser_disabled_for_update,
};
use app_auto_updater::{
@@ -111,20 +111,16 @@ async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), Strin
// Check if the main window exists and is ready
if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or(false) {
// Window is visible, emit event directly
println!("Main window is visible, emitting show-profile-selector event");
app
.emit("show-profile-selector", url.clone())
.map_err(|e| format!("Failed to emit URL open event: {e}"))?;
let _ = window.show();
let _ = window.set_focus();
} else {
// Window not visible yet - add to pending URLs
println!("Main window not visible, adding URL to pending list");
let mut pending = PENDING_URLS.lock().unwrap();
pending.push(url);
}
println!("Main window exists");
// Try to show and focus the window first
let _ = window.show();
let _ = window.set_focus();
let _ = window.unminimize();
app
.emit("show-profile-selector", url.clone())
.map_err(|e| format!("Failed to emit URL open event: {e}"))?;
} else {
// Window doesn't exist yet - add to pending URLs
println!("Main window doesn't exist, adding URL to pending list");
@@ -137,6 +133,8 @@ async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), Strin
#[tauri::command]
async fn check_and_handle_startup_url(app_handle: tauri::AppHandle) -> Result<bool, String> {
println!("check_and_handle_startup_url called");
let pending_urls = {
let mut pending = PENDING_URLS.lock().unwrap();
let urls = pending.clone();
@@ -144,12 +142,24 @@ async fn check_and_handle_startup_url(app_handle: tauri::AppHandle) -> Result<bo
urls
};
println!("Found {} pending URLs", pending_urls.len());
if !pending_urls.is_empty() {
println!(
"Handling {} pending URLs from frontend request",
pending_urls.len()
);
// Ensure the main window is visible and focused
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
let _ = window.unminimize();
// Give the window a moment to become visible
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
}
for url in pending_urls {
println!("Emitting show-profile-selector event for URL: {url}");
if let Err(e) = app_handle.emit("show-profile-selector", url.clone()) {
@@ -166,11 +176,23 @@ async fn check_and_handle_startup_url(app_handle: tauri::AppHandle) -> Result<bo
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let args: Vec<String> = env::args().collect();
let startup_url = args.iter().find(|arg| arg.starts_with("http")).cloned();
if let Some(url) = startup_url.clone() {
println!("Found startup URL in command line: {url}");
let mut pending = PENDING_URLS.lock().unwrap();
pending.push(url.clone());
}
tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|_, args, _cwd| {
println!("Single instance triggered with args: {args:?}");
}))
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_macos_permissions::init())
.setup(|app| {
@@ -180,7 +202,10 @@ pub fn run() {
.title("Donut Browser")
.inner_size(900.0, 600.0)
.resizable(false)
.fullscreen(false);
.fullscreen(false)
.center()
.focused(true)
.visible(true);
#[allow(unused_variables)]
let window = win_builder.build().unwrap();
@@ -199,16 +224,27 @@ pub fn run() {
#[cfg(any(windows, target_os = "linux"))]
{
// For Windows and Linux, register all deep links at runtime for development
app.deep_link().register_all()?;
if let Err(e) = app.deep_link().register_all() {
eprintln!("Failed to register deep links: {e}");
}
}
#[cfg(target_os = "macos")]
{
// On macOS, try to register deep links for development builds
if let Err(e) = app.deep_link().register_all() {
eprintln!(
"Note: Deep link registration failed on macOS (this is normal for production): {e}"
);
}
}
// Handle deep links - this works for both scenarios:
// 1. App is running and URL is opened
// 2. App is not running and URL causes app to launch
app.deep_link().on_open_url({
let handle = handle.clone();
move |event| {
let urls = event.urls();
println!("Deep link event received with {} URLs", urls.len());
for url in urls {
let url_string = url.to_string();
println!("Deep link received: {url_string}");
@@ -226,25 +262,50 @@ pub fn run() {
}
});
if let Some(startup_url) = startup_url {
let handle_clone = handle.clone();
tauri::async_runtime::spawn(async move {
println!("Processing startup URL from command line: {startup_url}");
if let Err(e) = handle_url_open(handle_clone, startup_url.clone()).await {
eprintln!("Failed to handle startup URL: {e}");
}
});
}
// Initialize and start background version updater
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
let version_updater = get_version_updater();
let mut updater_guard = version_updater.lock().await;
// Set the app handle
updater_guard.set_app_handle(app_handle).await;
{
let mut updater_guard = version_updater.lock().await;
updater_guard.set_app_handle(app_handle);
}
// Start the background updates
updater_guard.start_background_updates().await;
// Run startup check without holding the lock
{
let updater_guard = version_updater.lock().await;
if let Err(e) = updater_guard.start_background_updates().await {
eprintln!("Failed to start background updates: {e}");
}
}
});
// Start the background update task separately
tauri::async_runtime::spawn(async move {
version_updater::VersionUpdater::run_background_task().await;
});
let app_handle_auto_updater = app.handle().clone();
// Start the auto-update check task separately
tauri::async_runtime::spawn(async move {
auto_updater::check_for_updates_with_progress(app_handle_auto_updater).await;
});
// Check for app updates at startup
let app_handle_update = app.handle().clone();
tauri::async_runtime::spawn(async move {
// Add a small delay to ensure the app is fully loaded
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
println!("Starting app update check at startup...");
let updater = app_auto_updater::AppAutoUpdater::new();
match updater.check_for_updates().await {
@@ -284,6 +345,7 @@ pub fn run() {
fetch_browser_versions_cached_first,
fetch_browser_versions_with_count_cached_first,
get_downloaded_browser_versions,
get_browser_release_types,
update_profile_proxy,
update_profile_version,
check_browser_status,
@@ -306,9 +368,6 @@ pub fn run() {
is_browser_disabled_for_update,
dismiss_update_notification,
complete_browser_update_with_auto_update,
mark_auto_update_download,
remove_auto_update_download,
is_auto_update_download,
check_for_app_updates,
check_for_app_updates_manual,
download_and_install_app_update,
+1
View File
@@ -686,6 +686,7 @@ impl ProfileImporter {
proxy: None,
process_id: None,
last_launch: None,
release_type: "stable".to_string(),
};
// Save the profile metadata
+15 -38
View File
@@ -4,7 +4,7 @@ use std::fs::{self, create_dir_all};
use std::path::PathBuf;
use crate::api_client::ApiClient;
use crate::browser_version_service::BrowserVersionService;
use crate::version_updater;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TableSortingSettings {
@@ -25,40 +25,25 @@ impl Default for TableSortingSettings {
pub struct AppSettings {
#[serde(default)]
pub set_as_default_browser: bool,
#[serde(default = "default_show_settings_on_startup")]
#[serde(default)]
pub show_settings_on_startup: bool,
#[serde(default = "default_theme")]
pub theme: String, // "light", "dark", or "system"
#[serde(default = "default_auto_updates_enabled")]
pub auto_updates_enabled: bool,
#[serde(default = "default_auto_delete_unused_binaries")]
#[serde(default)]
pub auto_delete_unused_binaries: bool,
}
fn default_show_settings_on_startup() -> bool {
true
}
fn default_theme() -> String {
"system".to_string()
}
fn default_auto_updates_enabled() -> bool {
true
}
fn default_auto_delete_unused_binaries() -> bool {
true
}
impl Default for AppSettings {
fn default() -> Self {
Self {
set_as_default_browser: false,
show_settings_on_startup: default_show_settings_on_startup(),
theme: default_theme(),
auto_updates_enabled: default_auto_updates_enabled(),
auto_delete_unused_binaries: default_auto_delete_unused_binaries(),
show_settings_on_startup: true,
theme: "system".to_string(),
auto_delete_unused_binaries: true,
}
}
}
@@ -216,7 +201,9 @@ pub async fn save_table_sorting_settings(sorting: TableSortingSettings) -> Resul
}
#[tauri::command]
pub async fn clear_all_version_cache_and_refetch() -> Result<(), String> {
pub async fn clear_all_version_cache_and_refetch(
app_handle: tauri::AppHandle,
) -> Result<(), String> {
let api_client = ApiClient::new();
// Clear all cache first
@@ -224,23 +211,13 @@ pub async fn clear_all_version_cache_and_refetch() -> Result<(), String> {
.clear_all_cache()
.map_err(|e| format!("Failed to clear version cache: {e}"))?;
// Trigger auto-fetch for all supported browsers
let service = BrowserVersionService::new();
let supported_browsers = service.get_supported_browsers();
let updater = version_updater::get_version_updater();
let updater_guard = updater.lock().await;
for browser in supported_browsers {
// Start background fetch for each browser (don't wait for completion)
let service_clone = BrowserVersionService::new();
let browser_clone = browser.clone();
tokio::spawn(async move {
if let Err(e) = service_clone
.fetch_browser_versions_detailed(&browser_clone, false)
.await
{
eprintln!("Background version fetch failed for {browser_clone}: {e}");
}
});
}
updater_guard
.trigger_manual_update(&app_handle)
.await
.map_err(|e| format!("Failed to trigger version update: {e}"))?;
Ok(())
}
+115 -108
View File
@@ -1,4 +1,3 @@
use crate::browser_version_service::BrowserVersionService;
use directories::BaseDirs;
use serde::{Deserialize, Serialize};
use std::fs;
@@ -8,7 +7,10 @@ use std::sync::OnceLock;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tauri::Emitter;
use tokio::sync::Mutex;
use tokio::time::{interval, Interval};
use tokio::time::interval;
use crate::auto_updater::AutoUpdater;
use crate::browser_version_service::BrowserVersionService;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct VersionUpdateProgress {
@@ -46,22 +48,23 @@ impl Default for BackgroundUpdateState {
pub struct VersionUpdater {
version_service: BrowserVersionService,
app_handle: Arc<Mutex<Option<tauri::AppHandle>>>,
update_interval: Interval,
auto_updater: AutoUpdater,
app_handle: Option<tauri::AppHandle>,
}
impl VersionUpdater {
pub fn new() -> Self {
let mut update_interval = interval(Duration::from_secs(5 * 60)); // Check every 5 minutes
update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
Self {
version_service: BrowserVersionService::new(),
app_handle: Arc::new(Mutex::new(None)),
update_interval,
auto_updater: AutoUpdater::new(),
app_handle: None,
}
}
pub fn set_app_handle(&mut self, app_handle: tauri::AppHandle) {
self.app_handle = Some(app_handle);
}
fn get_cache_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let app_name = if cfg!(debug_assertions) {
@@ -143,11 +146,6 @@ impl VersionUpdater {
should_update
}
pub async fn set_app_handle(&self, app_handle: tauri::AppHandle) {
let mut handle = self.app_handle.lock().await;
*handle = Some(app_handle);
}
pub async fn check_and_run_startup_update(
&self,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
@@ -157,15 +155,10 @@ impl VersionUpdater {
return Ok(());
}
let app_handle = {
let handle_guard = self.app_handle.lock().await;
handle_guard.clone()
};
if let Some(handle) = app_handle {
if let Some(ref app_handle) = self.app_handle {
println!("Running startup version update...");
match self.update_all_browser_versions(&handle).await {
match self.update_all_browser_versions(app_handle).await {
Ok(_) => {
// Update the persistent state after successful update
let state = BackgroundUpdateState {
@@ -191,7 +184,9 @@ impl VersionUpdater {
Ok(())
}
pub async fn start_background_updates(&mut self) {
pub async fn start_background_updates(
&self,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!(
"Starting background version update service (checking every 5 minutes for 3-hour intervals)"
);
@@ -201,41 +196,54 @@ impl VersionUpdater {
eprintln!("Startup version update failed: {e}");
}
Ok(())
}
pub async fn run_background_task() {
let mut update_interval = interval(Duration::from_secs(5 * 60)); // Check every 5 minutes
update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
self.update_interval.tick().await;
update_interval.tick().await;
// Check if we should run an update based on persistent state
if !Self::should_run_background_update() {
continue;
}
// Check if we have an app handle
let app_handle = {
let handle_guard = self.app_handle.lock().await;
handle_guard.clone()
};
println!("Starting background version update...");
if let Some(handle) = app_handle {
println!("Starting background version update...");
// Get the updater instance for this update cycle
let updater = get_version_updater();
let result = {
let updater_guard = updater.lock().await;
if let Some(ref app_handle) = updater_guard.app_handle {
updater_guard.update_all_browser_versions(app_handle).await
} else {
Err("App handle not available for background update".into())
}
}; // Release the lock here
match self.update_all_browser_versions(&handle).await {
Ok(_) => {
// Update the persistent state after successful update
let state = BackgroundUpdateState {
last_update_time: Self::get_current_timestamp(),
update_interval_hours: 3,
};
match result {
Ok(_) => {
// Update the persistent state after successful update
let state = BackgroundUpdateState {
last_update_time: Self::get_current_timestamp(),
update_interval_hours: 3,
};
if let Err(e) = Self::save_background_update_state(&state) {
eprintln!("Failed to save background update state: {e}");
} else {
println!("Background version update completed successfully");
}
if let Err(e) = Self::save_background_update_state(&state) {
eprintln!("Failed to save background update state: {e}");
} else {
println!("Background version update completed successfully");
}
Err(e) => {
eprintln!("Background version update failed: {e}");
}
Err(e) => {
eprintln!("Background version update failed: {e}");
// Emit error event
// Try to emit error event if we have an app handle
let updater_guard = updater.lock().await;
if let Some(ref app_handle) = updater_guard.app_handle {
let progress = VersionUpdateProgress {
current_browser: "".to_string(),
total_browsers: 0,
@@ -244,11 +252,9 @@ impl VersionUpdater {
browser_new_versions: 0,
status: "error".to_string(),
};
let _ = handle.emit("version-update-progress", &progress);
let _ = app_handle.emit("version-update-progress", &progress);
}
}
} else {
println!("App handle not available, skipping background update");
}
}
}
@@ -257,107 +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 browsers = [
"firefox",
"firefox-developer",
"mullvad-browser",
"zen",
"brave",
"chromium",
"tor-browser",
];
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;
// 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(),
};
let _ = app_handle.emit("version-update-progress", &progress);
for (index, browser) in browsers.iter().enumerate() {
// Check if individual browser cache is expired before updating
if !self.version_service.should_update_cache(browser) {
println!("Skipping {browser} - cache is still fresh");
if let Err(e) = app_handle.emit("version-update-progress", &initial_progress) {
eprintln!("Failed to emit initial progress: {e}");
}
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;
}
for (index, browser) in supported_browsers.iter().enumerate() {
println!("Updating browser versions for: {browser}");
println!("Updating versions for browser: {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(),
};
let _ = app_handle.emit("version-update-progress", &progress);
let result = self.update_browser_versions(browser).await;
if let Err(e) = app_handle.emit("version-update-progress", &progress) {
eprintln!("Failed to emit progress for {browser}: {e}");
}
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(500)).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(),
};
let _ = app_handle.emit("version-update-progress", &progress);
println!("Background version update completed. Found {total_new_versions} new versions total");
if let Err(e) = app_handle.emit("version-update-progress", &final_progress) {
eprintln!("Failed to emit completion progress: {e}");
}
// 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)
}
+4 -3
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut Browser",
"version": "0.4.0",
"version": "0.5.2",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm dev",
@@ -61,8 +61,9 @@
},
"plugins": {
"deep-link": {
"schemes": ["http", "https"],
"domains": []
"desktop": {
"schemes": ["http", "https"]
}
}
}
}
+24 -4
View File
@@ -25,10 +25,12 @@ import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications"
import { usePermissions } from "@/hooks/use-permissions";
import type { PermissionType } from "@/hooks/use-permissions";
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
import { useVersionUpdater } from "@/hooks/use-version-updater";
import { showErrorToast } from "@/lib/toast-utils";
import type { BrowserProfile, ProxySettings } from "@/types";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { getCurrent } from "@tauri-apps/plugin-deep-link";
import { useCallback, useEffect, useRef, useState } from "react";
import { FaDownload } from "react-icons/fa";
import { GoGear, GoKebabHorizontal, GoPlus } from "react-icons/go";
@@ -80,7 +82,10 @@ export default function Home() {
}
}, []);
// Auto-update functionality - pass loadProfiles to refresh profiles after updates
// Version updater for handling version fetching progress events and auto-updates
useVersionUpdater();
// Auto-update functionality - use the existing hook for compatibility
const updateNotifications = useUpdateNotifications(loadProfiles);
const { checkForUpdates, isUpdating } = updateNotifications;
@@ -102,6 +107,18 @@ export default function Home() {
useAppUpdateNotifications();
// For some reason, app.deep_link().get_current() is not working properly
const checkCurrentUrl = useCallback(async () => {
try {
const currentUrl = await getCurrent();
if (currentUrl && currentUrl.length > 0) {
void handleUrlOpen(currentUrl[0]);
}
} catch (error) {
console.error("Failed to check current URL:", error);
}
}, []);
useEffect(() => {
void loadProfilesWithUpdateCheck();
@@ -113,6 +130,7 @@ export default function Home() {
// Check for startup URLs (when app was launched as default browser)
void checkStartupUrls();
void checkCurrentUrl();
// Set up periodic update checks (every 30 minutes)
const updateInterval = setInterval(
@@ -125,7 +143,7 @@ export default function Home() {
return () => {
clearInterval(updateInterval);
};
}, [loadProfilesWithUpdateCheck, checkForUpdates]);
}, [loadProfilesWithUpdateCheck, checkForUpdates, checkCurrentUrl]);
// Check permissions when they are initialized
useEffect(() => {
@@ -287,6 +305,7 @@ export default function Home() {
name: string;
browserStr: BrowserTypeString;
version: string;
releaseType: string;
proxy?: ProxySettings;
}) => {
setError(null);
@@ -298,6 +317,7 @@ export default function Home() {
name: profileData.name,
browserStr: profileData.browserStr,
version: profileData.version,
releaseType: profileData.releaseType,
},
);
@@ -460,7 +480,7 @@ export default function Home() {
);
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 gap-8 sm:p-12 font-[family-name:var(--font-geist-sans)] bg-white dark:bg-black">
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen gap-8 font-[family-name:var(--font-geist-sans)] bg-white dark:bg-black">
<main className="flex flex-col row-start-2 gap-8 items-center w-full max-w-3xl">
<Card className="w-full">
<CardHeader>
@@ -513,7 +533,7 @@ export default function Home() {
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
<CardContent>
<ProfilesDataTable
data={profiles}
onLaunchProfile={launchProfile}
+166 -55
View File
@@ -1,6 +1,7 @@
"use client";
import { LoadingButton } from "@/components/loading-button";
import { ReleaseTypeSelector } from "@/components/release-type-selector";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
@@ -12,9 +13,9 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { VersionSelector } from "@/components/version-selector";
import { useBrowserDownload } from "@/hooks/use-browser-download";
import type { BrowserProfile } from "@/types";
import { getBrowserDisplayName } from "@/lib/browser-utils";
import type { BrowserProfile, BrowserReleaseTypes } from "@/types";
import { invoke } from "@tauri-apps/api/core";
import { useEffect, useState } from "react";
import { LuTriangleAlert } from "react-icons/lu";
@@ -32,16 +33,18 @@ export function ChangeVersionDialog({
profile,
onVersionChanged,
}: ChangeVersionDialogProps) {
const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
const [selectedReleaseType, setSelectedReleaseType] = useState<
"stable" | "nightly" | null
>(null);
const [releaseTypes, setReleaseTypes] = useState<BrowserReleaseTypes>({});
const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [showDowngradeWarning, setShowDowngradeWarning] = useState(false);
const [acknowledgeDowngrade, setAcknowledgeDowngrade] = useState(false);
const {
availableVersions,
downloadedVersions,
isDownloading,
loadVersions,
loadDownloadedVersions,
downloadBrowser,
isVersionDownloaded,
@@ -49,49 +52,73 @@ export function ChangeVersionDialog({
useEffect(() => {
if (isOpen && profile) {
setSelectedVersion(profile.version);
// Set current release type based on profile
setSelectedReleaseType(profile.release_type as "stable" | "nightly");
setAcknowledgeDowngrade(false);
void loadVersions(profile.browser);
void loadReleaseTypes(profile.browser);
void loadDownloadedVersions(profile.browser);
}
}, [isOpen, profile, loadVersions, loadDownloadedVersions]);
}, [isOpen, profile, loadDownloadedVersions]);
const loadReleaseTypes = async (browser: string) => {
setIsLoadingReleaseTypes(true);
try {
const releaseTypes = await invoke<BrowserReleaseTypes>(
"get_browser_release_types",
{ browserStr: browser },
);
setReleaseTypes(releaseTypes);
} catch (error) {
console.error("Failed to load release types:", error);
} finally {
setIsLoadingReleaseTypes(false);
}
};
useEffect(() => {
if (profile && selectedVersion) {
// Check if this is a downgrade
const currentVersionIndex = availableVersions.findIndex(
(v) => v.tag_name === profile.version,
);
const selectedVersionIndex = availableVersions.findIndex(
(v) => v.tag_name === selectedVersion,
);
// If selected version has a higher index, it's older (downgrade)
if (
profile &&
selectedReleaseType &&
selectedReleaseType !== profile.release_type
) {
// For simplicity, we'll show downgrade warning when switching from stable to nightly
// since nightly versions might be considered "downgrades" in terms of stability
const isDowngrade =
currentVersionIndex !== -1 &&
selectedVersionIndex !== -1 &&
selectedVersionIndex > currentVersionIndex;
profile.release_type === "stable" && selectedReleaseType === "nightly";
setShowDowngradeWarning(isDowngrade);
if (!isDowngrade) {
setAcknowledgeDowngrade(false);
}
}
}, [selectedVersion, profile, availableVersions]);
}, [selectedReleaseType, profile]);
const handleDownload = async () => {
if (!profile || !selectedVersion) return;
await downloadBrowser(profile.browser, selectedVersion);
if (!profile || !selectedReleaseType) return;
const version =
selectedReleaseType === "stable"
? releaseTypes.stable
: releaseTypes.nightly;
if (!version) return;
await downloadBrowser(profile.browser, version);
};
const handleVersionChange = async () => {
if (!profile || !selectedVersion) return;
if (!profile || !selectedReleaseType) return;
const version =
selectedReleaseType === "stable"
? releaseTypes.stable
: releaseTypes.nightly;
if (!version) return;
setIsUpdating(true);
try {
await invoke("update_profile_version", {
profileName: profile.name,
version: selectedVersion,
version,
});
onVersionChanged();
onClose();
@@ -102,10 +129,15 @@ export function ChangeVersionDialog({
}
};
const selectedVersion =
selectedReleaseType === "stable"
? releaseTypes.stable
: releaseTypes.nightly;
const canUpdate =
profile &&
selectedVersion &&
selectedVersion !== profile.version &&
selectedReleaseType &&
selectedReleaseType !== profile.release_type &&
selectedVersion &&
isVersionDownloaded(selectedVersion) &&
(!showDowngradeWarning || acknowledgeDowngrade);
@@ -116,50 +148,129 @@ export function ChangeVersionDialog({
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Change Browser Version</DialogTitle>
<DialogTitle>Change Release Type</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label className="text-sm font-medium">Profile:</Label>
<div className="p-2 bg-muted rounded text-sm">{profile.name}</div>
<div className="p-2 text-sm rounded bg-muted">{profile.name}</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">Current Version:</Label>
<div className="p-2 bg-muted rounded text-sm">
{profile.version}
<Label className="text-sm font-medium">Current Release:</Label>
<div className="p-2 text-sm capitalize rounded bg-muted">
{profile.release_type} ({profile.version})
</div>
</div>
{/* Version Selection */}
<div className="grid gap-2">
<Label>New Version</Label>
<VersionSelector
selectedVersion={selectedVersion}
onVersionSelect={setSelectedVersion}
availableVersions={availableVersions}
downloadedVersions={downloadedVersions}
isDownloading={isDownloading}
onDownload={() => {
void handleDownload();
}}
placeholder="Select version..."
/>
</div>
{!releaseTypes.stable && !releaseTypes.nightly ? (
<Alert>
<AlertDescription>
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>
{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>
)}
{/* Downgrade Warning */}
{showDowngradeWarning && (
<Alert className="border-orange-700">
<LuTriangleAlert className="h-4 w-4 text-orange-700" />
<LuTriangleAlert className="w-4 h-4 text-orange-700" />
<AlertTitle className="text-orange-700">
Downgrade Warning
Stability Warning
</AlertTitle>
<AlertDescription className="text-orange-700">
You are about to downgrade from version {profile.version} to{" "}
{selectedVersion}. This may lead to compatibility issues, data
loss, or unexpected behavior.
<div className="flex items-center space-x-2 mt-3">
You are about to switch from stable to nightly releases. Nightly
versions may be less stable and could contain bugs or incomplete
features.
<div className="flex items-center mt-3 space-x-2">
<Checkbox
id="acknowledge-downgrade"
checked={acknowledgeDowngrade}
@@ -187,7 +298,7 @@ export function ChangeVersionDialog({
}}
disabled={!canUpdate}
>
{isUpdating ? "Updating..." : "Update Version"}
{isUpdating ? "Updating..." : "Update Release Type"}
</LoadingButton>
</DialogFooter>
</DialogContent>
+122 -45
View File
@@ -1,6 +1,7 @@
"use client";
import { LoadingButton } from "@/components/loading-button";
import { ReleaseTypeSelector } from "@/components/release-type-selector";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
@@ -24,14 +25,18 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { VersionSelector } from "@/components/version-selector";
import { useBrowserDownload } from "@/hooks/use-browser-download";
import { useBrowserSupport } from "@/hooks/use-browser-support";
import { getBrowserDisplayName } from "@/lib/browser-utils";
import type { BrowserProfile, ProxySettings } from "@/types";
import type {
BrowserProfile,
BrowserReleaseTypes,
ProxySettings,
} from "@/types";
import { invoke } from "@tauri-apps/api/core";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { Alert, AlertDescription } from "./ui/alert";
type BrowserTypeString =
| "mullvad-browser"
@@ -49,6 +54,7 @@ interface CreateProfileDialogProps {
name: string;
browserStr: BrowserTypeString;
version: string;
releaseType: string;
proxy?: ProxySettings;
}) => Promise<void>;
}
@@ -61,11 +67,18 @@ export function CreateProfileDialog({
const [profileName, setProfileName] = useState("");
const [selectedBrowser, setSelectedBrowser] =
useState<BrowserTypeString | null>("mullvad-browser");
const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
const [selectedReleaseType, setSelectedReleaseType] = useState<
"stable" | "nightly" | null
>(null);
const [releaseTypes, setReleaseTypes] = useState<BrowserReleaseTypes>({
stable: undefined,
nightly: undefined,
});
const [isCreating, setIsCreating] = useState(false);
const [existingProfiles, setExistingProfiles] = useState<BrowserProfile[]>(
[],
);
const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false);
// Proxy settings
const [proxyEnabled, setProxyEnabled] = useState(false);
@@ -76,12 +89,10 @@ export function CreateProfileDialog({
const [proxyPassword, setProxyPassword] = useState("");
const {
availableVersions,
downloadedVersions,
isDownloading,
loadVersions,
loadDownloadedVersions,
downloadBrowser,
isDownloading,
downloadedVersions,
loadDownloadedVersions,
isVersionDownloaded,
} = useBrowserDownload();
@@ -110,29 +121,26 @@ export function CreateProfileDialog({
useEffect(() => {
if (isOpen && selectedBrowser) {
// Reset selected version when browser changes
setSelectedVersion(null);
void loadVersions(selectedBrowser);
// Reset selected release type when browser changes
setSelectedReleaseType(null);
void loadReleaseTypes(selectedBrowser);
void loadDownloadedVersions(selectedBrowser);
}
}, [isOpen, selectedBrowser, loadVersions, loadDownloadedVersions]);
}, [isOpen, selectedBrowser, loadDownloadedVersions]);
// Set default version when versions are loaded and no version is selected
// Set default release type when release types are loaded
useEffect(() => {
if (availableVersions.length > 0 && selectedBrowser) {
// Always reset version when browser changes or versions are loaded
// Find the latest stable version (not alpha/beta)
const stableVersions = availableVersions.filter((v) => !v.is_nightly);
if (stableVersions.length > 0) {
// Select the first stable version (they're already sorted newest first)
setSelectedVersion(stableVersions[0].tag_name);
} else if (availableVersions.length > 0) {
// If no stable version found, select the first available version
setSelectedVersion(availableVersions[0].tag_name);
if (!selectedReleaseType && Object.keys(releaseTypes).length > 0) {
// First try to set stable if it exists
if (releaseTypes.stable) {
setSelectedReleaseType("stable");
}
// If stable doesn't exist but nightly does, set nightly as default
else if (releaseTypes.nightly && selectedBrowser !== "chromium") {
setSelectedReleaseType("nightly");
}
}
}, [availableVersions, selectedBrowser]);
}, [releaseTypes, selectedReleaseType, selectedBrowser]);
const loadExistingProfiles = async () => {
try {
@@ -143,9 +151,34 @@ export function CreateProfileDialog({
}
};
const loadReleaseTypes = async (browser: string) => {
try {
setIsLoadingReleaseTypes(true);
const types = await invoke<BrowserReleaseTypes>(
"get_browser_release_types",
{
browserStr: browser,
},
);
setReleaseTypes(types);
} catch (error) {
console.error("Failed to load release types:", error);
toast.error("Failed to load available versions");
} finally {
setIsLoadingReleaseTypes(false);
}
};
const handleDownload = async () => {
if (!selectedBrowser || !selectedVersion) return;
await downloadBrowser(selectedBrowser, selectedVersion);
if (!selectedBrowser || !selectedReleaseType) return;
const version =
selectedReleaseType === "stable"
? releaseTypes.stable
: releaseTypes.nightly;
if (!version) return;
await downloadBrowser(selectedBrowser, version);
};
const validateProfileName = (name: string): string | null => {
@@ -178,7 +211,7 @@ export function CreateProfileDialog({
}, [selectedBrowser, proxyEnabled]);
const handleCreate = async () => {
if (!profileName.trim() || !selectedBrowser || !selectedVersion) return;
if (!profileName.trim() || !selectedBrowser || !selectedReleaseType) return;
// Validate profile name
const nameError = validateProfileName(profileName);
@@ -187,6 +220,15 @@ export function CreateProfileDialog({
return;
}
const version =
selectedReleaseType === "stable"
? releaseTypes.stable
: releaseTypes.nightly;
if (!version) {
toast.error("Selected release type is not available");
return;
}
setIsCreating(true);
try {
const proxy =
@@ -204,13 +246,14 @@ export function CreateProfileDialog({
await onCreateProfile({
name: profileName.trim(),
browserStr: selectedBrowser,
version: selectedVersion,
version,
releaseType: selectedReleaseType,
proxy,
});
// Reset form
setProfileName("");
setSelectedVersion(null);
setSelectedReleaseType(null);
setProxyEnabled(false);
setProxyHost("");
setProxyPort(8080);
@@ -227,9 +270,16 @@ export function CreateProfileDialog({
const nameError = profileName.trim()
? validateProfileName(profileName)
: null;
const selectedVersion =
selectedReleaseType === "stable"
? releaseTypes.stable
: releaseTypes.nightly;
const canCreate =
profileName.trim() &&
selectedBrowser &&
selectedReleaseType &&
selectedVersion &&
isVersionDownloaded(selectedVersion) &&
(!proxyEnabled || isProxyDisabled || (proxyHost && proxyPort)) &&
@@ -322,21 +372,48 @@ export function CreateProfileDialog({
</Select>
</div>
{/* Version Selection */}
<div className="grid gap-2">
<Label>Version</Label>
<VersionSelector
selectedVersion={selectedVersion}
onVersionSelect={setSelectedVersion}
availableVersions={availableVersions}
downloadedVersions={downloadedVersions}
isDownloading={isDownloading}
onDownload={() => {
void handleDownload();
}}
placeholder="Select version..."
/>
</div>
{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>
) : (
<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">
+66 -22
View File
@@ -10,6 +10,7 @@
* - Progress bars for downloads/updates
* - Success/error states
* - Customizable icons and content
* - Auto-update notifications
*
* Usage Examples:
*
@@ -23,6 +24,11 @@
* });
* ```
*
* Auto-update toast:
* ```
* showAutoUpdateToast("Firefox", "125.0.1");
* ```
*
* Download progress toast:
* ```
* showToast({
@@ -47,6 +53,7 @@ import {
LuCheckCheck,
LuDownload,
LuRefreshCw,
LuRocket,
LuTriangleAlert,
} from "react-icons/lu";
@@ -90,6 +97,7 @@ interface VersionUpdateToastProps extends BaseToastProps {
current: number;
total: number;
found: number;
current_browser?: string;
};
}
@@ -138,6 +146,10 @@ function getToastIcon(type: ToastProps["type"], stage?: string) {
return (
<LuRefreshCw className="flex-shrink-0 w-4 h-4 text-purple-500 animate-spin" />
);
case "loading":
return (
<div className="flex-shrink-0 w-4 h-4 rounded-full border-2 border-blue-500 animate-spin border-t-transparent" />
);
default:
return (
<div className="flex-shrink-0 w-4 h-4 rounded-full border-2 border-blue-500 animate-spin border-t-transparent" />
@@ -150,11 +162,33 @@ export function UnifiedToast(props: ToastProps) {
const stage = "stage" in props ? props.stage : undefined;
const progress = "progress" in props ? props.progress : undefined;
// Check if this is an auto-update toast
const isAutoUpdate = title.includes("update started");
return (
<div className="flex items-start p-3 w-full bg-white rounded-lg border border-gray-200 shadow-lg dark:bg-gray-800 dark:border-gray-700">
<div className="mr-3 mt-0.5">{getToastIcon(type, stage)}</div>
<div
className={`flex items-start p-3 w-96 rounded-lg border shadow-lg ${
isAutoUpdate
? "bg-emerald-50 border-emerald-200 dark:bg-emerald-950 dark:border-emerald-800"
: "bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-700"
}`}
data-toast-type={isAutoUpdate ? "auto-update" : "default"}
>
<div className="mr-3 mt-0.5">
{isAutoUpdate ? (
<LuRocket className="flex-shrink-0 w-4 h-4 text-emerald-500" />
) : (
getToastIcon(type, stage)
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium leading-tight text-gray-900 dark:text-white">
<p
className={`text-sm font-medium leading-tight ${
isAutoUpdate
? "text-emerald-900 dark:text-emerald-100"
: "text-gray-900 dark:text-white"
}`}
>
{title}
</p>
@@ -181,26 +215,30 @@ export function UnifiedToast(props: ToastProps) {
)}
{/* Version update progress */}
{type === "version-update" && progress && "found" in progress && (
<div className="mt-2 space-y-1">
<p className="text-xs text-gray-600 dark:text-gray-300">
{progress.found} new versions found so far
</p>
<div className="flex items-center space-x-2">
<div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-1.5 min-w-0">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
style={{
width: `${(progress.current / progress.total) * 100}%`,
}}
/>
{type === "version-update" &&
progress &&
"current_browser" in progress && (
<div className="mt-2 space-y-1">
<p className="text-xs text-gray-600 dark:text-gray-300">
{progress.current_browser && (
<>Looking for updates for {progress.current_browser}</>
)}
</p>
<div className="flex items-center space-x-2">
<div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-1.5 min-w-0">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
style={{
width: `${(progress.current / progress.total) * 100}%`,
}}
/>
</div>
<span className="w-8 text-xs text-right text-gray-500 whitespace-nowrap dark:text-gray-400 shrink-0">
{progress.current}/{progress.total}
</span>
</div>
<span className="w-8 text-xs text-right text-gray-500 whitespace-nowrap dark:text-gray-400 shrink-0">
{progress.current}/{progress.total}
</span>
</div>
</div>
)}
)}
{/* Twilight update progress */}
{type === "twilight-update" && (
@@ -220,7 +258,13 @@ export function UnifiedToast(props: ToastProps) {
{/* Description */}
{description && (
<p className="mt-1 text-xs leading-tight text-gray-600 dark:text-gray-300">
<p
className={`mt-1 text-xs leading-tight ${
isAutoUpdate
? "text-emerald-700 dark:text-emerald-300"
: "text-gray-600 dark:text-gray-300"
}`}
>
{description}
</p>
)}
+24
View File
@@ -0,0 +1,24 @@
export const ZenBrowser = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
role="graphics-symbol img"
fill="currentColor"
viewBox="0 0 24 24"
{...props}
>
<path
d="M12 8.15c-2.12 0-3.85 1.72-3.85 3.85s1.72 3.85 3.85 3.85 3.85-1.72 3.85-3.85S14.13 8.15 12 8.15m0 6.92c-1.7 0-3.08-1.38-3.08-3.08S10.3 8.91 12 8.91s3.08 1.38 3.08 3.08-1.38 3.08-3.08 3.08"
className="b"
/>
<path
d="M12 5.33c-3.68 0-6.67 2.98-6.67 6.67s2.98 6.67 6.67 6.67 6.67-2.98 6.67-6.67S15.69 5.33 12 5.33m0 12.05c-2.97 0-5.38-2.41-5.38-5.38S9.03 6.62 12 6.62s5.38 2.41 5.38 5.38-2.41 5.38-5.38 5.38"
className="b"
/>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m0 18.2c-4.53 0-8.21-3.67-8.21-8.2S7.47 3.79 12 3.79s8.21 3.67 8.21 8.21-3.67 8.2-8.21 8.2"
className="b"
/>
</svg>
);
+61 -72
View File
@@ -1,6 +1,5 @@
"use client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -261,10 +260,21 @@ export function ProfilesDataTable({
cell: ({ row }) => {
const browser: string = row.getValue("browser");
const IconComponent = getBrowserIcon(browser);
return (
const browserDisplayName = getBrowserDisplayName(browser);
return browserDisplayName.length > 15 ? (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex gap-2 items-center">
{IconComponent && <IconComponent className="w-4 h-4" />}
<span>{browserDisplayName.slice(0, 15)}...</span>
</div>
</TooltipTrigger>
<TooltipContent>{browserDisplayName}</TooltipContent>
</Tooltip>
) : (
<div className="flex gap-2 items-center">
{IconComponent && <IconComponent className="w-4 h-4" />}
<span>{getBrowserDisplayName(browser)}</span>
<span>{browserDisplayName}</span>
</div>
);
},
@@ -276,67 +286,33 @@ export function ProfilesDataTable({
},
},
{
accessorKey: "version",
header: "Version",
},
{
id: "status",
header: ({ column }) => {
const isSorted = column.getIsSorted();
return (
<Button
variant="ghost"
onClick={() => {
column.toggleSorting(column.getIsSorted() === "asc");
}}
className="p-0 h-auto font-semibold hover:bg-transparent"
>
Status
{isSorted === "asc" && <LuChevronUp className="ml-2 w-4 h-4" />}
{isSorted === "desc" && (
<LuChevronDown className="ml-2 w-4 h-4" />
)}
{!isSorted && (
<LuChevronDown className="ml-2 w-4 h-4 opacity-50" />
)}
</Button>
);
},
accessorKey: "release_type",
header: "Release",
cell: ({ row }) => {
const profile = row.original;
const isRunning = isClient && runningProfiles.has(profile.name);
const releaseType: string = row.getValue("release_type");
const isNightly = releaseType === "nightly";
return (
<div className="flex flex-col gap-1">
<Badge
variant={isRunning ? "default" : "secondary"}
className="text-xs w-fit"
<div className="flex items-center">
<span
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
isNightly
? "text-yellow-800 bg-yellow-100 dark:bg-yellow-900 dark:text-yellow-200"
: "text-green-800 bg-green-100 dark:bg-green-900 dark:text-green-200"
}`}
>
{isClient ? (isRunning ? "Running" : "Stopped") : "Loading..."}
</Badge>
{isClient && isRunning && profile.process_id && (
<span className="text-xs text-muted-foreground">
PID: {profile.process_id}
</span>
)}
{isNightly ? "Nightly" : "Stable"}
</span>
</div>
);
},
enableSorting: true,
sortingFn: (rowA, rowB) => {
// If not on client, sort by name only to ensure consistency
if (!isClient) {
return rowA.original.name.localeCompare(rowB.original.name);
}
const isRunningA = runningProfiles.has(rowA.original.name);
const isRunningB = runningProfiles.has(rowB.original.name);
// Running profiles come first, then stopped ones
// Secondary sort by profile name
if (isRunningA === isRunningB) {
return rowA.original.name.localeCompare(rowB.original.name);
}
return isRunningA ? -1 : 1;
sortingFn: (rowA, rowB, columnId) => {
const releaseA: string = rowA.getValue(columnId);
const releaseB: string = rowB.getValue(columnId);
// Sort with "stable" before "nightly"
if (releaseA === "stable" && releaseB === "nightly") return -1;
if (releaseA === "nightly" && releaseB === "stable") return 1;
return 0;
},
},
{
@@ -345,6 +321,12 @@ export function ProfilesDataTable({
cell: ({ row }) => {
const profile = row.original;
const hasProxy = profile.proxy?.enabled;
const regularText = hasProxy ? profile.proxy?.proxy_type : "Disabled";
const regularTooltipText = hasProxy
? `${profile.proxy?.proxy_type.toUpperCase()} proxy enabled (${
profile.proxy?.host
}:${profile.proxy?.port})`
: "No proxy configured";
return (
<Tooltip>
<TooltipTrigger>
@@ -353,16 +335,16 @@ export function ProfilesDataTable({
<CiCircleCheck className="w-4 h-4 text-green-500" />
)}
<span className="text-sm text-muted-foreground">
{hasProxy ? profile.proxy?.proxy_type : "Disabled"}
{profile.browser === "tor-browser"
? "Not supported"
: regularText}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
{hasProxy
? `${profile.proxy?.proxy_type.toUpperCase()} proxy enabled (${
profile.proxy?.host
}:${profile.proxy?.port})`
: "No proxy configured"}
{profile.browser === "tor-browser"
? "Proxies are not supported for TOR browser"
: regularTooltipText}
</TooltipContent>
</Tooltip>
);
@@ -397,16 +379,18 @@ export function ProfilesDataTable({
}}
disabled={!isClient || isBrowserUpdating}
>
Configure proxy
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
onChangeVersion(profile);
}}
disabled={!isClient || isRunning || isBrowserUpdating}
>
Change version
Configure Proxy
</DropdownMenuItem>
{!["chromium", "zen"].includes(profile.browser) && (
<DropdownMenuItem
onClick={() => {
onChangeVersion(profile);
}}
disabled={!isClient || isRunning || isBrowserUpdating}
>
Switch Release
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
setProfileToRename(profile);
@@ -586,6 +570,11 @@ export function ProfilesDataTable({
setDeleteConfirmationName(e.target.value);
setDeleteError(null);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
void handleDelete();
}
}}
placeholder="Type the profile name here"
/>
</div>
+164
View File
@@ -0,0 +1,164 @@
"use client";
import { LoadingButton } from "@/components/loading-button";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import type { BrowserReleaseTypes } from "@/types";
import { useState } from "react";
import { LuDownload } from "react-icons/lu";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
interface ReleaseTypeSelectorProps {
selectedReleaseType: "stable" | "nightly" | null;
onReleaseTypeSelect: (releaseType: "stable" | "nightly" | null) => void;
availableReleaseTypes: BrowserReleaseTypes;
browser: string;
isDownloading: boolean;
onDownload: () => void;
placeholder?: string;
showDownloadButton?: boolean;
downloadedVersions?: string[];
}
export function ReleaseTypeSelector({
selectedReleaseType,
onReleaseTypeSelect,
availableReleaseTypes,
browser,
isDownloading,
onDownload,
placeholder = "Select release type...",
showDownloadButton = true,
downloadedVersions = [],
}: ReleaseTypeSelectorProps) {
const [popoverOpen, setPopoverOpen] = useState(false);
const releaseOptions = [
...(availableReleaseTypes.stable
? [{ type: "stable" as const, version: availableReleaseTypes.stable }]
: []),
...(availableReleaseTypes.nightly && browser !== "chromium"
? [{ type: "nightly" as const, version: availableReleaseTypes.nightly }]
: []),
];
const selectedDisplayText = selectedReleaseType
? selectedReleaseType === "stable"
? "Stable"
: "Nightly"
: placeholder;
const selectedVersion =
selectedReleaseType === "stable"
? availableReleaseTypes.stable
: selectedReleaseType === "nightly"
? availableReleaseTypes.nightly
: null;
const isVersionDownloaded =
selectedVersion && downloadedVersions.includes(selectedVersion);
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
</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>
{showDownloadButton &&
selectedReleaseType &&
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>
);
}
+11 -23
View File
@@ -21,7 +21,7 @@ import {
} from "@/components/ui/select";
import { usePermissions } from "@/hooks/use-permissions";
import type { PermissionType } from "@/hooks/use-permissions";
import { showSuccessToast } from "@/lib/toast-utils";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import { invoke } from "@tauri-apps/api/core";
import { useTheme } from "next-themes";
import { useCallback, useEffect, useState } from "react";
@@ -31,7 +31,6 @@ interface AppSettings {
set_as_default_browser: boolean;
show_settings_on_startup: boolean;
theme: string;
auto_updates_enabled: boolean;
auto_delete_unused_binaries: boolean;
}
@@ -51,14 +50,12 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
set_as_default_browser: false,
show_settings_on_startup: true,
theme: "system",
auto_updates_enabled: true,
auto_delete_unused_binaries: true,
});
const [originalSettings, setOriginalSettings] = useState<AppSettings>({
set_as_default_browser: false,
show_settings_on_startup: true,
theme: "system",
auto_updates_enabled: true,
auto_delete_unused_binaries: true,
});
const [isDefaultBrowser, setIsDefaultBrowser] = useState(false);
@@ -210,11 +207,16 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
await invoke("clear_all_version_cache_and_refetch");
showSuccessToast("Cache cleared successfully", {
description:
"All browser version cache has been cleared and browsers are being refreshed",
"All browser version cache has been cleared and browsers are being refreshed.",
duration: 4000,
});
} catch (error) {
console.error("Failed to clear cache:", error);
showErrorToast("Failed to clear cache", {
description:
error instanceof Error ? error.message : "Unknown error occurred",
duration: 4000,
});
} finally {
setIsClearingCache(false);
}
@@ -237,9 +239,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
const getPermissionIcon = (type: PermissionType) => {
switch (type) {
case "microphone":
return <BsMic className="h-4 w-4" />;
return <BsMic className="w-4 h-4" />;
case "camera":
return <BsCamera className="h-4 w-4" />;
return <BsCamera className="w-4 h-4" />;
}
};
@@ -255,7 +257,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
const getStatusBadge = (isGranted: boolean) => {
if (isGranted) {
return (
<Badge variant="default" className="bg-green-100 text-green-800">
<Badge variant="default" className="text-green-800 bg-green-100">
Granted
</Badge>
);
@@ -286,7 +288,6 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
settings.show_settings_on_startup !==
originalSettings.show_settings_on_startup ||
settings.theme !== originalSettings.theme ||
settings.auto_updates_enabled !== originalSettings.auto_updates_enabled ||
settings.auto_delete_unused_binaries !==
originalSettings.auto_delete_unused_binaries;
@@ -361,19 +362,6 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
<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-updates"
checked={settings.auto_updates_enabled}
onCheckedChange={(checked) => {
updateSetting("auto_updates_enabled", checked as boolean);
}}
/>
<Label htmlFor="auto-updates" className="text-sm">
Enable automatic browser updates
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="auto-delete-binaries"
@@ -436,7 +424,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
{permissions.map((permission) => (
<div
key={permission.permission_type}
className="flex items-center justify-between p-3 border rounded-lg"
className="flex justify-between items-center p-3 rounded-lg border"
>
<div className="flex items-center space-x-3">
{getPermissionIcon(permission.permission_type)}
-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>
);
}
-129
View File
@@ -1,129 +0,0 @@
"use client";
import { LoadingButton } from "@/components/loading-button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useVersionUpdater } from "@/hooks/use-version-updater";
import {
LuCheckCheck,
LuCircleAlert,
LuClock,
LuRefreshCw,
} from "react-icons/lu";
export function VersionUpdateSettings() {
const {
isUpdating,
lastUpdateTime,
timeUntilNextUpdate,
updateProgress,
triggerManualUpdate,
formatTimeUntilUpdate,
formatLastUpdateTime,
} = useVersionUpdater();
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<LuRefreshCw className="h-5 w-5" />
Background Version Updates
</CardTitle>
<CardDescription>
Browser versions are automatically checked every 3 hours in the
background. New versions are cached and ready when you need them.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Current Status */}
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm font-medium">
<LuClock className="h-4 w-4" />
Last Update
</div>
<div className="text-sm text-muted-foreground">
{formatLastUpdateTime(lastUpdateTime)}
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm font-medium">
<LuCheckCheck className="h-4 w-4" />
Next Update
</div>
<div className="text-sm text-muted-foreground">
{timeUntilNextUpdate <= 0
? "Now"
: `In ${formatTimeUntilUpdate(timeUntilNextUpdate)}`}
</div>
</div>
</div>
{/* Progress indicator */}
{isUpdating && updateProgress && (
<Alert>
<LuRefreshCw className="h-4 w-4 animate-spin" />
<AlertTitle>Updating Browser Versions</AlertTitle>
<AlertDescription>
{updateProgress.current_browser ? (
<>
Checking {updateProgress.current_browser} (
{updateProgress.completed_browsers}/
{updateProgress.total_browsers})
<br />
{updateProgress.new_versions_found} new versions found so far
</>
) : (
"Starting version update..."
)}
</AlertDescription>
</Alert>
)}
{/* Manual update button */}
<div className="flex items-center justify-between pt-2 border-t">
<div className="space-y-1">
<div className="text-sm font-medium">Manual Update</div>
<div className="text-xs text-muted-foreground">
Check for new browser versions now
</div>
</div>
<LoadingButton
isLoading={isUpdating}
onClick={() => {
void triggerManualUpdate();
}}
variant="outline"
size="sm"
disabled={isUpdating}
>
<LuRefreshCw className="h-4 w-4 mr-2" />
{isUpdating ? "Updating..." : "Check Now"}
</LoadingButton>
</div>
{/* Information about background updates */}
<Alert>
<LuCircleAlert className="h-4 w-4" />
<AlertTitle>How it works</AlertTitle>
<AlertDescription className="text-xs">
Version information is checked automatically every 3 hours
<br /> New versions are added to the cache without removing old
ones
<br /> When creating profiles or changing versions, you&apos;ll see
how many new versions were found
<br /> This keeps the app responsive while ensuring you have the
latest information
</AlertDescription>
</Alert>
</CardContent>
</Card>
);
}
+24 -110
View File
@@ -3,13 +3,11 @@ import {
dismissToast,
showDownloadToast,
showErrorToast,
showFetchingToast,
showSuccessToast,
} from "@/lib/toast-utils";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
interface GithubRelease {
tag_name: string;
@@ -45,15 +43,6 @@ interface BrowserVersionsResult {
total_versions_count: number;
}
interface VersionUpdateProgress {
current_browser: string;
total_browsers: number;
completed_browsers: number;
new_versions_found: number;
browser_new_versions: number;
status: string;
}
export function useBrowserDownload() {
const [availableVersions, setAvailableVersions] = useState<GithubRelease[]>(
[],
@@ -62,7 +51,6 @@ export function useBrowserDownload() {
const [isDownloading, setIsDownloading] = useState(false);
const [downloadProgress, setDownloadProgress] =
useState<DownloadProgress | null>(null);
const [isUpdatingVersions, setIsUpdatingVersions] = useState(false);
// Listen for download progress events
useEffect(() => {
@@ -72,53 +60,29 @@ export function useBrowserDownload() {
const browserName = getBrowserDisplayName(progress.browser);
// Check if this is an auto-update download to suppress completion toast
const checkAutoUpdate = async () => {
let isAutoUpdate = false;
try {
isAutoUpdate = await invoke<boolean>("is_auto_update_download", {
browser: progress.browser,
version: progress.version,
});
} catch (error) {
console.error("Failed to check auto-update status:", error);
}
// Show toast with progress
if (progress.stage === "downloading") {
const speedMBps = (
progress.speed_bytes_per_sec /
(1024 * 1024)
).toFixed(1);
const etaText = progress.eta_seconds
? formatTime(progress.eta_seconds)
: "calculating...";
// Show toast with progress
if (progress.stage === "downloading") {
const speedMBps = (
progress.speed_bytes_per_sec /
(1024 * 1024)
).toFixed(1);
const etaText = progress.eta_seconds
? formatTime(progress.eta_seconds)
: "calculating...";
showDownloadToast(browserName, progress.version, "downloading", {
percentage: progress.percentage,
speed: speedMBps,
eta: etaText,
});
} else if (progress.stage === "extracting") {
showDownloadToast(browserName, progress.version, "extracting");
} else if (progress.stage === "verifying") {
showDownloadToast(browserName, progress.version, "verifying");
} else if (progress.stage === "completed") {
// Suppress completion toast for auto-updates
showDownloadToast(
browserName,
progress.version,
"completed",
undefined,
{
suppressCompletionToast: isAutoUpdate,
},
);
setDownloadProgress(null);
}
};
void checkAutoUpdate();
showDownloadToast(browserName, progress.version, "downloading", {
percentage: progress.percentage,
speed: speedMBps,
eta: etaText,
});
} else if (progress.stage === "extracting") {
showDownloadToast(browserName, progress.version, "extracting");
} else if (progress.stage === "verifying") {
showDownloadToast(browserName, progress.version, "verifying");
} else if (progress.stage === "completed") {
showDownloadToast(browserName, progress.version, "completed");
setDownloadProgress(null);
}
});
return () => {
@@ -128,51 +92,6 @@ export function useBrowserDownload() {
};
}, []);
// Listen for version update progress events
useEffect(() => {
const unlisten = listen<VersionUpdateProgress>(
"version-update-progress",
(event) => {
const progress = event.payload;
if (progress.status === "updating") {
setIsUpdatingVersions(true);
if (progress.current_browser) {
const browserName = getBrowserDisplayName(progress.current_browser);
showFetchingToast(browserName, {
id: `version-update-${progress.current_browser}`,
description: "Fetching latest release information...",
});
}
} else if (progress.status === "completed") {
setIsUpdatingVersions(false);
if (progress.new_versions_found > 0) {
showSuccessToast(
`Found ${progress.new_versions_found} new browser versions!`,
{
duration: 3000,
},
);
}
// Dismiss any update toasts
toast.dismiss();
} else if (progress.status === "error") {
setIsUpdatingVersions(false);
showErrorToast("Failed to check for new versions", {
duration: 4000,
});
toast.dismiss();
}
},
);
return () => {
void unlisten.then((fn) => {
fn();
});
};
}, []);
const formatTime = (seconds: number): string => {
if (seconds < 60) {
return `${Math.round(seconds)}s`;
@@ -198,10 +117,8 @@ export function useBrowserDownload() {
const loadVersions = useCallback(async (browserStr: string) => {
const browserName = getBrowserDisplayName(browserStr);
// Show fetching toast
const toastId = showFetchingToast(browserName, {
id: `fetch-${browserStr}`,
});
// Use a simple loading state instead of toast for version fetching
console.log(`Fetching ${browserName} versions...`);
try {
const versionInfos = await invoke<BrowserVersionInfo[]>(
@@ -220,11 +137,9 @@ export function useBrowserDownload() {
);
setAvailableVersions(githubReleases);
dismissToast(toastId);
return githubReleases;
} catch (error) {
console.error("Failed to load versions:", error);
dismissToast(toastId);
showErrorToast(`Failed to fetch ${browserName} versions`, {
description:
error instanceof Error ? error.message : "Unknown error occurred",
@@ -358,7 +273,6 @@ export function useBrowserDownload() {
downloadedVersions,
isDownloading,
downloadProgress,
isUpdatingVersions,
loadVersions,
loadVersionsWithNewCount,
loadDownloadedVersions,
-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;
+103 -132
View File
@@ -1,9 +1,7 @@
import { UpdateNotificationComponent } from "@/components/update-notification";
import { getBrowserDisplayName } from "@/lib/browser-utils";
import { showToast } from "@/lib/toast-utils";
import { dismissToast, showToast } from "@/lib/toast-utils";
import { invoke } from "@tauri-apps/api/core";
import React, { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { useCallback, useRef, useState } from "react";
interface UpdateNotification {
id: string;
@@ -23,73 +21,93 @@ export function useUpdateNotifications(
const [updatingBrowsers, setUpdatingBrowsers] = useState<Set<string>>(
new Set(),
);
const [dismissedNotifications, setDismissedNotifications] = useState<
const [processedNotifications, setProcessedNotifications] = useState<
Set<string>
>(new Set());
const isUpdating = useCallback(
(browser: string) => updatingBrowsers.has(browser),
[updatingBrowsers],
);
// Add refs to track ongoing operations to prevent duplicates
const isCheckingForUpdates = useRef(false);
const activeDownloads = useRef<Set<string>>(new Set()); // Track "browser-version" keys
const checkForUpdates = useCallback(async () => {
// Prevent multiple simultaneous calls
if (isCheckingForUpdates.current) {
console.log("Already checking for updates, skipping duplicate call");
return;
}
isCheckingForUpdates.current = true;
try {
const updates = await invoke<UpdateNotification[]>(
"check_for_browser_updates",
);
// Filter out dismissed notifications unless they're for a newer version
const filteredUpdates = updates.filter((notification) => {
// Check if this exact notification was dismissed
if (dismissedNotifications.has(notification.id)) {
return false;
}
// Check if we dismissed an older version for this browser
const dismissedForBrowser = Array.from(dismissedNotifications).find(
(dismissedId) => {
const parts = dismissedId.split("_");
if (parts.length >= 2) {
const browser = parts[0];
return browser === notification.browser;
}
return false;
},
);
if (dismissedForBrowser) {
// Extract the dismissed version to compare
const dismissedParts = dismissedForBrowser.split("_to_");
if (dismissedParts.length === 2) {
const dismissedToVersion = dismissedParts[1];
// Only show if this is a newer version than what was dismissed
return notification.new_version !== dismissedToVersion;
}
}
return true;
// Filter out already processed notifications
const newUpdates = updates.filter((notification) => {
return !processedNotifications.has(notification.id);
});
setNotifications(filteredUpdates);
setNotifications(newUpdates);
// Show toasts for new notifications - we'll define handleUpdate and handleDismiss separately
// to avoid circular dependencies
// Automatically start downloads for new update notifications
for (const notification of newUpdates) {
if (!processedNotifications.has(notification.id)) {
setProcessedNotifications((prev) =>
new Set(prev).add(notification.id),
);
// Start automatic update without user interaction
void handleAutoUpdate(
notification.browser,
notification.new_version,
notification.id,
);
}
}
} catch (error) {
console.error("Failed to check for updates:", error);
} finally {
isCheckingForUpdates.current = false;
}
}, [dismissedNotifications]);
}, [processedNotifications]);
const handleAutoUpdate = useCallback(
async (browser: string, newVersion: string, notificationId: string) => {
const downloadKey = `${browser}-${newVersion}`;
// Check if this download is already in progress
if (activeDownloads.current.has(downloadKey)) {
console.log(
`Download already in progress for ${downloadKey}, skipping duplicate`,
);
return;
}
// Mark download as active and disable browser
activeDownloads.current.add(downloadKey);
setUpdatingBrowsers((prev) => new Set(prev).add(browser));
const handleUpdate = useCallback(
async (browser: string, newVersion: string) => {
try {
setUpdatingBrowsers((prev) => new Set(prev).add(browser));
const browserDisplayName = getBrowserDisplayName(browser);
// Dismiss all notifications for this browser first
const browserNotifications = notifications.filter(
(n) => n.browser === browser,
);
for (const notification of browserNotifications) {
toast.dismiss(notification.id);
await invoke("dismiss_update_notification", {
notificationId: notification.id,
});
}
// Dismiss the notification in the backend
await invoke("dismiss_update_notification", {
notificationId,
});
// Show update started notification
showToast({
id: `auto-update-started-${browser}-${newVersion}`,
type: "loading",
title: `${browserDisplayName} update started`,
description: `Version ${newVersion} download will begin shortly. Browser launch is disabled until update completes.`,
duration: 4000,
});
try {
// Check if browser already exists before downloading
@@ -103,14 +121,25 @@ export function useUpdateNotifications(
console.log(
`${browserDisplayName} ${newVersion} already exists, skipping download`,
);
showToast({
id: `auto-update-skip-download-${browser}-${newVersion}`,
type: "success",
title: `${browserDisplayName} ${newVersion} already available`,
description: "Updating profile configurations...",
duration: 3000,
});
} else {
// Mark download as auto-update in the backend for toast suppression
await invoke("mark_auto_update_download", {
browser,
version: newVersion,
// Show download starting notification
showToast({
id: `auto-update-download-starting-${browser}-${newVersion}`,
type: "loading",
title: `Starting ${browserDisplayName} ${newVersion} download`,
description: "Download progress will be shown below...",
duration: 4000,
});
// Download the browser (progress will be handled by use-browser-download hook)
// Download the browser - this will trigger download progress events automatically
await invoke("download_browser", {
browserStr: browser,
version: newVersion,
@@ -134,59 +163,45 @@ export function useUpdateNotifications(
: `${updatedProfiles.length} profiles have been updated`;
showToast({
id: `auto-update-success-${browser}-${newVersion}`,
type: "success",
title: `${browserDisplayName} update completed`,
description: `${profileText} to version ${newVersion}. Running profiles were not updated and can be updated manually.`,
duration: 5000,
description: `${profileText} to version ${newVersion}. You can now launch your browsers with the latest version.`,
duration: 6000,
});
} else {
showToast({
id: `auto-update-success-${browser}-${newVersion}`,
type: "success",
title: `${browserDisplayName} update ready`,
description:
"All affected profiles are currently running. Stop them and manually update their versions to use the new version.",
duration: 5000,
title: `${browserDisplayName} update completed`,
description: `Version ${newVersion} is now available. Running profiles will use the new version when restarted.`,
duration: 6000,
});
}
// Trigger profile refresh to update UI with new versions
if (onProfilesUpdated) {
void onProfilesUpdated();
await onProfilesUpdated();
}
} catch (downloadError) {
console.error("Failed to download browser:", downloadError);
// Clean up auto-update tracking on error
try {
await invoke("remove_auto_update_download", {
browser,
version: newVersion,
});
} catch (e) {
console.error("Failed to clean up auto-update tracking:", e);
}
dismissToast(`download-${browser}-${newVersion}`);
showToast({
id: `auto-update-error-${browser}-${newVersion}`,
type: "error",
title: `Failed to download ${browserDisplayName} ${newVersion}`,
description: String(downloadError),
duration: 6000,
duration: 8000,
});
throw downloadError;
}
// Refresh notifications to clear any remaining ones
await checkForUpdates();
} catch (error) {
console.error("Failed to start update:", error);
const browserDisplayName = getBrowserDisplayName(browser);
showToast({
type: "error",
title: `Failed to update ${browserDisplayName}`,
description: String(error),
duration: 6000,
});
console.error("Failed to start auto-update:", error);
throw error;
} finally {
// Clean up
activeDownloads.current.delete(downloadKey);
setUpdatingBrowsers((prev) => {
const next = new Set(prev);
next.delete(browser);
@@ -194,56 +209,12 @@ export function useUpdateNotifications(
});
}
},
[notifications, checkForUpdates, onProfilesUpdated],
[onProfilesUpdated],
);
const handleDismiss = useCallback(
async (notificationId: string) => {
try {
toast.dismiss(notificationId);
await invoke("dismiss_update_notification", { notificationId });
// Track this notification as dismissed to prevent showing it again
setDismissedNotifications((prev) => new Set(prev).add(notificationId));
await checkForUpdates();
} catch (error) {
console.error("Failed to dismiss notification:", error);
}
},
[checkForUpdates],
);
// Separate effect to show toasts when notifications change
useEffect(() => {
for (const notification of notifications) {
const isUpdating = updatingBrowsers.has(notification.browser);
toast.custom(
() => (
<UpdateNotificationComponent
notification={notification}
onUpdate={handleUpdate}
onDismiss={handleDismiss}
isUpdating={isUpdating}
/>
),
{
id: notification.id,
duration: Number.POSITIVE_INFINITY, // Persistent until user action
position: "top-right",
style: {
zIndex: 99999, // Ensure notifications appear above dialogs
pointerEvents: "auto", // Ensure notifications remain interactive
},
},
);
}
}, [notifications, updatingBrowsers, handleUpdate, handleDismiss]);
return {
notifications,
isUpdating,
checkForUpdates,
isUpdating: (browser: string) => updatingBrowsers.has(browser),
};
}
+165 -64
View File
@@ -1,9 +1,14 @@
import { getBrowserDisplayName } from "@/lib/browser-utils";
import { showLoadingToast, showVersionUpdateToast } from "@/lib/toast-utils";
import {
dismissToast,
showAutoUpdateToast,
showErrorToast,
showSuccessToast,
showUnifiedVersionUpdateToast,
} from "@/lib/toast-utils";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
interface VersionUpdateProgress {
current_browser: string;
@@ -28,6 +33,13 @@ interface BrowserVersionsResult {
total_versions_count: number;
}
interface AutoUpdateEvent {
browser: string;
new_version: string;
notification_id: string;
affected_profiles: string[];
}
export function useVersionUpdater() {
const [isUpdating, setIsUpdating] = useState(false);
const [lastUpdateTime, setLastUpdateTime] = useState<number | null>(null);
@@ -35,6 +47,18 @@ export function useVersionUpdater() {
const [updateProgress, setUpdateProgress] =
useState<VersionUpdateProgress | null>(null);
const loadUpdateStatus = useCallback(async () => {
try {
const [lastUpdate, timeUntilNext] = await invoke<[number | null, number]>(
"get_version_update_status",
);
setLastUpdateTime(lastUpdate);
setTimeUntilNextUpdate(timeUntilNext);
} catch (error) {
console.error("Failed to load version update status:", error);
}
}, []);
// Listen for version update progress events
useEffect(() => {
const unlisten = listen<VersionUpdateProgress>(
@@ -46,42 +70,35 @@ export function useVersionUpdater() {
if (progress.status === "updating") {
setIsUpdating(true);
if (progress.current_browser) {
const browserName = getBrowserDisplayName(progress.current_browser);
showVersionUpdateToast(
`Downloading release information for ${browserName}`,
{
id: "version-update-progress",
progress: {
current: progress.completed_browsers + 1,
total: progress.total_browsers,
found: progress.new_versions_found,
},
},
);
} else {
showLoadingToast("Starting version update check...", {
id: "version-update-progress",
description: "Initializing browser version check...",
});
}
// Show unified progress toast
const currentBrowserName = progress.current_browser
? getBrowserDisplayName(progress.current_browser)
: undefined;
showUnifiedVersionUpdateToast("Checking for browser updates...", {
description: currentBrowserName
? `Fetching ${currentBrowserName} release information...`
: "Initializing version check...",
progress: {
current: progress.completed_browsers,
total: progress.total_browsers,
found: progress.new_versions_found,
current_browser: currentBrowserName,
},
});
} else if (progress.status === "completed") {
setIsUpdating(false);
setUpdateProgress(null);
dismissToast("unified-version-update");
if (progress.new_versions_found > 0) {
toast.success(
`Found ${progress.new_versions_found} new browser versions!`,
{
id: "version-update-progress",
duration: 4000,
description:
"Version information has been updated in the background",
},
);
showSuccessToast("Browser versions updated successfully", {
duration: 5000,
description:
"Auto-downloads will start shortly for available updates.",
});
} else {
toast.success("No new browser versions found", {
id: "version-update-progress",
showSuccessToast("No new browser versions found", {
duration: 3000,
description: "All browser versions are up to date",
});
@@ -92,16 +109,115 @@ export function useVersionUpdater() {
} else if (progress.status === "error") {
setIsUpdating(false);
setUpdateProgress(null);
dismissToast("unified-version-update");
toast.error("Failed to update browser versions", {
id: "version-update-progress",
duration: 4000,
showErrorToast("Failed to update browser versions", {
duration: 6000,
description: "Check your internet connection and try again",
});
}
},
);
return () => {
void unlisten.then((fn) => {
fn();
});
};
}, [loadUpdateStatus]);
// Listen for browser auto-update events
useEffect(() => {
const unlisten = listen<AutoUpdateEvent>(
"browser-auto-update-available",
(event) => {
const handleAutoUpdate = async () => {
const { browser, new_version, notification_id } = event.payload;
console.log("Browser auto-update event received:", event.payload);
const browserDisplayName = getBrowserDisplayName(browser);
try {
// Show auto-update start notification
showAutoUpdateToast(browserDisplayName, new_version, {
description: `Downloading ${browserDisplayName} ${new_version} automatically. Progress will be shown below.`,
});
// Dismiss the update notification in the backend
await invoke("dismiss_update_notification", {
notificationId: notification_id,
});
// Check if browser already exists before downloading
const isDownloaded = await invoke<boolean>("check_browser_exists", {
browserStr: browser,
version: new_version,
});
if (isDownloaded) {
// Browser already exists, skip download and go straight to profile update
console.log(
`${browserDisplayName} ${new_version} already exists, skipping download`,
);
showSuccessToast(
`${browserDisplayName} ${new_version} already available`,
{
description: "Updating profile configurations...",
duration: 3000,
},
);
} else {
// Download the browser - this will trigger download progress events automatically
await invoke("download_browser", {
browserStr: browser,
version: new_version,
});
}
// Complete the update with auto-update of profile versions
const updatedProfiles = await invoke<string[]>(
"complete_browser_update_with_auto_update",
{
browser,
newVersion: new_version,
},
);
// Show success message based on whether profiles were updated
if (updatedProfiles.length > 0) {
const profileText =
updatedProfiles.length === 1
? `Profile "${updatedProfiles[0]}" has been updated`
: `${updatedProfiles.length} profiles have been updated`;
showSuccessToast(`${browserDisplayName} update completed`, {
description: `${profileText} to version ${new_version}. You can now launch your browsers with the latest version.`,
duration: 6000,
});
} else {
showSuccessToast(`${browserDisplayName} update completed`, {
description: `Version ${new_version} is now available. Running profiles will use the new version when restarted.`,
duration: 6000,
});
}
} catch (error) {
console.error("Failed to handle browser auto-update:", error);
showErrorToast(`Failed to auto-update ${browserDisplayName}`, {
description:
error instanceof Error
? error.message
: "Unknown error occurred",
duration: 8000,
});
}
};
// Call the async handler
void handleAutoUpdate();
},
);
return () => {
void unlisten.then((fn) => {
fn();
@@ -121,19 +237,7 @@ export function useVersionUpdater() {
return () => {
clearInterval(interval);
};
}, []);
const loadUpdateStatus = useCallback(async () => {
try {
const [lastUpdate, timeUntilNext] = await invoke<[number | null, number]>(
"get_version_update_status",
);
setLastUpdateTime(lastUpdate);
setTimeUntilNextUpdate(timeUntilNext);
} catch (error) {
console.error("Failed to load version update status:", error);
}
}, []);
}, [loadUpdateStatus]);
const triggerManualUpdate = useCallback(async () => {
try {
@@ -154,17 +258,17 @@ export function useVersionUpdater() {
).length;
if (failedUpdates > 0) {
toast.warning("Update completed with some errors", {
showErrorToast("Update completed with some errors", {
description: `${totalNewVersions} new versions found, ${failedUpdates} browsers failed to update`,
duration: 5000,
});
} else if (totalNewVersions > 0) {
toast.success(`Found ${totalNewVersions} new browser versions!`, {
description: `Updated ${successfulUpdates} browsers successfully`,
showSuccessToast("Browser versions updated successfully", {
description: `Found ${totalNewVersions} new versions across ${successfulUpdates} browsers. Auto-downloads will start shortly.`,
duration: 4000,
});
} else {
toast.success("No new browser versions found", {
showSuccessToast("No new browser versions found", {
description: "All browser versions are up to date",
duration: 3000,
});
@@ -174,7 +278,7 @@ export function useVersionUpdater() {
return results;
} catch (error) {
console.error("Failed to trigger manual update:", error);
toast.error("Failed to update browser versions", {
showErrorToast("Failed to update browser versions", {
description:
error instanceof Error ? error.message : "Unknown error occurred",
duration: 4000,
@@ -196,7 +300,7 @@ export function useVersionUpdater() {
// Show notification about new versions if any were found
if (result.new_versions_count && result.new_versions_count > 0) {
const browserName = getBrowserDisplayName(browserStr);
toast.success(
showSuccessToast(
`Found ${result.new_versions_count} new ${browserName} versions!`,
{
duration: 3000,
@@ -215,18 +319,15 @@ export function useVersionUpdater() {
);
const formatTimeUntilUpdate = useCallback((seconds: number): string => {
if (seconds <= 0) return "Update overdue";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
if (seconds < 60) {
return `${seconds} seconds`;
}
if (minutes > 0) {
return `${minutes}m`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
}
return "< 1m";
const hours = Math.floor(minutes / 60);
return `${hours} hour${hours === 1 ? "" : "s"}`;
}, []);
const formatLastUpdateTime = useCallback(
+2 -12
View File
@@ -3,6 +3,7 @@
* Centralized helpers for browser name mapping, icons, etc.
*/
import { ZenBrowser } from "@/components/icons/zen-browser";
import { FaChrome, FaFirefox } from "react-icons/fa";
import { SiBrave, SiMullvad, SiTorbrowser } from "react-icons/si";
@@ -38,21 +39,10 @@ export function getBrowserIcon(browserType: string) {
case "firefox-developer":
return FaFirefox;
case "zen":
return FaFirefox;
return ZenBrowser;
case "tor-browser":
return SiTorbrowser;
default:
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(" ");
}
+44 -99
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,48 +36,33 @@ export interface DownloadToastProps extends BaseToastProps {
};
}
export interface VersionUpdateToastProps extends BaseToastProps {
interface VersionUpdateToastProps extends BaseToastProps {
type: "version-update";
progress?: {
current: number;
total: number;
found: number;
current_browser?: string;
};
}
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
@@ -90,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;
}
@@ -151,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,
@@ -205,44 +170,6 @@ export function showDownloadToast(
});
}
export function showVersionUpdateToast(
title: string,
options?: {
id?: string;
description?: string;
progress?: {
current: number;
total: number;
found: number;
};
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?: {
@@ -273,31 +200,49 @@ export function showErrorToast(
});
}
export function showTwilightUpdateToast(
export function showAutoUpdateToast(
browserName: string,
version: 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,
type: "loading",
title: `${browserName} update started`,
description:
options?.description ??
`Automatically downloading ${browserName} ${version}. Progress will be shown in download notifications.`,
id: options?.id ?? `auto-update-${browserName.toLowerCase()}-${version}`,
duration: options?.duration ?? 4000,
});
}
// Generic helper for dismissing toasts
export function dismissToast(id: string) {
sonnerToast.dismiss(id);
}
// Dismiss all toasts
export function dismissAllToasts() {
sonnerToast.dismiss();
export function showUnifiedVersionUpdateToast(
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,
id: "unified-version-update",
duration: Number.POSITIVE_INFINITY, // Keep showing until completed
...options,
});
}
+6 -19
View File
@@ -20,6 +20,7 @@ export interface BrowserProfile {
proxy?: ProxySettings;
process_id?: number;
last_launch?: number;
release_type: string; // "stable" or "nightly"
}
export interface DetectedProfile {
@@ -29,6 +30,11 @@ export interface DetectedProfile {
description: string;
}
export interface BrowserReleaseTypes {
stable?: string;
nightly?: string;
}
export interface AppUpdateInfo {
current_version: string;
new_version: string;
@@ -37,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;
}