mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-25 16:09:58 +02:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97de246ac6 | |||
| b00f62ebec | |||
| 2025a2a690 | |||
| 2f1faa02e4 | |||
| 7a5b807828 | |||
| d0a5c16ce9 | |||
| e2e1ad1582 | |||
| cb61861503 | |||
| 1950ef0098 | |||
| 814875c28e | |||
| b06ca4f11e | |||
| 3ab1ea61e8 | |||
| a0599ecfc1 | |||
| 6c834b3003 | |||
| 269b4dbe77 | |||
| ef00854307 | |||
| 03d915e5c7 | |||
| 91b12e80e5 | |||
| 3af581c4ab | |||
| 7a85edfb8a | |||
| 141a5f06a4 | |||
| 7a3857c06a |
+46
-4
@@ -1,28 +1,70 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
# Enable version updates for Node.js dependencies
|
# Frontend dependencies (root package.json)
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "09:00"
|
||||||
allow:
|
allow:
|
||||||
- dependency-type: "all"
|
- dependency-type: "all"
|
||||||
groups:
|
groups:
|
||||||
all:
|
frontend-dependencies:
|
||||||
patterns:
|
patterns:
|
||||||
- "*"
|
- "*"
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "eslint"
|
- dependency-name: "eslint"
|
||||||
versions: ">= 9"
|
versions: ">= 9"
|
||||||
|
commit-message:
|
||||||
|
prefix: "deps"
|
||||||
|
include: "scope"
|
||||||
|
|
||||||
# Enable version updates for rust
|
# Nodecar dependencies
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/nodecar"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "09:00"
|
||||||
|
allow:
|
||||||
|
- dependency-type: "all"
|
||||||
|
groups:
|
||||||
|
nodecar-dependencies:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
commit-message:
|
||||||
|
prefix: "deps(nodecar)"
|
||||||
|
include: "scope"
|
||||||
|
|
||||||
|
# Rust dependencies
|
||||||
- package-ecosystem: "cargo"
|
- package-ecosystem: "cargo"
|
||||||
directory: "/src-tauri"
|
directory: "/src-tauri"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "09:00"
|
||||||
allow:
|
allow:
|
||||||
- dependency-type: "all"
|
- dependency-type: "all"
|
||||||
groups:
|
groups:
|
||||||
all:
|
rust-dependencies:
|
||||||
patterns:
|
patterns:
|
||||||
- "*"
|
- "*"
|
||||||
|
commit-message:
|
||||||
|
prefix: "deps(rust)"
|
||||||
|
include: "scope"
|
||||||
|
|
||||||
|
# GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "09:00"
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
commit-message:
|
||||||
|
prefix: "ci"
|
||||||
|
include: "scope"
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
# Automatically squashes and merges Dependabot dependency upgrades if tests pass
|
|
||||||
|
|
||||||
name: Dependabot Auto-merge
|
|
||||||
|
|
||||||
on: pull_request_target
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
dependabot:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
|
||||||
steps:
|
|
||||||
- name: Fetch Dependabot metadata
|
|
||||||
id: dependabot-metadata
|
|
||||||
uses: dependabot/fetch-metadata@v2
|
|
||||||
with:
|
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
|
|
||||||
- name: Approve Dependabot PR
|
|
||||||
run: gh pr review --approve "$PR_URL"
|
|
||||||
env:
|
|
||||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Auto-merge (squash) Dependabot PR
|
|
||||||
if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }}
|
|
||||||
run: gh pr merge --auto --squash "$PR_URL"
|
|
||||||
env:
|
|
||||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -13,6 +13,8 @@ on:
|
|||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "src-tauri/**"
|
- "src-tauri/**"
|
||||||
- "README.md"
|
- "README.md"
|
||||||
|
- ".github/workflows/lint-rs.yml"
|
||||||
|
- ".github/workflows/osv.yml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -49,4 +51,4 @@ jobs:
|
|||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run lint step
|
- name: Run lint step
|
||||||
run: pnpm lint
|
run: pnpm run lint:js
|
||||||
|
|||||||
@@ -12,11 +12,18 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "src/**"
|
- "src/**"
|
||||||
|
- "nodecar/**"
|
||||||
- "package.json"
|
- "package.json"
|
||||||
- "package-lock.json"
|
- "package-lock.json"
|
||||||
- "yarn.lock"
|
- "yarn.lock"
|
||||||
- "pnpm-lock.yaml"
|
- "pnpm-lock.yaml"
|
||||||
- "README.md"
|
- "README.md"
|
||||||
|
- ".github/workflows/lint-js.yml"
|
||||||
|
- ".github/workflows/osv.yml"
|
||||||
|
- "next.config.js"
|
||||||
|
- "tailwind.config.js"
|
||||||
|
- "tsconfig.json"
|
||||||
|
- "biome.json"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
|
# They are provided by a third-party and are governed by
|
||||||
|
# separate terms of service, privacy policy, and support
|
||||||
|
# documentation.
|
||||||
|
|
||||||
|
# A sample workflow which sets up periodic OSV-Scanner scanning for vulnerabilities,
|
||||||
|
# in addition to a PR check which fails if new vulnerabilities are introduced.
|
||||||
|
#
|
||||||
|
# For more examples and options, including how to ignore specific vulnerabilities,
|
||||||
|
# see https://google.github.io/osv-scanner/github-action/
|
||||||
|
|
||||||
|
# Security vulnerability scanning for Donut Browser
|
||||||
|
# Scans dependencies in package managers (npm/pnpm, Cargo) for known vulnerabilities
|
||||||
|
# Runs on schedule and when dependencies change
|
||||||
|
|
||||||
|
name: Security Vulnerability Scan
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
paths:
|
||||||
|
- "package.json"
|
||||||
|
- "pnpm-lock.yaml"
|
||||||
|
- "package-lock.json"
|
||||||
|
- "src-tauri/Cargo.toml"
|
||||||
|
- "src-tauri/Cargo.lock"
|
||||||
|
- "nodecar/package.json"
|
||||||
|
- "nodecar/package-lock.json"
|
||||||
|
- ".github/workflows/osv.yml"
|
||||||
|
merge_group:
|
||||||
|
branches: ["main"]
|
||||||
|
schedule:
|
||||||
|
# Run weekly on Tuesdays at 2:20 PM UTC
|
||||||
|
- cron: "20 14 * * 2"
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
paths:
|
||||||
|
- "package.json"
|
||||||
|
- "pnpm-lock.yaml"
|
||||||
|
- "package-lock.json"
|
||||||
|
- "src-tauri/Cargo.toml"
|
||||||
|
- "src-tauri/Cargo.lock"
|
||||||
|
- "nodecar/package.json"
|
||||||
|
- "nodecar/package-lock.json"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
# Require writing security events to upload SARIF file to security tab
|
||||||
|
security-events: write
|
||||||
|
# Read commit contents
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
scan-scheduled:
|
||||||
|
name: Scheduled Security Scan
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
|
||||||
|
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@1f1242919d8a60496dd1874b24b62b2370ed4c78" # v1.7.1
|
||||||
|
with:
|
||||||
|
scan-args: |-
|
||||||
|
-r
|
||||||
|
--skip-git
|
||||||
|
--lockfile=package-lock.json
|
||||||
|
--lockfile=pnpm-lock.yaml
|
||||||
|
--lockfile=src-tauri/Cargo.lock
|
||||||
|
--lockfile=nodecar/package-lock.json
|
||||||
|
./
|
||||||
|
|
||||||
|
scan-pr:
|
||||||
|
name: PR Security Scan
|
||||||
|
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
||||||
|
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@1f1242919d8a60496dd1874b24b62b2370ed4c78" # v1.7.1
|
||||||
|
with:
|
||||||
|
scan-args: |-
|
||||||
|
-r
|
||||||
|
--skip-git
|
||||||
|
--lockfile=package-lock.json
|
||||||
|
--lockfile=pnpm-lock.yaml
|
||||||
|
--lockfile=src-tauri/Cargo.lock
|
||||||
|
--lockfile=nodecar/package-lock.json
|
||||||
|
./
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: Pull Request Checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
merge_group:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
# Required for OSV scanner to upload SARIF file to security tab
|
||||||
|
security-events: write
|
||||||
|
# Read commit contents
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-js:
|
||||||
|
name: Lint JavaScript/TypeScript
|
||||||
|
uses: ./.github/workflows/lint-js.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
lint-rust:
|
||||||
|
name: Lint Rust
|
||||||
|
uses: ./.github/workflows/lint-rs.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
security-scan:
|
||||||
|
name: Security Vulnerability Scan
|
||||||
|
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
||||||
|
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@1f1242919d8a60496dd1874b24b62b2370ed4c78" # v1.7.1
|
||||||
|
with:
|
||||||
|
scan-args: |-
|
||||||
|
-r
|
||||||
|
--skip-git
|
||||||
|
--lockfile=pnpm-lock.yaml
|
||||||
|
--lockfile=nodecar/pnpm-lock.yaml
|
||||||
|
--lockfile=src-tauri/Cargo.lock
|
||||||
|
./
|
||||||
|
|
||||||
|
pr-status:
|
||||||
|
name: PR Status Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint-js, lint-rust, security-scan]
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- name: Check all jobs succeeded
|
||||||
|
run: |
|
||||||
|
if [[ "${{ needs.lint-js.result }}" != "success" || "${{ needs.lint-rust.result }}" != "success" || "${{ needs.security-scan.result }}" != "success" ]]; then
|
||||||
|
echo "One or more checks failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "All checks passed!"
|
||||||
+2
-2
@@ -30,8 +30,8 @@ yarn-error.log*
|
|||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# nodecar
|
# nodecar
|
||||||
nodecar/dist
|
**/dist
|
||||||
nodecar/node_modules
|
**/node_modules
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|||||||
Vendored
+23
-1
@@ -1,23 +1,45 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
|
"applescript",
|
||||||
|
"autoconfig",
|
||||||
"autologin",
|
"autologin",
|
||||||
|
"cdylib",
|
||||||
"CFURL",
|
"CFURL",
|
||||||
|
"checkin",
|
||||||
"clippy",
|
"clippy",
|
||||||
|
"codegen",
|
||||||
"donutbrowser",
|
"donutbrowser",
|
||||||
|
"dtolnay",
|
||||||
|
"elif",
|
||||||
|
"gifs",
|
||||||
"launchservices",
|
"launchservices",
|
||||||
"mountpoint",
|
"mountpoint",
|
||||||
|
"Mullvad",
|
||||||
"nodecar",
|
"nodecar",
|
||||||
"ntlm",
|
"ntlm",
|
||||||
|
"objc",
|
||||||
|
"osascript",
|
||||||
|
"plasmohq",
|
||||||
"propertylist",
|
"propertylist",
|
||||||
|
"reqwest",
|
||||||
"rlib",
|
"rlib",
|
||||||
"rustc",
|
"rustc",
|
||||||
"serde",
|
"serde",
|
||||||
"shadcn",
|
"shadcn",
|
||||||
"signon",
|
"signon",
|
||||||
|
"sonner",
|
||||||
"sspi",
|
"sspi",
|
||||||
"staticlib",
|
"staticlib",
|
||||||
|
"swatinem",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"systempreferences",
|
"systempreferences",
|
||||||
"turbopack"
|
"tauri",
|
||||||
|
"titlebar",
|
||||||
|
"Torbrowser",
|
||||||
|
"turbopack",
|
||||||
|
"unlisten",
|
||||||
|
"wiremock",
|
||||||
|
"xattr",
|
||||||
|
"zhom"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
> As of right now, the app is not signed by Apple. You need to have Gatekeeper disabled to run it.
|
> As of right now, the app is not signed by Apple. You need to have Gatekeeper disabled to run it. The app automatically checks for updates on each launch.
|
||||||
|
|
||||||
The app can be downloaded from the [releases page](https://github.com/zhom/donutbrowser/releases/latest).
|
The app can be downloaded from the [releases page](https://github.com/zhom/donutbrowser/releases/latest).
|
||||||
|
|
||||||
@@ -54,6 +54,10 @@ Have questions or want to contribute? We'd love to hear from you!
|
|||||||
- **Issues**: [GitHub Issues](https://github.com/zhom/donutbrowser/issues)
|
- **Issues**: [GitHub Issues](https://github.com/zhom/donutbrowser/issues)
|
||||||
- **Discussions**: [GitHub Discussions](https://github.com/zhom/donutbrowser/discussions)
|
- **Discussions**: [GitHub Discussions](https://github.com/zhom/donutbrowser/discussions)
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Have an urgent question or want to report a security vulnerability? Send an email to contact at donutbrowser dot com and we'll get back to you as fast as possible.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the AGPL-3.0 License - see the [LICENSE](LICENSE) file for details.
|
This project is licensed under the AGPL-3.0 License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
"rsc": true,
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "tailwind.config.js",
|
||||||
"css": "src/styles/globals.css",
|
"css": "src/styles/globals.css",
|
||||||
"baseColor": "zinc",
|
"baseColor": "zinc",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
@@ -18,4 +18,4 @@
|
|||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide"
|
"iconLibrary": "lucide"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "AGPL-3.0",
|
||||||
"packageManager": "pnpm@10.6.1",
|
"packageManager": "pnpm@10.6.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^22.15.17",
|
"@types/node": "^22.15.17",
|
||||||
|
|||||||
+9
-7
@@ -1,21 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "donutbrowser",
|
"name": "donutbrowser",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.2",
|
"license": "AGPL-3.0",
|
||||||
|
"version": "0.2.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "biome check src/ && tsc --noEmit && next lint",
|
"lint": "pnpm lint:js && pnpm lint:rust",
|
||||||
|
"lint:js": "biome check src/ && tsc --noEmit && next lint",
|
||||||
"lint:rust": "cd src-tauri && cargo clippy --all-targets --all-features -- -D warnings -D clippy::all && cargo fmt --all",
|
"lint:rust": "cd src-tauri && cargo clippy --all-targets --all-features -- -D warnings -D clippy::all && cargo fmt --all",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"shadcn:add": "pnpm dlx shadcn@latest add",
|
"shadcn:add": "pnpm dlx shadcn@latest add",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"format:js": "biome check src/ --fix",
|
|
||||||
"format:rust": "cd src-tauri && cargo clippy --fix --allow-dirty --all-targets --all-features -- -D warnings -D clippy::all && cargo fmt --all",
|
"format:rust": "cd src-tauri && cargo clippy --fix --allow-dirty --all-targets --all-features -- -D warnings -D clippy::all && cargo fmt --all",
|
||||||
"format:biome": "biome check src/ --fix",
|
"format:js": "biome check src/ --fix",
|
||||||
"format": "pnpm format:js && pnpm format:rust"
|
"format": "pnpm format:js && pnpm format:rust",
|
||||||
|
"cargo": "cd src-tauri && cargo"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
@@ -35,7 +37,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"next": "^15.3.2",
|
"next": "^15.3.3",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
@@ -60,7 +62,7 @@
|
|||||||
"eslint-config-next": "^15.3.2",
|
"eslint-config-next": "^15.3.2",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^15.3.0",
|
"lint-staged": "^16.1.0",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.7",
|
||||||
"tw-animate-css": "^1.3.0",
|
"tw-animate-css": "^1.3.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
|
|||||||
Generated
+928
-942
File diff suppressed because it is too large
Load Diff
Generated
+473
-624
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "donutbrowser"
|
name = "donutbrowser"
|
||||||
version = "0.2.2"
|
version = "0.2.5"
|
||||||
description = "Browser Orchestrator"
|
description = "Browser Orchestrator"
|
||||||
authors = ["zhom@github"]
|
authors = ["zhom@github"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -37,6 +37,8 @@ futures-util = "0.3"
|
|||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
core-foundation="0.10"
|
core-foundation="0.10"
|
||||||
|
objc2 = "0.6.1"
|
||||||
|
objc2-app-kit = { version = "0.3.1", features = ["NSWindow"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.13.0"
|
tempfile = "3.13.0"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.2.2</string>
|
<string>0.2.5</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:event:default",
|
"core:event:default",
|
||||||
|
"core:window:default",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
|
"core:window:allow-close",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-toggle-maximize",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"fs:default",
|
"fs:default",
|
||||||
"shell:allow-execute",
|
"shell:allow-execute",
|
||||||
|
|||||||
+188
-67
@@ -36,12 +36,16 @@ impl VersionComponent {
|
|||||||
let version = version.trim();
|
let version = version.trim();
|
||||||
|
|
||||||
// Handle special case for Zen Browser twilight releases
|
// Handle special case for Zen Browser twilight releases
|
||||||
if version.to_lowercase().contains("twilight") {
|
if version.to_lowercase() == "twilight" {
|
||||||
|
// Pure twilight release without base version
|
||||||
return VersionComponent {
|
return VersionComponent {
|
||||||
major: u32::MAX,
|
major: 999, // High major version to indicate it's a rolling release
|
||||||
minor: u32::MAX,
|
minor: 0,
|
||||||
patch: u32::MAX,
|
patch: 0,
|
||||||
pre_release: None,
|
pre_release: Some(PreRelease {
|
||||||
|
kind: PreReleaseKind::Alpha,
|
||||||
|
number: Some(999), // High number to indicate it's a rolling release
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +144,38 @@ impl Ord for VersionComponent {
|
|||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
// Check for twilight versions
|
||||||
|
let self_is_twilight = self
|
||||||
|
.pre_release
|
||||||
|
.as_ref()
|
||||||
|
.map(|pr| pr.kind == PreReleaseKind::Alpha && pr.number == Some(999))
|
||||||
|
.unwrap_or(false);
|
||||||
|
let other_is_twilight = other
|
||||||
|
.pre_release
|
||||||
|
.as_ref()
|
||||||
|
.map(|pr| pr.kind == PreReleaseKind::Alpha && pr.number == Some(999))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
// If one is twilight and the other isn't, twilight always has priority
|
||||||
|
if self_is_twilight && !other_is_twilight {
|
||||||
|
return Ordering::Greater; // twilight > non-twilight
|
||||||
|
}
|
||||||
|
if !self_is_twilight && other_is_twilight {
|
||||||
|
return Ordering::Less; // non-twilight < twilight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both are twilight or both are not twilight - use normal comparison
|
||||||
|
match (self_is_twilight, other_is_twilight) {
|
||||||
|
(true, true) => {
|
||||||
|
// Both are twilight, compare by base version
|
||||||
|
return (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch));
|
||||||
|
}
|
||||||
|
(false, false) => {
|
||||||
|
// Neither is twilight, continue with normal comparison
|
||||||
|
}
|
||||||
|
_ => unreachable!(), // Already handled above
|
||||||
|
}
|
||||||
|
|
||||||
// Compare major.minor.patch first
|
// Compare major.minor.patch first
|
||||||
match (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch)) {
|
match (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch)) {
|
||||||
Ordering::Equal => {
|
Ordering::Equal => {
|
||||||
@@ -193,6 +229,12 @@ pub fn is_alpha_version(version: &str) -> bool {
|
|||||||
version_comp.pre_release.is_some()
|
version_comp.pre_release.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Browser-specific alpha version detection for Zen Browser
|
||||||
|
pub fn is_zen_alpha_version(version: &str) -> bool {
|
||||||
|
// For Zen Browser, only "twilight" is considered alpha/pre-release
|
||||||
|
version.to_lowercase() == "twilight"
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct FirefoxRelease {
|
pub struct FirefoxRelease {
|
||||||
pub build_number: u32,
|
pub build_number: u32,
|
||||||
@@ -273,7 +315,7 @@ impl ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cache_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
fn get_cache_dir() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
|
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
|
||||||
let app_name = if cfg!(debug_assertions) {
|
let app_name = if cfg!(debug_assertions) {
|
||||||
"DonutBrowserDev"
|
"DonutBrowserDev"
|
||||||
@@ -343,7 +385,7 @@ impl ApiClient {
|
|||||||
&self,
|
&self,
|
||||||
browser: &str,
|
browser: &str,
|
||||||
versions: &[String],
|
versions: &[String],
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let cache_dir = Self::get_cache_dir()?;
|
let cache_dir = Self::get_cache_dir()?;
|
||||||
let cache_file = cache_dir.join(format!("{browser}_versions.json"));
|
let cache_file = cache_dir.join(format!("{browser}_versions.json"));
|
||||||
|
|
||||||
@@ -378,7 +420,7 @@ impl ApiClient {
|
|||||||
&self,
|
&self,
|
||||||
browser: &str,
|
browser: &str,
|
||||||
releases: &[GithubRelease],
|
releases: &[GithubRelease],
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let cache_dir = Self::get_cache_dir()?;
|
let cache_dir = Self::get_cache_dir()?;
|
||||||
let cache_file = cache_dir.join(format!("{browser}_github.json"));
|
let cache_file = cache_dir.join(format!("{browser}_github.json"));
|
||||||
|
|
||||||
@@ -569,13 +611,6 @@ impl ApiClient {
|
|||||||
Ok(releases)
|
Ok(releases)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn fetch_mullvad_releases(
|
|
||||||
&self,
|
|
||||||
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
self.fetch_mullvad_releases_with_caching(false).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_mullvad_releases_with_caching(
|
pub async fn fetch_mullvad_releases_with_caching(
|
||||||
&self,
|
&self,
|
||||||
no_caching: bool,
|
no_caching: bool,
|
||||||
@@ -622,13 +657,6 @@ impl ApiClient {
|
|||||||
Ok(releases)
|
Ok(releases)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn fetch_zen_releases(
|
|
||||||
&self,
|
|
||||||
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
self.fetch_zen_releases_with_caching(false).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_zen_releases_with_caching(
|
pub async fn fetch_zen_releases_with_caching(
|
||||||
&self,
|
&self,
|
||||||
no_caching: bool,
|
no_caching: bool,
|
||||||
@@ -654,7 +682,25 @@ impl ApiClient {
|
|||||||
.json::<Vec<GithubRelease>>()
|
.json::<Vec<GithubRelease>>()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Sort releases using the new version sorting system (twilight releases will be at top)
|
// Check for twilight updates and mark alpha releases
|
||||||
|
for release in &mut releases {
|
||||||
|
// Use browser-specific alpha detection for Zen Browser
|
||||||
|
release.is_alpha = is_zen_alpha_version(&release.tag_name) || release.prerelease;
|
||||||
|
|
||||||
|
// Check for twilight update if this is a twilight release
|
||||||
|
if release.tag_name.to_lowercase() == "twilight" {
|
||||||
|
if let Ok(has_update) = self.check_twilight_update(release).await {
|
||||||
|
if has_update {
|
||||||
|
println!(
|
||||||
|
"Detected update for Zen twilight release: {}",
|
||||||
|
release.tag_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort releases using the new version sorting system
|
||||||
sort_github_releases(&mut releases);
|
sort_github_releases(&mut releases);
|
||||||
|
|
||||||
// Cache the results (unless bypassing cache)
|
// Cache the results (unless bypassing cache)
|
||||||
@@ -667,13 +713,6 @@ impl ApiClient {
|
|||||||
Ok(releases)
|
Ok(releases)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn fetch_brave_releases(
|
|
||||||
&self,
|
|
||||||
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
self.fetch_brave_releases_with_caching(false).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_brave_releases_with_caching(
|
pub async fn fetch_brave_releases_with_caching(
|
||||||
&self,
|
&self,
|
||||||
no_caching: bool,
|
no_caching: bool,
|
||||||
@@ -935,6 +974,64 @@ impl ApiClient {
|
|||||||
// Check if there's a macOS DMG file in this version directory
|
// Check if there's a macOS DMG file in this version directory
|
||||||
Ok(html.contains("tor-browser-macos-") && html.contains(".dmg"))
|
Ok(html.contains("tor-browser-macos-") && html.contains(".dmg"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a Zen twilight release has been updated by comparing file size
|
||||||
|
pub async fn check_twilight_update(
|
||||||
|
&self,
|
||||||
|
release: &GithubRelease,
|
||||||
|
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
if release.tag_name.to_lowercase() != "twilight" {
|
||||||
|
return Ok(false); // Not a twilight release
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the macOS universal DMG asset
|
||||||
|
let asset = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|asset| asset.name == "zen.macos-universal.dmg")
|
||||||
|
.ok_or("No macOS universal asset found for twilight release")?;
|
||||||
|
|
||||||
|
// Check if we have cached file size information
|
||||||
|
let cache_dir = Self::get_cache_dir()?;
|
||||||
|
let twilight_cache_file = cache_dir.join("zen_twilight_info.json");
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
struct TwilightInfo {
|
||||||
|
file_size: u64,
|
||||||
|
last_updated: u64,
|
||||||
|
download_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_info = TwilightInfo {
|
||||||
|
file_size: asset.size,
|
||||||
|
last_updated: Self::get_current_timestamp(),
|
||||||
|
download_url: asset.browser_download_url.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !twilight_cache_file.exists() {
|
||||||
|
// No cache exists, save current info and return true (new)
|
||||||
|
let content = serde_json::to_string_pretty(¤t_info)?;
|
||||||
|
fs::write(&twilight_cache_file, content)?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cached_content = fs::read_to_string(&twilight_cache_file)?;
|
||||||
|
let cached_info: TwilightInfo = serde_json::from_str(&cached_content)?;
|
||||||
|
|
||||||
|
// Check if file size has changed
|
||||||
|
if cached_info.file_size != current_info.file_size {
|
||||||
|
// File size changed, update cache and return true
|
||||||
|
let content = serde_json::to_string_pretty(¤t_info)?;
|
||||||
|
fs::write(&twilight_cache_file, content)?;
|
||||||
|
println!(
|
||||||
|
"Zen twilight release updated: file size changed from {} to {}",
|
||||||
|
cached_info.file_size, current_info.file_size
|
||||||
|
);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false) // No update detected
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -989,10 +1086,14 @@ mod tests {
|
|||||||
assert_eq!(pre.number, Some(5));
|
assert_eq!(pre.number, Some(5));
|
||||||
|
|
||||||
// Test twilight version (Zen Browser)
|
// Test twilight version (Zen Browser)
|
||||||
let v4 = VersionComponent::parse("1.0.0-twilight");
|
let v4 = VersionComponent::parse("twilight");
|
||||||
assert_eq!(v4.major, u32::MAX);
|
assert_eq!(v4.major, 999);
|
||||||
assert_eq!(v4.minor, u32::MAX);
|
assert_eq!(v4.minor, 0);
|
||||||
assert_eq!(v4.patch, u32::MAX);
|
assert_eq!(v4.patch, 0);
|
||||||
|
assert!(v4.pre_release.is_some());
|
||||||
|
let pre = v4.pre_release.unwrap();
|
||||||
|
assert_eq!(pre.kind, PreReleaseKind::Alpha);
|
||||||
|
assert_eq!(pre.number, Some(999));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1022,10 +1123,15 @@ mod tests {
|
|||||||
let v10 = VersionComponent::parse("137.0b5");
|
let v10 = VersionComponent::parse("137.0b5");
|
||||||
assert!(v10 > v9); // b5 > b4
|
assert!(v10 > v9); // b5 > b4
|
||||||
|
|
||||||
// Test twilight version (should be highest)
|
// Test twilight version (should have highest priority)
|
||||||
let v11 = VersionComponent::parse("1.0.0-twilight");
|
let v11 = VersionComponent::parse("twilight");
|
||||||
let v12 = VersionComponent::parse("999.999.999");
|
let v12 = VersionComponent::parse("1.0.0");
|
||||||
assert!(v11 > v12);
|
assert!(v11 > v12); // twilight > stable due to high major version
|
||||||
|
|
||||||
|
// Test twilight vs other pre-releases
|
||||||
|
let v13 = VersionComponent::parse("twilight");
|
||||||
|
let v14 = VersionComponent::parse("1.0.0a1");
|
||||||
|
assert!(v13 > v14); // twilight > a1 due to high major version
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1037,14 +1143,14 @@ mod tests {
|
|||||||
"137.0b4".to_string(),
|
"137.0b4".to_string(),
|
||||||
"137.0b5".to_string(),
|
"137.0b5".to_string(),
|
||||||
"137.0".to_string(),
|
"137.0".to_string(),
|
||||||
"1.0.0-twilight".to_string(),
|
"twilight".to_string(),
|
||||||
"2.0.0a1".to_string(),
|
"2.0.0a1".to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
sort_versions(&mut versions);
|
sort_versions(&mut versions);
|
||||||
|
|
||||||
// Expected order: twilight, 137.0, 137.0b5, 137.0b4, 2.0.0a1, 1.12.6b, 1.10.0, 1.9.9b
|
// Expected order with twilight priority: twilight first due to high major version (999), then normal semantic versioning
|
||||||
assert_eq!(versions[0], "1.0.0-twilight");
|
assert_eq!(versions[0], "twilight");
|
||||||
assert_eq!(versions[1], "137.0");
|
assert_eq!(versions[1], "137.0");
|
||||||
assert_eq!(versions[2], "137.0b5");
|
assert_eq!(versions[2], "137.0b5");
|
||||||
assert_eq!(versions[3], "137.0b4");
|
assert_eq!(versions[3], "137.0b4");
|
||||||
@@ -1054,6 +1160,31 @@ mod tests {
|
|||||||
assert_eq!(versions[7], "1.9.9b");
|
assert_eq!(versions[7], "1.9.9b");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort_versions_comprehensive() {
|
||||||
|
let mut versions = vec![
|
||||||
|
"1.0.0".to_string(),
|
||||||
|
"1.0.1".to_string(),
|
||||||
|
"1.1.0".to_string(),
|
||||||
|
"2.0.0a1".to_string(),
|
||||||
|
"2.0.0b1".to_string(),
|
||||||
|
"2.0.0rc1".to_string(),
|
||||||
|
"2.0.0".to_string(),
|
||||||
|
"10.0.0".to_string(),
|
||||||
|
"twilight".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
sort_versions(&mut versions);
|
||||||
|
|
||||||
|
// Expected order with twilight priority: twilight first due to high major version (999), then normal semantic versioning
|
||||||
|
assert_eq!(versions[0], "twilight");
|
||||||
|
assert_eq!(versions[1], "10.0.0");
|
||||||
|
assert_eq!(versions[2], "2.0.0");
|
||||||
|
assert_eq!(versions[3], "2.0.0rc1");
|
||||||
|
assert_eq!(versions[4], "2.0.0b1");
|
||||||
|
assert_eq!(versions[5], "2.0.0a1");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_firefox_api() {
|
async fn test_firefox_api() {
|
||||||
let server = setup_mock_server().await;
|
let server = setup_mock_server().await;
|
||||||
@@ -1167,7 +1298,8 @@ mod tests {
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"name": "mullvad-browser-macos-14.5a6.dmg",
|
"name": "mullvad-browser-macos-14.5a6.dmg",
|
||||||
"browser_download_url": "https://example.com/mullvad-14.5a6.dmg"
|
"browser_download_url": "https://example.com/mullvad-14.5a6.dmg",
|
||||||
|
"size": 100000000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1200,14 +1332,15 @@ mod tests {
|
|||||||
|
|
||||||
let mock_response = r#"[
|
let mock_response = r#"[
|
||||||
{
|
{
|
||||||
"tag_name": "1.0.0-twilight",
|
"tag_name": "twilight",
|
||||||
"name": "Zen Browser Twilight",
|
"name": "Zen Browser Twilight",
|
||||||
"prerelease": false,
|
"prerelease": false,
|
||||||
"published_at": "2024-01-15T10:00:00Z",
|
"published_at": "2024-01-15T10:00:00Z",
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"name": "zen.macos-universal.dmg",
|
"name": "zen.macos-universal.dmg",
|
||||||
"browser_download_url": "https://example.com/zen-twilight.dmg"
|
"browser_download_url": "https://example.com/zen-twilight.dmg",
|
||||||
|
"size": 120000000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1229,7 +1362,7 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
let releases = result.unwrap();
|
let releases = result.unwrap();
|
||||||
assert!(!releases.is_empty());
|
assert!(!releases.is_empty());
|
||||||
assert_eq!(releases[0].tag_name, "1.0.0-twilight");
|
assert_eq!(releases[0].tag_name, "twilight");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -1246,7 +1379,8 @@ mod tests {
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"name": "brave-v1.81.9-universal.dmg",
|
"name": "brave-v1.81.9-universal.dmg",
|
||||||
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg"
|
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
|
||||||
|
"size": 200000000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1472,28 +1606,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sort_versions_comprehensive() {
|
fn test_is_zen_alpha_version() {
|
||||||
let mut versions = vec![
|
// Only "twilight" should be considered alpha for Zen Browser
|
||||||
"1.0.0".to_string(),
|
assert!(is_zen_alpha_version("twilight"));
|
||||||
"1.0.1".to_string(),
|
assert!(is_zen_alpha_version("TWILIGHT")); // Case insensitive
|
||||||
"1.1.0".to_string(),
|
|
||||||
"2.0.0a1".to_string(),
|
|
||||||
"2.0.0b1".to_string(),
|
|
||||||
"2.0.0rc1".to_string(),
|
|
||||||
"2.0.0".to_string(),
|
|
||||||
"10.0.0".to_string(),
|
|
||||||
"1.0.0-twilight".to_string(),
|
|
||||||
];
|
|
||||||
|
|
||||||
sort_versions(&mut versions);
|
// Versions with "b" should NOT be considered alpha for Zen Browser
|
||||||
|
assert!(!is_zen_alpha_version("1.12.8b"));
|
||||||
// Twilight should be first, then normal semantic versioning
|
assert!(!is_zen_alpha_version("1.0.0b1"));
|
||||||
assert_eq!(versions[0], "1.0.0-twilight");
|
assert!(!is_zen_alpha_version("2.0.0"));
|
||||||
assert_eq!(versions[1], "10.0.0");
|
|
||||||
assert_eq!(versions[2], "2.0.0");
|
|
||||||
assert_eq!(versions[3], "2.0.0rc1");
|
|
||||||
assert_eq!(versions[4], "2.0.0b1");
|
|
||||||
assert_eq!(versions[5], "2.0.0a1");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -170,6 +170,10 @@ impl AppAutoUpdater {
|
|||||||
|
|
||||||
/// Determine if an update should be performed
|
/// Determine if an update should be performed
|
||||||
fn should_update(&self, current_version: &str, new_version: &str, is_nightly: bool) -> bool {
|
fn should_update(&self, current_version: &str, new_version: &str, is_nightly: bool) -> bool {
|
||||||
|
if current_version.starts_with("dev-") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Comparing versions: current={current_version}, new={new_version}, is_nightly={is_nightly}"
|
"Comparing versions: current={current_version}, new={new_version}, is_nightly={is_nightly}"
|
||||||
);
|
);
|
||||||
@@ -608,8 +612,10 @@ mod tests {
|
|||||||
// Upgrade from stable to nightly
|
// Upgrade from stable to nightly
|
||||||
assert!(updater.should_update("v1.0.0", "nightly-abc123", true));
|
assert!(updater.should_update("v1.0.0", "nightly-abc123", true));
|
||||||
|
|
||||||
// Upgrade from dev to nightly
|
// Don't upgrade dev, ever
|
||||||
assert!(updater.should_update("dev-0.1.0", "nightly-abc123", true));
|
assert!(!updater.should_update("dev-0.1.0", "nightly-xyz987", false));
|
||||||
|
assert!(!updater.should_update("dev-0.1.0", "nightly-xyz987", true));
|
||||||
|
assert!(!updater.should_update("dev-0.1.0", "v1.2.3", false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -303,6 +303,8 @@ pub struct GithubRelease {
|
|||||||
pub struct GithubAsset {
|
pub struct GithubAsset {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub browser_download_url: String,
|
pub browser_download_url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ impl BrowserVersionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn new_with_api_client(api_client: ApiClient) -> Self {
|
||||||
|
Self { api_client }
|
||||||
|
}
|
||||||
|
|
||||||
/// Get cached browser versions immediately (returns None if no cache exists)
|
/// Get cached browser versions immediately (returns None if no cache exists)
|
||||||
pub fn get_cached_browser_versions(&self, browser: &str) -> Option<Vec<String>> {
|
pub fn get_cached_browser_versions(&self, browser: &str) -> Option<Vec<String>> {
|
||||||
self.api_client.load_cached_versions(browser)
|
self.api_client.load_cached_versions(browser)
|
||||||
@@ -541,6 +546,335 @@ impl BrowserVersionService {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use wiremock::matchers::{header, method, path};
|
||||||
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||||
|
|
||||||
|
async fn setup_mock_server() -> MockServer {
|
||||||
|
MockServer::start().await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_test_api_client(server: &MockServer) -> ApiClient {
|
||||||
|
let base_url = server.uri();
|
||||||
|
ApiClient::new_with_base_urls(
|
||||||
|
base_url.clone(), // firefox_api_base
|
||||||
|
base_url.clone(), // firefox_dev_api_base
|
||||||
|
base_url.clone(), // github_api_base
|
||||||
|
base_url.clone(), // chromium_api_base
|
||||||
|
base_url.clone(), // tor_archive_base
|
||||||
|
base_url.clone(), // mozilla_download_base
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_test_service(api_client: ApiClient) -> BrowserVersionService {
|
||||||
|
BrowserVersionService::new_with_api_client(api_client)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_firefox_mocks(server: &MockServer) {
|
||||||
|
let mock_response = r#"{
|
||||||
|
"releases": {
|
||||||
|
"firefox-139.0": {
|
||||||
|
"build_number": 1,
|
||||||
|
"category": "major",
|
||||||
|
"date": "2024-01-15",
|
||||||
|
"description": "Firefox 139.0 Release",
|
||||||
|
"is_security_driven": false,
|
||||||
|
"product": "firefox",
|
||||||
|
"version": "139.0"
|
||||||
|
},
|
||||||
|
"firefox-138.0": {
|
||||||
|
"build_number": 1,
|
||||||
|
"category": "major",
|
||||||
|
"date": "2024-01-01",
|
||||||
|
"description": "Firefox 138.0 Release",
|
||||||
|
"is_security_driven": false,
|
||||||
|
"product": "firefox",
|
||||||
|
"version": "138.0"
|
||||||
|
},
|
||||||
|
"firefox-137.0": {
|
||||||
|
"build_number": 1,
|
||||||
|
"category": "major",
|
||||||
|
"date": "2023-12-15",
|
||||||
|
"description": "Firefox 137.0 Release",
|
||||||
|
"is_security_driven": false,
|
||||||
|
"product": "firefox",
|
||||||
|
"version": "137.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/firefox.json"))
|
||||||
|
.and(header("user-agent", "donutbrowser"))
|
||||||
|
.respond_with(
|
||||||
|
ResponseTemplate::new(200)
|
||||||
|
.set_body_string(mock_response)
|
||||||
|
.insert_header("content-type", "application/json"),
|
||||||
|
)
|
||||||
|
.mount(server)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_firefox_dev_mocks(server: &MockServer) {
|
||||||
|
let mock_response = r#"{
|
||||||
|
"releases": {
|
||||||
|
"devedition-140.0b1": {
|
||||||
|
"build_number": 1,
|
||||||
|
"category": "major",
|
||||||
|
"date": "2024-01-20",
|
||||||
|
"description": "Firefox Developer Edition 140.0b1",
|
||||||
|
"is_security_driven": false,
|
||||||
|
"product": "devedition",
|
||||||
|
"version": "140.0b1"
|
||||||
|
},
|
||||||
|
"devedition-139.0b5": {
|
||||||
|
"build_number": 1,
|
||||||
|
"category": "major",
|
||||||
|
"date": "2024-01-10",
|
||||||
|
"description": "Firefox Developer Edition 139.0b5",
|
||||||
|
"is_security_driven": false,
|
||||||
|
"product": "devedition",
|
||||||
|
"version": "139.0b5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/devedition.json"))
|
||||||
|
.and(header("user-agent", "donutbrowser"))
|
||||||
|
.respond_with(
|
||||||
|
ResponseTemplate::new(200)
|
||||||
|
.set_body_string(mock_response)
|
||||||
|
.insert_header("content-type", "application/json"),
|
||||||
|
)
|
||||||
|
.mount(server)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_mullvad_mocks(server: &MockServer) {
|
||||||
|
let mock_response = r#"[
|
||||||
|
{
|
||||||
|
"tag_name": "14.5a6",
|
||||||
|
"name": "Mullvad Browser 14.5a6",
|
||||||
|
"prerelease": true,
|
||||||
|
"published_at": "2024-01-15T10:00:00Z",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"name": "mullvad-browser-macos-14.5a6.dmg",
|
||||||
|
"browser_download_url": "https://example.com/mullvad-14.5a6.dmg",
|
||||||
|
"size": 100000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag_name": "14.5a5",
|
||||||
|
"name": "Mullvad Browser 14.5a5",
|
||||||
|
"prerelease": true,
|
||||||
|
"published_at": "2024-01-10T10:00:00Z",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"name": "mullvad-browser-macos-14.5a5.dmg",
|
||||||
|
"browser_download_url": "https://example.com/mullvad-14.5a5.dmg",
|
||||||
|
"size": 99000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]"#;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/repos/mullvad/mullvad-browser/releases"))
|
||||||
|
.and(header("user-agent", "donutbrowser"))
|
||||||
|
.respond_with(
|
||||||
|
ResponseTemplate::new(200)
|
||||||
|
.set_body_string(mock_response)
|
||||||
|
.insert_header("content-type", "application/json"),
|
||||||
|
)
|
||||||
|
.mount(server)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_zen_mocks(server: &MockServer) {
|
||||||
|
let mock_response = r#"[
|
||||||
|
{
|
||||||
|
"tag_name": "twilight",
|
||||||
|
"name": "Zen Browser Twilight",
|
||||||
|
"prerelease": false,
|
||||||
|
"published_at": "2024-01-15T10:00:00Z",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"name": "zen.macos-universal.dmg",
|
||||||
|
"browser_download_url": "https://example.com/zen-twilight.dmg",
|
||||||
|
"size": 120000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag_name": "1.11b",
|
||||||
|
"name": "Zen Browser 1.11b",
|
||||||
|
"prerelease": false,
|
||||||
|
"published_at": "2024-01-10T10:00:00Z",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"name": "zen.macos-universal.dmg",
|
||||||
|
"browser_download_url": "https://example.com/zen-1.11b.dmg",
|
||||||
|
"size": 115000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]"#;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/repos/zen-browser/desktop/releases"))
|
||||||
|
.and(header("user-agent", "donutbrowser"))
|
||||||
|
.respond_with(
|
||||||
|
ResponseTemplate::new(200)
|
||||||
|
.set_body_string(mock_response)
|
||||||
|
.insert_header("content-type", "application/json"),
|
||||||
|
)
|
||||||
|
.mount(server)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_brave_mocks(server: &MockServer) {
|
||||||
|
let mock_response = r#"[
|
||||||
|
{
|
||||||
|
"tag_name": "v1.81.9",
|
||||||
|
"name": "Brave Release 1.81.9",
|
||||||
|
"prerelease": false,
|
||||||
|
"published_at": "2024-01-15T10:00:00Z",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"name": "brave-v1.81.9-universal.dmg",
|
||||||
|
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
|
||||||
|
"size": 200000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag_name": "v1.81.8",
|
||||||
|
"name": "Brave Release 1.81.8",
|
||||||
|
"prerelease": false,
|
||||||
|
"published_at": "2024-01-10T10:00:00Z",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"name": "brave-v1.81.8-universal.dmg",
|
||||||
|
"browser_download_url": "https://example.com/brave-1.81.8-universal.dmg",
|
||||||
|
"size": 199000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]"#;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/repos/brave/brave-browser/releases"))
|
||||||
|
.and(header("user-agent", "donutbrowser"))
|
||||||
|
.respond_with(
|
||||||
|
ResponseTemplate::new(200)
|
||||||
|
.set_body_string(mock_response)
|
||||||
|
.insert_header("content-type", "application/json"),
|
||||||
|
)
|
||||||
|
.mount(server)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_chromium_mocks(server: &MockServer) {
|
||||||
|
let arch = if cfg!(target_arch = "aarch64") {
|
||||||
|
"Mac_Arm"
|
||||||
|
} else {
|
||||||
|
"Mac"
|
||||||
|
};
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path(format!("/{arch}/LAST_CHANGE")))
|
||||||
|
.and(header("user-agent", "donutbrowser"))
|
||||||
|
.respond_with(
|
||||||
|
ResponseTemplate::new(200)
|
||||||
|
.set_body_string("1465660")
|
||||||
|
.insert_header("content-type", "text/plain"),
|
||||||
|
)
|
||||||
|
.mount(server)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_tor_mocks(server: &MockServer) {
|
||||||
|
let mock_html = r#"
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<a href="../">../</a>
|
||||||
|
<a href="14.0.4/">14.0.4/</a>
|
||||||
|
<a href="14.0.3/">14.0.3/</a>
|
||||||
|
<a href="14.0.2/">14.0.2/</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let version_html_144 = r#"
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<a href="tor-browser-macos-14.0.4.dmg">tor-browser-macos-14.0.4.dmg</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let version_html_143 = r#"
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<a href="tor-browser-macos-14.0.3.dmg">tor-browser-macos-14.0.3.dmg</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let version_html_142 = r#"
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<a href="tor-browser-macos-14.0.2.dmg">tor-browser-macos-14.0.2.dmg</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/"))
|
||||||
|
.and(header("user-agent", "donutbrowser"))
|
||||||
|
.respond_with(
|
||||||
|
ResponseTemplate::new(200)
|
||||||
|
.set_body_string(mock_html)
|
||||||
|
.insert_header("content-type", "text/html"),
|
||||||
|
)
|
||||||
|
.mount(server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/14.0.4/"))
|
||||||
|
.and(header("user-agent", "donutbrowser"))
|
||||||
|
.respond_with(
|
||||||
|
ResponseTemplate::new(200)
|
||||||
|
.set_body_string(version_html_144)
|
||||||
|
.insert_header("content-type", "text/html"),
|
||||||
|
)
|
||||||
|
.mount(server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/14.0.3/"))
|
||||||
|
.and(header("user-agent", "donutbrowser"))
|
||||||
|
.respond_with(
|
||||||
|
ResponseTemplate::new(200)
|
||||||
|
.set_body_string(version_html_143)
|
||||||
|
.insert_header("content-type", "text/html"),
|
||||||
|
)
|
||||||
|
.mount(server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/14.0.2/"))
|
||||||
|
.and(header("user-agent", "donutbrowser"))
|
||||||
|
.respond_with(
|
||||||
|
ResponseTemplate::new(200)
|
||||||
|
.set_body_string(version_html_142)
|
||||||
|
.insert_header("content-type", "text/html"),
|
||||||
|
)
|
||||||
|
.mount(server)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_browser_version_service_creation() {
|
async fn test_browser_version_service_creation() {
|
||||||
@@ -550,7 +884,11 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_fetch_firefox_versions() {
|
async fn test_fetch_firefox_versions() {
|
||||||
let service = BrowserVersionService::new();
|
let server = setup_mock_server().await;
|
||||||
|
setup_firefox_mocks(&server).await;
|
||||||
|
|
||||||
|
let api_client = create_test_api_client(&server);
|
||||||
|
let service = create_test_service(api_client);
|
||||||
|
|
||||||
// Test with caching
|
// Test with caching
|
||||||
let result_cached = service.fetch_browser_versions("firefox", false).await;
|
let result_cached = service.fetch_browser_versions("firefox", false).await;
|
||||||
@@ -561,15 +899,13 @@ mod tests {
|
|||||||
|
|
||||||
if let Ok(versions) = result_cached {
|
if let Ok(versions) = result_cached {
|
||||||
assert!(!versions.is_empty(), "Should have Firefox versions");
|
assert!(!versions.is_empty(), "Should have Firefox versions");
|
||||||
|
assert_eq!(versions[0], "139.0", "Should have latest version first");
|
||||||
println!(
|
println!(
|
||||||
"Firefox cached test passed. Found {versions_count} versions",
|
"Firefox cached test passed. Found {versions_count} versions",
|
||||||
versions_count = versions.len()
|
versions_count = versions.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small delay to avoid rate limiting
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
||||||
|
|
||||||
// Test without caching
|
// Test without caching
|
||||||
let result_no_cache = service.fetch_browser_versions("firefox", true).await;
|
let result_no_cache = service.fetch_browser_versions("firefox", true).await;
|
||||||
assert!(
|
assert!(
|
||||||
@@ -582,6 +918,7 @@ mod tests {
|
|||||||
!versions.is_empty(),
|
!versions.is_empty(),
|
||||||
"Should have Firefox versions without caching"
|
"Should have Firefox versions without caching"
|
||||||
);
|
);
|
||||||
|
assert_eq!(versions[0], "139.0", "Should have latest version first");
|
||||||
println!(
|
println!(
|
||||||
"Firefox no-cache test passed. Found {versions_count} versions",
|
"Firefox no-cache test passed. Found {versions_count} versions",
|
||||||
versions_count = versions.len()
|
versions_count = versions.len()
|
||||||
@@ -591,7 +928,11 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_fetch_browser_versions_with_count() {
|
async fn test_fetch_browser_versions_with_count() {
|
||||||
let service = BrowserVersionService::new();
|
let server = setup_mock_server().await;
|
||||||
|
setup_firefox_mocks(&server).await;
|
||||||
|
|
||||||
|
let api_client = create_test_api_client(&server);
|
||||||
|
let service = create_test_service(api_client);
|
||||||
|
|
||||||
let result = service
|
let result = service
|
||||||
.fetch_browser_versions_with_count("firefox", false)
|
.fetch_browser_versions_with_count("firefox", false)
|
||||||
@@ -605,6 +946,10 @@ mod tests {
|
|||||||
result.versions.len(),
|
result.versions.len(),
|
||||||
"Total count should match versions length"
|
"Total count should match versions length"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
result.versions[0], "139.0",
|
||||||
|
"Should have latest version first"
|
||||||
|
);
|
||||||
println!(
|
println!(
|
||||||
"Firefox count test passed. Found {} versions, new: {}",
|
"Firefox count test passed. Found {} versions, new: {}",
|
||||||
result.total_versions_count,
|
result.total_versions_count,
|
||||||
@@ -615,7 +960,11 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_fetch_detailed_versions() {
|
async fn test_fetch_detailed_versions() {
|
||||||
let service = BrowserVersionService::new();
|
let server = setup_mock_server().await;
|
||||||
|
setup_firefox_mocks(&server).await;
|
||||||
|
|
||||||
|
let api_client = create_test_api_client(&server);
|
||||||
|
let service = create_test_service(api_client);
|
||||||
|
|
||||||
let result = service
|
let result = service
|
||||||
.fetch_browser_versions_detailed("firefox", false)
|
.fetch_browser_versions_detailed("firefox", false)
|
||||||
@@ -631,6 +980,12 @@ mod tests {
|
|||||||
!first_version.version.is_empty(),
|
!first_version.version.is_empty(),
|
||||||
"Version should not be empty"
|
"Version should not be empty"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
first_version.version, "139.0",
|
||||||
|
"Should have latest version first"
|
||||||
|
);
|
||||||
|
assert_eq!(first_version.date, "2024-01-15", "Should have correct date");
|
||||||
|
assert!(!first_version.is_prerelease, "Should be stable release");
|
||||||
println!(
|
println!(
|
||||||
"Firefox detailed test passed. Found {versions_count} detailed versions",
|
"Firefox detailed test passed. Found {versions_count} detailed versions",
|
||||||
versions_count = versions.len()
|
versions_count = versions.len()
|
||||||
@@ -640,7 +995,9 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_unsupported_browser() {
|
async fn test_unsupported_browser() {
|
||||||
let service = BrowserVersionService::new();
|
let server = setup_mock_server().await;
|
||||||
|
let api_client = create_test_api_client(&server);
|
||||||
|
let service = create_test_service(api_client);
|
||||||
|
|
||||||
let result = service.fetch_browser_versions("unsupported", false).await;
|
let result = service.fetch_browser_versions("unsupported", false).await;
|
||||||
assert!(
|
assert!(
|
||||||
@@ -658,7 +1015,11 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_incremental_update() {
|
async fn test_incremental_update() {
|
||||||
let service = BrowserVersionService::new();
|
let server = setup_mock_server().await;
|
||||||
|
setup_firefox_mocks(&server).await;
|
||||||
|
|
||||||
|
let api_client = create_test_api_client(&server);
|
||||||
|
let service = create_test_service(api_client);
|
||||||
|
|
||||||
// This test might fail if there are no cached versions yet, which is fine
|
// This test might fail if there are no cached versions yet, which is fine
|
||||||
let result = service
|
let result = service
|
||||||
@@ -678,7 +1039,20 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_all_supported_browsers() {
|
async fn test_all_supported_browsers() {
|
||||||
let service = BrowserVersionService::new();
|
let server = setup_mock_server().await;
|
||||||
|
|
||||||
|
// Setup all browser mocks
|
||||||
|
setup_firefox_mocks(&server).await;
|
||||||
|
setup_firefox_dev_mocks(&server).await;
|
||||||
|
setup_mullvad_mocks(&server).await;
|
||||||
|
setup_zen_mocks(&server).await;
|
||||||
|
setup_brave_mocks(&server).await;
|
||||||
|
setup_chromium_mocks(&server).await;
|
||||||
|
setup_tor_mocks(&server).await;
|
||||||
|
|
||||||
|
let api_client = create_test_api_client(&server);
|
||||||
|
let service = create_test_service(api_client);
|
||||||
|
|
||||||
let browsers = vec![
|
let browsers = vec![
|
||||||
"firefox",
|
"firefox",
|
||||||
"firefox-developer",
|
"firefox-developer",
|
||||||
@@ -690,30 +1064,30 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for browser in browsers {
|
for browser in browsers {
|
||||||
// Test that we can at least call the function without panicking
|
|
||||||
let result = service.fetch_browser_versions(browser, false).await;
|
let result = service.fetch_browser_versions(browser, false).await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(versions) => {
|
Ok(versions) => {
|
||||||
|
assert!(!versions.is_empty(), "Should have versions for {browser}");
|
||||||
println!(
|
println!(
|
||||||
"{browser} test passed. Found {versions_count} versions",
|
"{browser} test passed. Found {versions_count} versions",
|
||||||
versions_count = versions.len()
|
versions_count = versions.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Some browsers might fail due to network issues, but shouldn't panic
|
panic!("{browser} test failed: {e}");
|
||||||
println!("{browser} test failed (network issue): {e}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small delay between requests to avoid rate limiting
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_no_caching_parameter() {
|
async fn test_no_caching_parameter() {
|
||||||
let service = BrowserVersionService::new();
|
let server = setup_mock_server().await;
|
||||||
|
setup_firefox_mocks(&server).await;
|
||||||
|
|
||||||
|
let api_client = create_test_api_client(&server);
|
||||||
|
let service = create_test_service(api_client);
|
||||||
|
|
||||||
// Test with caching enabled (default)
|
// Test with caching enabled (default)
|
||||||
let result_cached = service.fetch_browser_versions("firefox", false).await;
|
let result_cached = service.fetch_browser_versions("firefox", false).await;
|
||||||
@@ -722,9 +1096,6 @@ mod tests {
|
|||||||
"Should fetch Firefox versions with caching"
|
"Should fetch Firefox versions with caching"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Small delay to avoid rate limiting
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
||||||
|
|
||||||
// Test with caching disabled (no_caching = true)
|
// Test with caching disabled (no_caching = true)
|
||||||
let result_no_cache = service.fetch_browser_versions("firefox", true).await;
|
let result_no_cache = service.fetch_browser_versions("firefox", true).await;
|
||||||
assert!(
|
assert!(
|
||||||
@@ -742,6 +1113,10 @@ mod tests {
|
|||||||
!no_cache_versions.is_empty(),
|
!no_cache_versions.is_empty(),
|
||||||
"No-cache versions should not be empty"
|
"No-cache versions should not be empty"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cached_versions, no_cache_versions,
|
||||||
|
"Both should return same versions"
|
||||||
|
);
|
||||||
println!(
|
println!(
|
||||||
"No-caching test passed. Cached: {} versions, No-cache: {} versions",
|
"No-caching test passed. Cached: {} versions, No-cache: {} versions",
|
||||||
cached_versions.len(),
|
cached_versions.len(),
|
||||||
@@ -752,7 +1127,11 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_detailed_versions_with_no_caching() {
|
async fn test_detailed_versions_with_no_caching() {
|
||||||
let service = BrowserVersionService::new();
|
let server = setup_mock_server().await;
|
||||||
|
setup_firefox_mocks(&server).await;
|
||||||
|
|
||||||
|
let api_client = create_test_api_client(&server);
|
||||||
|
let service = create_test_service(api_client);
|
||||||
|
|
||||||
// Test detailed versions with caching
|
// Test detailed versions with caching
|
||||||
let result_cached = service
|
let result_cached = service
|
||||||
@@ -763,9 +1142,6 @@ mod tests {
|
|||||||
"Should fetch detailed Firefox versions with caching"
|
"Should fetch detailed Firefox versions with caching"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Small delay to avoid rate limiting
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
||||||
|
|
||||||
// Test detailed versions without caching
|
// Test detailed versions without caching
|
||||||
let result_no_cache = service
|
let result_no_cache = service
|
||||||
.fetch_browser_versions_detailed("firefox", true)
|
.fetch_browser_versions_detailed("firefox", true)
|
||||||
@@ -799,6 +1175,17 @@ mod tests {
|
|||||||
"No-cache version should not be empty"
|
"No-cache version should not be empty"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!(first_cached.version, "139.0", "Should have correct version");
|
||||||
|
assert_eq!(
|
||||||
|
first_no_cache.version, "139.0",
|
||||||
|
"Should have correct version"
|
||||||
|
);
|
||||||
|
assert_eq!(first_cached.date, "2024-01-15", "Should have correct date");
|
||||||
|
assert_eq!(
|
||||||
|
first_no_cache.date, "2024-01-15",
|
||||||
|
"Should have correct date"
|
||||||
|
);
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Detailed no-caching test passed. Cached: {} versions, No-cache: {} versions",
|
"Detailed no-caching test passed. Cached: {} versions, No-cache: {} versions",
|
||||||
cached_versions.len(),
|
cached_versions.len(),
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ pub async fn open_url_with_profile(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn smart_open_url(
|
pub async fn smart_open_url(
|
||||||
_app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
_url: String,
|
url: String,
|
||||||
_is_startup: Option<bool>,
|
_is_startup: Option<bool>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
use crate::browser_runner::BrowserRunner;
|
use crate::browser_runner::BrowserRunner;
|
||||||
@@ -171,10 +171,75 @@ pub async fn smart_open_url(
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"URL opening - Total profiles: {}, showing profile selector",
|
"URL opening - Total profiles: {}, checking for running profiles",
|
||||||
profiles.len()
|
profiles.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Always show the profile selector so the user can choose
|
// Check for running profiles and find the first one that can handle URLs
|
||||||
|
for profile in &profiles {
|
||||||
|
// Check if this profile is running
|
||||||
|
let is_running = runner
|
||||||
|
.check_browser_status(app_handle.clone(), profile)
|
||||||
|
.await
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if is_running {
|
||||||
|
println!(
|
||||||
|
"Found running profile '{}', attempting to open URL",
|
||||||
|
profile.name
|
||||||
|
);
|
||||||
|
|
||||||
|
// For TOR browser: Check if any other TOR browser is running
|
||||||
|
if profile.browser == "tor-browser" {
|
||||||
|
let mut other_tor_running = false;
|
||||||
|
for p in &profiles {
|
||||||
|
if p.browser == "tor-browser"
|
||||||
|
&& p.name != profile.name
|
||||||
|
&& runner
|
||||||
|
.check_browser_status(app_handle.clone(), p)
|
||||||
|
.await
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
other_tor_running = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if other_tor_running {
|
||||||
|
continue; // Skip this one, can't have multiple TOR instances
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Mullvad browser: skip if running (can't open URLs in running Mullvad)
|
||||||
|
if profile.browser == "mullvad-browser" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to open the URL with this running profile
|
||||||
|
match runner
|
||||||
|
.launch_or_open_url(app_handle.clone(), profile, Some(url.clone()))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
println!(
|
||||||
|
"Successfully opened URL '{}' with running profile '{}'",
|
||||||
|
url, profile.name
|
||||||
|
);
|
||||||
|
return Ok(format!("opened_with_profile:{}", profile.name));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"Failed to open URL with running profile '{}': {}",
|
||||||
|
profile.name, e
|
||||||
|
);
|
||||||
|
// Continue to try other profiles or show selector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("No suitable running profiles found, showing profile selector");
|
||||||
|
|
||||||
|
// No suitable running profile found, show the profile selector
|
||||||
Err("show_selector".to_string())
|
Err("show_selector".to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,10 @@ impl Downloader {
|
|||||||
.resolve_download_url(browser_type.clone(), version, download_info)
|
.resolve_download_url(browser_type.clone(), version, download_info)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Check if this is a twilight release for special handling
|
||||||
|
let is_twilight =
|
||||||
|
browser_type == BrowserType::Zen && version.to_lowercase().contains("twilight");
|
||||||
|
|
||||||
// Emit initial progress
|
// Emit initial progress
|
||||||
let progress = DownloadProgress {
|
let progress = DownloadProgress {
|
||||||
browser: browser_type.as_str().to_string(),
|
browser: browser_type.as_str().to_string(),
|
||||||
@@ -153,7 +157,11 @@ impl Downloader {
|
|||||||
percentage: 0.0,
|
percentage: 0.0,
|
||||||
speed_bytes_per_sec: 0.0,
|
speed_bytes_per_sec: 0.0,
|
||||||
eta_seconds: None,
|
eta_seconds: None,
|
||||||
stage: "downloading".to_string(),
|
stage: if is_twilight {
|
||||||
|
"downloading (twilight rolling release)".to_string()
|
||||||
|
} else {
|
||||||
|
"downloading".to_string()
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = app_handle.emit("download-progress", &progress);
|
let _ = app_handle.emit("download-progress", &progress);
|
||||||
@@ -205,6 +213,12 @@ impl Downloader {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let stage_description = if is_twilight {
|
||||||
|
"downloading (twilight rolling release)".to_string()
|
||||||
|
} else {
|
||||||
|
"downloading".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
let progress = DownloadProgress {
|
let progress = DownloadProgress {
|
||||||
browser: browser_type.as_str().to_string(),
|
browser: browser_type.as_str().to_string(),
|
||||||
version: version.to_string(),
|
version: version.to_string(),
|
||||||
@@ -213,7 +227,7 @@ impl Downloader {
|
|||||||
percentage,
|
percentage,
|
||||||
speed_bytes_per_sec: speed,
|
speed_bytes_per_sec: speed,
|
||||||
eta_seconds: eta,
|
eta_seconds: eta,
|
||||||
stage: "downloading".to_string(),
|
stage: stage_description,
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = app_handle.emit("download-progress", &progress);
|
let _ = app_handle.emit("download-progress", &progress);
|
||||||
@@ -267,7 +281,8 @@ mod tests {
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"name": "brave-v1.81.9-universal.dmg",
|
"name": "brave-v1.81.9-universal.dmg",
|
||||||
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg"
|
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
|
||||||
|
"size": 200000000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -314,7 +329,8 @@ mod tests {
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"name": "zen.macos-universal.dmg",
|
"name": "zen.macos-universal.dmg",
|
||||||
"browser_download_url": "https://example.com/zen-1.11b-universal.dmg"
|
"browser_download_url": "https://example.com/zen-1.11b-universal.dmg",
|
||||||
|
"size": 120000000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -361,7 +377,8 @@ mod tests {
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"name": "mullvad-browser-macos-14.5a6.dmg",
|
"name": "mullvad-browser-macos-14.5a6.dmg",
|
||||||
"browser_download_url": "https://example.com/mullvad-14.5a6.dmg"
|
"browser_download_url": "https://example.com/mullvad-14.5a6.dmg",
|
||||||
|
"size": 100000000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -471,7 +488,8 @@ mod tests {
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"name": "brave-v1.81.8-universal.dmg",
|
"name": "brave-v1.81.8-universal.dmg",
|
||||||
"browser_download_url": "https://example.com/brave-1.81.8-universal.dmg"
|
"browser_download_url": "https://example.com/brave-1.81.8-universal.dmg",
|
||||||
|
"size": 200000000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -520,7 +538,8 @@ mod tests {
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"name": "zen.linux-universal.tar.bz2",
|
"name": "zen.linux-universal.tar.bz2",
|
||||||
"browser_download_url": "https://example.com/zen-1.11b-linux.tar.bz2"
|
"browser_download_url": "https://example.com/zen-1.11b-linux.tar.bz2",
|
||||||
|
"size": 150000000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -663,7 +682,8 @@ mod tests {
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"name": "mullvad-browser-linux-14.5a6.tar.xz",
|
"name": "mullvad-browser-linux-14.5a6.tar.xz",
|
||||||
"browser_download_url": "https://example.com/mullvad-14.5a6.tar.xz"
|
"browser_download_url": "https://example.com/mullvad-14.5a6.tar.xz",
|
||||||
|
"size": 80000000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -712,7 +732,8 @@ mod tests {
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"name": "brave-v1.81.9-universal.dmg",
|
"name": "brave-v1.81.9-universal.dmg",
|
||||||
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg"
|
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
|
||||||
|
"size": 200000000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ pub struct DownloadedBrowserInfo {
|
|||||||
pub file_path: PathBuf,
|
pub file_path: PathBuf,
|
||||||
pub verified: bool,
|
pub verified: bool,
|
||||||
pub actual_version: Option<String>, // For browsers like Chromium where we track the actual version
|
pub actual_version: Option<String>, // For browsers like Chromium where we track the actual version
|
||||||
|
pub file_size: Option<u64>, // For tracking file size changes (useful for rolling releases)
|
||||||
|
#[serde(default)] // Add default value (false) for backwards compatibility
|
||||||
|
pub is_rolling_release: bool, // True for Zen's twilight releases and other rolling releases
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||||
@@ -98,6 +101,7 @@ impl DownloadedBrowsersRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_download_started(&mut self, browser: &str, version: &str, file_path: PathBuf) {
|
pub fn mark_download_started(&mut self, browser: &str, version: &str, file_path: PathBuf) {
|
||||||
|
let is_rolling = Self::is_rolling_release(browser, version);
|
||||||
let info = DownloadedBrowserInfo {
|
let info = DownloadedBrowserInfo {
|
||||||
browser: browser.to_string(),
|
browser: browser.to_string(),
|
||||||
version: version.to_string(),
|
version: version.to_string(),
|
||||||
@@ -108,6 +112,8 @@ impl DownloadedBrowsersRegistry {
|
|||||||
file_path,
|
file_path,
|
||||||
verified: false,
|
verified: false,
|
||||||
actual_version: None,
|
actual_version: None,
|
||||||
|
file_size: None,
|
||||||
|
is_rolling_release: is_rolling,
|
||||||
};
|
};
|
||||||
self.add_browser(info);
|
self.add_browser(info);
|
||||||
}
|
}
|
||||||
@@ -131,6 +137,11 @@ impl DownloadedBrowsersRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_rolling_release(browser: &str, version: &str) -> bool {
|
||||||
|
// Check if this is a rolling release like twilight
|
||||||
|
browser == "zen" && version.to_lowercase() == "twilight"
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cleanup_failed_download(
|
pub fn cleanup_failed_download(
|
||||||
&mut self,
|
&mut self,
|
||||||
browser: &str,
|
browser: &str,
|
||||||
@@ -186,6 +197,8 @@ mod tests {
|
|||||||
file_path: PathBuf::from("/test/path"),
|
file_path: PathBuf::from("/test/path"),
|
||||||
verified: true,
|
verified: true,
|
||||||
actual_version: None,
|
actual_version: None,
|
||||||
|
file_size: None,
|
||||||
|
is_rolling_release: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.add_browser(info.clone());
|
registry.add_browser(info.clone());
|
||||||
@@ -206,6 +219,8 @@ mod tests {
|
|||||||
file_path: PathBuf::from("/test/path1"),
|
file_path: PathBuf::from("/test/path1"),
|
||||||
verified: true,
|
verified: true,
|
||||||
actual_version: None,
|
actual_version: None,
|
||||||
|
file_size: None,
|
||||||
|
is_rolling_release: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let info2 = DownloadedBrowserInfo {
|
let info2 = DownloadedBrowserInfo {
|
||||||
@@ -215,6 +230,8 @@ mod tests {
|
|||||||
file_path: PathBuf::from("/test/path2"),
|
file_path: PathBuf::from("/test/path2"),
|
||||||
verified: false, // Not verified, should not be included
|
verified: false, // Not verified, should not be included
|
||||||
actual_version: None,
|
actual_version: None,
|
||||||
|
file_size: None,
|
||||||
|
is_rolling_release: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let info3 = DownloadedBrowserInfo {
|
let info3 = DownloadedBrowserInfo {
|
||||||
@@ -224,6 +241,8 @@ mod tests {
|
|||||||
file_path: PathBuf::from("/test/path3"),
|
file_path: PathBuf::from("/test/path3"),
|
||||||
verified: true,
|
verified: true,
|
||||||
actual_version: None,
|
actual_version: None,
|
||||||
|
file_size: None,
|
||||||
|
is_rolling_release: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.add_browser(info1);
|
registry.add_browser(info1);
|
||||||
@@ -266,6 +285,8 @@ mod tests {
|
|||||||
file_path: PathBuf::from("/test/path"),
|
file_path: PathBuf::from("/test/path"),
|
||||||
verified: true,
|
verified: true,
|
||||||
actual_version: None,
|
actual_version: None,
|
||||||
|
file_size: None,
|
||||||
|
is_rolling_release: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.add_browser(info);
|
registry.add_browser(info);
|
||||||
@@ -275,4 +296,17 @@ mod tests {
|
|||||||
assert!(removed.is_some());
|
assert!(removed.is_some());
|
||||||
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
|
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_twilight_rolling_release() {
|
||||||
|
let mut registry = DownloadedBrowsersRegistry::new();
|
||||||
|
|
||||||
|
// Mark twilight download started
|
||||||
|
registry.mark_download_started("zen", "twilight", PathBuf::from("/test/zen-twilight"));
|
||||||
|
|
||||||
|
// Check that it's marked as rolling release
|
||||||
|
let zen_versions = ®istry.browsers["zen"];
|
||||||
|
let twilight_info = &zen_versions["twilight"];
|
||||||
|
assert!(twilight_info.is_rolling_release);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+119
-1
@@ -1,7 +1,7 @@
|
|||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tauri::{Emitter, Manager};
|
use tauri::{Emitter, Manager, Runtime, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
|
|
||||||
// Store pending URLs that need to be handled when the window is ready
|
// Store pending URLs that need to be handled when the window is ready
|
||||||
@@ -58,6 +58,51 @@ use app_auto_updater::{
|
|||||||
get_app_version_info,
|
get_app_version_info,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Trait to extend WebviewWindow with transparent titlebar functionality
|
||||||
|
pub trait WindowExt {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn set_transparent_titlebar(&self, transparent: bool) -> Result<(), String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Runtime> WindowExt for WebviewWindow<R> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn set_transparent_titlebar(&self, transparent: bool) -> Result<(), String> {
|
||||||
|
use objc2::rc::Retained;
|
||||||
|
use objc2_app_kit::{NSWindow, NSWindowStyleMask, NSWindowTitleVisibility};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let ns_window: Retained<NSWindow> =
|
||||||
|
Retained::retain(self.ns_window().unwrap().cast()).unwrap();
|
||||||
|
|
||||||
|
if transparent {
|
||||||
|
// Hide the title text
|
||||||
|
ns_window.setTitleVisibility(NSWindowTitleVisibility(2)); // NSWindowTitleHidden
|
||||||
|
|
||||||
|
// Make titlebar transparent
|
||||||
|
ns_window.setTitlebarAppearsTransparent(true);
|
||||||
|
|
||||||
|
// Set full size content view
|
||||||
|
let current_mask = ns_window.styleMask();
|
||||||
|
let new_mask = NSWindowStyleMask(current_mask.0 | (1 << 15)); // NSFullSizeContentViewWindowMask
|
||||||
|
ns_window.setStyleMask(new_mask);
|
||||||
|
} else {
|
||||||
|
// Show the title text
|
||||||
|
ns_window.setTitleVisibility(NSWindowTitleVisibility(0)); // NSWindowTitleVisible
|
||||||
|
|
||||||
|
// Make titlebar opaque
|
||||||
|
ns_window.setTitlebarAppearsTransparent(false);
|
||||||
|
|
||||||
|
// Remove full size content view
|
||||||
|
let current_mask = ns_window.styleMask();
|
||||||
|
let new_mask = NSWindowStyleMask(current_mask.0 & !(1 << 15));
|
||||||
|
ns_window.setStyleMask(new_mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn greet() -> String {
|
fn greet() -> String {
|
||||||
let now = SystemTime::now();
|
let now = SystemTime::now();
|
||||||
@@ -124,6 +169,61 @@ async fn check_and_handle_startup_url(app_handle: tauri::AppHandle) -> Result<bo
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn set_window_background_color(
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
is_dark_mode: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
|
use objc2::rc::Retained;
|
||||||
|
use objc2_app_kit::{NSColor, NSWindow};
|
||||||
|
|
||||||
|
let ns_window: Retained<NSWindow> =
|
||||||
|
unsafe { Retained::retain(window.ns_window().unwrap().cast()).unwrap() };
|
||||||
|
|
||||||
|
let bg_color = if is_dark_mode {
|
||||||
|
// Dark mode - pure black background
|
||||||
|
unsafe { NSColor::colorWithRed_green_blue_alpha(0.0, 0.0, 0.0, 1.0) }
|
||||||
|
} else {
|
||||||
|
// Light mode - pure white background
|
||||||
|
unsafe { NSColor::colorWithRed_green_blue_alpha(1.0, 1.0, 1.0, 1.0) }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure this runs on the main thread for immediate visual update
|
||||||
|
unsafe {
|
||||||
|
// Set the window background color
|
||||||
|
ns_window.setBackgroundColor(Some(&bg_color));
|
||||||
|
|
||||||
|
// Force immediate visual updates using multiple refresh methods
|
||||||
|
ns_window.invalidateShadow();
|
||||||
|
ns_window.display();
|
||||||
|
|
||||||
|
// Ensure the window content is redrawn
|
||||||
|
if let Some(content_view) = ns_window.contentView() {
|
||||||
|
content_view.setNeedsDisplay(true);
|
||||||
|
content_view.displayIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger a window update
|
||||||
|
ns_window.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also emit an event to the frontend to ensure synchronization
|
||||||
|
let _ = app_handle.emit("window-background-updated", is_dark_mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
// For non-macOS platforms, we can't change the native window background
|
||||||
|
let _ = (app_handle, is_dark_mode); // Suppress unused variable warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
@@ -132,6 +232,23 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
// Create the main window programmatically
|
||||||
|
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
|
||||||
|
.title("Donut Browser")
|
||||||
|
.inner_size(900.0, 600.0)
|
||||||
|
.resizable(false)
|
||||||
|
.fullscreen(false);
|
||||||
|
|
||||||
|
let window = win_builder.build().unwrap();
|
||||||
|
|
||||||
|
// Set transparent titlebar for macOS
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
if let Err(e) = window.set_transparent_titlebar(true) {
|
||||||
|
eprintln!("Failed to set transparent titlebar: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set up deep link handler
|
// Set up deep link handler
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
|
|
||||||
@@ -264,6 +381,7 @@ pub fn run() {
|
|||||||
check_for_app_updates_manual,
|
check_for_app_updates_manual,
|
||||||
download_and_install_app_update,
|
download_and_install_app_update,
|
||||||
get_app_version_info,
|
get_app_version_info,
|
||||||
|
set_window_background_color,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Donut Browser",
|
"productName": "Donut Browser",
|
||||||
"version": "0.2.2",
|
"version": "0.2.5",
|
||||||
"identifier": "com.donutbrowser",
|
"identifier": "com.donutbrowser",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
@@ -10,15 +10,7 @@
|
|||||||
"frontendDist": "../dist"
|
"frontendDist": "../dist"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [],
|
||||||
{
|
|
||||||
"title": "Donut Browser",
|
|
||||||
"width": 900,
|
|
||||||
"height": 600,
|
|
||||||
"resizable": false,
|
|
||||||
"fullscreen": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import "@/styles/globals.css";
|
|||||||
import { CustomThemeProvider } from "@/components/theme-provider";
|
import { CustomThemeProvider } from "@/components/theme-provider";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
import { WindowDragArea } from "@/components/window-drag-area";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -26,6 +27,7 @@ export default function RootLayout({
|
|||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<CustomThemeProvider>
|
<CustomThemeProvider>
|
||||||
|
<WindowDragArea />
|
||||||
<TooltipProvider>{children}</TooltipProvider>
|
<TooltipProvider>{children}</TooltipProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</CustomThemeProvider>
|
</CustomThemeProvider>
|
||||||
|
|||||||
+62
-118
@@ -48,24 +48,27 @@ export default function Home() {
|
|||||||
useState<BrowserProfile | null>(null);
|
useState<BrowserProfile | null>(null);
|
||||||
const [currentProfileForVersionChange, setCurrentProfileForVersionChange] =
|
const [currentProfileForVersionChange, setCurrentProfileForVersionChange] =
|
||||||
useState<BrowserProfile | null>(null);
|
useState<BrowserProfile | null>(null);
|
||||||
const [isClient, setIsClient] = useState(false);
|
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
|
||||||
|
|
||||||
// Auto-update functionality - only initialize on client
|
// Simple profiles loader without updates check (for use as callback)
|
||||||
const updateNotifications = useUpdateNotifications();
|
const loadProfiles = useCallback(async () => {
|
||||||
const { checkForUpdates, isUpdating } = updateNotifications;
|
try {
|
||||||
|
const profileList = await invoke<BrowserProfile[]>(
|
||||||
// App auto-update functionality
|
"list_browser_profiles",
|
||||||
const appUpdateNotifications = useAppUpdateNotifications();
|
);
|
||||||
const { checkForAppUpdatesManual } = appUpdateNotifications;
|
setProfiles(profileList);
|
||||||
|
} catch (err: unknown) {
|
||||||
// Ensure we're on the client side to prevent hydration mismatches
|
console.error("Failed to load profiles:", err);
|
||||||
useEffect(() => {
|
setError(`Failed to load profiles: ${JSON.stringify(err)}`);
|
||||||
setIsClient(true);
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadProfiles = useCallback(async () => {
|
// Auto-update functionality - pass loadProfiles to refresh profiles after updates
|
||||||
if (!isClient) return; // Only run on client side
|
const updateNotifications = useUpdateNotifications(loadProfiles);
|
||||||
|
const { checkForUpdates, isUpdating } = updateNotifications;
|
||||||
|
|
||||||
|
// Profiles loader with update check (for initial load and manual refresh)
|
||||||
|
const loadProfilesWithUpdateCheck = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const profileList = await invoke<BrowserProfile[]>(
|
const profileList = await invoke<BrowserProfile[]>(
|
||||||
"list_browser_profiles",
|
"list_browser_profiles",
|
||||||
@@ -78,12 +81,12 @@ export default function Home() {
|
|||||||
console.error("Failed to load profiles:", err);
|
console.error("Failed to load profiles:", err);
|
||||||
setError(`Failed to load profiles: ${JSON.stringify(err)}`);
|
setError(`Failed to load profiles: ${JSON.stringify(err)}`);
|
||||||
}
|
}
|
||||||
}, [checkForUpdates, isClient]);
|
}, [checkForUpdates]);
|
||||||
|
|
||||||
|
useAppUpdateNotifications();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isClient) return; // Only run on client side
|
void loadProfilesWithUpdateCheck();
|
||||||
|
|
||||||
void loadProfiles();
|
|
||||||
|
|
||||||
// Check for startup default browser prompt
|
// Check for startup default browser prompt
|
||||||
void checkStartupPrompt();
|
void checkStartupPrompt();
|
||||||
@@ -105,10 +108,11 @@ export default function Home() {
|
|||||||
return () => {
|
return () => {
|
||||||
clearInterval(updateInterval);
|
clearInterval(updateInterval);
|
||||||
};
|
};
|
||||||
}, [loadProfiles, checkForUpdates, isClient]);
|
}, [loadProfilesWithUpdateCheck, checkForUpdates]);
|
||||||
|
|
||||||
const checkStartupPrompt = async () => {
|
const checkStartupPrompt = async () => {
|
||||||
if (!isClient) return; // Only run on client side
|
// Only check once during app startup to prevent reopening after dismissing notifications
|
||||||
|
if (hasCheckedStartupPrompt) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const shouldShow = await invoke<boolean>(
|
const shouldShow = await invoke<boolean>(
|
||||||
@@ -117,14 +121,14 @@ export default function Home() {
|
|||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
setSettingsDialogOpen(true);
|
setSettingsDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
setHasCheckedStartupPrompt(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to check startup prompt:", error);
|
console.error("Failed to check startup prompt:", error);
|
||||||
|
setHasCheckedStartupPrompt(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkStartupUrls = async () => {
|
const checkStartupUrls = async () => {
|
||||||
if (!isClient) return; // Only run on client side
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hasStartupUrl = await invoke<boolean>(
|
const hasStartupUrl = await invoke<boolean>(
|
||||||
"check_and_handle_startup_url",
|
"check_and_handle_startup_url",
|
||||||
@@ -138,8 +142,6 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const listenForUrlEvents = async () => {
|
const listenForUrlEvents = async () => {
|
||||||
if (!isClient) return; // Only run on client side
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Listen for URL open events from the deep link handler (when app is already running)
|
// Listen for URL open events from the deep link handler (when app is already running)
|
||||||
await listen<string>("url-open-request", (event) => {
|
await listen<string>("url-open-request", (event) => {
|
||||||
@@ -150,10 +152,7 @@ export default function Home() {
|
|||||||
// Listen for show profile selector events
|
// Listen for show profile selector events
|
||||||
await listen<string>("show-profile-selector", (event) => {
|
await listen<string>("show-profile-selector", (event) => {
|
||||||
console.log("Received show profile selector request:", event.payload);
|
console.log("Received show profile selector request:", event.payload);
|
||||||
setPendingUrls((prev) => [
|
setPendingUrls([{ id: Date.now().toString(), url: event.payload }]);
|
||||||
...prev,
|
|
||||||
{ id: Date.now().toString(), url: event.payload },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for show create profile dialog events
|
// Listen for show create profile dialog events
|
||||||
@@ -173,15 +172,13 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUrlOpen = async (url: string) => {
|
const handleUrlOpen = async (url: string) => {
|
||||||
if (!isClient) return; // Only run on client side
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use smart profile selection
|
// Use smart profile selection
|
||||||
const result = await invoke<string>("smart_open_url", {
|
const result = await invoke<string>("smart_open_url", {
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
console.log("Smart URL opening succeeded:", result);
|
console.log("Smart URL opening succeeded:", result);
|
||||||
// URL was handled successfully
|
// URL was handled successfully, no need to show selector
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.log(
|
console.log(
|
||||||
"Smart URL opening failed or requires profile selection:",
|
"Smart URL opening failed or requires profile selection:",
|
||||||
@@ -189,7 +186,8 @@ export default function Home() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Show profile selector for manual selection
|
// Show profile selector for manual selection
|
||||||
setPendingUrls((prev) => [...prev, { id: Date.now().toString(), url }]);
|
// Replace any existing pending URL with the new one
|
||||||
|
setPendingUrls([{ id: Date.now().toString(), url }]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -270,40 +268,33 @@ export default function Home() {
|
|||||||
|
|
||||||
const runningProfilesRef = useRef<Set<string>>(new Set());
|
const runningProfilesRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
const checkBrowserStatus = useCallback(
|
const checkBrowserStatus = useCallback(async (profile: BrowserProfile) => {
|
||||||
async (profile: BrowserProfile) => {
|
try {
|
||||||
if (!isClient) return; // Only run on client side
|
const isRunning = await invoke<boolean>("check_browser_status", {
|
||||||
|
profile,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
const currentRunning = runningProfilesRef.current.has(profile.name);
|
||||||
const isRunning = await invoke<boolean>("check_browser_status", {
|
|
||||||
profile,
|
if (isRunning !== currentRunning) {
|
||||||
|
setRunningProfiles((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (isRunning) {
|
||||||
|
next.add(profile.name);
|
||||||
|
} else {
|
||||||
|
next.delete(profile.name);
|
||||||
|
}
|
||||||
|
runningProfilesRef.current = next;
|
||||||
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentRunning = runningProfilesRef.current.has(profile.name);
|
|
||||||
|
|
||||||
if (isRunning !== currentRunning) {
|
|
||||||
setRunningProfiles((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (isRunning) {
|
|
||||||
next.add(profile.name);
|
|
||||||
} else {
|
|
||||||
next.delete(profile.name);
|
|
||||||
}
|
|
||||||
runningProfilesRef.current = next;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to check browser status:", err);
|
|
||||||
}
|
}
|
||||||
},
|
} catch (err) {
|
||||||
[isClient],
|
console.error("Failed to check browser status:", err);
|
||||||
);
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const launchProfile = useCallback(
|
const launchProfile = useCallback(
|
||||||
async (profile: BrowserProfile) => {
|
async (profile: BrowserProfile) => {
|
||||||
if (!isClient) return; // Only run on client side
|
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Check if browser is disabled due to ongoing update
|
// Check if browser is disabled due to ongoing update
|
||||||
@@ -337,11 +328,11 @@ export default function Home() {
|
|||||||
setError(`Failed to launch browser: ${JSON.stringify(err)}`);
|
setError(`Failed to launch browser: ${JSON.stringify(err)}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[loadProfiles, checkBrowserStatus, isUpdating, isClient],
|
[loadProfiles, checkBrowserStatus, isUpdating],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profiles.length === 0 || !isClient) return;
|
if (profiles.length === 0) return;
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
for (const profile of profiles) {
|
for (const profile of profiles) {
|
||||||
@@ -352,7 +343,7 @@ export default function Home() {
|
|||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, [profiles, checkBrowserStatus, isClient]);
|
}, [profiles, checkBrowserStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runningProfilesRef.current = runningProfiles;
|
runningProfilesRef.current = runningProfiles;
|
||||||
@@ -408,61 +399,14 @@ export default function Home() {
|
|||||||
[loadProfiles],
|
[loadProfiles],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Don't render anything until we're on the client side to prevent hydration issues
|
|
||||||
if (!isClient) {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 gap-8 sm:p-12 font-[family-name:var(--font-geist-sans)]">
|
|
||||||
<main className="flex flex-col gap-8 row-start-2 items-center w-full max-w-3xl">
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<CardTitle>Profiles</CardTitle>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
disabled
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<GoGear className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Settings</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
disabled
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<GoPlus className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Create a new profile</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-8 text-center">
|
|
||||||
<div className="animate-pulse">Loading...</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
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)]">
|
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 gap-8 sm:p-12 font-[family-name:var(--font-geist-sans)] bg-white dark:bg-black">
|
||||||
<main className="flex flex-col gap-8 row-start-2 items-center w-full max-w-3xl">
|
<main className="flex flex-col row-start-2 gap-8 items-center w-full max-w-3xl">
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex justify-between items-center">
|
||||||
<CardTitle>Profiles</CardTitle>
|
<CardTitle>Profiles</CardTitle>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex gap-2 items-center">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -471,9 +415,9 @@ export default function Home() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSettingsDialogOpen(true);
|
setSettingsDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2"
|
className="flex gap-2 items-center"
|
||||||
>
|
>
|
||||||
<GoGear className="h-4 w-4" />
|
<GoGear className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Settings</TooltipContent>
|
<TooltipContent>Settings</TooltipContent>
|
||||||
@@ -485,9 +429,9 @@ export default function Home() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCreateProfileDialogOpen(true);
|
setCreateProfileDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2"
|
className="flex gap-2 items-center"
|
||||||
>
|
>
|
||||||
<GoPlus className="h-4 w-4" />
|
<GoPlus className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Create a new profile</TooltipContent>
|
<TooltipContent>Create a new profile</TooltipContent>
|
||||||
|
|||||||
@@ -71,7 +71,12 @@ interface ErrorToastProps extends BaseToastProps {
|
|||||||
|
|
||||||
interface DownloadToastProps extends BaseToastProps {
|
interface DownloadToastProps extends BaseToastProps {
|
||||||
type: "download";
|
type: "download";
|
||||||
stage?: "downloading" | "extracting" | "verifying" | "completed";
|
stage?:
|
||||||
|
| "downloading"
|
||||||
|
| "extracting"
|
||||||
|
| "verifying"
|
||||||
|
| "completed"
|
||||||
|
| "downloading (twilight rolling release)";
|
||||||
progress?: {
|
progress?: {
|
||||||
percentage: number;
|
percentage: number;
|
||||||
speed?: string;
|
speed?: string;
|
||||||
@@ -93,13 +98,20 @@ interface FetchingToastProps extends BaseToastProps {
|
|||||||
browserName?: string;
|
browserName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TwilightUpdateToastProps extends BaseToastProps {
|
||||||
|
type: "twilight-update";
|
||||||
|
browserName?: string;
|
||||||
|
hasUpdate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
type ToastProps =
|
type ToastProps =
|
||||||
| LoadingToastProps
|
| LoadingToastProps
|
||||||
| SuccessToastProps
|
| SuccessToastProps
|
||||||
| ErrorToastProps
|
| ErrorToastProps
|
||||||
| DownloadToastProps
|
| DownloadToastProps
|
||||||
| VersionUpdateToastProps
|
| VersionUpdateToastProps
|
||||||
| FetchingToastProps;
|
| FetchingToastProps
|
||||||
|
| TwilightUpdateToastProps;
|
||||||
|
|
||||||
function getToastIcon(type: ToastProps["type"], stage?: string) {
|
function getToastIcon(type: ToastProps["type"], stage?: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -122,6 +134,10 @@ function getToastIcon(type: ToastProps["type"], stage?: string) {
|
|||||||
return (
|
return (
|
||||||
<LuRefreshCw className="h-4 w-4 text-blue-500 animate-spin flex-shrink-0" />
|
<LuRefreshCw className="h-4 w-4 text-blue-500 animate-spin flex-shrink-0" />
|
||||||
);
|
);
|
||||||
|
case "twilight-update":
|
||||||
|
return (
|
||||||
|
<LuRefreshCw className="h-4 w-4 text-purple-500 animate-spin flex-shrink-0" />
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-500 border-t-transparent flex-shrink-0" />
|
<div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-500 border-t-transparent flex-shrink-0" />
|
||||||
@@ -186,6 +202,22 @@ export function UnifiedToast(props: ToastProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Twilight update progress */}
|
||||||
|
{type === "twilight-update" && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-300">
|
||||||
|
{"hasUpdate" in props && props.hasUpdate
|
||||||
|
? "New twilight build available for download"
|
||||||
|
: "Checking for twilight updates..."}
|
||||||
|
</p>
|
||||||
|
{props.browserName && (
|
||||||
|
<p className="text-xs text-purple-600 dark:text-purple-400 mt-1">
|
||||||
|
{props.browserName} • Rolling Release
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{description && (
|
{description && (
|
||||||
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300 leading-tight">
|
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300 leading-tight">
|
||||||
@@ -206,6 +238,11 @@ export function UnifiedToast(props: ToastProps) {
|
|||||||
Verifying installation...
|
Verifying installation...
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{stage === "downloading (twilight rolling release)" && (
|
||||||
|
<p className="mt-1 text-xs text-purple-600 dark:text-purple-400">
|
||||||
|
Downloading rolling release build...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ export function ProfilesDataTable({
|
|||||||
}}
|
}}
|
||||||
disabled={!isClient || isRunning || isBrowserUpdating}
|
disabled={!isClient || isRunning || isBrowserUpdating}
|
||||||
>
|
>
|
||||||
Rename profile
|
Rename
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -411,7 +411,7 @@ export function ProfilesDataTable({
|
|||||||
className="text-red-600"
|
className="text-red-600"
|
||||||
disabled={!isClient || isRunning || isBrowserUpdating}
|
disabled={!isClient || isRunning || isBrowserUpdating}
|
||||||
>
|
>
|
||||||
Delete profile
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -69,16 +69,29 @@ export function ProfileSelectorDialog({
|
|||||||
|
|
||||||
// Auto-select first available profile for link opening
|
// Auto-select first available profile for link opening
|
||||||
if (profileList.length > 0) {
|
if (profileList.length > 0) {
|
||||||
// Find the first profile that can be used for opening links
|
// First, try to find a running profile that can be used for opening links
|
||||||
const availableProfile = profileList.find((profile) => {
|
const runningAvailableProfile = profileList.find((profile) => {
|
||||||
return canUseProfileForLinks(profile, profileList, runningProfiles);
|
const isRunning = runningProfiles.has(profile.name);
|
||||||
|
return (
|
||||||
|
isRunning &&
|
||||||
|
canUseProfileForLinks(profile, profileList, runningProfiles)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (availableProfile) {
|
if (runningAvailableProfile) {
|
||||||
setSelectedProfile(availableProfile.name);
|
setSelectedProfile(runningAvailableProfile.name);
|
||||||
} else {
|
} else {
|
||||||
// If no suitable profile found, still select the first one to show UI
|
// If no running profile is suitable, find the first profile that can be used for opening links
|
||||||
setSelectedProfile(profileList[0].name);
|
const availableProfile = profileList.find((profile) => {
|
||||||
|
return canUseProfileForLinks(profile, profileList, runningProfiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (availableProfile) {
|
||||||
|
setSelectedProfile(availableProfile.name);
|
||||||
|
} else {
|
||||||
|
// If no suitable profile found, still select the first one to show UI
|
||||||
|
setSelectedProfile(profileList[0].name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -277,7 +290,7 @@ export function ProfileSelectorDialog({
|
|||||||
!canUseForLinks ? "opacity-50" : ""
|
!canUseForLinks ? "opacity-50" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 p-3 rounded-lg hover:bg-accent cursor-pointer">
|
<div className="flex items-center gap-3 py-1 px-2 rounded-lg hover:bg-accent cursor-pointer">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{(() => {
|
{(() => {
|
||||||
const IconComponent = getBrowserIcon(
|
const IconComponent = getBrowserIcon(
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
<DialogTitle>Settings</DialogTitle>
|
<DialogTitle>Settings</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="grid gap-6 py-4 overflow-y-auto flex-1 min-h-0">
|
<div className="grid overflow-y-auto flex-1 gap-6 py-4 min-h-0">
|
||||||
{/* Appearance Section */}
|
{/* Appearance Section */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Label className="text-base font-medium">Appearance</Label>
|
<Label className="text-base font-medium">Appearance</Label>
|
||||||
@@ -172,7 +172,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
|
|
||||||
{/* Default Browser Section */}
|
{/* Default Browser Section */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex justify-between items-center">
|
||||||
<Label className="text-base font-medium">Default Browser</Label>
|
<Label className="text-base font-medium">Default Browser</Label>
|
||||||
<Badge variant={isDefaultBrowser ? "default" : "secondary"}>
|
<Badge variant={isDefaultBrowser ? "default" : "secondary"}>
|
||||||
{isDefaultBrowser ? "Active" : "Inactive"}
|
{isDefaultBrowser ? "Active" : "Inactive"}
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ function getSystemTheme(): string {
|
|||||||
export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [defaultTheme, setDefaultTheme] = useState<string>("system");
|
const [defaultTheme, setDefaultTheme] = useState<string>("system");
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadTheme = async () => {
|
const loadTheme = async () => {
|
||||||
@@ -65,11 +70,18 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
// Detect system theme to show appropriate loading screen
|
// Use a consistent loading screen that doesn't depend on system theme during SSR
|
||||||
const systemTheme = getSystemTheme();
|
// This prevents hydration mismatch by ensuring server and client render the same initially
|
||||||
const loadingBgColor = systemTheme === "dark" ? "bg-gray-900" : "bg-white";
|
let loadingBgColor = "bg-white";
|
||||||
const spinnerColor =
|
let spinnerColor = "border-gray-900";
|
||||||
systemTheme === "dark" ? "border-white" : "border-gray-900";
|
|
||||||
|
// Only apply system theme detection after component is mounted (client-side only)
|
||||||
|
if (mounted) {
|
||||||
|
const systemTheme = getSystemTheme();
|
||||||
|
loadingBgColor = systemTheme === "dark" ? "bg-gray-900" : "bg-white";
|
||||||
|
spinnerColor =
|
||||||
|
systemTheme === "dark" ? "border-white" : "border-gray-900";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -15,8 +15,15 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
"--normal-bg": "var(--popover)",
|
"--normal-bg": "var(--popover)",
|
||||||
"--normal-text": "var(--popover-foreground)",
|
"--normal-text": "var(--popover-foreground)",
|
||||||
"--normal-border": "var(--border)",
|
"--normal-border": "var(--border)",
|
||||||
|
zIndex: 99999,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
|
toastOptions={{
|
||||||
|
style: {
|
||||||
|
zIndex: 99999,
|
||||||
|
pointerEvents: "auto",
|
||||||
|
},
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,17 +47,17 @@ export function UpdateNotificationComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-4 max-w-md bg-background border border-border rounded-lg shadow-lg">
|
<div className="flex flex-col gap-3 p-4 max-w-md rounded-lg border shadow-lg bg-background border-border">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex gap-2 justify-between items-start">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex gap-2 items-center">
|
||||||
<span className="font-semibold text-foreground">
|
<span className="font-semibold text-foreground">
|
||||||
{browserDisplayName} Update Available
|
{browserDisplayName} Update Available
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
variant={notification.is_stable_update ? "default" : "secondary"}
|
variant={notification.is_stable_update ? "default" : "secondary"}
|
||||||
>
|
>
|
||||||
{notification.is_stable_update ? "Stable" : "Beta"}
|
{notification.is_stable_update ? "Stable" : "Nightly"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
@@ -71,20 +71,20 @@ export function UpdateNotificationComponent({
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await onDismiss(notification.id);
|
await onDismiss(notification.id);
|
||||||
}}
|
}}
|
||||||
className="h-6 w-6 p-0 shrink-0"
|
className="p-0 w-6 h-6 shrink-0"
|
||||||
>
|
>
|
||||||
<FaTimes className="h-3 w-3" />
|
<FaTimes className="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex gap-2 items-center">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleUpdateClick}
|
onClick={handleUpdateClick}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex items-center gap-2"
|
className="flex gap-2 items-center"
|
||||||
>
|
>
|
||||||
<FaDownload className="h-3 w-3" />
|
<FaDownload className="w-3 h-3" />
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function WindowDragArea() {
|
||||||
|
const [isMacOS, setIsMacOS] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if we're on macOS using user agent detection
|
||||||
|
const checkPlatform = () => {
|
||||||
|
const userAgent = navigator.userAgent.toLowerCase();
|
||||||
|
setIsMacOS(userAgent.includes("mac"));
|
||||||
|
};
|
||||||
|
|
||||||
|
checkPlatform();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
// Only handle left mouse button
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
|
// Start dragging asynchronously
|
||||||
|
const startDrag = async () => {
|
||||||
|
try {
|
||||||
|
const window = getCurrentWindow();
|
||||||
|
await window.startDragging();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to start window dragging:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void startDrag();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only render on macOS
|
||||||
|
if (!isMacOS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed top-0 right-0 left-0 z-50 h-8 cursor-move"
|
||||||
|
style={{
|
||||||
|
// Ensure it's above all other content
|
||||||
|
zIndex: 9999,
|
||||||
|
// Make it transparent but still capture mouse events
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
// Prevent text selection during drag
|
||||||
|
userSelect: "none",
|
||||||
|
WebkitUserSelect: "none",
|
||||||
|
}}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
// Prevent context menu
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -134,7 +134,11 @@ export function useAppUpdateNotifications() {
|
|||||||
{
|
{
|
||||||
id: "app-update",
|
id: "app-update",
|
||||||
duration: Number.POSITIVE_INFINITY, // Persistent until user action
|
duration: Number.POSITIVE_INFINITY, // Persistent until user action
|
||||||
position: "top-right",
|
position: "top-left",
|
||||||
|
style: {
|
||||||
|
zIndex: 99999, // Ensure app updates appear above dialogs
|
||||||
|
pointerEvents: "auto", // Ensure app updates remain interactive
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
|
|||||||
@@ -13,35 +13,66 @@ interface UpdateNotification {
|
|||||||
affected_profiles: string[];
|
affected_profiles: string[];
|
||||||
is_stable_update: boolean;
|
is_stable_update: boolean;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
is_rolling_release: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateNotifications() {
|
export function useUpdateNotifications(
|
||||||
|
onProfilesUpdated?: () => Promise<void>,
|
||||||
|
) {
|
||||||
const [notifications, setNotifications] = useState<UpdateNotification[]>([]);
|
const [notifications, setNotifications] = useState<UpdateNotification[]>([]);
|
||||||
const [updatingBrowsers, setUpdatingBrowsers] = useState<Set<string>>(
|
const [updatingBrowsers, setUpdatingBrowsers] = useState<Set<string>>(
|
||||||
new Set(),
|
new Set(),
|
||||||
);
|
);
|
||||||
const [isClient, setIsClient] = useState(false);
|
const [dismissedNotifications, setDismissedNotifications] = useState<
|
||||||
|
Set<string>
|
||||||
// Ensure we're on the client side to prevent hydration mismatches
|
>(new Set());
|
||||||
useEffect(() => {
|
|
||||||
setIsClient(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const checkForUpdates = useCallback(async () => {
|
const checkForUpdates = useCallback(async () => {
|
||||||
if (!isClient) return; // Only run on client side
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updates = await invoke<UpdateNotification[]>(
|
const updates = await invoke<UpdateNotification[]>(
|
||||||
"check_for_browser_updates",
|
"check_for_browser_updates",
|
||||||
);
|
);
|
||||||
setNotifications(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;
|
||||||
|
});
|
||||||
|
|
||||||
|
setNotifications(filteredUpdates);
|
||||||
|
|
||||||
// Show toasts for new notifications - we'll define handleUpdate and handleDismiss separately
|
// Show toasts for new notifications - we'll define handleUpdate and handleDismiss separately
|
||||||
// to avoid circular dependencies
|
// to avoid circular dependencies
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to check for updates:", error);
|
console.error("Failed to check for updates:", error);
|
||||||
}
|
}
|
||||||
}, [isClient]);
|
}, [dismissedNotifications]);
|
||||||
|
|
||||||
const handleUpdate = useCallback(
|
const handleUpdate = useCallback(
|
||||||
async (browser: string, newVersion: string) => {
|
async (browser: string, newVersion: string) => {
|
||||||
@@ -117,6 +148,11 @@ export function useUpdateNotifications() {
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger profile refresh to update UI with new versions
|
||||||
|
if (onProfilesUpdated) {
|
||||||
|
void onProfilesUpdated();
|
||||||
|
}
|
||||||
} catch (downloadError) {
|
} catch (downloadError) {
|
||||||
console.error("Failed to download browser:", downloadError);
|
console.error("Failed to download browser:", downloadError);
|
||||||
|
|
||||||
@@ -158,28 +194,28 @@ export function useUpdateNotifications() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[notifications, checkForUpdates],
|
[notifications, checkForUpdates, onProfilesUpdated],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDismiss = useCallback(
|
const handleDismiss = useCallback(
|
||||||
async (notificationId: string) => {
|
async (notificationId: string) => {
|
||||||
if (!isClient) return; // Only run on client side
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
toast.dismiss(notificationId);
|
toast.dismiss(notificationId);
|
||||||
await invoke("dismiss_update_notification", { 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();
|
await checkForUpdates();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to dismiss notification:", error);
|
console.error("Failed to dismiss notification:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[checkForUpdates, isClient],
|
[checkForUpdates],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Separate effect to show toasts when notifications change
|
// Separate effect to show toasts when notifications change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isClient) return;
|
|
||||||
|
|
||||||
for (const notification of notifications) {
|
for (const notification of notifications) {
|
||||||
const isUpdating = updatingBrowsers.has(notification.browser);
|
const isUpdating = updatingBrowsers.has(notification.browser);
|
||||||
|
|
||||||
@@ -196,12 +232,14 @@ export function useUpdateNotifications() {
|
|||||||
id: notification.id,
|
id: notification.id,
|
||||||
duration: Number.POSITIVE_INFINITY, // Persistent until user action
|
duration: Number.POSITIVE_INFINITY, // Persistent until user action
|
||||||
position: "top-right",
|
position: "top-right",
|
||||||
// Remove transparent styling to fix background issue
|
style: {
|
||||||
style: undefined,
|
zIndex: 99999, // Ensure notifications appear above dialogs
|
||||||
|
pointerEvents: "auto", // Ensure notifications remain interactive
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [notifications, updatingBrowsers, handleUpdate, handleDismiss, isClient]);
|
}, [notifications, updatingBrowsers, handleUpdate, handleDismiss]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
notifications,
|
notifications,
|
||||||
|
|||||||
+51
-4
@@ -24,7 +24,12 @@ export interface ErrorToastProps extends BaseToastProps {
|
|||||||
|
|
||||||
export interface DownloadToastProps extends BaseToastProps {
|
export interface DownloadToastProps extends BaseToastProps {
|
||||||
type: "download";
|
type: "download";
|
||||||
stage?: "downloading" | "extracting" | "verifying" | "completed";
|
stage?:
|
||||||
|
| "downloading"
|
||||||
|
| "extracting"
|
||||||
|
| "verifying"
|
||||||
|
| "completed"
|
||||||
|
| "downloading (twilight rolling release)";
|
||||||
progress?: {
|
progress?: {
|
||||||
percentage: number;
|
percentage: number;
|
||||||
speed?: string;
|
speed?: string;
|
||||||
@@ -46,13 +51,20 @@ export interface FetchingToastProps extends BaseToastProps {
|
|||||||
browserName?: string;
|
browserName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TwilightUpdateToastProps extends BaseToastProps {
|
||||||
|
type: "twilight-update";
|
||||||
|
browserName?: string;
|
||||||
|
hasUpdate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type ToastProps =
|
export type ToastProps =
|
||||||
| LoadingToastProps
|
| LoadingToastProps
|
||||||
| SuccessToastProps
|
| SuccessToastProps
|
||||||
| ErrorToastProps
|
| ErrorToastProps
|
||||||
| DownloadToastProps
|
| DownloadToastProps
|
||||||
| VersionUpdateToastProps
|
| VersionUpdateToastProps
|
||||||
| FetchingToastProps;
|
| FetchingToastProps
|
||||||
|
| TwilightUpdateToastProps;
|
||||||
|
|
||||||
// Unified toast function
|
// Unified toast function
|
||||||
export function showToast(props: ToastProps & { id?: string }) {
|
export function showToast(props: ToastProps & { id?: string }) {
|
||||||
@@ -81,6 +93,9 @@ export function showToast(props: ToastProps & { id?: string }) {
|
|||||||
case "version-update":
|
case "version-update":
|
||||||
duration = 15000;
|
duration = 15000;
|
||||||
break;
|
break;
|
||||||
|
case "twilight-update":
|
||||||
|
duration = 10000;
|
||||||
|
break;
|
||||||
case "success":
|
case "success":
|
||||||
duration = 3000;
|
duration = 3000;
|
||||||
break;
|
break;
|
||||||
@@ -101,6 +116,8 @@ export function showToast(props: ToastProps & { id?: string }) {
|
|||||||
border: "none",
|
border: "none",
|
||||||
boxShadow: "none",
|
boxShadow: "none",
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
zIndex: 99999,
|
||||||
|
pointerEvents: "auto",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (props.type === "error") {
|
} else if (props.type === "error") {
|
||||||
@@ -112,6 +129,8 @@ export function showToast(props: ToastProps & { id?: string }) {
|
|||||||
border: "none",
|
border: "none",
|
||||||
boxShadow: "none",
|
boxShadow: "none",
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
zIndex: 99999,
|
||||||
|
pointerEvents: "auto",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -123,6 +142,8 @@ export function showToast(props: ToastProps & { id?: string }) {
|
|||||||
border: "none",
|
border: "none",
|
||||||
boxShadow: "none",
|
boxShadow: "none",
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
zIndex: 99999,
|
||||||
|
pointerEvents: "auto",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -149,7 +170,12 @@ export function showLoadingToast(
|
|||||||
export function showDownloadToast(
|
export function showDownloadToast(
|
||||||
browserName: string,
|
browserName: string,
|
||||||
version: string,
|
version: string,
|
||||||
stage: "downloading" | "extracting" | "verifying" | "completed",
|
stage:
|
||||||
|
| "downloading"
|
||||||
|
| "extracting"
|
||||||
|
| "verifying"
|
||||||
|
| "completed"
|
||||||
|
| "downloading (twilight rolling release)",
|
||||||
progress?: { percentage: number; speed?: string; eta?: string },
|
progress?: { percentage: number; speed?: string; eta?: string },
|
||||||
options?: { suppressCompletionToast?: boolean },
|
options?: { suppressCompletionToast?: boolean },
|
||||||
) {
|
) {
|
||||||
@@ -160,7 +186,9 @@ export function showDownloadToast(
|
|||||||
? `Downloading ${browserName} ${version}`
|
? `Downloading ${browserName} ${version}`
|
||||||
: stage === "extracting"
|
: stage === "extracting"
|
||||||
? `Extracting ${browserName} ${version}`
|
? `Extracting ${browserName} ${version}`
|
||||||
: `Verifying ${browserName} ${version}`;
|
: stage === "downloading (twilight rolling release)"
|
||||||
|
? `Downloading ${browserName} ${version}`
|
||||||
|
: `Verifying ${browserName} ${version}`;
|
||||||
|
|
||||||
// Don't show completion toast if suppressed (for auto-update scenarios)
|
// Don't show completion toast if suppressed (for auto-update scenarios)
|
||||||
if (stage === "completed" && options?.suppressCompletionToast) {
|
if (stage === "completed" && options?.suppressCompletionToast) {
|
||||||
@@ -245,6 +273,25 @@ export function showErrorToast(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function showTwilightUpdateToast(
|
||||||
|
browserName: string,
|
||||||
|
options?: {
|
||||||
|
id?: string;
|
||||||
|
description?: string;
|
||||||
|
hasUpdate?: boolean;
|
||||||
|
duration?: number;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return showToast({
|
||||||
|
type: "twilight-update",
|
||||||
|
title: options?.hasUpdate
|
||||||
|
? `${browserName} twilight update available`
|
||||||
|
: `Checking for ${browserName} twilight updates...`,
|
||||||
|
browserName,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Generic helper for dismissing toasts
|
// Generic helper for dismissing toasts
|
||||||
export function dismissToast(id: string) {
|
export function dismissToast(id: string) {
|
||||||
sonnerToast.dismiss(id);
|
sonnerToast.dismiss(id);
|
||||||
|
|||||||
@@ -123,3 +123,23 @@
|
|||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure Sonner toasts appear above all dialogs and remain interactive */
|
||||||
|
.toaster,
|
||||||
|
[data-sonner-toaster] {
|
||||||
|
z-index: 99999 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-sonner-toast] {
|
||||||
|
z-index: 99999 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure toast buttons and interactive elements work */
|
||||||
|
[data-sonner-toast] button,
|
||||||
|
[data-sonner-toast] [role="button"],
|
||||||
|
[data-sonner-toast] input,
|
||||||
|
[data-sonner-toast] select {
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
black: "#000000",
|
||||||
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
dark: "#000000",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user