mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-24 23:49:55 +02:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbcec2cbc1 | |||
| 5d395f606e | |||
| 6963e07be5 | |||
| c28537d304 | |||
| c303de4a8a | |||
| 21a13fb217 | |||
| 90d8e782de | |||
| ad2d9b73f2 | |||
| e645e212f2 | |||
| 7df92ae8ee | |||
| 18a0254ca7 | |||
| b0a58c3131 | |||
| 467e82ca93 | |||
| 8d9654044a | |||
| 711d94c9c1 | |||
| 305051d03d | |||
| 8695884535 | |||
| bb96936550 | |||
| 730621e5a1 | |||
| 714102eb25 | |||
| e5378c1bb7 | |||
| b7c8d5672a | |||
| 3fb81e2ca0 | |||
| 510de96393 | |||
| 3688e88d67 | |||
| 9646a41788 | |||
| f7679d25ca | |||
| 2d42772718 | |||
| 3980f835d6 | |||
| d0185dd5ae | |||
| de39fa4555 | |||
| f773eb6f1c | |||
| 458c30433d | |||
| 1cb9ffa249 | |||
| 5c58b5c644 | |||
| f41311a7bb | |||
| c8c09c296e | |||
| ca0c2614f4 | |||
| dca5a2970e | |||
| e0a1dd5a8a | |||
| e48b681215 | |||
| 6796912606 | |||
| 7105f6544f | |||
| 3003f868e7 | |||
| b733d26f10 | |||
| 675c2417d7 | |||
| e10a7bf089 | |||
| c8e3cd39ff | |||
| 0103150dc7 | |||
| be57ac3219 | |||
| b4067b5e34 | |||
| 3fa8822139 | |||
| 1e0ef0b497 | |||
| 2da832f100 |
@@ -73,7 +73,7 @@ jobs:
|
||||
compat-lookup: true
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Auto-merge minor and patch updates
|
||||
uses: ridedott/merge-me-action@d288b479e76cb993344ca8b5e0fcaa7d6e667eed #v2.10.123
|
||||
uses: ridedott/merge-me-action@f96a67511b4be051e77760230e6a3fb9cb7b1903 #v2.10.124
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.SECRET_DEPENDABOT_GITHUB_TOKEN }}
|
||||
MERGE_METHOD: SQUASH
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
- name: Validate issue with AI
|
||||
id: validate
|
||||
uses: actions/ai-inference@9693b137b6566bb66055a713613bf4f0493701eb # v1.2.3
|
||||
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
|
||||
with:
|
||||
prompt-file: issue_analysis.txt
|
||||
system-prompt: |
|
||||
|
||||
@@ -30,9 +30,8 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
os: [macos-latest, ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -87,9 +86,9 @@ jobs:
|
||||
fi
|
||||
|
||||
# TODO: replace with an integration test that fetches everything from rust
|
||||
- name: Download Camoufox for testing
|
||||
run: npx camoufox-js fetch
|
||||
continue-on-error: true
|
||||
# - name: Download Camoufox for testing
|
||||
# run: npx camoufox-js fetch
|
||||
# continue-on-error: true
|
||||
|
||||
- name: Copy nodecar binary to Tauri binaries
|
||||
shell: bash
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
- name: Generate release notes with AI
|
||||
id: generate-notes
|
||||
uses: actions/ai-inference@9693b137b6566bb66055a713613bf4f0493701eb # v1.2.3
|
||||
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
|
||||
with:
|
||||
prompt-file: commits.txt
|
||||
system-prompt: |
|
||||
|
||||
@@ -154,9 +154,9 @@ jobs:
|
||||
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-${{ matrix.target }}
|
||||
fi
|
||||
|
||||
- name: Download Camoufox for testing
|
||||
run: npx camoufox-js fetch
|
||||
continue-on-error: true
|
||||
# - name: Download Camoufox for testing
|
||||
# run: npx camoufox-js fetch
|
||||
# continue-on-error: true
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm build
|
||||
@@ -166,6 +166,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||
NO_STRIP: ${{ matrix.platform == 'ubuntu-22.04' && 'true' || '' }}
|
||||
with:
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: "Donut Browser ${{ github.ref_name }}"
|
||||
@@ -179,4 +180,3 @@ jobs:
|
||||
with:
|
||||
branch: main
|
||||
commit_message: "docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]"
|
||||
file_pattern: CHANGELOG.md
|
||||
|
||||
@@ -153,9 +153,9 @@ jobs:
|
||||
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-${{ matrix.target }}
|
||||
fi
|
||||
|
||||
- name: Download Camoufox for testing
|
||||
run: npx camoufox-js fetch
|
||||
continue-on-error: true
|
||||
# - name: Download Camoufox for testing
|
||||
# run: npx camoufox-js fetch
|
||||
# continue-on-error: true
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm build
|
||||
@@ -176,6 +176,7 @@ jobs:
|
||||
BUILD_TAG: "nightly-${{ steps.timestamp.outputs.timestamp }}"
|
||||
GITHUB_REF_NAME: "nightly-${{ steps.timestamp.outputs.timestamp }}"
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
NO_STRIP: ${{ matrix.platform == 'ubuntu-22.04' && 'true' || '' }}
|
||||
with:
|
||||
tagName: "nightly-${{ steps.timestamp.outputs.timestamp }}"
|
||||
releaseName: "Donut Browser Nightly (Build ${{ steps.timestamp.outputs.timestamp }})"
|
||||
|
||||
@@ -23,4 +23,4 @@ jobs:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||
- name: Spell Check Repo
|
||||
uses: crate-ci/typos@392b78fe18a52790c53f42456e46124f77346842 #v1.34.0
|
||||
uses: crate-ci/typos@52bd719c2c91f9d676e2aa359fc8e0db8925e6d8 #v1.35.3
|
||||
|
||||
Vendored
+5
@@ -25,7 +25,9 @@
|
||||
"cmdk",
|
||||
"codegen",
|
||||
"codesign",
|
||||
"commitish",
|
||||
"CTYPE",
|
||||
"daijro",
|
||||
"dataclasses",
|
||||
"datareporting",
|
||||
"datas",
|
||||
@@ -44,6 +46,7 @@
|
||||
"esac",
|
||||
"esbuild",
|
||||
"etree",
|
||||
"flate",
|
||||
"frontmost",
|
||||
"geoip",
|
||||
"getcwd",
|
||||
@@ -78,6 +81,7 @@
|
||||
"libxdo",
|
||||
"localtime",
|
||||
"lxml",
|
||||
"lzma",
|
||||
"mmdb",
|
||||
"mountpoint",
|
||||
"msiexec",
|
||||
@@ -175,6 +179,7 @@
|
||||
"xfconf",
|
||||
"xsettings",
|
||||
"zhom",
|
||||
"zipball",
|
||||
"zoneinfo"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@ Have questions or want to contribute? We'd love to hear from you!
|
||||
|
||||
<!-- readme: collaborators,contributors -start -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/zhom">
|
||||
<img src="https://avatars.githubusercontent.com/u/2717306?v=4" width="100;" alt="zhom"/>
|
||||
@@ -93,8 +93,8 @@ Have questions or want to contribute? We'd love to hear from you!
|
||||
<sub><b>zhom</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tbody>
|
||||
</tr>
|
||||
<tbody>
|
||||
</table>
|
||||
<!-- readme: collaborators,contributors -end -->
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"author": "",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"commander": "^14.0.0",
|
||||
"donutbrowser-camoufox-js": "^0.6.4",
|
||||
"dotenv": "^17.2.1",
|
||||
@@ -30,7 +30,7 @@
|
||||
"nodemon": "^3.1.10",
|
||||
"playwright-core": "^1.54.2",
|
||||
"proxy-chain": "^2.5.9",
|
||||
"tmp": "^0.2.4",
|
||||
"tmp": "^0.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
|
||||
+6
-5
@@ -46,6 +46,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"motion": "^12.23.12",
|
||||
"next": "^15.4.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.1",
|
||||
@@ -56,15 +57,15 @@
|
||||
"tauri-plugin-macos-permissions-api": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.1.3",
|
||||
"@biomejs/biome": "2.1.4",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tauri-apps/cli": "^2.7.1",
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.4",
|
||||
"lint-staged": "^16.1.5",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"ts-unused-exports": "^11.0.1",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
@@ -72,7 +73,7 @@
|
||||
},
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"lint-staged": {
|
||||
"**/*.{js,jsx,ts,tsx,json,css,md}": [
|
||||
"**/*.{js,jsx,ts,tsx,json,css}": [
|
||||
"biome check --fix"
|
||||
],
|
||||
"src-tauri/**/*.rs": [
|
||||
|
||||
Generated
+131
-71
@@ -74,6 +74,9 @@ importers:
|
||||
cmdk:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
motion:
|
||||
specifier: ^12.23.12
|
||||
version: 12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
next:
|
||||
specifier: ^15.4.6
|
||||
version: 15.4.6(@babel/core@7.28.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
@@ -100,8 +103,8 @@ importers:
|
||||
version: 2.3.0
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: 2.1.3
|
||||
version: 2.1.3
|
||||
specifier: 2.1.4
|
||||
version: 2.1.4
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11
|
||||
@@ -109,8 +112,8 @@ importers:
|
||||
specifier: ^2.7.1
|
||||
version: 2.7.1
|
||||
'@types/node':
|
||||
specifier: ^24.2.0
|
||||
version: 24.2.0
|
||||
specifier: ^24.2.1
|
||||
version: 24.2.1
|
||||
'@types/react':
|
||||
specifier: ^19.1.9
|
||||
version: 19.1.9
|
||||
@@ -118,14 +121,14 @@ importers:
|
||||
specifier: ^19.1.7
|
||||
version: 19.1.7(@types/react@19.1.9)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0(vite@7.0.6(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))
|
||||
husky:
|
||||
specifier: ^9.1.7
|
||||
version: 9.1.7
|
||||
lint-staged:
|
||||
specifier: ^16.1.4
|
||||
version: 16.1.4
|
||||
specifier: ^16.1.5
|
||||
version: 16.1.5
|
||||
tailwindcss:
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11
|
||||
@@ -142,8 +145,8 @@ importers:
|
||||
nodecar:
|
||||
dependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.2.0
|
||||
version: 24.2.0
|
||||
specifier: ^24.2.1
|
||||
version: 24.2.1
|
||||
commander:
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0
|
||||
@@ -169,11 +172,11 @@ importers:
|
||||
specifier: ^2.5.9
|
||||
version: 2.5.9
|
||||
tmp:
|
||||
specifier: ^0.2.4
|
||||
version: 0.2.4
|
||||
specifier: ^0.2.5
|
||||
version: 0.2.5
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@24.2.0)(typescript@5.9.2)
|
||||
version: 10.9.2(@types/node@24.2.1)(typescript@5.9.2)
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
@@ -279,55 +282,55 @@ packages:
|
||||
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@biomejs/biome@2.1.3':
|
||||
resolution: {integrity: sha512-KE/tegvJIxTkl7gJbGWSgun7G6X/n2M6C35COT6ctYrAy7SiPyNvi6JtoQERVK/VRbttZfgGq96j2bFmhmnH4w==}
|
||||
'@biomejs/biome@2.1.4':
|
||||
resolution: {integrity: sha512-QWlrqyxsU0FCebuMnkvBIkxvPqH89afiJzjMl+z67ybutse590jgeaFdDurE9XYtzpjRGTI1tlUZPGWmbKsElA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
hasBin: true
|
||||
|
||||
'@biomejs/cli-darwin-arm64@2.1.3':
|
||||
resolution: {integrity: sha512-LFLkSWRoSGS1wVUD/BE6Nlt2dSn0ulH3XImzg2O/36BoToJHKXjSxzPEMAqT9QvwVtk7/9AQhZpTneERU9qaXA==}
|
||||
'@biomejs/cli-darwin-arm64@2.1.4':
|
||||
resolution: {integrity: sha512-sCrNENE74I9MV090Wq/9Dg7EhPudx3+5OiSoQOkIe3DLPzFARuL1dOwCWhKCpA3I5RHmbrsbNSRfZwCabwd8Qg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-darwin-x64@2.1.3':
|
||||
resolution: {integrity: sha512-Q/4OTw8P9No9QeowyxswcWdm0n2MsdCwWcc5NcKQQvzwPjwuPdf8dpPPf4r+x0RWKBtl1FLiAUtJvBlri6DnYw==}
|
||||
'@biomejs/cli-darwin-x64@2.1.4':
|
||||
resolution: {integrity: sha512-gOEICJbTCy6iruBywBDcG4X5rHMbqCPs3clh3UQ+hRKlgvJTk4NHWQAyHOXvaLe+AxD1/TNX1jbZeffBJzcrOw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@2.1.3':
|
||||
resolution: {integrity: sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w==}
|
||||
'@biomejs/cli-linux-arm64-musl@2.1.4':
|
||||
resolution: {integrity: sha512-nYr7H0CyAJPaLupFE2cH16KZmRC5Z9PEftiA2vWxk+CsFkPZQ6dBRdcC6RuS+zJlPc/JOd8xw3uCCt9Pv41WvQ==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.1.3':
|
||||
resolution: {integrity: sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA==}
|
||||
'@biomejs/cli-linux-arm64@2.1.4':
|
||||
resolution: {integrity: sha512-juhEkdkKR4nbUi5k/KRp1ocGPNWLgFRD4NrHZSveYrD6i98pyvuzmS9yFYgOZa5JhaVqo0HPnci0+YuzSwT2fw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.1.3':
|
||||
resolution: {integrity: sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew==}
|
||||
'@biomejs/cli-linux-x64-musl@2.1.4':
|
||||
resolution: {integrity: sha512-lvwvb2SQQHctHUKvBKptR6PLFCM7JfRjpCCrDaTmvB7EeZ5/dQJPhTYBf36BE/B4CRWR2ZiBLRYhK7hhXBCZAg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64@2.1.3':
|
||||
resolution: {integrity: sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA==}
|
||||
'@biomejs/cli-linux-x64@2.1.4':
|
||||
resolution: {integrity: sha512-Eoy9ycbhpJVYuR+LskV9s3uyaIkp89+qqgqhGQsWnp/I02Uqg2fXFblHJOpGZR8AxdB9ADy87oFVxn9MpFKUrw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.1.3':
|
||||
resolution: {integrity: sha512-V9CUZCtWH4u0YwyCYbQ3W5F4ZGPWp2C2TYcsiWFNNyRfmOW1j/TY/jAurl33SaRjgZPO5UUhGyr9m6BN9t84NQ==}
|
||||
'@biomejs/cli-win32-arm64@2.1.4':
|
||||
resolution: {integrity: sha512-3WRYte7orvyi6TRfIZkDN9Jzoogbv+gSvR+b9VOXUg1We1XrjBg6WljADeVEaKTvOcpVdH0a90TwyOQ6ue4fGw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@biomejs/cli-win32-x64@2.1.3':
|
||||
resolution: {integrity: sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g==}
|
||||
'@biomejs/cli-win32-x64@2.1.4':
|
||||
resolution: {integrity: sha512-tBc+W7anBPSFXGAoQW+f/+svkpt8/uXfRwDzN1DvnatkRMt16KIYpEi/iw8u9GahJlFv98kgHcIrSsZHZTR0sw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1135,8 +1138,8 @@ packages:
|
||||
'@radix-ui/rect@1.1.1':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27':
|
||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
||||
'@rolldown/pluginutils@1.0.0-beta.30':
|
||||
resolution: {integrity: sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.46.2':
|
||||
resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==}
|
||||
@@ -1467,8 +1470,8 @@ packages:
|
||||
'@types/node-fetch@2.6.13':
|
||||
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
|
||||
|
||||
'@types/node@24.2.0':
|
||||
resolution: {integrity: sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==}
|
||||
'@types/node@24.2.1':
|
||||
resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==}
|
||||
|
||||
'@types/react-dom@19.1.7':
|
||||
resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==}
|
||||
@@ -1481,9 +1484,9 @@ packages:
|
||||
'@types/tmp@0.2.6':
|
||||
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
|
||||
|
||||
'@vitejs/plugin-react@4.7.0':
|
||||
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
'@vitejs/plugin-react@5.0.0':
|
||||
resolution: {integrity: sha512-Jx9JfsTa05bYkS9xo0hkofp2dCmp1blrKjw9JONs5BTHOvJCgLbaPSuZLGSVJW6u2qe0tc4eevY0+gSNNi0YCw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
|
||||
@@ -1860,6 +1863,20 @@ packages:
|
||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
framer-motion@12.23.12:
|
||||
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
@@ -2215,8 +2232,8 @@ packages:
|
||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
lint-staged@16.1.4:
|
||||
resolution: {integrity: sha512-xy7rnzQrhTVGKMpv6+bmIA3C0yET31x8OhKBYfvGo0/byeZ6E0BjGARrir3Kg/RhhYHutpsi01+2J5IpfVoueA==}
|
||||
lint-staged@16.1.5:
|
||||
resolution: {integrity: sha512-uAeQQwByI6dfV7wpt/gVqg+jAPaSp8WwOA8kKC/dv1qw14oGpnpAisY65ibGHUGDUv0rYaZ8CAJZ/1U8hUvC2A==}
|
||||
engines: {node: '>=20.17'}
|
||||
hasBin: true
|
||||
|
||||
@@ -2343,6 +2360,26 @@ packages:
|
||||
resolution: {integrity: sha512-DXO4L9W+08T+A7h5+xdT32l7IMot8z7WOH+7C1Maol571PnktQ8un7Ni4CyPFp4H+vht/FDA5/tpjRvWMFQDMw==}
|
||||
engines: {node: '>=10', npm: '>=6'}
|
||||
|
||||
motion-dom@12.23.12:
|
||||
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
|
||||
|
||||
motion-utils@12.23.6:
|
||||
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
||||
|
||||
motion@12.23.12:
|
||||
resolution: {integrity: sha512-8jCD8uW5GD1csOoqh1WhH1A6j5APHVE15nuBkFeRiMzYBdRwyAHmSP/oXSuW0WJPZRXTFdBoG4hY9TFWNhhwng==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -2782,8 +2819,8 @@ packages:
|
||||
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tmp@0.2.4:
|
||||
resolution: {integrity: sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==}
|
||||
tmp@0.2.5:
|
||||
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
@@ -3100,39 +3137,39 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
|
||||
'@biomejs/biome@2.1.3':
|
||||
'@biomejs/biome@2.1.4':
|
||||
optionalDependencies:
|
||||
'@biomejs/cli-darwin-arm64': 2.1.3
|
||||
'@biomejs/cli-darwin-x64': 2.1.3
|
||||
'@biomejs/cli-linux-arm64': 2.1.3
|
||||
'@biomejs/cli-linux-arm64-musl': 2.1.3
|
||||
'@biomejs/cli-linux-x64': 2.1.3
|
||||
'@biomejs/cli-linux-x64-musl': 2.1.3
|
||||
'@biomejs/cli-win32-arm64': 2.1.3
|
||||
'@biomejs/cli-win32-x64': 2.1.3
|
||||
'@biomejs/cli-darwin-arm64': 2.1.4
|
||||
'@biomejs/cli-darwin-x64': 2.1.4
|
||||
'@biomejs/cli-linux-arm64': 2.1.4
|
||||
'@biomejs/cli-linux-arm64-musl': 2.1.4
|
||||
'@biomejs/cli-linux-x64': 2.1.4
|
||||
'@biomejs/cli-linux-x64-musl': 2.1.4
|
||||
'@biomejs/cli-win32-arm64': 2.1.4
|
||||
'@biomejs/cli-win32-x64': 2.1.4
|
||||
|
||||
'@biomejs/cli-darwin-arm64@2.1.3':
|
||||
'@biomejs/cli-darwin-arm64@2.1.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-darwin-x64@2.1.3':
|
||||
'@biomejs/cli-darwin-x64@2.1.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@2.1.3':
|
||||
'@biomejs/cli-linux-arm64-musl@2.1.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.1.3':
|
||||
'@biomejs/cli-linux-arm64@2.1.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.1.3':
|
||||
'@biomejs/cli-linux-x64-musl@2.1.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64@2.1.3':
|
||||
'@biomejs/cli-linux-x64@2.1.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.1.3':
|
||||
'@biomejs/cli-win32-arm64@2.1.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-x64@2.1.3':
|
||||
'@biomejs/cli-win32-x64@2.1.4':
|
||||
optional: true
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
@@ -3826,7 +3863,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/rect@1.1.1': {}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||
'@rolldown/pluginutils@1.0.0-beta.30': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.46.2':
|
||||
optional: true
|
||||
@@ -4077,10 +4114,10 @@ snapshots:
|
||||
|
||||
'@types/node-fetch@2.6.13':
|
||||
dependencies:
|
||||
'@types/node': 24.2.0
|
||||
'@types/node': 24.2.1
|
||||
form-data: 4.0.4
|
||||
|
||||
'@types/node@24.2.0':
|
||||
'@types/node@24.2.1':
|
||||
dependencies:
|
||||
undici-types: 7.10.0
|
||||
|
||||
@@ -4094,15 +4131,15 @@ snapshots:
|
||||
|
||||
'@types/tmp@0.2.6': {}
|
||||
|
||||
'@vitejs/plugin-react@4.7.0(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))':
|
||||
'@vitejs/plugin-react@5.0.0(vite@7.0.6(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.0
|
||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0)
|
||||
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0)
|
||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||
'@rolldown/pluginutils': 1.0.0-beta.30
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.17.0
|
||||
vite: 7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1)
|
||||
vite: 7.0.6(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -4520,6 +4557,15 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
framer-motion@12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||
dependencies:
|
||||
motion-dom: 12.23.12
|
||||
motion-utils: 12.23.6
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
|
||||
fs-constants@1.0.0: {}
|
||||
|
||||
fs-minipass@2.1.0:
|
||||
@@ -4825,7 +4871,7 @@ snapshots:
|
||||
|
||||
lilconfig@3.1.3: {}
|
||||
|
||||
lint-staged@16.1.4:
|
||||
lint-staged@16.1.5:
|
||||
dependencies:
|
||||
chalk: 5.5.0
|
||||
commander: 14.0.0
|
||||
@@ -4981,6 +5027,20 @@ snapshots:
|
||||
|
||||
mmdb-lib@2.2.1: {}
|
||||
|
||||
motion-dom@12.23.12:
|
||||
dependencies:
|
||||
motion-utils: 12.23.6
|
||||
|
||||
motion-utils@12.23.6: {}
|
||||
|
||||
motion@12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||
dependencies:
|
||||
framer-motion: 12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nano-spawn@1.0.2: {}
|
||||
@@ -5495,7 +5555,7 @@ snapshots:
|
||||
fdir: 6.4.6(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
tmp@0.2.4: {}
|
||||
tmp@0.2.5: {}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
@@ -5505,14 +5565,14 @@ snapshots:
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
ts-node@10.9.2(@types/node@24.2.0)(typescript@5.9.2):
|
||||
ts-node@10.9.2(@types/node@24.2.1)(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
'@tsconfig/node10': 1.0.11
|
||||
'@tsconfig/node12': 1.0.11
|
||||
'@tsconfig/node14': 1.0.3
|
||||
'@tsconfig/node16': 1.0.4
|
||||
'@types/node': 24.2.0
|
||||
'@types/node': 24.2.1
|
||||
acorn: 8.15.0
|
||||
acorn-walk: 8.3.4
|
||||
arg: 4.1.3
|
||||
@@ -5599,7 +5659,7 @@ snapshots:
|
||||
|
||||
vali-date@1.0.0: {}
|
||||
|
||||
vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1):
|
||||
vite@7.0.6(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.8
|
||||
fdir: 6.4.6(picomatch@4.0.3)
|
||||
@@ -5608,7 +5668,7 @@ snapshots:
|
||||
rollup: 4.46.2
|
||||
tinyglobby: 0.2.14
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
'@types/node': 24.2.1
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.5.1
|
||||
lightningcss: 1.30.1
|
||||
|
||||
Generated
+132
-16
@@ -401,9 +401,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.23.1"
|
||||
version = "1.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
|
||||
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
@@ -429,6 +429,18 @@ dependencies = [
|
||||
"libbz2-rs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cab"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "171228650e6721d5acc0868a462cd864f49ac5f64e4a42cde270406e64e404d2"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"flate2",
|
||||
"lzxd",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.18.5"
|
||||
@@ -456,9 +468,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
|
||||
checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -498,9 +510,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.31"
|
||||
version = "1.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
|
||||
checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
@@ -524,6 +536,17 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfb"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a4f8e55be323b378facfcf1f06aa97f6ec17cf4ac84fb17325093aaf62da41"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"fnv",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-expr"
|
||||
version = "0.15.8"
|
||||
@@ -691,6 +714,21 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -987,20 +1025,25 @@ version = "0.8.2"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"bzip2",
|
||||
"chrono",
|
||||
"core-foundation 0.10.1",
|
||||
"directories",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"lazy_static",
|
||||
"lzma-rs",
|
||||
"msi-extract",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit 0.3.1",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sysinfo",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-deep-link",
|
||||
@@ -1192,6 +1235,18 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.2"
|
||||
@@ -2066,7 +2121,7 @@ version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
|
||||
dependencies = [
|
||||
"cfb",
|
||||
"cfb 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2330,6 +2385,7 @@ checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2369,6 +2425,22 @@ version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzxd"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9"
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@@ -2468,6 +2540,30 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "msi"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a2332f87a064dea9cce571408c879e0da8dc193b3af06a2b3b2604ee4182a32"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cfb 0.10.0",
|
||||
"encoding_rs",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "msi-extract"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32047fe35aac08636833dcae558307dafc7679726b97f56449c3c126ed4be108"
|
||||
dependencies = [
|
||||
"cab",
|
||||
"cfb 0.10.0",
|
||||
"msi",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.17.1"
|
||||
@@ -3704,9 +3800,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -4093,9 +4189,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.10"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
|
||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
@@ -4361,6 +4457,17 @@ dependencies = [
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -4720,12 +4827,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-winres"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c6d9028d41d4de835e3c482c677a8cb88137ac435d6ff9a71f392d4421576c9"
|
||||
checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074"
|
||||
dependencies = [
|
||||
"embed-resource",
|
||||
"indexmap 2.10.0",
|
||||
"toml 0.9.5",
|
||||
]
|
||||
|
||||
@@ -4739,7 +4845,7 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5576,7 +5682,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6212,6 +6318,16 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xdg-home"
|
||||
version = "1.3.0"
|
||||
|
||||
@@ -36,6 +36,12 @@ lazy_static = "1.4"
|
||||
base64 = "0.22"
|
||||
async-trait = "0.1"
|
||||
futures-util = "0.3"
|
||||
zip = "4"
|
||||
tar = "0"
|
||||
bzip2 = "0"
|
||||
flate2 = "1"
|
||||
lzma-rs = "0"
|
||||
msi-extract = "0"
|
||||
|
||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||
url = "2.5"
|
||||
@@ -44,9 +50,6 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
||||
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
zip = "4"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.10"
|
||||
objc2 = "0.6.1"
|
||||
|
||||
@@ -1678,11 +1678,22 @@ mod tests {
|
||||
"name": "Release v1.81.9 (Chromium 137.0.7151.104)",
|
||||
"prerelease": false,
|
||||
"published_at": "2024-01-15T10:00:00Z",
|
||||
"draft": false,
|
||||
"assets": [
|
||||
{
|
||||
"name": "brave-v1.81.9-universal.dmg",
|
||||
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
|
||||
"size": 200000000
|
||||
},
|
||||
{
|
||||
"name": "brave-browser-1.81.9-linux-amd64.zip",
|
||||
"browser_download_url": "https://example.com/brave-1.81.9-linux-amd64.zip",
|
||||
"size": 180000000
|
||||
},
|
||||
{
|
||||
"name": "BraveBrowserStandaloneSetup.exe",
|
||||
"browser_download_url": "https://example.com/brave-1.81.9-setup.exe",
|
||||
"size": 150000000
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
use crate::api_client::is_browser_version_nightly;
|
||||
use crate::browser_version_service::{BrowserVersionInfo, BrowserVersionService};
|
||||
use crate::browser_version_manager::{BrowserVersionInfo, BrowserVersionManager};
|
||||
use crate::profile::BrowserProfile;
|
||||
use crate::settings_manager::SettingsManager;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -29,14 +29,14 @@ pub struct AutoUpdateState {
|
||||
}
|
||||
|
||||
pub struct AutoUpdater {
|
||||
version_service: &'static BrowserVersionService,
|
||||
version_service: &'static BrowserVersionManager,
|
||||
settings_manager: &'static SettingsManager,
|
||||
}
|
||||
|
||||
impl AutoUpdater {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
version_service: BrowserVersionService::instance(),
|
||||
version_service: BrowserVersionManager::instance(),
|
||||
settings_manager: SettingsManager::instance(),
|
||||
}
|
||||
}
|
||||
|
||||
+69
-24
@@ -168,7 +168,7 @@ mod linux {
|
||||
install_dir: &Path,
|
||||
browser_type: &BrowserType,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// Expected structure: install_dir/<browser>/<binary>
|
||||
// Expected structure by default: install_dir/<browser>/<binary>
|
||||
let browser_subdir = install_dir.join(browser_type.as_str());
|
||||
|
||||
// Try firefox first (preferred), then firefox-bin
|
||||
@@ -198,8 +198,8 @@ mod linux {
|
||||
}
|
||||
BrowserType::Camoufox => {
|
||||
vec![
|
||||
browser_subdir.join("camoufox-bin"),
|
||||
browser_subdir.join("camoufox"),
|
||||
install_dir.join("camoufox-bin"),
|
||||
install_dir.join("camoufox"),
|
||||
]
|
||||
}
|
||||
_ => vec![],
|
||||
@@ -213,9 +213,9 @@ mod linux {
|
||||
|
||||
Err(
|
||||
format!(
|
||||
"Firefox executable not found in {}/{}",
|
||||
"Executable not found for {} in {}",
|
||||
browser_type.as_str(),
|
||||
install_dir.display(),
|
||||
browser_type.as_str()
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
@@ -256,10 +256,6 @@ mod linux {
|
||||
// Expected structure: install_dir/<browser>/<binary>
|
||||
let browser_subdir = install_dir.join(browser_type.as_str());
|
||||
|
||||
if !browser_subdir.exists() || !browser_subdir.is_dir() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let possible_executables = match browser_type {
|
||||
BrowserType::Firefox | BrowserType::FirefoxDeveloper => {
|
||||
vec![
|
||||
@@ -286,8 +282,8 @@ mod linux {
|
||||
}
|
||||
BrowserType::Camoufox => {
|
||||
vec![
|
||||
browser_subdir.join("camoufox-bin"),
|
||||
browser_subdir.join("camoufox"),
|
||||
install_dir.join("camoufox-bin"),
|
||||
install_dir.join("camoufox"),
|
||||
]
|
||||
}
|
||||
_ => vec![],
|
||||
@@ -1037,9 +1033,34 @@ mod tests {
|
||||
let browser_dir = binaries_dir.join("firefox").join("139.0");
|
||||
fs::create_dir_all(&browser_dir).unwrap();
|
||||
|
||||
// Create a mock .app directory
|
||||
let app_dir = browser_dir.join("Firefox.app");
|
||||
fs::create_dir_all(&app_dir).unwrap();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Create a mock .app directory for macOS
|
||||
let app_dir = browser_dir.join("Firefox.app");
|
||||
fs::create_dir_all(&app_dir).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Create a mock firefox subdirectory and executable for Linux
|
||||
let firefox_subdir = browser_dir.join("firefox");
|
||||
fs::create_dir_all(&firefox_subdir).unwrap();
|
||||
let executable_path = firefox_subdir.join("firefox");
|
||||
fs::write(&executable_path, "mock executable").unwrap();
|
||||
|
||||
// Set executable permissions on Linux
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut permissions = executable_path.metadata().unwrap().permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(&executable_path, permissions).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Create a mock firefox.exe for Windows
|
||||
let executable_path = browser_dir.join("firefox.exe");
|
||||
fs::write(&executable_path, "mock executable").unwrap();
|
||||
}
|
||||
|
||||
let browser = FirefoxBrowser::new(BrowserType::Firefox);
|
||||
assert!(browser.is_version_downloaded("139.0", binaries_dir));
|
||||
@@ -1048,15 +1069,39 @@ mod tests {
|
||||
// Test with Chromium browser with new path structure
|
||||
let chromium_dir = binaries_dir.join("chromium").join("1465660");
|
||||
fs::create_dir_all(&chromium_dir).unwrap();
|
||||
let chromium_app_dir = chromium_dir.join("Chromium.app");
|
||||
fs::create_dir_all(chromium_app_dir.join("Contents").join("MacOS")).unwrap();
|
||||
|
||||
// Create a mock executable
|
||||
let executable_path = chromium_app_dir
|
||||
.join("Contents")
|
||||
.join("MacOS")
|
||||
.join("Chromium");
|
||||
fs::write(&executable_path, "mock executable").unwrap();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let chromium_app_dir = chromium_dir.join("Chromium.app");
|
||||
fs::create_dir_all(chromium_app_dir.join("Contents").join("MacOS")).unwrap();
|
||||
|
||||
// Create a mock executable
|
||||
let executable_path = chromium_app_dir
|
||||
.join("Contents")
|
||||
.join("MacOS")
|
||||
.join("Chromium");
|
||||
fs::write(&executable_path, "mock executable").unwrap();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Create a mock chromium executable for Linux
|
||||
let executable_path = chromium_dir.join("chromium");
|
||||
fs::write(&executable_path, "mock executable").unwrap();
|
||||
|
||||
// Set executable permissions on Linux
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut permissions = executable_path.metadata().unwrap().permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(&executable_path, permissions).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Create a mock chromium.exe for Windows
|
||||
let executable_path = chromium_dir.join("chromium.exe");
|
||||
fs::write(&executable_path, "mock executable").unwrap();
|
||||
}
|
||||
|
||||
let chromium_browser = ChromiumBrowser::new(BrowserType::Chromium);
|
||||
assert!(chromium_browser.is_version_downloaded("1465660", binaries_dir));
|
||||
@@ -1068,11 +1113,11 @@ mod tests {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let binaries_dir = temp_dir.path();
|
||||
|
||||
// Create browser directory but no .app directory with new path structure
|
||||
// Create browser directory but no proper executable structure
|
||||
let browser_dir = binaries_dir.join("firefox").join("139.0");
|
||||
fs::create_dir_all(&browser_dir).unwrap();
|
||||
|
||||
// Create some other files but no .app
|
||||
// Create some other files but no proper executable structure
|
||||
fs::write(browser_dir.join("readme.txt"), "Some content").unwrap();
|
||||
|
||||
let browser = FirefoxBrowser::new(BrowserType::Firefox);
|
||||
|
||||
+151
-34
@@ -11,8 +11,8 @@ use sysinfo::System;
|
||||
use tauri::Emitter;
|
||||
|
||||
use crate::browser::{create_browser, BrowserType, ProxySettings};
|
||||
use crate::browser_version_service::{
|
||||
BrowserVersionInfo, BrowserVersionService, BrowserVersionsResult,
|
||||
use crate::browser_version_manager::{
|
||||
BrowserVersionInfo, BrowserVersionManager, BrowserVersionsResult,
|
||||
};
|
||||
use crate::camoufox::CamoufoxConfig;
|
||||
use crate::download::DownloadProgress;
|
||||
@@ -1044,6 +1044,27 @@ impl BrowserRunner {
|
||||
Ok(missing_binaries)
|
||||
}
|
||||
|
||||
/// Check if GeoIP database is missing for Camoufox profiles
|
||||
pub fn check_missing_geoip_database(
|
||||
&self,
|
||||
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Get all profiles
|
||||
let profiles = self
|
||||
.list_profiles()
|
||||
.map_err(|e| format!("Failed to list profiles: {e}"))?;
|
||||
|
||||
// Check if there are any Camoufox profiles
|
||||
let has_camoufox_profiles = profiles.iter().any(|profile| profile.browser == "camoufox");
|
||||
|
||||
if has_camoufox_profiles {
|
||||
// Check if GeoIP database is available
|
||||
use crate::geoip_downloader::GeoIPDownloader;
|
||||
return Ok(!GeoIPDownloader::is_geoip_database_available());
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Automatically download missing binaries for all profiles
|
||||
pub async fn ensure_all_binaries_exist(
|
||||
&self,
|
||||
@@ -1082,6 +1103,25 @@ impl BrowserRunner {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if GeoIP database is missing for Camoufox profiles
|
||||
if self.check_missing_geoip_database()? {
|
||||
println!("GeoIP database is missing for Camoufox profiles, downloading...");
|
||||
|
||||
use crate::geoip_downloader::GeoIPDownloader;
|
||||
let geoip_downloader = GeoIPDownloader::instance();
|
||||
|
||||
match geoip_downloader.download_geoip_database(app_handle).await {
|
||||
Ok(_) => {
|
||||
downloaded.push("GeoIP database for Camoufox".to_string());
|
||||
println!("GeoIP database downloaded successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to download GeoIP database: {e}");
|
||||
// Don't fail the entire operation if GeoIP download fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(downloaded)
|
||||
}
|
||||
|
||||
@@ -1129,7 +1169,7 @@ impl BrowserRunner {
|
||||
}
|
||||
|
||||
// Check if browser is supported on current platform before attempting download
|
||||
let version_service = BrowserVersionService::instance();
|
||||
let version_service = BrowserVersionManager::instance();
|
||||
|
||||
if !version_service
|
||||
.is_browser_supported(&browser_str)
|
||||
@@ -1156,11 +1196,6 @@ impl BrowserRunner {
|
||||
browser_dir.push(browser_type.as_str());
|
||||
browser_dir.push(&version);
|
||||
|
||||
// Clean up any failed previous download
|
||||
if let Err(e) = registry.cleanup_failed_download(&browser_str, &version) {
|
||||
println!("Warning: Failed to cleanup previous download: {e}");
|
||||
}
|
||||
|
||||
create_dir_all(&browser_dir).map_err(|e| format!("Failed to create browser directory: {e}"))?;
|
||||
|
||||
// Mark download as started in registry
|
||||
@@ -1171,7 +1206,9 @@ impl BrowserRunner {
|
||||
|
||||
// Use the download module
|
||||
let downloader = crate::download::Downloader::instance();
|
||||
let download_path = match downloader
|
||||
// Attempt to download the archive. If the download fails but an archive with the
|
||||
// expected filename already exists (manual download), continue using that file.
|
||||
let download_path: PathBuf = match downloader
|
||||
.download_browser(
|
||||
&app_handle,
|
||||
browser_type.clone(),
|
||||
@@ -1183,15 +1220,25 @@ impl BrowserRunner {
|
||||
{
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
// Clean up failed download
|
||||
let _ = registry.cleanup_failed_download(&browser_str, &version);
|
||||
let _ = registry.save();
|
||||
// Remove browser-version pair from downloading set on error
|
||||
{
|
||||
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
|
||||
downloading.remove(&download_key);
|
||||
// Check if the expected archive is already present (manual download)
|
||||
let expected_archive_path = browser_dir.join(&download_info.filename);
|
||||
if expected_archive_path.exists() {
|
||||
println!(
|
||||
"Download failed, but found existing archive at {}. Continuing with extraction.",
|
||||
expected_archive_path.display()
|
||||
);
|
||||
expected_archive_path
|
||||
} else {
|
||||
// Remove only the registry entry; keep any files (including a partially downloaded archive)
|
||||
let _ = registry.remove_browser(&browser_str, &version);
|
||||
let _ = registry.save();
|
||||
// Remove browser-version pair from downloading set on error
|
||||
{
|
||||
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
|
||||
downloading.remove(&download_key);
|
||||
}
|
||||
return Err(format!("Failed to download browser: {e}").into());
|
||||
}
|
||||
return Err(format!("Failed to download browser: {e}").into());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1209,14 +1256,12 @@ impl BrowserRunner {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
// Clean up the downloaded archive
|
||||
if let Err(e) = std::fs::remove_file(&download_path) {
|
||||
println!("Warning: Could not delete archive file: {e}");
|
||||
}
|
||||
// Do not remove the archive here. We keep it until verification succeeds.
|
||||
}
|
||||
Err(e) => {
|
||||
// Clean up failed download
|
||||
let _ = registry.cleanup_failed_download(&browser_str, &version);
|
||||
// Do not remove the archive or extracted files. Just drop the registry entry
|
||||
// so it won't be reported as downloaded.
|
||||
let _ = registry.remove_browser(&browser_str, &version);
|
||||
let _ = registry.save();
|
||||
// Remove browser-version pair from downloading set on error
|
||||
{
|
||||
@@ -1250,14 +1295,68 @@ impl BrowserRunner {
|
||||
// Use the browser's own verification method
|
||||
let binaries_dir = self.get_binaries_dir();
|
||||
if !browser.is_version_downloaded(&version, &binaries_dir) {
|
||||
let _ = registry.cleanup_failed_download(&browser_str, &version);
|
||||
// Provide detailed error information for debugging
|
||||
let browser_dir = binaries_dir.join(&browser_str).join(&version);
|
||||
let mut error_details = format!(
|
||||
"Browser download completed but verification failed for {} {}. Expected directory: {}",
|
||||
browser_str,
|
||||
version,
|
||||
browser_dir.display()
|
||||
);
|
||||
|
||||
// List what files actually exist
|
||||
if browser_dir.exists() {
|
||||
error_details.push_str("\nFiles found in directory:");
|
||||
if let Ok(entries) = std::fs::read_dir(&browser_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let file_type = if path.is_dir() { "DIR" } else { "FILE" };
|
||||
error_details.push_str(&format!("\n {} {}", file_type, path.display()));
|
||||
}
|
||||
} else {
|
||||
error_details.push_str("\n (Could not read directory contents)");
|
||||
}
|
||||
} else {
|
||||
error_details.push_str("\nDirectory does not exist!");
|
||||
}
|
||||
|
||||
// For Camoufox on Linux, provide specific expected files
|
||||
if browser_str == "camoufox" && cfg!(target_os = "linux") {
|
||||
let camoufox_subdir = browser_dir.join("camoufox");
|
||||
error_details.push_str("\nExpected Camoufox executable locations:");
|
||||
error_details.push_str(&format!("\n {}/camoufox-bin", camoufox_subdir.display()));
|
||||
error_details.push_str(&format!("\n {}/camoufox", camoufox_subdir.display()));
|
||||
|
||||
if camoufox_subdir.exists() {
|
||||
error_details.push_str(&format!(
|
||||
"\nCamoufox subdirectory exists: {}",
|
||||
camoufox_subdir.display()
|
||||
));
|
||||
if let Ok(entries) = std::fs::read_dir(&camoufox_subdir) {
|
||||
error_details.push_str("\nFiles in camoufox subdirectory:");
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let file_type = if path.is_dir() { "DIR" } else { "FILE" };
|
||||
error_details.push_str(&format!("\n {} {}", file_type, path.display()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error_details.push_str(&format!(
|
||||
"\nCamoufox subdirectory does not exist: {}",
|
||||
camoufox_subdir.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Do not delete files on verification failure; keep archive for manual retry.
|
||||
let _ = registry.remove_browser(&browser_str, &version);
|
||||
let _ = registry.save();
|
||||
// Remove browser-version pair from downloading set on verification failure
|
||||
{
|
||||
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
|
||||
downloading.remove(&download_key);
|
||||
}
|
||||
return Err("Browser download completed but verification failed".into());
|
||||
return Err(error_details.into());
|
||||
}
|
||||
|
||||
registry
|
||||
@@ -1267,6 +1366,16 @@ impl BrowserRunner {
|
||||
.save()
|
||||
.map_err(|e| format!("Failed to save registry: {e}"))?;
|
||||
|
||||
// Now that verification succeeded, remove the archive file if it exists
|
||||
if download_info.is_archive {
|
||||
let archive_path = browser_dir.join(&download_info.filename);
|
||||
if archive_path.exists() {
|
||||
if let Err(e) = std::fs::remove_file(&archive_path) {
|
||||
println!("Warning: Could not delete archive file after verification: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If this is Camoufox, automatically download GeoIP database
|
||||
if browser_str == "camoufox" {
|
||||
use crate::geoip_downloader::GeoIPDownloader;
|
||||
@@ -1529,13 +1638,13 @@ pub fn delete_profile(_app_handle: tauri::AppHandle, profile_name: String) -> Re
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_supported_browsers() -> Result<Vec<String>, String> {
|
||||
let service = BrowserVersionService::instance();
|
||||
let service = BrowserVersionManager::instance();
|
||||
Ok(service.get_supported_browsers())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn is_browser_supported_on_platform(browser_str: String) -> Result<bool, String> {
|
||||
let service = BrowserVersionService::instance();
|
||||
let service = BrowserVersionManager::instance();
|
||||
service
|
||||
.is_browser_supported(&browser_str)
|
||||
.map_err(|e| format!("Failed to check browser support: {e}"))
|
||||
@@ -1545,14 +1654,14 @@ pub fn is_browser_supported_on_platform(browser_str: String) -> Result<bool, Str
|
||||
pub async fn fetch_browser_versions_cached_first(
|
||||
browser_str: String,
|
||||
) -> Result<Vec<BrowserVersionInfo>, String> {
|
||||
let service = BrowserVersionService::instance();
|
||||
let service = BrowserVersionManager::instance();
|
||||
|
||||
// Get cached versions immediately if available
|
||||
if let Some(cached_versions) = service.get_cached_browser_versions_detailed(&browser_str) {
|
||||
// Check if we should update cache in background
|
||||
if service.should_update_cache(&browser_str) {
|
||||
// Start background update but return cached data immediately
|
||||
let service_clone = BrowserVersionService::instance();
|
||||
let service_clone = BrowserVersionManager::instance();
|
||||
let browser_str_clone = browser_str.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = service_clone
|
||||
@@ -1577,14 +1686,14 @@ pub async fn fetch_browser_versions_cached_first(
|
||||
pub async fn fetch_browser_versions_with_count_cached_first(
|
||||
browser_str: String,
|
||||
) -> Result<BrowserVersionsResult, String> {
|
||||
let service = BrowserVersionService::instance();
|
||||
let service = BrowserVersionManager::instance();
|
||||
|
||||
// Get cached versions immediately if available
|
||||
if let Some(cached_versions) = service.get_cached_browser_versions(&browser_str) {
|
||||
// Check if we should update cache in background
|
||||
if service.should_update_cache(&browser_str) {
|
||||
// Start background update but return cached data immediately
|
||||
let service_clone = BrowserVersionService::instance();
|
||||
let service_clone = BrowserVersionManager::instance();
|
||||
let browser_str_clone = browser_str.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = service_clone
|
||||
@@ -1692,7 +1801,7 @@ pub async fn update_camoufox_config(
|
||||
pub async fn fetch_browser_versions_with_count(
|
||||
browser_str: String,
|
||||
) -> Result<BrowserVersionsResult, String> {
|
||||
let service = BrowserVersionService::instance();
|
||||
let service = BrowserVersionManager::instance();
|
||||
service
|
||||
.fetch_browser_versions_with_count(&browser_str, false)
|
||||
.await
|
||||
@@ -1708,8 +1817,8 @@ pub fn get_downloaded_browser_versions(browser_str: String) -> Result<Vec<String
|
||||
#[tauri::command]
|
||||
pub async fn get_browser_release_types(
|
||||
browser_str: String,
|
||||
) -> Result<crate::browser_version_service::BrowserReleaseTypes, String> {
|
||||
let service = BrowserVersionService::instance();
|
||||
) -> Result<crate::browser_version_manager::BrowserReleaseTypes, String> {
|
||||
let service = BrowserVersionManager::instance();
|
||||
service
|
||||
.get_browser_release_types(&browser_str)
|
||||
.await
|
||||
@@ -1725,6 +1834,14 @@ pub async fn check_missing_binaries() -> Result<Vec<(String, String, String)>, S
|
||||
.map_err(|e| format!("Failed to check missing binaries: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_missing_geoip_database() -> Result<bool, String> {
|
||||
let browser_runner = BrowserRunner::instance();
|
||||
browser_runner
|
||||
.check_missing_geoip_database()
|
||||
.map_err(|e| format!("Failed to check missing GeoIP database: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ensure_all_binaries_exist(
|
||||
app_handle: tauri::AppHandle,
|
||||
|
||||
+167
-28
@@ -30,18 +30,18 @@ pub struct DownloadInfo {
|
||||
pub is_archive: bool, // true for .dmg, .zip, etc.
|
||||
}
|
||||
|
||||
pub struct BrowserVersionService {
|
||||
pub struct BrowserVersionManager {
|
||||
api_client: &'static ApiClient,
|
||||
}
|
||||
|
||||
impl BrowserVersionService {
|
||||
impl BrowserVersionManager {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
api_client: ApiClient::instance(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instance() -> &'static BrowserVersionService {
|
||||
pub fn instance() -> &'static BrowserVersionManager {
|
||||
&BROWSER_VERSION_SERVICE
|
||||
}
|
||||
|
||||
@@ -982,13 +982,13 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn create_test_service(_api_client: ApiClient) -> &'static BrowserVersionService {
|
||||
BrowserVersionService::instance()
|
||||
fn create_test_service(_api_client: ApiClient) -> &'static BrowserVersionManager {
|
||||
BrowserVersionManager::instance()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_browser_version_service_creation() {
|
||||
let _ = BrowserVersionService::instance();
|
||||
async fn test_browser_version_manager_creation() {
|
||||
let _ = BrowserVersionManager::instance();
|
||||
// Test passes if we can create the service without panicking
|
||||
}
|
||||
|
||||
@@ -1014,61 +1014,200 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_download_info() {
|
||||
let service = BrowserVersionService::instance();
|
||||
let service = BrowserVersionManager::instance();
|
||||
|
||||
// Test Firefox
|
||||
// Test Firefox - platform-specific expectations
|
||||
let firefox_info = service.get_download_info("firefox", "139.0").unwrap();
|
||||
assert_eq!(firefox_info.filename, "Firefox 139.0.dmg");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
assert_eq!(firefox_info.filename, "Firefox 139.0.dmg");
|
||||
assert!(firefox_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert_eq!(firefox_info.filename, "firefox-139.0.tar.xz");
|
||||
assert!(firefox_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
assert_eq!(firefox_info.filename, "Firefox Setup 139.0.exe");
|
||||
assert!(!firefox_info.is_archive);
|
||||
}
|
||||
|
||||
assert!(firefox_info
|
||||
.url
|
||||
.contains("download-installer.cdn.mozilla.net"));
|
||||
assert!(firefox_info.url.contains("/pub/firefox/releases/139.0/"));
|
||||
assert!(firefox_info.is_archive);
|
||||
|
||||
// Test Firefox Developer
|
||||
let firefox_dev_info = service
|
||||
.get_download_info("firefox-developer", "139.0b1")
|
||||
.unwrap();
|
||||
assert_eq!(firefox_dev_info.filename, "Firefox 139.0b1.dmg");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
assert_eq!(firefox_dev_info.filename, "Firefox 139.0b1.dmg");
|
||||
assert!(firefox_dev_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert_eq!(firefox_dev_info.filename, "firefox-139.0b1.tar.xz");
|
||||
assert!(firefox_dev_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
assert_eq!(firefox_dev_info.filename, "Firefox Setup 139.0b1.exe");
|
||||
assert!(!firefox_dev_info.is_archive);
|
||||
}
|
||||
|
||||
assert!(firefox_dev_info
|
||||
.url
|
||||
.contains("download-installer.cdn.mozilla.net"));
|
||||
assert!(firefox_dev_info
|
||||
.url
|
||||
.contains("/pub/devedition/releases/139.0b1/"));
|
||||
assert!(firefox_dev_info.is_archive);
|
||||
|
||||
// Test Mullvad Browser
|
||||
let mullvad_info = service
|
||||
.get_download_info("mullvad-browser", "14.5a6")
|
||||
.unwrap();
|
||||
assert_eq!(mullvad_info.filename, "mullvad-browser-macos-14.5a6.dmg");
|
||||
assert!(mullvad_info.url.contains("mullvad-browser-macos-14.5a6"));
|
||||
assert!(mullvad_info.is_archive);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
assert_eq!(mullvad_info.filename, "mullvad-browser-macos-14.5a6.dmg");
|
||||
assert!(mullvad_info.url.contains("mullvad-browser-macos-14.5a6"));
|
||||
assert!(mullvad_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert_eq!(
|
||||
mullvad_info.filename,
|
||||
"mullvad-browser-x86_64-14.5a6.tar.xz"
|
||||
);
|
||||
assert!(mullvad_info.url.contains("mullvad-browser-x86_64-14.5a6"));
|
||||
assert!(mullvad_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
assert_eq!(
|
||||
mullvad_info.filename,
|
||||
"mullvad-browser-windows-x86_64-14.5a6.exe"
|
||||
);
|
||||
assert!(mullvad_info
|
||||
.url
|
||||
.contains("mullvad-browser-windows-x86_64-14.5a6"));
|
||||
assert!(!mullvad_info.is_archive);
|
||||
}
|
||||
|
||||
// Test Zen Browser
|
||||
let zen_info = service.get_download_info("zen", "1.11b").unwrap();
|
||||
assert_eq!(zen_info.filename, "zen-1.11b.dmg");
|
||||
assert!(zen_info.url.contains("zen.macos-universal.dmg"));
|
||||
assert!(zen_info.is_archive);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
assert_eq!(zen_info.filename, "zen-1.11b.dmg");
|
||||
assert!(zen_info.url.contains("zen.macos-universal.dmg"));
|
||||
assert!(zen_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert_eq!(zen_info.filename, "zen-1.11b-x86_64.tar.xz");
|
||||
assert!(zen_info.url.contains("zen.linux-x86_64.tar.xz"));
|
||||
assert!(zen_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
assert_eq!(zen_info.filename, "zen-1.11b.exe");
|
||||
assert!(zen_info.url.contains("zen.installer.exe"));
|
||||
assert!(!zen_info.is_archive);
|
||||
}
|
||||
|
||||
// Test Tor Browser
|
||||
let tor_info = service.get_download_info("tor-browser", "14.0.4").unwrap();
|
||||
assert_eq!(tor_info.filename, "tor-browser-macos-14.0.4.dmg");
|
||||
assert!(tor_info.url.contains("tor-browser-macos-14.0.4"));
|
||||
assert!(tor_info.is_archive);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
assert_eq!(tor_info.filename, "tor-browser-macos-14.0.4.dmg");
|
||||
assert!(tor_info.url.contains("tor-browser-macos-14.0.4"));
|
||||
assert!(tor_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert_eq!(tor_info.filename, "tor-browser-linux-x86_64-14.0.4.tar.xz");
|
||||
assert!(tor_info.url.contains("tor-browser-linux-x86_64-14.0.4"));
|
||||
assert!(tor_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
assert_eq!(
|
||||
tor_info.filename,
|
||||
"tor-browser-windows-x86_64-portable-14.0.4.exe"
|
||||
);
|
||||
assert!(tor_info
|
||||
.url
|
||||
.contains("tor-browser-windows-x86_64-portable-14.0.4"));
|
||||
assert!(!tor_info.is_archive);
|
||||
}
|
||||
|
||||
// Test Chromium
|
||||
let chromium_info = service.get_download_info("chromium", "1465660").unwrap();
|
||||
assert_eq!(chromium_info.filename, "chromium-1465660-mac.zip");
|
||||
assert!(chromium_info.url.contains("chrome-mac.zip"));
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
assert_eq!(chromium_info.filename, "chromium-1465660-mac.zip");
|
||||
assert!(chromium_info.url.contains("chrome-mac.zip"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert_eq!(chromium_info.filename, "chromium-1465660-linux.zip");
|
||||
assert!(chromium_info.url.contains("chrome-linux.zip"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
assert_eq!(chromium_info.filename, "chromium-1465660-win.zip");
|
||||
assert!(chromium_info.url.contains("chrome-win.zip"));
|
||||
}
|
||||
|
||||
assert!(chromium_info.is_archive);
|
||||
|
||||
// Test Brave - Note: Brave uses dynamic URL resolution, so get_download_info provides a template URL
|
||||
let brave_info = service.get_download_info("brave", "v1.81.9").unwrap();
|
||||
assert_eq!(brave_info.filename, "Brave-Browser-universal.dmg");
|
||||
assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/Brave-Browser-universal.dmg");
|
||||
assert!(brave_info.is_archive);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
assert_eq!(brave_info.filename, "Brave-Browser-universal.dmg");
|
||||
assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/Brave-Browser-universal.dmg");
|
||||
assert!(brave_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert_eq!(brave_info.filename, "brave-browser-v1.81.9-linux-amd64.zip");
|
||||
assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/brave-browser-v1.81.9-linux-amd64.zip");
|
||||
assert!(brave_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
assert_eq!(brave_info.filename, "brave-v1.81.9.exe");
|
||||
assert_eq!(
|
||||
brave_info.url,
|
||||
"https://github.com/brave/brave-browser/releases/download/v1.81.9/brave-v1.81.9.exe"
|
||||
);
|
||||
assert!(!brave_info.is_archive);
|
||||
}
|
||||
|
||||
// Test unsupported browser
|
||||
let unsupported_result = service.get_download_info("unsupported", "1.0.0");
|
||||
@@ -1080,5 +1219,5 @@ mod tests {
|
||||
|
||||
// Global singleton instance
|
||||
lazy_static::lazy_static! {
|
||||
static ref BROWSER_VERSION_SERVICE: BrowserVersionService = BrowserVersionService::new();
|
||||
static ref BROWSER_VERSION_SERVICE: BrowserVersionManager = BrowserVersionManager::new();
|
||||
}
|
||||
+65
-13
@@ -1,13 +1,12 @@
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::Emitter;
|
||||
|
||||
use crate::api_client::ApiClient;
|
||||
use crate::browser::BrowserType;
|
||||
use crate::browser_version_service::DownloadInfo;
|
||||
use crate::browser_version_manager::DownloadInfo;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DownloadProgress {
|
||||
@@ -415,25 +414,74 @@ impl Downloader {
|
||||
|
||||
let _ = app_handle.emit("download-progress", &progress);
|
||||
|
||||
// Start download
|
||||
let response = self
|
||||
// Determine if we have a partial file to resume
|
||||
let mut existing_size: u64 = 0;
|
||||
if let Ok(meta) = std::fs::metadata(&file_path) {
|
||||
existing_size = meta.len();
|
||||
}
|
||||
|
||||
// Build request, add Range only if we have bytes
|
||||
let mut request = self
|
||||
.client
|
||||
.get(&download_url)
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
.send()
|
||||
.await?;
|
||||
.header(
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
||||
);
|
||||
|
||||
if existing_size > 0 {
|
||||
request = request.header("Range", format!("bytes={existing_size}-"));
|
||||
}
|
||||
|
||||
// Start download (or resume)
|
||||
let response = request.send().await?;
|
||||
|
||||
// Check if the response is successful
|
||||
if !response.status().is_success() {
|
||||
if !(response.status().is_success() || response.status().as_u16() == 206) {
|
||||
return Err(format!("Download failed with status: {}", response.status()).into());
|
||||
}
|
||||
|
||||
let total_size = response.content_length();
|
||||
let mut downloaded = 0u64;
|
||||
// Determine total size
|
||||
let mut total_size = response.content_length();
|
||||
|
||||
// If resuming (206) and Content-Range is present, parse total
|
||||
if response.status().as_u16() == 206 {
|
||||
if let Some(content_range) = response.headers().get(reqwest::header::CONTENT_RANGE) {
|
||||
if let Ok(cr) = content_range.to_str() {
|
||||
// Format: bytes start-end/total
|
||||
if let Some((_, total_str)) = cr.split('/').collect::<Vec<_>>().split_first() {
|
||||
if let Some(total_str) = total_str.first() {
|
||||
if let Ok(total) = total_str.parse::<u64>() {
|
||||
total_size = Some(total);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(len) = response.headers().get(reqwest::header::CONTENT_LENGTH) {
|
||||
// Fallback: total = existing + incoming length
|
||||
if let Ok(len_str) = len.to_str() {
|
||||
if let Ok(incoming) = len_str.parse::<u64>() {
|
||||
total_size = Some(existing_size + incoming);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if existing_size > 0 && response.status().is_success() {
|
||||
// Server ignored range or we asked from 0; if 200 and existing file has content, start fresh
|
||||
// Truncate existing file so we don't append duplicate bytes
|
||||
let _ = std::fs::remove_file(&file_path);
|
||||
existing_size = 0;
|
||||
}
|
||||
|
||||
let mut downloaded = existing_size;
|
||||
let start_time = std::time::Instant::now();
|
||||
let mut last_update = start_time;
|
||||
|
||||
let mut file = File::create(&file_path)?;
|
||||
// Open file in append mode (resuming) or create new
|
||||
use std::fs::OpenOptions;
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&file_path)?;
|
||||
let mut stream = response.bytes_stream();
|
||||
|
||||
use futures_util::StreamExt;
|
||||
@@ -452,7 +500,11 @@ impl Downloader {
|
||||
0.0
|
||||
};
|
||||
let percentage = if let Some(total) = total_size {
|
||||
(downloaded as f64 / total as f64) * 100.0
|
||||
if total > 0 {
|
||||
(downloaded as f64 / total as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
@@ -493,7 +545,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::api_client::ApiClient;
|
||||
use crate::browser::BrowserType;
|
||||
use crate::browser_version_service::DownloadInfo;
|
||||
use crate::browser_version_manager::DownloadInfo;
|
||||
|
||||
use tempfile::TempDir;
|
||||
use wiremock::matchers::{method, path};
|
||||
|
||||
+646
-727
File diff suppressed because it is too large
Load Diff
+57
-13
@@ -12,7 +12,7 @@ mod app_auto_updater;
|
||||
mod auto_updater;
|
||||
mod browser;
|
||||
mod browser_runner;
|
||||
mod browser_version_service;
|
||||
mod browser_version_manager;
|
||||
mod camoufox;
|
||||
mod default_browser;
|
||||
mod download;
|
||||
@@ -31,13 +31,13 @@ mod version_updater;
|
||||
extern crate lazy_static;
|
||||
|
||||
use browser_runner::{
|
||||
check_browser_exists, check_browser_status, check_missing_binaries, create_browser_profile_new,
|
||||
delete_profile, download_browser, ensure_all_binaries_exist, fetch_browser_versions_cached_first,
|
||||
fetch_browser_versions_with_count, fetch_browser_versions_with_count_cached_first,
|
||||
get_browser_release_types, get_downloaded_browser_versions, get_supported_browsers,
|
||||
is_browser_supported_on_platform, kill_browser_profile, launch_browser_profile,
|
||||
list_browser_profiles, rename_profile, update_camoufox_config, update_profile_proxy,
|
||||
update_profile_version,
|
||||
check_browser_exists, check_browser_status, check_missing_binaries, check_missing_geoip_database,
|
||||
create_browser_profile_new, delete_profile, download_browser, ensure_all_binaries_exist,
|
||||
fetch_browser_versions_cached_first, fetch_browser_versions_with_count,
|
||||
fetch_browser_versions_with_count_cached_first, get_browser_release_types,
|
||||
get_downloaded_browser_versions, get_supported_browsers, is_browser_supported_on_platform,
|
||||
kill_browser_profile, launch_browser_profile, list_browser_profiles, rename_profile,
|
||||
update_camoufox_config, update_profile_proxy, update_profile_version,
|
||||
};
|
||||
|
||||
use settings_manager::{
|
||||
@@ -69,6 +69,8 @@ use group_manager::{
|
||||
get_groups_with_profile_counts, get_profile_groups, update_profile_group,
|
||||
};
|
||||
|
||||
use geoip_downloader::GeoIPDownloader;
|
||||
|
||||
// Trait to extend WebviewWindow with transparent titlebar functionality
|
||||
pub trait WindowExt {
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -173,6 +175,20 @@ async fn delete_stored_proxy(proxy_id: String) -> Result<(), String> {
|
||||
.map_err(|e| format!("Failed to delete stored proxy: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn is_geoip_database_available() -> Result<bool, String> {
|
||||
Ok(GeoIPDownloader::is_geoip_database_available())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn download_geoip_database(app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
let downloader = GeoIPDownloader::instance();
|
||||
downloader
|
||||
.download_geoip_database(&app_handle)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to download GeoIP database: {e}"))
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
@@ -386,6 +402,35 @@ pub fn run() {
|
||||
}
|
||||
});
|
||||
|
||||
// Check and download GeoIP database at startup if needed
|
||||
let app_handle_geoip = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// Wait a bit for the app to fully initialize
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
let browser_runner = crate::browser_runner::BrowserRunner::instance();
|
||||
match browser_runner.check_missing_geoip_database() {
|
||||
Ok(true) => {
|
||||
println!("GeoIP database is missing for Camoufox profiles, downloading at startup...");
|
||||
let geoip_downloader = GeoIPDownloader::instance();
|
||||
if let Err(e) = geoip_downloader
|
||||
.download_geoip_database(&app_handle_geoip)
|
||||
.await
|
||||
{
|
||||
eprintln!("Failed to download GeoIP database at startup: {e}");
|
||||
} else {
|
||||
println!("GeoIP database downloaded successfully at startup");
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
// No Camoufox profiles or GeoIP database already available
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to check GeoIP database status at startup: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start proxy cleanup task for dead browser processes
|
||||
let app_handle_proxy_cleanup = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
@@ -419,11 +464,7 @@ pub fn run() {
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
// Send a ping request to nodecar to trigger unpacking/warm-up
|
||||
match tokio::process::Command::new("nodecar")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
match tokio::process::Command::new("nodecar").output().await {
|
||||
Ok(output) => {
|
||||
let duration = start_time.elapsed();
|
||||
if output.status.success() {
|
||||
@@ -491,6 +532,7 @@ pub fn run() {
|
||||
detect_existing_profiles,
|
||||
import_browser_profile,
|
||||
check_missing_binaries,
|
||||
check_missing_geoip_database,
|
||||
ensure_all_binaries_exist,
|
||||
create_stored_proxy,
|
||||
get_stored_proxies,
|
||||
@@ -504,6 +546,8 @@ pub fn run() {
|
||||
delete_profile_group,
|
||||
assign_profiles_to_group,
|
||||
delete_selected_profiles,
|
||||
is_geoip_database_available,
|
||||
download_geoip_database,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -603,6 +603,11 @@ impl ProfileManager {
|
||||
})?;
|
||||
}
|
||||
|
||||
// Emit profile update event so frontend UIs can refresh immediately (e.g. proxy manager)
|
||||
if let Err(e) = app_handle.emit("profile-updated", &profile) {
|
||||
println!("Warning: Failed to emit profile update event: {e}");
|
||||
}
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ pub async fn clear_all_version_cache_and_refetch(
|
||||
// Disable all browsers during the update process
|
||||
let auto_updater = crate::auto_updater::AutoUpdater::instance();
|
||||
let supported_browsers =
|
||||
crate::browser_version_service::BrowserVersionService::instance().get_supported_browsers();
|
||||
crate::browser_version_manager::BrowserVersionManager::instance().get_supported_browsers();
|
||||
|
||||
// Load current state and disable all browsers
|
||||
let mut state = auto_updater
|
||||
|
||||
@@ -10,7 +10,7 @@ use tokio::sync::Mutex;
|
||||
use tokio::time::interval;
|
||||
|
||||
use crate::auto_updater::AutoUpdater;
|
||||
use crate::browser_version_service::BrowserVersionService;
|
||||
use crate::browser_version_manager::BrowserVersionManager;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct VersionUpdateProgress {
|
||||
@@ -47,7 +47,7 @@ impl Default for BackgroundUpdateState {
|
||||
}
|
||||
|
||||
pub struct VersionUpdater {
|
||||
version_service: &'static BrowserVersionService,
|
||||
version_service: &'static BrowserVersionManager,
|
||||
auto_updater: &'static AutoUpdater,
|
||||
app_handle: Option<tauri::AppHandle>,
|
||||
}
|
||||
@@ -55,7 +55,7 @@ pub struct VersionUpdater {
|
||||
impl VersionUpdater {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
version_service: BrowserVersionService::instance(),
|
||||
version_service: BrowserVersionManager::instance(),
|
||||
auto_updater: AutoUpdater::instance(),
|
||||
app_handle: None,
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Hello, World!
|
||||
Binary file not shown.
+68
-50
@@ -19,7 +19,6 @@ import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
|
||||
import { ProxyManagementDialog } from "@/components/proxy-management-dialog";
|
||||
import { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
|
||||
import { SettingsDialog } from "@/components/settings-dialog";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
||||
import type { PermissionType } from "@/hooks/use-permissions";
|
||||
import { usePermissions } from "@/hooks/use-permissions";
|
||||
@@ -94,8 +93,18 @@ export default function Home() {
|
||||
"check_missing_binaries",
|
||||
);
|
||||
|
||||
if (missingBinaries.length > 0) {
|
||||
console.log("Found missing binaries:", missingBinaries);
|
||||
// Also check for missing GeoIP database
|
||||
const missingGeoIP = await invoke<boolean>(
|
||||
"check_missing_geoip_database",
|
||||
);
|
||||
|
||||
if (missingBinaries.length > 0 || missingGeoIP) {
|
||||
if (missingBinaries.length > 0) {
|
||||
console.log("Found missing binaries:", missingBinaries);
|
||||
}
|
||||
if (missingGeoIP) {
|
||||
console.log("Found missing GeoIP database for Camoufox");
|
||||
}
|
||||
|
||||
// Group missing binaries by browser type to avoid concurrent downloads
|
||||
const browserMap = new Map<string, string[]>();
|
||||
@@ -110,34 +119,45 @@ export default function Home() {
|
||||
}
|
||||
|
||||
// Show a toast notification about missing binaries and auto-download them
|
||||
const missingList = Array.from(browserMap.entries())
|
||||
let missingList = Array.from(browserMap.entries())
|
||||
.map(([browser, versions]) => `${browser}: ${versions.join(", ")}`)
|
||||
.join(", ");
|
||||
|
||||
console.log(`Downloading missing binaries: ${missingList}`);
|
||||
if (missingGeoIP) {
|
||||
if (missingList) {
|
||||
missingList += ", GeoIP database for Camoufox";
|
||||
} else {
|
||||
missingList = "GeoIP database for Camoufox";
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Downloading missing components: ${missingList}`);
|
||||
|
||||
try {
|
||||
// Download missing binaries sequentially by browser type to prevent conflicts
|
||||
// Download missing binaries and GeoIP database sequentially to prevent conflicts
|
||||
const downloaded = await invoke<string[]>(
|
||||
"ensure_all_binaries_exist",
|
||||
);
|
||||
if (downloaded.length > 0) {
|
||||
console.log(
|
||||
"Successfully downloaded missing binaries:",
|
||||
"Successfully downloaded missing components:",
|
||||
downloaded,
|
||||
);
|
||||
}
|
||||
} catch (downloadError) {
|
||||
console.error("Failed to download missing binaries:", downloadError);
|
||||
console.error(
|
||||
"Failed to download missing components:",
|
||||
downloadError,
|
||||
);
|
||||
setError(
|
||||
`Failed to download missing binaries: ${JSON.stringify(
|
||||
`Failed to download missing components: ${JSON.stringify(
|
||||
downloadError,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error("Failed to check missing binaries:", err);
|
||||
console.error("Failed to check missing components:", err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -727,46 +747,44 @@ export default function Home() {
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen gap-8 font-[family-name:var(--font-geist-sans)] bg-white dark:bg-black">
|
||||
<main className="flex flex-col row-start-2 gap-8 items-center w-full max-w-3xl">
|
||||
<Card className="gap-2 w-full">
|
||||
<CardHeader>
|
||||
<HomeHeader
|
||||
selectedProfiles={selectedProfiles}
|
||||
onBulkDelete={handleBulkDelete}
|
||||
onBulkGroupAssignment={handleBulkGroupAssignment}
|
||||
onCreateProfileDialogOpen={setCreateProfileDialogOpen}
|
||||
onGroupManagementDialogOpen={setGroupManagementDialogOpen}
|
||||
onImportProfileDialogOpen={setImportProfileDialogOpen}
|
||||
onProxyManagementDialogOpen={setProxyManagementDialogOpen}
|
||||
onSettingsDialogOpen={setSettingsDialogOpen}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<GroupBadges
|
||||
selectedGroupId={selectedGroupId}
|
||||
onGroupSelect={handleSelectGroup}
|
||||
groups={groups}
|
||||
isLoading={areGroupsLoading}
|
||||
/>
|
||||
<ProfilesDataTable
|
||||
data={profiles}
|
||||
onLaunchProfile={launchProfile}
|
||||
onKillProfile={handleKillProfile}
|
||||
onProxySettings={openProxyDialog}
|
||||
onDeleteProfile={handleDeleteProfile}
|
||||
onRenameProfile={handleRenameProfile}
|
||||
onChangeVersion={openChangeVersionDialog}
|
||||
onConfigureCamoufox={handleConfigureCamoufox}
|
||||
runningProfiles={runningProfiles}
|
||||
isUpdating={isUpdating}
|
||||
onDeleteSelectedProfiles={handleDeleteSelectedProfiles}
|
||||
onAssignProfilesToGroup={handleAssignProfilesToGroup}
|
||||
selectedGroupId={selectedGroupId}
|
||||
selectedProfiles={selectedProfiles}
|
||||
onSelectedProfilesChange={setSelectedProfiles}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<main className="flex flex-col row-start-2 gap-6 items-center w-full max-w-3xl">
|
||||
<div className="w-full">
|
||||
<HomeHeader
|
||||
selectedProfiles={selectedProfiles}
|
||||
onBulkDelete={handleBulkDelete}
|
||||
onBulkGroupAssignment={handleBulkGroupAssignment}
|
||||
onCreateProfileDialogOpen={setCreateProfileDialogOpen}
|
||||
onGroupManagementDialogOpen={setGroupManagementDialogOpen}
|
||||
onImportProfileDialogOpen={setImportProfileDialogOpen}
|
||||
onProxyManagementDialogOpen={setProxyManagementDialogOpen}
|
||||
onSettingsDialogOpen={setSettingsDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4 w-full">
|
||||
<GroupBadges
|
||||
selectedGroupId={selectedGroupId}
|
||||
onGroupSelect={handleSelectGroup}
|
||||
groups={groups}
|
||||
isLoading={areGroupsLoading}
|
||||
/>
|
||||
<ProfilesDataTable
|
||||
data={profiles}
|
||||
onLaunchProfile={launchProfile}
|
||||
onKillProfile={handleKillProfile}
|
||||
onProxySettings={openProxyDialog}
|
||||
onDeleteProfile={handleDeleteProfile}
|
||||
onRenameProfile={handleRenameProfile}
|
||||
onChangeVersion={openChangeVersionDialog}
|
||||
onConfigureCamoufox={handleConfigureCamoufox}
|
||||
runningProfiles={runningProfiles}
|
||||
isUpdating={isUpdating}
|
||||
onDeleteSelectedProfiles={handleDeleteSelectedProfiles}
|
||||
onAssignProfilesToGroup={handleAssignProfilesToGroup}
|
||||
selectedGroupId={selectedGroupId}
|
||||
selectedProfiles={selectedProfiles}
|
||||
onSelectedProfilesChange={setSelectedProfiles}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<ProxySettingsDialog
|
||||
|
||||
@@ -5,6 +5,7 @@ import { LuCheckCheck, LuCog, LuRefreshCw } from "react-icons/lu";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { AppUpdateInfo, AppUpdateProgress } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface AppUpdateToastProps {
|
||||
updateInfo: AppUpdateInfo;
|
||||
@@ -78,7 +79,7 @@ export function AppUpdateToast({
|
||||
updateProgress.stage === "completed");
|
||||
|
||||
return (
|
||||
<div className="flex items-start p-4 w-full max-w-md bg-white rounded-lg border border-gray-200 shadow-lg dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="flex items-start p-4 w-full max-w-md bg-card rounded-lg border border-border shadow-lg text-card-foreground">
|
||||
<div className="mr-3 mt-0.5">
|
||||
{getStageIcon(updateProgress?.stage, isUpdating)}
|
||||
</div>
|
||||
@@ -133,9 +134,9 @@ export function AppUpdateToast({
|
||||
{updateProgress.eta && ` • ${updateProgress.eta} remaining`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
||||
<div className="w-full bg-muted rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
|
||||
className="bg-primary h-1.5 rounded-full transition-all duration-300"
|
||||
style={{ width: `${updateProgress.percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -146,12 +147,12 @@ export function AppUpdateToast({
|
||||
{showOtherStageProgress && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{/* Progress indicator for non-downloading stages */}
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
||||
<div className="w-full bg-muted rounded-full h-1.5">
|
||||
<div
|
||||
className={`h-1.5 rounded-full transition-all duration-500 ${
|
||||
updateProgress.stage === "completed"
|
||||
? "bg-green-500 w-full"
|
||||
: "bg-blue-500 w-full animate-pulse"
|
||||
: "bg-primary w-full animate-pulse"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
@@ -160,22 +161,22 @@ export function AppUpdateToast({
|
||||
|
||||
{!isUpdating && (
|
||||
<div className="flex gap-2 items-center mt-3">
|
||||
<Button
|
||||
<RippleButton
|
||||
onClick={() => void handleUpdateClick()}
|
||||
size="sm"
|
||||
className="flex gap-2 items-center text-xs"
|
||||
>
|
||||
<FaDownload className="w-3 h-3" />
|
||||
Update Now
|
||||
</Button>
|
||||
<Button
|
||||
</RippleButton>
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
onClick={onDismiss}
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
>
|
||||
Later
|
||||
</Button>
|
||||
</RippleButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -12,6 +11,8 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import type { BrowserProfile, CamoufoxConfig } from "@/types";
|
||||
import { LoadingButton } from "./loading-button";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface CamoufoxConfigDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -106,7 +107,7 @@ export function CamoufoxConfigDialog({
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="flex-1 pr-6 h-[400px]">
|
||||
<ScrollArea className="flex-1 h-[400px]">
|
||||
<div className="py-4">
|
||||
<SharedCamoufoxConfigForm
|
||||
config={config}
|
||||
@@ -117,12 +118,16 @@ export function CamoufoxConfigDialog({
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter className="flex-shrink-0 pt-4 border-t">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
<RippleButton variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? "Saving..." : "Save Configuration"}
|
||||
</Button>
|
||||
</RippleButton>
|
||||
<LoadingButton
|
||||
isLoading={isSaving}
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { LuTriangleAlert } from "react-icons/lu";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { ReleaseTypeSelector } from "@/components/release-type-selector";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -19,6 +18,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { useBrowserDownload } from "@/hooks/use-browser-download";
|
||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||
import type { BrowserProfile, BrowserReleaseTypes } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface ChangeVersionDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -288,9 +288,9 @@ export function ChangeVersionDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<RippleButton variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</RippleButton>
|
||||
<LoadingButton
|
||||
isLoading={isUpdating}
|
||||
onClick={() => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import type { ProfileGroup } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface CreateGroupDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -98,15 +98,19 @@ export function CreateGroupDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose} disabled={isCreating}>
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isCreating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</RippleButton>
|
||||
<LoadingButton
|
||||
isLoading={isCreating}
|
||||
onClick={() => void handleCreate()}
|
||||
disabled={!groupName.trim()}
|
||||
>
|
||||
Create Group
|
||||
Create
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { GoPlus } from "react-icons/go";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
|
||||
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Combobox } from "@/components/ui/combobox";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -27,6 +28,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useBrowserDownload } from "@/hooks/use-browser-download";
|
||||
import { getBrowserIcon } from "@/lib/browser-utils";
|
||||
import type { BrowserReleaseTypes, CamoufoxConfig, StoredProxy } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
type BrowserTypeString =
|
||||
| "mullvad-browser"
|
||||
@@ -104,7 +106,7 @@ export function CreateProfileDialog({
|
||||
selectedGroupId,
|
||||
}: CreateProfileDialogProps) {
|
||||
const [profileName, setProfileName] = useState("");
|
||||
const [activeTab, setActiveTab] = useState("regular");
|
||||
const [activeTab, setActiveTab] = useState("anti-detect");
|
||||
|
||||
// Regular browser states
|
||||
const [selectedBrowser, setSelectedBrowser] = useState<BrowserTypeString>();
|
||||
@@ -132,6 +134,7 @@ export function CreateProfileDialog({
|
||||
useState<BrowserReleaseTypes>({});
|
||||
const [supportedBrowsers, setSupportedBrowsers] = useState<string[]>([]);
|
||||
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
|
||||
const [showProxyForm, setShowProxyForm] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const loadingBrowserRef = useRef<string | null>(null);
|
||||
|
||||
@@ -161,6 +164,20 @@ export function CreateProfileDialog({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkAndDownloadGeoIPDatabase = useCallback(async () => {
|
||||
try {
|
||||
const isAvailable = await invoke<boolean>("is_geoip_database_available");
|
||||
if (!isAvailable) {
|
||||
console.log("GeoIP database not available, downloading...");
|
||||
await invoke("download_geoip_database");
|
||||
console.log("GeoIP database downloaded successfully");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check/download GeoIP database:", error);
|
||||
// Don't show error to user as this is not critical for profile creation
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadReleaseTypes = useCallback(
|
||||
async (browser: string) => {
|
||||
// Set loading state
|
||||
@@ -202,8 +219,16 @@ export function CreateProfileDialog({
|
||||
void loadStoredProxies();
|
||||
// Load camoufox release types when dialog opens
|
||||
void loadReleaseTypes("camoufox");
|
||||
// Check and download GeoIP database if needed for Camoufox
|
||||
void checkAndDownloadGeoIPDatabase();
|
||||
}
|
||||
}, [isOpen, loadSupportedBrowsers, loadStoredProxies, loadReleaseTypes]);
|
||||
}, [
|
||||
isOpen,
|
||||
loadSupportedBrowsers,
|
||||
loadStoredProxies,
|
||||
loadReleaseTypes,
|
||||
checkAndDownloadGeoIPDatabase,
|
||||
]);
|
||||
|
||||
// Load release types when browser selection changes
|
||||
useEffect(() => {
|
||||
@@ -334,7 +359,7 @@ export function CreateProfileDialog({
|
||||
setCamoufoxConfig({
|
||||
geoip: true, // Reset to automatic geoip
|
||||
});
|
||||
setActiveTab("regular");
|
||||
setActiveTab("anti-detect");
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -375,7 +400,7 @@ export function CreateProfileDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
|
||||
<DialogContent className="w-full max-h-[90vh] flex flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle>Create New Profile</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -385,214 +410,244 @@ export function CreateProfileDialog({
|
||||
onValueChange={handleTabChange}
|
||||
className="flex flex-col flex-1 w-full min-h-0"
|
||||
>
|
||||
<TabsList className="grid flex-shrink-0 grid-cols-2 w-full">
|
||||
<TabsTrigger value="regular">Regular Browsers</TabsTrigger>
|
||||
<TabsList
|
||||
className="grid flex-shrink-0 grid-cols-2 w-full"
|
||||
defaultValue="anti-detect"
|
||||
>
|
||||
<TabsTrigger value="anti-detect">Anti-Detect</TabsTrigger>
|
||||
<TabsTrigger value="regular">Regular</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<ScrollArea className="flex-1 pr-6 h-[320px]">
|
||||
<div className="py-4 space-y-6">
|
||||
{/* Profile Name - Common to both tabs */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="profile-name">Profile Name</Label>
|
||||
<Input
|
||||
id="profile-name"
|
||||
value={profileName}
|
||||
onChange={(e) => setProfileName(e.target.value)}
|
||||
placeholder="Enter profile name"
|
||||
/>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 h-[330px] overflow-y-hidden">
|
||||
<div className="flex flex-col justify-center items-center w-full">
|
||||
<div className="py-4 space-y-6 w-full max-w-md">
|
||||
{/* Profile Name - Common to both tabs */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="profile-name">Profile Name</Label>
|
||||
<Input
|
||||
id="profile-name"
|
||||
value={profileName}
|
||||
onChange={(e) => setProfileName(e.target.value)}
|
||||
placeholder="Enter profile name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TabsContent value="regular" className="mt-0 space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Browser</Label>
|
||||
<Combobox
|
||||
options={browserOptions
|
||||
.filter((browser) =>
|
||||
supportedBrowsers.includes(browser.value),
|
||||
)
|
||||
.map((browser) => {
|
||||
const IconComponent = getBrowserIcon(browser.value);
|
||||
return {
|
||||
value: browser.value,
|
||||
label: browser.label,
|
||||
icon: IconComponent,
|
||||
};
|
||||
})}
|
||||
value={selectedBrowser || ""}
|
||||
onValueChange={(value) =>
|
||||
setSelectedBrowser(value as BrowserTypeString)
|
||||
}
|
||||
placeholder="Select a browser..."
|
||||
searchPlaceholder="Search browsers..."
|
||||
/>
|
||||
</div>
|
||||
<TabsContent value="regular" className="mt-0 space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Browser</Label>
|
||||
<Combobox
|
||||
options={browserOptions
|
||||
.filter((browser) =>
|
||||
supportedBrowsers.includes(browser.value),
|
||||
)
|
||||
.map((browser) => {
|
||||
const IconComponent = getBrowserIcon(browser.value);
|
||||
return {
|
||||
value: browser.value,
|
||||
label: browser.label,
|
||||
icon: IconComponent,
|
||||
};
|
||||
})}
|
||||
value={selectedBrowser || ""}
|
||||
onValueChange={(value) =>
|
||||
setSelectedBrowser(value as BrowserTypeString)
|
||||
}
|
||||
placeholder="Select a browser..."
|
||||
searchPlaceholder="Search browsers..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedBrowser && (
|
||||
<div className="space-y-3">
|
||||
{!isBrowserCurrentlyDownloading(selectedBrowser) &&
|
||||
!isBrowserVersionAvailable(selectedBrowser) &&
|
||||
getBestAvailableVersion(
|
||||
availableReleaseTypes,
|
||||
selectedBrowser,
|
||||
) && (
|
||||
<div className="flex gap-3 items-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{selectedBrowser && (
|
||||
<div className="space-y-3">
|
||||
{!isBrowserCurrentlyDownloading(selectedBrowser) &&
|
||||
!isBrowserVersionAvailable(selectedBrowser) &&
|
||||
getBestAvailableVersion(
|
||||
availableReleaseTypes,
|
||||
selectedBrowser,
|
||||
) && (
|
||||
<div className="flex gap-3 items-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{(() => {
|
||||
const bestVersion = getBestAvailableVersion(
|
||||
availableReleaseTypes,
|
||||
selectedBrowser,
|
||||
);
|
||||
return `${bestVersion?.releaseType === "stable" ? "Latest stable" : "Latest nightly"} version (${bestVersion?.version}) needs to be downloaded`;
|
||||
})()}
|
||||
</p>
|
||||
<LoadingButton
|
||||
onClick={() => handleDownload(selectedBrowser)}
|
||||
isLoading={isBrowserCurrentlyDownloading(
|
||||
selectedBrowser,
|
||||
)}
|
||||
size="sm"
|
||||
disabled={isBrowserCurrentlyDownloading(
|
||||
selectedBrowser,
|
||||
)}
|
||||
>
|
||||
Download
|
||||
</LoadingButton>
|
||||
</div>
|
||||
)}
|
||||
{!isBrowserCurrentlyDownloading(selectedBrowser) &&
|
||||
isBrowserVersionAvailable(selectedBrowser) && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{(() => {
|
||||
const bestVersion = getBestAvailableVersion(
|
||||
availableReleaseTypes,
|
||||
selectedBrowser,
|
||||
);
|
||||
return `${bestVersion?.releaseType === "stable" ? "Latest stable" : "Latest nightly"} version (${bestVersion?.version}) needs to be downloaded`;
|
||||
return `✓ ${bestVersion?.releaseType === "stable" ? "Latest stable" : "Latest nightly"} version (${bestVersion?.version}) is available`;
|
||||
})()}
|
||||
</p>
|
||||
<LoadingButton
|
||||
onClick={() => handleDownload(selectedBrowser)}
|
||||
isLoading={isBrowserCurrentlyDownloading(
|
||||
selectedBrowser,
|
||||
)}
|
||||
size="sm"
|
||||
disabled={isBrowserCurrentlyDownloading(
|
||||
selectedBrowser,
|
||||
)}
|
||||
>
|
||||
Download
|
||||
</LoadingButton>
|
||||
</div>
|
||||
)}
|
||||
{!isBrowserCurrentlyDownloading(selectedBrowser) &&
|
||||
isBrowserVersionAvailable(selectedBrowser) && (
|
||||
</div>
|
||||
)}
|
||||
{isBrowserCurrentlyDownloading(selectedBrowser) && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{(() => {
|
||||
const bestVersion = getBestAvailableVersion(
|
||||
availableReleaseTypes,
|
||||
selectedBrowser,
|
||||
);
|
||||
return `✓ ${bestVersion?.releaseType === "stable" ? "Latest stable" : "Latest nightly"} version (${bestVersion?.version}) is available`;
|
||||
return `Downloading ${bestVersion?.releaseType === "stable" ? "stable" : "nightly"} version (${bestVersion?.version})...`;
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{isBrowserCurrentlyDownloading(selectedBrowser) && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{(() => {
|
||||
const bestVersion = getBestAvailableVersion(
|
||||
availableReleaseTypes,
|
||||
selectedBrowser,
|
||||
);
|
||||
return `Downloading ${bestVersion?.releaseType === "stable" ? "stable" : "nightly"} version (${bestVersion?.version})...`;
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="anti-detect" className="mt-0 space-y-6">
|
||||
<div className="space-y-6">
|
||||
{/* Camoufox Download Status */}
|
||||
{!isBrowserCurrentlyDownloading("camoufox") &&
|
||||
!isBrowserVersionAvailable("camoufox") &&
|
||||
getBestAvailableVersion(
|
||||
camoufoxReleaseTypes,
|
||||
"camoufox",
|
||||
) && (
|
||||
<div className="flex gap-3 items-center p-3 rounded-md border">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{(() => {
|
||||
const bestVersion = getBestAvailableVersion(
|
||||
camoufoxReleaseTypes,
|
||||
"camoufox",
|
||||
);
|
||||
return `Camoufox ${bestVersion?.releaseType} version (${bestVersion?.version}) needs to be downloaded`;
|
||||
})()}
|
||||
</p>
|
||||
<LoadingButton
|
||||
onClick={() => handleDownload("camoufox")}
|
||||
isLoading={isBrowserCurrentlyDownloading(
|
||||
"camoufox",
|
||||
)}
|
||||
size="sm"
|
||||
disabled={isBrowserCurrentlyDownloading("camoufox")}
|
||||
>
|
||||
{isBrowserCurrentlyDownloading("camoufox")
|
||||
? "Downloading..."
|
||||
: "Download"}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="anti-detect" className="mt-0 space-y-6">
|
||||
<div className="space-y-6">
|
||||
{/* Camoufox Download Status */}
|
||||
{!isBrowserCurrentlyDownloading("camoufox") &&
|
||||
!isBrowserVersionAvailable("camoufox") &&
|
||||
getBestAvailableVersion(
|
||||
camoufoxReleaseTypes,
|
||||
"camoufox",
|
||||
) && (
|
||||
<div className="flex gap-3 items-center p-3 rounded-md border">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{!isBrowserCurrentlyDownloading("camoufox") &&
|
||||
isBrowserVersionAvailable("camoufox") && (
|
||||
<div className="p-3 text-sm rounded-md border text-muted-foreground">
|
||||
{(() => {
|
||||
const bestVersion = getBestAvailableVersion(
|
||||
camoufoxReleaseTypes,
|
||||
"camoufox",
|
||||
);
|
||||
return `Camoufox ${bestVersion?.releaseType} version (${bestVersion?.version}) needs to be downloaded`;
|
||||
return `✓ Camoufox ${bestVersion?.releaseType} version (${bestVersion?.version}) is available`;
|
||||
})()}
|
||||
</p>
|
||||
<LoadingButton
|
||||
onClick={() => handleDownload("camoufox")}
|
||||
isLoading={isBrowserCurrentlyDownloading("camoufox")}
|
||||
size="sm"
|
||||
disabled={isBrowserCurrentlyDownloading("camoufox")}
|
||||
>
|
||||
{isBrowserCurrentlyDownloading("camoufox")
|
||||
? "Downloading..."
|
||||
: "Download"}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
)}
|
||||
{!isBrowserCurrentlyDownloading("camoufox") &&
|
||||
isBrowserVersionAvailable("camoufox") && (
|
||||
</div>
|
||||
)}
|
||||
{isBrowserCurrentlyDownloading("camoufox") && (
|
||||
<div className="p-3 text-sm rounded-md border text-muted-foreground">
|
||||
{(() => {
|
||||
const bestVersion = getBestAvailableVersion(
|
||||
camoufoxReleaseTypes,
|
||||
"camoufox",
|
||||
);
|
||||
return `✓ Camoufox ${bestVersion?.releaseType} version (${bestVersion?.version}) is available`;
|
||||
return `Downloading Camoufox ${bestVersion?.releaseType} version (${bestVersion?.version})...`;
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{isBrowserCurrentlyDownloading("camoufox") && (
|
||||
<div className="p-3 text-sm text-muted-foreground rounded-md border">
|
||||
{(() => {
|
||||
const bestVersion = getBestAvailableVersion(
|
||||
camoufoxReleaseTypes,
|
||||
"camoufox",
|
||||
);
|
||||
return `Downloading Camoufox ${bestVersion?.releaseType} version (${bestVersion?.version})...`;
|
||||
})()}
|
||||
|
||||
<SharedCamoufoxConfigForm
|
||||
config={camoufoxConfig}
|
||||
onConfigChange={updateCamoufoxConfig}
|
||||
isCreating
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Proxy Selection - Common to both tabs - Always visible */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label>Proxy</Label>
|
||||
<RippleButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setShowProxyForm(true)}
|
||||
className="px-2 h-7 text-xs"
|
||||
>
|
||||
<GoPlus className="mr-1 w-3 h-3" /> Add Proxy
|
||||
</RippleButton>
|
||||
</div>
|
||||
{storedProxies.length > 0 ? (
|
||||
<Select
|
||||
value={selectedProxyId || "none"}
|
||||
onValueChange={(value) =>
|
||||
setSelectedProxyId(value === "none" ? undefined : value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="No proxy" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No proxy</SelectItem>
|
||||
{storedProxies.map((proxy) => (
|
||||
<SelectItem key={proxy.id} value={proxy.id}>
|
||||
{proxy.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="flex gap-3 items-center p-3 text-sm rounded-md border text-muted-foreground">
|
||||
No proxies available. Add one to route this profile's
|
||||
traffic.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SharedCamoufoxConfigForm
|
||||
config={camoufoxConfig}
|
||||
onConfigChange={updateCamoufoxConfig}
|
||||
isCreating
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Proxy Selection - Common to both tabs - Compact without card */}
|
||||
{storedProxies.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<Label>Proxy</Label>
|
||||
<Select
|
||||
value={selectedProxyId || "none"}
|
||||
onValueChange={(value) =>
|
||||
setSelectedProxyId(value === "none" ? undefined : value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="No proxy" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No proxy</SelectItem>
|
||||
{storedProxies.map((proxy) => (
|
||||
<SelectItem key={proxy.id} value={proxy.id}>
|
||||
{proxy.name}{" "}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter className="flex-shrink-0 pt-4 border-t">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
<RippleButton variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</RippleButton>
|
||||
<LoadingButton
|
||||
onClick={handleCreate}
|
||||
isLoading={isCreating}
|
||||
disabled={isCreateDisabled()}
|
||||
>
|
||||
Create Profile
|
||||
Create
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
<ProxyFormDialog
|
||||
isOpen={showProxyForm}
|
||||
onClose={() => setShowProxyForm(false)}
|
||||
onSave={(proxy) => {
|
||||
setStoredProxies((prev) => [...prev, proxy]);
|
||||
setSelectedProxyId(proxy.id);
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,6 +8,8 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { LoadingButton } from "./loading-button";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface DeleteConfirmationDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -59,16 +60,20 @@ export function DeleteConfirmationDialog({
|
||||
)}
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} disabled={isLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => void handleConfirm()}
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Deleting..." : confirmButtonText}
|
||||
</Button>
|
||||
Cancel
|
||||
</RippleButton>
|
||||
<LoadingButton
|
||||
variant="destructive"
|
||||
onClick={() => void handleConfirm()}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{confirmButtonText}
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -17,6 +16,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import type { BrowserProfile, ProfileGroup } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface DeleteGroupDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -188,9 +188,13 @@ export function DeleteGroupDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose} disabled={isDeleting}>
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</RippleButton>
|
||||
<LoadingButton
|
||||
variant="destructive"
|
||||
isLoading={isDeleting}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import type { ProfileGroup } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface EditGroupDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -108,9 +108,13 @@ export function EditGroupDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose} disabled={isUpdating}>
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</RippleButton>
|
||||
<LoadingButton
|
||||
isLoading={isUpdating}
|
||||
onClick={() => void handleUpdate()}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { GoPlus } from "react-icons/go";
|
||||
import { toast } from "sonner";
|
||||
import { CreateGroupDialog } from "@/components/create-group-dialog";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { ProfileGroup } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface GroupAssignmentDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -41,6 +43,7 @@ export function GroupAssignmentDialog({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isAssigning, setIsAssigning] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
|
||||
const loadGroups = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
@@ -126,7 +129,17 @@ export function GroupAssignmentDialog({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="group-select">Assign to Group:</Label>
|
||||
<div className="flex justify-between items-center">
|
||||
<Label htmlFor="group-select">Assign to Group:</Label>
|
||||
<RippleButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
>
|
||||
<GoPlus className="mr-1 w-3 h-3" /> Create Group
|
||||
</RippleButton>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Loading groups...
|
||||
@@ -161,9 +174,13 @@ export function GroupAssignmentDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} disabled={isAssigning}>
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={isAssigning}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</RippleButton>
|
||||
<LoadingButton
|
||||
isLoading={isAssigning}
|
||||
onClick={() => void handleAssign()}
|
||||
@@ -173,6 +190,15 @@ export function GroupAssignmentDialog({
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
<CreateGroupDialog
|
||||
isOpen={createDialogOpen}
|
||||
onClose={() => setCreateDialogOpen(false)}
|
||||
onGroupCreated={(group) => {
|
||||
setGroups((prev) => [...prev, group]);
|
||||
setSelectedGroupId(group.id);
|
||||
setCreateDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function GroupBadges({
|
||||
<Badge
|
||||
key={group.id}
|
||||
variant={selectedGroupId === group.id ? "default" : "secondary"}
|
||||
className="cursor-pointer hover:bg-primary/80 transition-colors flex items-center gap-2 px-3 py-1"
|
||||
className="flex gap-2 items-center px-3 py-1 transition-colors cursor-pointer dark:hover:bg-primary/60 hover:bg-primary/80"
|
||||
onClick={() => {
|
||||
onGroupSelect(selectedGroupId === group.id ? "default" : group.id);
|
||||
}}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { ProfileGroup } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface GroupManagementDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -119,14 +120,14 @@ export function GroupManagementDialog({
|
||||
{/* Create new group button */}
|
||||
<div className="flex justify-between items-center">
|
||||
<Label>Groups</Label>
|
||||
<Button
|
||||
<RippleButton
|
||||
size="sm"
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<GoPlus className="w-4 h-4" />
|
||||
Create Group
|
||||
</Button>
|
||||
Create
|
||||
</RippleButton>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@@ -187,9 +188,9 @@ export function GroupManagementDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<RippleButton variant="outline" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</RippleButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "./ui/dropdown-menu";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||
|
||||
type Props = {
|
||||
@@ -34,7 +35,7 @@ const HomeHeader = ({
|
||||
onImportProfileDialogOpen,
|
||||
onCreateProfileDialogOpen,
|
||||
}: Props) => {
|
||||
const handleLogoClick = () => {
|
||||
const _handleLogoClick = () => {
|
||||
// Trigger the same URL handling logic as if the URL came from the system
|
||||
const event = new CustomEvent("url-open-request", {
|
||||
detail: "https://donutbrowser.com",
|
||||
@@ -46,11 +47,11 @@ const HomeHeader = ({
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogoClick}
|
||||
className="p-1 cursor-pointer"
|
||||
title="Open donutbrowser.com"
|
||||
onClick={_handleLogoClick}
|
||||
>
|
||||
<Logo className="w-10 h-10" />
|
||||
<Logo className="w-10 h-10 transition-transform duration-300 ease-out will-change-transform hover:scale-110" />
|
||||
</button>
|
||||
{selectedProfiles.length > 0 ? (
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -59,7 +60,7 @@ const HomeHeader = ({
|
||||
{selectedProfiles.length !== 1 ? "s" : ""} selected
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onBulkGroupAssignment}
|
||||
@@ -67,8 +68,8 @@ const HomeHeader = ({
|
||||
>
|
||||
<LuUsers className="w-4 h-4" />
|
||||
Assign to Group
|
||||
</Button>
|
||||
<Button
|
||||
</RippleButton>
|
||||
<RippleButton
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={onBulkDelete}
|
||||
@@ -76,7 +77,7 @@ const HomeHeader = ({
|
||||
>
|
||||
<LuTrash2 className="w-4 h-4" />
|
||||
Delete Selected
|
||||
</Button>
|
||||
</RippleButton>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { useBrowserSupport } from "@/hooks/use-browser-support";
|
||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||
import type { DetectedProfile } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface ImportProfileDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -242,7 +243,7 @@ export function ImportProfileDialog({
|
||||
);
|
||||
if (profile) {
|
||||
const browserName = getBrowserDisplayName(profile.browser);
|
||||
const defaultName = `Imported ${browserName} Profile`;
|
||||
const defaultName = `Old ${browserName}`;
|
||||
setAutoDetectProfileName(defaultName);
|
||||
}
|
||||
}
|
||||
@@ -268,7 +269,7 @@ export function ImportProfileDialog({
|
||||
<div className="overflow-y-auto flex-1 space-y-6 min-h-0">
|
||||
{/* Mode Selection */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
<RippleButton
|
||||
variant={importMode === "auto-detect" ? "default" : "outline"}
|
||||
onClick={() => {
|
||||
setImportMode("auto-detect");
|
||||
@@ -277,8 +278,8 @@ export function ImportProfileDialog({
|
||||
disabled={isLoading}
|
||||
>
|
||||
Auto-Detect
|
||||
</Button>
|
||||
<Button
|
||||
</RippleButton>
|
||||
<RippleButton
|
||||
variant={importMode === "manual" ? "default" : "outline"}
|
||||
onClick={() => {
|
||||
setImportMode("manual");
|
||||
@@ -287,7 +288,7 @@ export function ImportProfileDialog({
|
||||
disabled={isLoading}
|
||||
>
|
||||
Manual Import
|
||||
</Button>
|
||||
</RippleButton>
|
||||
</div>
|
||||
|
||||
{/* Auto-Detect Mode */}
|
||||
@@ -479,9 +480,9 @@ export function ImportProfileDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-shrink-0">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
<RippleButton variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</RippleButton>
|
||||
{importMode === "auto-detect" ? (
|
||||
<LoadingButton
|
||||
isLoading={isImporting}
|
||||
@@ -494,7 +495,7 @@ export function ImportProfileDialog({
|
||||
isLoading
|
||||
}
|
||||
>
|
||||
Import Profile
|
||||
Import
|
||||
</LoadingButton>
|
||||
) : (
|
||||
<LoadingButton
|
||||
@@ -508,7 +509,7 @@ export function ImportProfileDialog({
|
||||
!manualProfileName.trim()
|
||||
}
|
||||
>
|
||||
Import Profile
|
||||
Import
|
||||
</LoadingButton>
|
||||
)}
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { LuLoaderCircle } from "react-icons/lu";
|
||||
import { type ButtonProps, Button as UIButton } from "./ui/button";
|
||||
import {
|
||||
type RippleButtonProps as ButtonProps,
|
||||
RippleButton as UIButton,
|
||||
} from "./ui/ripple";
|
||||
|
||||
type Props = ButtonProps & {
|
||||
isLoading: boolean;
|
||||
@@ -7,7 +10,11 @@ type Props = ButtonProps & {
|
||||
};
|
||||
export const LoadingButton = ({ isLoading, ...props }: Props) => {
|
||||
return (
|
||||
<UIButton className="grid place-items-center" {...props}>
|
||||
<UIButton
|
||||
className="grid place-items-center"
|
||||
{...props}
|
||||
disabled={props.disabled || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LuLoaderCircle className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { BsCamera, BsMic } from "react-icons/bs";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
import type { PermissionType } from "@/hooks/use-permissions";
|
||||
import { usePermissions } from "@/hooks/use-permissions";
|
||||
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface PermissionDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -148,9 +148,9 @@ export function PermissionDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<RippleButton variant="outline" onClick={onClose}>
|
||||
{isCurrentPermissionGranted ? "Done" : "Cancel"}
|
||||
</Button>
|
||||
</RippleButton>
|
||||
|
||||
{!isCurrentPermissionGranted && (
|
||||
<LoadingButton
|
||||
|
||||
@@ -27,8 +27,6 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
@@ -57,6 +55,7 @@ import { cn } from "@/lib/utils";
|
||||
import type { BrowserProfile, StoredProxy } from "@/types";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface ProfilesDataTableProps {
|
||||
data: BrowserProfile[];
|
||||
@@ -548,7 +547,7 @@ export function ProfilesDataTable({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex">
|
||||
<Button
|
||||
<RippleButton
|
||||
variant={isRunning ? "destructive" : "default"}
|
||||
size="sm"
|
||||
disabled={!canLaunch || isLaunching || isStopping}
|
||||
@@ -567,7 +566,7 @@ export function ProfilesDataTable({
|
||||
) : (
|
||||
"Launch"
|
||||
)}
|
||||
</Button>
|
||||
</RippleButton>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
{tooltipContent && (
|
||||
@@ -748,6 +747,10 @@ export function ProfilesDataTable({
|
||||
browserState.isClient && runningProfiles.has(profile.name);
|
||||
const isBrowserUpdating =
|
||||
browserState.isClient && isUpdating(profile.browser);
|
||||
const isLaunching = launchingProfiles.has(profile.name);
|
||||
const isStopping = stoppingProfiles.has(profile.name);
|
||||
const isDisabled =
|
||||
isRunning || isLaunching || isStopping || isBrowserUpdating;
|
||||
|
||||
return (
|
||||
<div className="flex justify-end items-center">
|
||||
@@ -763,15 +766,11 @@ export function ProfilesDataTable({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
onProxySettings(profile);
|
||||
}}
|
||||
disabled={
|
||||
!browserState.isClient || isBrowserUpdating || isRunning
|
||||
}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Configure Proxy
|
||||
</DropdownMenuItem>
|
||||
@@ -781,9 +780,7 @@ export function ProfilesDataTable({
|
||||
onAssignProfilesToGroup([profile.name]);
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
!browserState.isClient || isBrowserUpdating || isRunning
|
||||
}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Assign to Group
|
||||
</DropdownMenuItem>
|
||||
@@ -792,9 +789,7 @@ export function ProfilesDataTable({
|
||||
onClick={() => {
|
||||
onConfigureCamoufox(profile);
|
||||
}}
|
||||
disabled={
|
||||
!browserState.isClient || isRunning || isBrowserUpdating
|
||||
}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Configure Camoufox
|
||||
</DropdownMenuItem>
|
||||
@@ -806,9 +801,7 @@ export function ProfilesDataTable({
|
||||
onClick={() => {
|
||||
onChangeVersion(profile);
|
||||
}}
|
||||
disabled={
|
||||
!browserState.isClient || isRunning || isBrowserUpdating
|
||||
}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Switch Release
|
||||
</DropdownMenuItem>
|
||||
@@ -818,9 +811,7 @@ export function ProfilesDataTable({
|
||||
setProfileToRename(profile);
|
||||
setNewProfileName(profile.name);
|
||||
}}
|
||||
disabled={
|
||||
!browserState.isClient || isRunning || isBrowserUpdating
|
||||
}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
@@ -828,9 +819,7 @@ export function ProfilesDataTable({
|
||||
onClick={() => {
|
||||
setProfileToDelete(profile);
|
||||
}}
|
||||
disabled={
|
||||
!browserState.isClient || isRunning || isBrowserUpdating
|
||||
}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
@@ -971,15 +960,17 @@ export function ProfilesDataTable({
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setProfileToRename(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => void handleRename()}>Save</Button>
|
||||
</RippleButton>
|
||||
<RippleButton onClick={() => void handleRename()}>
|
||||
Save
|
||||
</RippleButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { LuCopy } from "react-icons/lu";
|
||||
import { toast } from "sonner";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
import { useBrowserState } from "@/hooks/use-browser-state";
|
||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||
import type { BrowserProfile, StoredProxy } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface ProfileSelectorDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -196,7 +196,7 @@ export function ProfileSelectorDialog({
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label className="text-sm font-medium">Opening URL:</Label>
|
||||
<Button
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void handleCopyUrl()}
|
||||
@@ -204,7 +204,7 @@ export function ProfileSelectorDialog({
|
||||
>
|
||||
<LuCopy className="w-3 h-3" />
|
||||
Copy
|
||||
</Button>
|
||||
</RippleButton>
|
||||
</div>
|
||||
<div className="p-2 text-sm break-all rounded bg-muted">
|
||||
{url}
|
||||
@@ -312,9 +312,9 @@ export function ProfileSelectorDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
<RippleButton variant="outline" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</RippleButton>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex">
|
||||
|
||||
@@ -4,7 +4,6 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { StoredProxy } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface ProxyFormData {
|
||||
name: string;
|
||||
@@ -264,13 +264,13 @@ export function ProxyFormDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</RippleButton>
|
||||
<LoadingButton
|
||||
isLoading={isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { FiEdit2, FiPlus, FiTrash2, FiWifi } from "react-icons/fi";
|
||||
import { toast } from "sonner";
|
||||
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -20,6 +22,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { trimName } from "@/lib/name-utils";
|
||||
import type { StoredProxy } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface ProxyManagementDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -34,6 +37,7 @@ export function ProxyManagementDialog({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showProxyForm, setShowProxyForm] = useState(false);
|
||||
const [editingProxy, setEditingProxy] = useState<StoredProxy | null>(null);
|
||||
const [proxyUsage, setProxyUsage] = useState<Record<string, number>>({});
|
||||
|
||||
const loadStoredProxies = useCallback(async () => {
|
||||
try {
|
||||
@@ -48,11 +52,44 @@ export function ProxyManagementDialog({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadProxyUsage = useCallback(async () => {
|
||||
try {
|
||||
const profiles = await invoke<Array<{ proxy_id?: string }>>(
|
||||
"list_browser_profiles",
|
||||
);
|
||||
const counts: Record<string, number> = {};
|
||||
for (const p of profiles) {
|
||||
if (p.proxy_id) counts[p.proxy_id] = (counts[p.proxy_id] ?? 0) + 1;
|
||||
}
|
||||
setProxyUsage(counts);
|
||||
} catch (_err) {
|
||||
// ignore non-critical errors
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadStoredProxies();
|
||||
void loadProxyUsage();
|
||||
}
|
||||
}, [isOpen, loadStoredProxies]);
|
||||
}, [isOpen, loadStoredProxies, loadProxyUsage]);
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: (() => void) | undefined;
|
||||
const setup = async () => {
|
||||
try {
|
||||
unlisten = await listen("profile-updated", () => {
|
||||
void loadProxyUsage();
|
||||
});
|
||||
} catch (_err) {
|
||||
// ignore non-critical errors
|
||||
}
|
||||
};
|
||||
if (isOpen) void setup();
|
||||
return () => {
|
||||
if (unlisten) unlisten();
|
||||
};
|
||||
}, [isOpen, loadProxyUsage]);
|
||||
|
||||
const handleDeleteProxy = useCallback(async (proxy: StoredProxy) => {
|
||||
if (
|
||||
@@ -124,13 +161,13 @@ export function ProxyManagementDialog({
|
||||
profiles
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
<RippleButton
|
||||
onClick={handleCreateProxy}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<FiPlus className="w-4 h-4" />
|
||||
Create Proxy
|
||||
</Button>
|
||||
</RippleButton>
|
||||
</div>
|
||||
|
||||
{/* Proxy List - Scrollable */}
|
||||
@@ -150,10 +187,10 @@ export function ProxyManagementDialog({
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Create your first proxy configuration to get started
|
||||
</p>
|
||||
<Button variant="outline" onClick={handleCreateProxy}>
|
||||
<RippleButton variant="outline" onClick={handleCreateProxy}>
|
||||
<FiPlus className="mr-2 w-4 h-4" />
|
||||
Create First Proxy
|
||||
</Button>
|
||||
</RippleButton>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-y-auto pr-2 space-y-2 h-full">
|
||||
@@ -182,6 +219,11 @@ export function ProxyManagementDialog({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<Badge variant="secondary">
|
||||
{proxyUsage[proxy.id] ?? 0}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 gap-1 items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -221,7 +263,7 @@ export function ProxyManagementDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-shrink-0">
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
<RippleButton onClick={onClose}>Close</RippleButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { FiPlus } from "react-icons/fi";
|
||||
import { toast } from "sonner";
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { StoredProxy } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface ProxySettingsDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -45,6 +47,7 @@ export function ProxySettingsDialog({
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showProxyForm, setShowProxyForm] = useState(false);
|
||||
const [proxyUsage, setProxyUsage] = useState<Record<string, number>>({});
|
||||
|
||||
// Helper to determine if proxy should be disabled for the selected browser
|
||||
const isProxyDisabled = browserType === "tor-browser";
|
||||
@@ -62,9 +65,28 @@ export function ProxySettingsDialog({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadProxyUsage = useCallback(async () => {
|
||||
try {
|
||||
const profiles = await invoke<Array<{ proxy_id?: string }>>(
|
||||
"list_browser_profiles",
|
||||
);
|
||||
const counts: Record<string, number> = {};
|
||||
for (const p of profiles) {
|
||||
if (p.proxy_id) {
|
||||
counts[p.proxy_id] = (counts[p.proxy_id] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
setProxyUsage(counts);
|
||||
} catch (error) {
|
||||
// Non-fatal
|
||||
console.error("Failed to load proxy usage:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadStoredProxies();
|
||||
void loadProxyUsage();
|
||||
if (isProxyDisabled) {
|
||||
setSelectedProxyId(null);
|
||||
} else {
|
||||
@@ -72,7 +94,31 @@ export function ProxySettingsDialog({
|
||||
setSelectedProxyId(initialProxyId || null);
|
||||
}
|
||||
}
|
||||
}, [isOpen, isProxyDisabled, loadStoredProxies, initialProxyId]);
|
||||
}, [
|
||||
isOpen,
|
||||
isProxyDisabled,
|
||||
loadStoredProxies,
|
||||
initialProxyId,
|
||||
loadProxyUsage,
|
||||
]);
|
||||
|
||||
// Refresh usage when profiles change
|
||||
useEffect(() => {
|
||||
let unlisten: (() => void) | undefined;
|
||||
const setup = async () => {
|
||||
try {
|
||||
unlisten = await listen("profile-updated", () => {
|
||||
void loadProxyUsage();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
if (isOpen) void setup();
|
||||
return () => {
|
||||
if (unlisten) unlisten();
|
||||
};
|
||||
}, [isOpen, loadProxyUsage]);
|
||||
|
||||
const handleCreateProxy = useCallback(() => {
|
||||
setShowProxyForm(true);
|
||||
@@ -142,15 +188,15 @@ export function ProxySettingsDialog({
|
||||
</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCreateProxy}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<FiPlus className="w-4 h-4" />
|
||||
Create New
|
||||
</Button>
|
||||
Create
|
||||
</RippleButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Create a new proxy configuration</p>
|
||||
@@ -233,6 +279,7 @@ export function ProxySettingsDialog({
|
||||
<Badge variant="outline">
|
||||
{proxy.proxy_settings.proxy_type.toUpperCase()}
|
||||
</Badge>
|
||||
<Badge>{proxyUsage[proxy.id] ?? 0}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -263,12 +310,12 @@ export function ProxySettingsDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<RippleButton variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!hasChanged()}>
|
||||
</RippleButton>
|
||||
<RippleButton onClick={handleSave} disabled={!hasChanged()}>
|
||||
Save
|
||||
</Button>
|
||||
</RippleButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useState } from "react";
|
||||
import { LuCheck, LuChevronsUpDown, LuDownload } from "react-icons/lu";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { BrowserReleaseTypes } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface ReleaseTypeSelectorProps {
|
||||
selectedReleaseType: "stable" | "nightly" | null;
|
||||
@@ -85,7 +85,7 @@ export function ReleaseTypeSelector({
|
||||
{showDropdown ? (
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen} modal={true}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={popoverOpen}
|
||||
@@ -93,7 +93,7 @@ export function ReleaseTypeSelector({
|
||||
>
|
||||
{selectedDisplayText}
|
||||
<LuChevronsUpDown className="ml-2 w-4 h-4 opacity-50 shrink-0" />
|
||||
</Button>
|
||||
</RippleButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { BsCamera, BsMic } from "react-icons/bs";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
showSuccessToast,
|
||||
showUnifiedVersionUpdateToast,
|
||||
} from "@/lib/toast-utils";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface AppSettings {
|
||||
set_as_default_browser: boolean;
|
||||
@@ -529,9 +529,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-shrink-0">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<RippleButton variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</RippleButton>
|
||||
<LoadingButton
|
||||
isLoading={isSaving}
|
||||
onClick={() => {
|
||||
|
||||
@@ -12,7 +12,7 @@ const badgeVariants = cva(
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
"border-transparent bg-secondary dark:bg-secondary/60 text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { type HTMLMotionProps, motion, type Transition } from "motion/react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"relative overflow-hidden cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-9 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-11 px-8 has-[>svg]:px-6",
|
||||
icon: "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const rippleVariants = cva("absolute rounded-full size-5 pointer-events-none", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary-foreground",
|
||||
destructive: "bg-destructive",
|
||||
outline: "bg-input",
|
||||
secondary: "bg-secondary",
|
||||
ghost: "bg-accent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
type Ripple = {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
type RippleButtonProps = HTMLMotionProps<"button"> & {
|
||||
children: React.ReactNode;
|
||||
rippleClassName?: string;
|
||||
scale?: number;
|
||||
transition?: Transition;
|
||||
} & VariantProps<typeof buttonVariants>;
|
||||
|
||||
function RippleButton({
|
||||
ref,
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
rippleClassName,
|
||||
variant,
|
||||
size,
|
||||
scale = 10,
|
||||
transition = { duration: 0.6, ease: "easeOut" },
|
||||
...props
|
||||
}: RippleButtonProps) {
|
||||
const [ripples, setRipples] = React.useState<Ripple[]>([]);
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||
React.useImperativeHandle(ref, () => buttonRef.current as HTMLButtonElement);
|
||||
|
||||
const createRipple = React.useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const button = buttonRef.current;
|
||||
if (!button) return;
|
||||
|
||||
const rect = button.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
const newRipple: Ripple = {
|
||||
id: Date.now(),
|
||||
x,
|
||||
y,
|
||||
};
|
||||
|
||||
setRipples((prev) => [...prev, newRipple]);
|
||||
|
||||
setTimeout(() => {
|
||||
setRipples((prev) => prev.filter((r) => r.id !== newRipple.id));
|
||||
}, 600);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleClick = React.useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
createRipple(event);
|
||||
if (onClick) {
|
||||
onClick(event);
|
||||
}
|
||||
},
|
||||
[createRipple, onClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
ref={buttonRef}
|
||||
data-slot="ripple-button"
|
||||
onClick={handleClick}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{ripples.map((ripple) => (
|
||||
<motion.span
|
||||
key={ripple.id}
|
||||
initial={{ scale: 0, opacity: 0.5 }}
|
||||
animate={{ scale, opacity: 0 }}
|
||||
transition={transition}
|
||||
className={cn(
|
||||
rippleVariants({ variant, className: rippleClassName }),
|
||||
)}
|
||||
style={{
|
||||
top: ripple.y - 10,
|
||||
left: ripple.x - 10,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
|
||||
export { RippleButton, type RippleButtonProps };
|
||||
@@ -12,8 +12,8 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-bg": "var(--card)",
|
||||
"--normal-text": "var(--card-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
zIndex: 99999,
|
||||
} as React.CSSProperties
|
||||
@@ -22,6 +22,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
style: {
|
||||
zIndex: 99999,
|
||||
pointerEvents: "auto",
|
||||
backdropFilter: "saturate(1.2)",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
|
||||
@@ -156,6 +156,7 @@ export function useAppUpdateNotifications() {
|
||||
style: {
|
||||
zIndex: 99999, // Ensure app updates appear above dialogs
|
||||
pointerEvents: "auto", // Ensure app updates remain interactive
|
||||
marginTop: "16px", // slightly lower on macOS-like top controls
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -210,9 +210,21 @@ export function useBrowserDownload() {
|
||||
if (!suppressNotifications) {
|
||||
// Dismiss any existing download toast and show error
|
||||
dismissToast(`download-${browserStr}-${version}`);
|
||||
|
||||
let errorMessage = "Unknown error occurred";
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
} else if (typeof error === "string") {
|
||||
errorMessage = error;
|
||||
} else if (error && typeof error === "object" && "message" in error) {
|
||||
errorMessage = String(error.message);
|
||||
}
|
||||
|
||||
// Ensure the long-running download toast is dismissed, and show a finite error toast
|
||||
dismissToast(`download-${browserStr}-${version}`);
|
||||
showErrorToast(`Failed to download ${browserName} ${version}`, {
|
||||
description:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
description: errorMessage,
|
||||
duration: 8000,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -245,11 +245,22 @@ export function useVersionUpdater() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to handle browser auto-update:", error);
|
||||
|
||||
let errorMessage = "Unknown error occurred";
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
} else if (typeof error === "string") {
|
||||
errorMessage = error;
|
||||
} else if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
"message" in error
|
||||
) {
|
||||
errorMessage = String(error.message);
|
||||
}
|
||||
|
||||
showErrorToast(`Failed to auto-update ${browserDisplayName}`, {
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unknown error occurred",
|
||||
description: errorMessage,
|
||||
duration: 8000,
|
||||
});
|
||||
} finally {
|
||||
@@ -336,9 +347,17 @@ export function useVersionUpdater() {
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error("Failed to trigger manual update:", error);
|
||||
let errorMessage = "Unknown error occurred";
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
} else if (typeof error === "string") {
|
||||
errorMessage = error;
|
||||
} else if (error && typeof error === "object" && "message" in error) {
|
||||
errorMessage = String(error.message);
|
||||
}
|
||||
|
||||
showErrorToast("Failed to update browser versions", {
|
||||
description:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
description: errorMessage,
|
||||
duration: 4000,
|
||||
});
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user