fix: linter and formatting

This commit is contained in:
zhom
2025-05-29 10:39:04 +04:00
parent 08678dcacc
commit 56c1f94616
37 changed files with 4013 additions and 2894 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- name: Set up pnpm package manager
uses: pnpm/action-setup@v4
with:
version: latest
version: "10.11.0"
- name: Set up Node.js v22
uses: actions/setup-node@v4
+1 -1
View File
@@ -67,7 +67,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
version: "10.11.0"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
+1
View File
@@ -0,0 +1 @@
pnpm lint-staged
+1 -1
View File
@@ -18,7 +18,7 @@ program
.option(
"-p, --port <number>",
"local port to use (random if not specified)",
parseInt
Number.parseInt
)
.option("--ignore-certificate", "ignore certificate errors for HTTPS proxies")
.option("--id <id>", "proxy ID for stop command")
+1 -1
View File
@@ -2,7 +2,7 @@ import { spawn } from "child_process";
import path from "path";
import getPort from "get-port";
import {
ProxyConfig,
type ProxyConfig,
saveProxyConfig,
getProxyConfig,
deleteProxyConfig,
+18 -2
View File
@@ -9,7 +9,12 @@
"start": "next start",
"lint": "biome check src/ && next lint",
"tauri": "tauri",
"shadcn:add": "pnpm dlx shadcn@latest add"
"shadcn:add": "pnpm dlx shadcn@latest add",
"prepare": "husky",
"format:js": "biome format --write src/",
"format:rust": "cd src-tauri && cargo fmt --all",
"format": "pnpm format:js && pnpm format:rust",
"prettier": "prettier --write"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.2",
@@ -53,10 +58,21 @@
"eslint": "^9.27.0",
"eslint-config-next": "^15.3.2",
"eslint-plugin-react-hooks": "^5.2.0",
"husky": "^9.1.7",
"lint-staged": "^15.3.0",
"prettier": "^3.5.3",
"tailwindcss": "^4.1.7",
"tw-animate-css": "^1.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.32.1"
},
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx,json,css,md}": [
"prettier --write"
],
"src-tauri/**/*.rs": [
"cd src-tauri && cargo fmt --all"
]
}
}
+342 -4
View File
@@ -116,7 +116,7 @@ importers:
version: 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)
'@vitejs/plugin-react':
specifier: ^4.5.0
version: 4.5.0(vite@6.2.0(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1))
version: 4.5.0(vite@6.2.0(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))
eslint:
specifier: ^9.27.0
version: 9.27.0(jiti@2.4.2)
@@ -126,6 +126,15 @@ importers:
eslint-plugin-react-hooks:
specifier: ^5.2.0
version: 5.2.0(eslint@9.27.0(jiti@2.4.2))
husky:
specifier: ^9.1.7
version: 9.1.7
lint-staged:
specifier: ^15.3.0
version: 15.5.2
prettier:
specifier: ^3.5.3
version: 3.5.3
tailwindcss:
specifier: ^4.1.7
version: 4.1.7
@@ -1599,10 +1608,22 @@ packages:
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
ansi-escapes@7.0.0:
resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
engines: {node: '>=18'}
ansi-regex@6.1.0:
resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -1710,6 +1731,10 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
chalk@5.4.1:
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
chownr@3.0.0:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
@@ -1717,6 +1742,14 @@ packages:
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
cli-truncate@4.0.0:
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
engines: {node: '>=18'}
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
@@ -1744,6 +1777,13 @@ packages:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
commander@13.1.0:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1818,6 +1858,9 @@ packages:
electron-to-chromium@1.5.157:
resolution: {integrity: sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==}
emoji-regex@10.4.0:
resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
@@ -1825,6 +1868,10 @@ packages:
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
engines: {node: '>=10.13.0'}
environment@1.1.0:
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
engines: {node: '>=18'}
es-abstract@1.23.10:
resolution: {integrity: sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==}
engines: {node: '>= 0.4'}
@@ -1986,6 +2033,13 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
execa@8.0.1:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -2056,6 +2110,10 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
get-east-asian-width@1.3.0:
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
engines: {node: '>=18'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@@ -2068,6 +2126,10 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-stream@8.0.1:
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
engines: {node: '>=16'}
get-symbol-description@1.1.0:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
@@ -2132,6 +2194,15 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
human-signals@5.0.0:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'}
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
hasBin: true
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -2198,6 +2269,14 @@ packages:
resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
engines: {node: '>= 0.4'}
is-fullwidth-code-point@4.0.0:
resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
engines: {node: '>=12'}
is-fullwidth-code-point@5.0.0:
resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==}
engines: {node: '>=18'}
is-generator-function@1.1.0:
resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==}
engines: {node: '>= 0.4'}
@@ -2230,6 +2309,10 @@ packages:
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
engines: {node: '>= 0.4'}
is-stream@3.0.0:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
is-string@1.1.1:
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
engines: {node: '>= 0.4'}
@@ -2380,6 +2463,19 @@ packages:
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
engines: {node: '>= 12.0.0'}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
lint-staged@15.5.2:
resolution: {integrity: sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==}
engines: {node: '>=18.12.0'}
hasBin: true
listr2@8.3.3:
resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==}
engines: {node: '>=18.0.0'}
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -2387,6 +2483,10 @@ packages:
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
log-update@6.1.0:
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
engines: {node: '>=18'}
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@@ -2401,6 +2501,9 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -2409,6 +2512,14 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mimic-fn@4.0.0:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
engines: {node: '>=12'}
mimic-function@5.0.1:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -2478,6 +2589,10 @@ packages:
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
npm-run-path@5.3.0:
resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -2510,6 +2625,14 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
onetime@6.0.0:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
engines: {node: '>=12'}
onetime@7.0.0:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -2538,6 +2661,10 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-key@4.0.0:
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
engines: {node: '>=12'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -2552,6 +2679,11 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
pidtree@0.6.0:
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
engines: {node: '>=0.10'}
hasBin: true
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -2568,6 +2700,11 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
prettier@3.5.3:
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
engines: {node: '>=14'}
hasBin: true
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
@@ -2653,10 +2790,17 @@ packages:
resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
hasBin: true
restore-cursor@5.1.0:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
rollup@4.41.0:
resolution: {integrity: sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -2729,9 +2873,21 @@ packages:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
slice-ansi@5.0.0:
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
engines: {node: '>=12'}
slice-ansi@7.1.0:
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
engines: {node: '>=18'}
sonner@2.0.3:
resolution: {integrity: sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==}
peerDependencies:
@@ -2749,6 +2905,14 @@ packages:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
string-argv@0.3.2:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'}
string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
string.prototype.includes@2.0.1:
resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==}
engines: {node: '>= 0.4'}
@@ -2772,10 +2936,18 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
strip-ansi@7.1.0:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
strip-final-newline@3.0.0:
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
engines: {node: '>=12'}
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
@@ -2974,6 +3146,10 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
wrap-ansi@9.0.0:
resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
engines: {node: '>=18'}
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -2981,6 +3157,11 @@ packages:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
yaml@2.8.0:
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
engines: {node: '>= 14.6'}
hasBin: true
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@@ -4243,7 +4424,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.7.2':
optional: true
'@vitejs/plugin-react@4.5.0(vite@6.2.0(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1))':
'@vitejs/plugin-react@4.5.0(vite@6.2.0(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))':
dependencies:
'@babel/core': 7.27.1
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
@@ -4251,7 +4432,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.9
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 6.2.0(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)
vite: 6.2.0(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
@@ -4268,10 +4449,18 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ansi-escapes@7.0.0:
dependencies:
environment: 1.1.0
ansi-regex@6.1.0: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.1: {}
argparse@2.0.1: {}
aria-hidden@1.2.6:
@@ -4409,12 +4598,23 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
chalk@5.4.1: {}
chownr@3.0.0: {}
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
cli-truncate@4.0.0:
dependencies:
slice-ansi: 5.0.0
string-width: 7.2.0
client-only@0.0.1: {}
clsx@2.1.1: {}
@@ -4449,6 +4649,10 @@ snapshots:
color-string: 1.9.1
optional: true
colorette@2.0.20: {}
commander@13.1.0: {}
concat-map@0.0.1: {}
convert-source-map@2.0.0: {}
@@ -4519,6 +4723,8 @@ snapshots:
electron-to-chromium@1.5.157: {}
emoji-regex@10.4.0: {}
emoji-regex@9.2.2: {}
enhanced-resolve@5.18.1:
@@ -4526,6 +4732,8 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.2.2
environment@1.1.0: {}
es-abstract@1.23.10:
dependencies:
array-buffer-byte-length: 1.0.2
@@ -4853,6 +5061,20 @@ snapshots:
esutils@2.0.3: {}
eventemitter3@5.0.1: {}
execa@8.0.1:
dependencies:
cross-spawn: 7.0.6
get-stream: 8.0.1
human-signals: 5.0.0
is-stream: 3.0.0
merge-stream: 2.0.0
npm-run-path: 5.3.0
onetime: 6.0.0
signal-exit: 4.1.0
strip-final-newline: 3.0.0
fast-deep-equal@3.1.3: {}
fast-glob@3.3.1:
@@ -4925,6 +5147,8 @@ snapshots:
gensync@1.0.0-beta.2: {}
get-east-asian-width@1.3.0: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -4945,6 +5169,8 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
get-stream@8.0.1: {}
get-symbol-description@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -5000,6 +5226,10 @@ snapshots:
dependencies:
function-bind: 1.1.2
human-signals@5.0.0: {}
husky@9.1.7: {}
ignore@5.3.2: {}
ignore@7.0.4: {}
@@ -5070,6 +5300,12 @@ snapshots:
dependencies:
call-bound: 1.0.4
is-fullwidth-code-point@4.0.0: {}
is-fullwidth-code-point@5.0.0:
dependencies:
get-east-asian-width: 1.3.0
is-generator-function@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -5103,6 +5339,8 @@ snapshots:
dependencies:
call-bound: 1.0.4
is-stream@3.0.0: {}
is-string@1.1.1:
dependencies:
call-bound: 1.0.4
@@ -5231,12 +5469,46 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.30.1
lightningcss-win32-x64-msvc: 1.30.1
lilconfig@3.1.3: {}
lint-staged@15.5.2:
dependencies:
chalk: 5.4.1
commander: 13.1.0
debug: 4.4.1
execa: 8.0.1
lilconfig: 3.1.3
listr2: 8.3.3
micromatch: 4.0.8
pidtree: 0.6.0
string-argv: 0.3.2
yaml: 2.8.0
transitivePeerDependencies:
- supports-color
listr2@8.3.3:
dependencies:
cli-truncate: 4.0.0
colorette: 2.0.20
eventemitter3: 5.0.1
log-update: 6.1.0
rfdc: 1.4.1
wrap-ansi: 9.0.0
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
lodash.merge@4.6.2: {}
log-update@6.1.0:
dependencies:
ansi-escapes: 7.0.0
cli-cursor: 5.0.0
slice-ansi: 7.1.0
strip-ansi: 7.1.0
wrap-ansi: 9.0.0
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
@@ -5251,6 +5523,8 @@ snapshots:
math-intrinsics@1.1.0: {}
merge-stream@2.0.0: {}
merge2@1.4.1: {}
micromatch@4.0.8:
@@ -5258,6 +5532,10 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
mimic-fn@4.0.0: {}
mimic-function@5.0.1: {}
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.11
@@ -5316,6 +5594,10 @@ snapshots:
node-releases@2.0.19: {}
npm-run-path@5.3.0:
dependencies:
path-key: 4.0.0
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
@@ -5358,6 +5640,14 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
onetime@6.0.0:
dependencies:
mimic-fn: 4.0.0
onetime@7.0.0:
dependencies:
mimic-function: 5.0.1
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -5389,6 +5679,8 @@ snapshots:
path-key@3.1.1: {}
path-key@4.0.0: {}
path-parse@1.0.7: {}
picocolors@1.1.1: {}
@@ -5397,6 +5689,8 @@ snapshots:
picomatch@4.0.2: {}
pidtree@0.6.0: {}
possible-typed-array-names@1.1.0: {}
postcss@8.4.31:
@@ -5413,6 +5707,8 @@ snapshots:
prelude-ls@1.2.1: {}
prettier@3.5.3: {}
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
@@ -5501,8 +5797,15 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
restore-cursor@5.1.0:
dependencies:
onetime: 7.0.0
signal-exit: 4.1.0
reusify@1.1.0: {}
rfdc@1.4.1: {}
rollup@4.41.0:
dependencies:
'@types/estree': 1.0.7
@@ -5643,11 +5946,23 @@ snapshots:
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
signal-exit@4.1.0: {}
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
optional: true
slice-ansi@5.0.0:
dependencies:
ansi-styles: 6.2.1
is-fullwidth-code-point: 4.0.0
slice-ansi@7.1.0:
dependencies:
ansi-styles: 6.2.1
is-fullwidth-code-point: 5.0.0
sonner@2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
@@ -5659,6 +5974,14 @@ snapshots:
streamsearch@1.1.0: {}
string-argv@0.3.2: {}
string-width@7.2.0:
dependencies:
emoji-regex: 10.4.0
get-east-asian-width: 1.3.0
strip-ansi: 7.1.0
string.prototype.includes@2.0.1:
dependencies:
call-bind: 1.0.8
@@ -5709,8 +6032,14 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
strip-ansi@7.1.0:
dependencies:
ansi-regex: 6.1.0
strip-bom@3.0.0: {}
strip-final-newline@3.0.0: {}
strip-json-comments@3.1.1: {}
styled-jsx@5.1.6(@babel/core@7.27.1)(react@19.1.0):
@@ -5870,7 +6199,7 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.5
vite@6.2.0(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1):
vite@6.2.0(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0):
dependencies:
esbuild: 0.25.4
postcss: 8.5.3
@@ -5880,6 +6209,7 @@ snapshots:
fsevents: 2.3.3
jiti: 2.4.2
lightningcss: 1.30.1
yaml: 2.8.0
which-boxed-primitive@1.1.1:
dependencies:
@@ -5928,8 +6258,16 @@ snapshots:
word-wrap@1.2.5: {}
wrap-ansi@9.0.0:
dependencies:
ansi-styles: 6.2.1
string-width: 7.2.0
strip-ansi: 7.1.0
yallist@3.1.1: {}
yallist@5.0.0: {}
yaml@2.8.0: {}
yocto-queue@0.1.0: {}
+1 -1
View File
@@ -4,6 +4,6 @@ fn main() {
println!("cargo:rustc-link-lib=framework=CoreFoundation");
println!("cargo:rustc-link-lib=framework=CoreServices");
}
tauri_build::build()
}
+294 -153
View File
@@ -1,10 +1,10 @@
use directories::BaseDirs;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use directories::BaseDirs;
use crate::browser::GithubRelease;
@@ -34,7 +34,7 @@ enum PreReleaseKind {
impl VersionComponent {
fn parse(version: &str) -> Self {
let version = version.trim();
// Handle special case for Zen Browser twilight releases
if version.to_lowercase().contains("twilight") {
return VersionComponent {
@@ -47,20 +47,22 @@ impl VersionComponent {
// Split version into numeric and pre-release parts
let (numeric_part, pre_release_part) = Self::split_version(version);
// Parse numeric parts (major.minor.patch)
let parts: Vec<u32> = numeric_part
.split('.')
.filter_map(|part| part.parse().ok())
.collect();
let major = parts.get(0).copied().unwrap_or(0);
let minor = parts.get(1).copied().unwrap_or(0);
let patch = parts.get(2).copied().unwrap_or(0);
// Parse pre-release part
let pre_release = pre_release_part.as_deref().and_then(Self::parse_pre_release);
let pre_release = pre_release_part
.as_deref()
.and_then(Self::parse_pre_release);
VersionComponent {
major,
minor,
@@ -68,39 +70,49 @@ impl VersionComponent {
pre_release,
}
}
fn split_version(version: &str) -> (String, Option<String>) {
let version = version.to_lowercase();
// Look for pre-release indicators
for (i, ch) in version.char_indices() {
if ch.is_alphabetic() && i > 0 {
// Check if this is a pre-release indicator
let remaining = &version[i..];
if remaining.starts_with('a') || remaining.starts_with('b') ||
remaining.starts_with("alpha") || remaining.starts_with("beta") ||
remaining.starts_with("rc") || remaining.starts_with("dev") ||
remaining.starts_with("pre") {
if remaining.starts_with('a')
|| remaining.starts_with('b')
|| remaining.starts_with("alpha")
|| remaining.starts_with("beta")
|| remaining.starts_with("rc")
|| remaining.starts_with("dev")
|| remaining.starts_with("pre")
{
return (version[..i].to_string(), Some(remaining.to_string()));
}
}
}
(version, None)
}
fn parse_pre_release(pre_release: &str) -> Option<PreRelease> {
let pre_release = pre_release.trim().to_lowercase();
if pre_release.is_empty() {
return None;
}
// Extract kind and number
let (kind, number) = if pre_release.starts_with("alpha") {
(PreReleaseKind::Alpha, Self::extract_number(&pre_release[5..]))
(
PreReleaseKind::Alpha,
Self::extract_number(&pre_release[5..]),
)
} else if pre_release.starts_with("beta") {
(PreReleaseKind::Beta, Self::extract_number(&pre_release[4..]))
(
PreReleaseKind::Beta,
Self::extract_number(&pre_release[4..]),
)
} else if pre_release.starts_with("rc") {
(PreReleaseKind::RC, Self::extract_number(&pre_release[2..]))
} else if pre_release.starts_with("dev") {
@@ -108,16 +120,22 @@ impl VersionComponent {
} else if pre_release.starts_with("pre") {
(PreReleaseKind::Pre, Self::extract_number(&pre_release[3..]))
} else if pre_release.starts_with('a') {
(PreReleaseKind::Alpha, Self::extract_number(&pre_release[1..]))
(
PreReleaseKind::Alpha,
Self::extract_number(&pre_release[1..]),
)
} else if pre_release.starts_with('b') {
(PreReleaseKind::Beta, Self::extract_number(&pre_release[1..]))
(
PreReleaseKind::Beta,
Self::extract_number(&pre_release[1..]),
)
} else {
return None;
};
Some(PreRelease { kind, number })
}
fn extract_number(s: &str) -> Option<u32> {
let numeric_part: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
numeric_part.parse().ok()
@@ -133,7 +151,7 @@ impl PartialOrd for VersionComponent {
impl Ord for VersionComponent {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
use std::cmp::Ordering;
// Compare major.minor.patch first
match (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch)) {
Ordering::Equal => {
@@ -182,8 +200,6 @@ pub fn sort_github_releases(releases: &mut [GithubRelease]) {
});
}
pub fn is_alpha_version(version: &str) -> bool {
let version_comp = VersionComponent::parse(version);
version_comp.pre_release.is_some()
@@ -264,14 +280,14 @@ impl ApiClient {
pub fn load_cached_versions(&self, browser: &str) -> Option<Vec<String>> {
let cache_dir = Self::get_cache_dir().ok()?;
let cache_file = cache_dir.join(format!("{}_versions.json", browser));
if !cache_file.exists() {
return None;
}
let content = fs::read_to_string(&cache_file).ok()?;
let cached_data: CachedVersionData = serde_json::from_str(&content).ok()?;
// Always return cached versions regardless of age - they're always valid
println!("Using cached versions for {}", browser);
Some(cached_data.versions)
@@ -283,7 +299,7 @@ impl ApiClient {
Err(_) => return true, // If we can't get cache dir, consider expired
};
let cache_file = cache_dir.join(format!("{}_versions.json", browser));
if !cache_file.exists() {
return true; // No cache file means expired
}
@@ -297,20 +313,24 @@ impl ApiClient {
Ok(data) => data,
Err(_) => return true, // Can't parse cache, consider expired
};
// Check if cache is older than 10 minutes
!Self::is_cache_valid(cached_data.timestamp)
}
pub fn save_cached_versions(&self, browser: &str, versions: &[String]) -> Result<(), Box<dyn std::error::Error>> {
pub fn save_cached_versions(
&self,
browser: &str,
versions: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
let cache_dir = Self::get_cache_dir()?;
let cache_file = cache_dir.join(format!("{}_versions.json", browser));
let cached_data = CachedVersionData {
versions: versions.to_vec(),
timestamp: Self::get_current_timestamp(),
};
let content = serde_json::to_string_pretty(&cached_data)?;
fs::write(&cache_file, content)?;
println!("Cached {} versions for {}", versions.len(), browser);
@@ -320,56 +340,69 @@ impl ApiClient {
fn load_cached_github_releases(&self, browser: &str) -> Option<Vec<GithubRelease>> {
let cache_dir = Self::get_cache_dir().ok()?;
let cache_file = cache_dir.join(format!("{}_github.json", browser));
if !cache_file.exists() {
return None;
}
let content = fs::read_to_string(&cache_file).ok()?;
let cached_data: CachedGithubData = serde_json::from_str(&content).ok()?;
// Always use cached GitHub releases - cache never expires, only gets updated with new versions
println!("Using cached GitHub releases for {}", browser);
Some(cached_data.releases)
}
fn save_cached_github_releases(&self, browser: &str, releases: &[GithubRelease]) -> Result<(), Box<dyn std::error::Error>> {
fn save_cached_github_releases(
&self,
browser: &str,
releases: &[GithubRelease],
) -> Result<(), Box<dyn std::error::Error>> {
let cache_dir = Self::get_cache_dir()?;
let cache_file = cache_dir.join(format!("{}_github.json", browser));
let cached_data = CachedGithubData {
releases: releases.to_vec(),
timestamp: Self::get_current_timestamp(),
};
let content = serde_json::to_string_pretty(&cached_data)?;
fs::write(&cache_file, content)?;
println!("Cached {} GitHub releases for {}", releases.len(), browser);
Ok(())
}
pub async fn fetch_firefox_releases_with_caching(&self, no_caching: bool) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_firefox_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_versions) = self.load_cached_versions("firefox") {
return Ok(cached_versions.into_iter().map(|version| {
BrowserRelease {
version: version.clone(),
date: "".to_string(), // Cache doesn't store dates
is_prerelease: is_alpha_version(&version),
download_url: Some(format!(
"https://download.mozilla.org/?product=firefox-{}&os=osx&lang=en-US",
version
)),
}
}).collect());
return Ok(
cached_versions
.into_iter()
.map(|version| {
BrowserRelease {
version: version.clone(),
date: "".to_string(), // Cache doesn't store dates
is_prerelease: is_alpha_version(&version),
download_url: Some(format!(
"https://download.mozilla.org/?product=firefox-{}&os=osx&lang=en-US",
version
)),
}
})
.collect(),
);
}
}
println!("Fetching Firefox releases from Mozilla API...");
let url = "https://product-details.mozilla.org/1.0/firefox.json";
let response = self.client
let response = self
.client
.get(url)
.header("User-Agent", "donutbrowser")
.send()
@@ -380,7 +413,7 @@ impl ApiClient {
}
let firefox_response: FirefoxApiResponse = response.json().await?;
// Extract releases and filter for stable versions
let mut releases: Vec<BrowserRelease> = firefox_response
.releases
@@ -413,7 +446,7 @@ impl ApiClient {
// Extract versions for caching
let versions: Vec<String> = releases.iter().map(|r| r.version.clone()).collect();
// Cache the results (unless bypassing cache)
if !no_caching {
if let Err(e) = self.save_cached_versions("firefox", &versions) {
@@ -424,35 +457,50 @@ impl ApiClient {
Ok(releases)
}
pub async fn fetch_firefox_developer_releases_with_caching(&self, no_caching: bool) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_firefox_developer_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_versions) = self.load_cached_versions("firefox-developer") {
return Ok(cached_versions.into_iter().map(|version| {
BrowserRelease {
version: version.clone(),
date: "".to_string(), // Cache doesn't store dates
is_prerelease: is_alpha_version(&version),
download_url: Some(format!(
"https://download.mozilla.org/?product=devedition-{}&os=osx&lang=en-US",
version
)),
}
}).collect());
return Ok(
cached_versions
.into_iter()
.map(|version| {
BrowserRelease {
version: version.clone(),
date: "".to_string(), // Cache doesn't store dates
is_prerelease: is_alpha_version(&version),
download_url: Some(format!(
"https://download.mozilla.org/?product=devedition-{}&os=osx&lang=en-US",
version
)),
}
})
.collect(),
);
}
}
println!("Fetching Firefox Developer Edition releases from Mozilla API...");
let url = "https://product-details.mozilla.org/1.0/devedition.json";
let response = self.client
let response = self
.client
.get(url)
.header("User-Agent", "donutbrowser")
.send()
.await?;
if !response.status().is_success() {
return Err(format!("Failed to fetch Firefox Developer Edition versions: {}", response.status()).into());
return Err(
format!(
"Failed to fetch Firefox Developer Edition versions: {}",
response.status()
)
.into(),
);
}
let firefox_response: FirefoxApiResponse = response.json().await?;
@@ -489,7 +537,7 @@ impl ApiClient {
// Extract versions for caching
let versions: Vec<String> = releases.iter().map(|r| r.version.clone()).collect();
// Cache the results (unless bypassing cache)
if !no_caching {
if let Err(e) = self.save_cached_versions("firefox-developer", &versions) {
@@ -500,11 +548,16 @@ impl ApiClient {
Ok(releases)
}
pub async fn fetch_mullvad_releases(&self) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_mullvad_releases(
&self,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self.fetch_mullvad_releases_with_caching(false).await
}
pub async fn fetch_mullvad_releases_with_caching(&self, no_caching: bool) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_mullvad_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_releases) = self.load_cached_github_releases("mullvad") {
@@ -544,11 +597,16 @@ impl ApiClient {
Ok(releases)
}
pub async fn fetch_zen_releases(&self) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_zen_releases(
&self,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self.fetch_zen_releases_with_caching(false).await
}
pub async fn fetch_zen_releases_with_caching(&self, no_caching: bool) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_zen_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_releases) = self.load_cached_github_releases("zen") {
@@ -580,11 +638,16 @@ impl ApiClient {
Ok(releases)
}
pub async fn fetch_brave_releases(&self) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_brave_releases(
&self,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self.fetch_brave_releases_with_caching(false).await
}
pub async fn fetch_brave_releases_with_caching(&self, no_caching: bool) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_brave_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_releases) = self.load_cached_github_releases("brave") {
@@ -608,10 +671,11 @@ impl ApiClient {
.into_iter()
.filter_map(|mut release| {
// Check if this release has a universal DMG asset
let has_universal_dmg = release.assets.iter().any(|asset| {
asset.name.contains(".dmg") && asset.name.contains("universal")
});
let has_universal_dmg = release
.assets
.iter()
.any(|asset| asset.name.contains(".dmg") && asset.name.contains("universal"));
if has_universal_dmg {
// Set is_alpha based on the release name
// Nightly releases contain "Nightly", stable contain "Release"
@@ -636,10 +700,19 @@ impl ApiClient {
Ok(filtered_releases)
}
pub async fn fetch_chromium_latest_version(&self) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_chromium_latest_version(
&self,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// Use architecture-aware URL for Chromium
let arch = if cfg!(target_arch = "aarch64") { "Mac_Arm" } else { "Mac" };
let url = format!("https://commondatastorage.googleapis.com/chromium-browser-snapshots/{}/LAST_CHANGE", arch);
let arch = if cfg!(target_arch = "aarch64") {
"Mac_Arm"
} else {
"Mac"
};
let url = format!(
"https://commondatastorage.googleapis.com/chromium-browser-snapshots/{}/LAST_CHANGE",
arch
);
let version = self
.client
.get(&url)
@@ -654,27 +727,35 @@ impl ApiClient {
Ok(version)
}
pub async fn fetch_chromium_releases_with_caching(&self, no_caching: bool) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_chromium_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_versions) = self.load_cached_versions("chromium") {
return Ok(cached_versions.into_iter().map(|version| {
BrowserRelease {
version: version.clone(),
date: "".to_string(), // Cache doesn't store dates
is_prerelease: false, // Chromium versions are generally stable builds
download_url: None,
}
}).collect());
return Ok(
cached_versions
.into_iter()
.map(|version| {
BrowserRelease {
version: version.clone(),
date: "".to_string(), // Cache doesn't store dates
is_prerelease: false, // Chromium versions are generally stable builds
download_url: None,
}
})
.collect(),
);
}
}
println!("Fetching Chromium releases...");
// Get the latest version first
let latest_version = self.fetch_chromium_latest_version().await?;
let latest_num: u32 = latest_version.parse().unwrap_or(0);
// Generate a list of recent versions (last 20 builds, going back by 1000 each time)
let mut versions = Vec::new();
for i in 0..20 {
@@ -683,7 +764,7 @@ impl ApiClient {
versions.push(version_num.to_string());
}
}
// Cache the results (unless bypassing cache)
if !no_caching {
if let Err(e) = self.save_cached_versions("chromium", &versions) {
@@ -691,17 +772,23 @@ impl ApiClient {
}
}
Ok(versions.into_iter().map(|version| {
BrowserRelease {
version: version.clone(),
date: "".to_string(),
is_prerelease: false,
download_url: None,
}
}).collect())
Ok(
versions
.into_iter()
.map(|version| BrowserRelease {
version: version.clone(),
date: "".to_string(),
is_prerelease: false,
download_url: None,
})
.collect(),
)
}
pub async fn fetch_tor_releases_with_caching(&self, no_caching: bool) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_tor_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_versions) = self.load_cached_versions("tor-browser") {
@@ -732,7 +819,7 @@ impl ApiClient {
// Parse HTML to extract version directories
let mut version_candidates = Vec::new();
// Look for directory links in the HTML
for line in html.lines() {
if line.contains("<a href=\"") && line.contains("/\">") {
@@ -741,9 +828,12 @@ impl ApiClient {
let start = start + 9; // Length of "<a href=\""
if let Some(end) = line[start..].find("/\">") {
let version = &line[start..start + end];
// Skip parent directory and non-version entries
if version != ".." && !version.is_empty() && version.chars().next().unwrap_or('a').is_ascii_digit() {
if version != ".."
&& !version.is_empty()
&& version.chars().next().unwrap_or('a').is_ascii_digit()
{
version_candidates.push(version.to_string());
}
}
@@ -763,7 +853,7 @@ impl ApiClient {
version_strings.push(version);
}
}
// Add a small delay to avoid overwhelming the server
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
@@ -788,8 +878,14 @@ impl ApiClient {
}).collect())
}
async fn check_tor_version_has_macos(&self, version: &str) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://archive.torproject.org/tor-package-archive/torbrowser/{}/", version);
async fn check_tor_version_has_macos(
&self,
version: &str,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let url = format!(
"https://archive.torproject.org/tor-package-archive/torbrowser/{}/",
version
);
let html = self
.client
.get(&url)
@@ -908,16 +1004,22 @@ mod tests {
async fn test_firefox_api() {
let client = ApiClient::new();
let result = client.fetch_firefox_releases_with_caching(false).await;
match result {
Ok(releases) => {
assert!(!releases.is_empty(), "Should have Firefox releases");
// Check that releases have required fields
let first_release = &releases[0];
assert!(!first_release.version.is_empty(), "Version should not be empty");
assert!(first_release.download_url.is_some(), "Should have download URL");
assert!(
!first_release.version.is_empty(),
"Version should not be empty"
);
assert!(
first_release.download_url.is_some(),
"Should have download URL"
);
println!("Firefox API test passed. Found {} releases", releases.len());
println!("Latest version: {}", first_release.version);
}
@@ -931,19 +1033,33 @@ mod tests {
#[tokio::test]
async fn test_firefox_developer_api() {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; // Rate limiting
let client = ApiClient::new();
let result = client.fetch_firefox_developer_releases_with_caching(false).await;
let result = client
.fetch_firefox_developer_releases_with_caching(false)
.await;
match result {
Ok(releases) => {
assert!(!releases.is_empty(), "Should have Firefox Developer releases");
assert!(
!releases.is_empty(),
"Should have Firefox Developer releases"
);
let first_release = &releases[0];
assert!(!first_release.version.is_empty(), "Version should not be empty");
assert!(first_release.download_url.is_some(), "Should have download URL");
println!("Firefox Developer API test passed. Found {} releases", releases.len());
assert!(
!first_release.version.is_empty(),
"Version should not be empty"
);
assert!(
first_release.download_url.is_some(),
"Should have download URL"
);
println!(
"Firefox Developer API test passed. Found {} releases",
releases.len()
);
println!("Latest version: {}", first_release.version);
}
Err(e) => {
@@ -956,17 +1072,20 @@ mod tests {
#[tokio::test]
async fn test_mullvad_api() {
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; // Rate limiting
let client = ApiClient::new();
let result = client.fetch_mullvad_releases().await;
match result {
Ok(releases) => {
assert!(!releases.is_empty(), "Should have Mullvad releases");
let first_release = &releases[0];
assert!(!first_release.tag_name.is_empty(), "Tag name should not be empty");
assert!(
!first_release.tag_name.is_empty(),
"Tag name should not be empty"
);
println!("Mullvad API test passed. Found {} releases", releases.len());
println!("Latest version: {}", first_release.tag_name);
}
@@ -980,17 +1099,20 @@ mod tests {
#[tokio::test]
async fn test_zen_api() {
tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await; // Rate limiting
let client = ApiClient::new();
let result = client.fetch_zen_releases().await;
match result {
Ok(releases) => {
assert!(!releases.is_empty(), "Should have Zen releases");
let first_release = &releases[0];
assert!(!first_release.tag_name.is_empty(), "Tag name should not be empty");
assert!(
!first_release.tag_name.is_empty(),
"Tag name should not be empty"
);
println!("Zen API test passed. Found {} releases", releases.len());
println!("Latest version: {}", first_release.tag_name);
}
@@ -1004,14 +1126,17 @@ mod tests {
#[tokio::test]
async fn test_brave_api() {
tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await; // Rate limiting
let client = ApiClient::new();
let result = client.fetch_brave_releases().await;
match result {
Ok(releases) => {
// Note: Brave might not always have macOS releases, so we don't assert non-empty
println!("Brave API test passed. Found {} releases with macOS assets", releases.len());
println!(
"Brave API test passed. Found {} releases with macOS assets",
releases.len()
);
if !releases.is_empty() {
println!("Latest version: {}", releases[0].tag_name);
}
@@ -1026,15 +1151,18 @@ mod tests {
#[tokio::test]
async fn test_chromium_api() {
tokio::time::sleep(tokio::time::Duration::from_millis(2500)).await; // Rate limiting
let client = ApiClient::new();
let result = client.fetch_chromium_latest_version().await;
match result {
Ok(version) => {
assert!(!version.is_empty(), "Version should not be empty");
assert!(version.chars().all(|c| c.is_ascii_digit()), "Version should be numeric");
assert!(
version.chars().all(|c| c.is_ascii_digit()),
"Version should be numeric"
);
println!("Chromium API test passed. Latest version: {}", version);
}
Err(e) => {
@@ -1047,21 +1175,31 @@ mod tests {
#[tokio::test]
async fn test_tor_api() {
tokio::time::sleep(tokio::time::Duration::from_millis(3000)).await; // Rate limiting
let client = ApiClient::new();
// Use a timeout for this test since TOR API can be slow
let timeout_duration = tokio::time::Duration::from_secs(30);
let result = tokio::time::timeout(timeout_duration, client.fetch_tor_releases_with_caching(false)).await;
let result = tokio::time::timeout(
timeout_duration,
client.fetch_tor_releases_with_caching(false),
)
.await;
match result {
Ok(Ok(releases)) => {
assert!(!releases.is_empty(), "Should have TOR releases");
let first_release = &releases[0];
assert!(!first_release.version.is_empty(), "Version should not be empty");
assert!(first_release.download_url.is_some(), "Should have download URL");
assert!(
!first_release.version.is_empty(),
"Version should not be empty"
);
assert!(
first_release.download_url.is_some(),
"Should have download URL"
);
println!("TOR API test passed. Found {} releases", releases.len());
println!("Latest version: {}", first_release.version);
}
@@ -1081,14 +1219,17 @@ mod tests {
#[tokio::test]
async fn test_tor_version_check() {
tokio::time::sleep(tokio::time::Duration::from_millis(3500)).await; // Rate limiting
let client = ApiClient::new();
let result = client.check_tor_version_has_macos("14.0.4").await;
match result {
Ok(has_macos) => {
assert!(has_macos, "Version 14.0.4 should have macOS support");
println!("TOR version check test passed. Version 14.0.4 has macOS: {}", has_macos);
println!(
"TOR version check test passed. Version 14.0.4 has macOS: {}",
has_macos
);
}
Err(e) => {
println!("TOR version check test failed: {}", e);
@@ -1096,4 +1237,4 @@ mod tests {
}
}
}
}
}
File diff suppressed because it is too large Load Diff
+95 -51
View File
@@ -61,8 +61,6 @@ pub trait Browser: Send + Sync {
fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool;
}
pub struct FirefoxBrowser {
browser_type: BrowserType,
}
@@ -71,7 +69,6 @@ impl FirefoxBrowser {
pub fn new(browser_type: BrowserType) -> Self {
Self { browser_type }
}
}
impl Browser for FirefoxBrowser {
@@ -79,8 +76,6 @@ impl Browser for FirefoxBrowser {
self.browser_type.clone()
}
fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
// Find the .app directory
let app_path = std::fs::read_dir(install_dir)?
@@ -99,7 +94,11 @@ impl Browser for FirefoxBrowser {
.find(|entry| {
let binding = entry.file_name();
let name = binding.to_string_lossy();
name.starts_with("firefox") || name.starts_with("mullvad") || name.starts_with("zen") || name.starts_with("tor") || name.contains("Browser")
name.starts_with("firefox")
|| name.starts_with("mullvad")
|| name.starts_with("zen")
|| name.starts_with("tor")
|| name.contains("Browser")
})
.map(|entry| entry.path())
.ok_or("No executable found in MacOS directory")?;
@@ -113,11 +112,8 @@ impl Browser for FirefoxBrowser {
_proxy_settings: Option<&ProxySettings>,
url: Option<String>,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut args = vec![
"-profile".to_string(),
profile_path.to_string(),
];
let mut args = vec!["-profile".to_string(), profile_path.to_string()];
// Only use -no-remote for browsers that require it for security (Mullvad, Tor)
// Regular Firefox browsers can use remote commands for better URL handling
match self.browser_type {
@@ -129,7 +125,7 @@ impl Browser for FirefoxBrowser {
}
_ => {}
}
// Firefox-based browsers use profile directory and user.js for proxy configuration
if let Some(url) = url {
args.push(url);
@@ -143,7 +139,10 @@ impl Browser for FirefoxBrowser {
.join(self.browser_type().as_str())
.join(version);
println!("Firefox browser checking version {} in directory: {:?}", version, browser_dir);
println!(
"Firefox browser checking version {} in directory: {:?}",
version, browser_dir
);
// Only check if directory exists and contains a .app file
if browser_dir.exists() {
@@ -183,7 +182,6 @@ impl Browser for ChromiumBrowser {
self.browser_type.clone()
}
fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
// Find the .app directory
let app_path = std::fs::read_dir(install_dir)?
@@ -253,7 +251,10 @@ impl Browser for ChromiumBrowser {
.join(self.browser_type().as_str())
.join(version);
println!("Chromium browser checking version {} in directory: {:?}", version, browser_dir);
println!(
"Chromium browser checking version {} in directory: {:?}",
version, browser_dir
);
// Check if directory exists and contains at least one .app file
if browser_dir.exists() {
@@ -286,9 +287,11 @@ impl Browser for ChromiumBrowser {
// Factory function to create browser instances
pub fn create_browser(browser_type: BrowserType) -> Box<dyn Browser> {
match browser_type {
BrowserType::MullvadBrowser | BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen | BrowserType::TorBrowser => {
Box::new(FirefoxBrowser::new(browser_type))
}
BrowserType::MullvadBrowser
| BrowserType::Firefox
| BrowserType::FirefoxDeveloper
| BrowserType::Zen
| BrowserType::TorBrowser => Box::new(FirefoxBrowser::new(browser_type)),
BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)),
}
}
@@ -332,13 +335,28 @@ mod tests {
assert_eq!(BrowserType::TorBrowser.as_str(), "tor-browser");
// Test from_str
assert_eq!(BrowserType::from_str("mullvad-browser").unwrap(), BrowserType::MullvadBrowser);
assert_eq!(BrowserType::from_str("firefox").unwrap(), BrowserType::Firefox);
assert_eq!(BrowserType::from_str("firefox-developer").unwrap(), BrowserType::FirefoxDeveloper);
assert_eq!(BrowserType::from_str("chromium").unwrap(), BrowserType::Chromium);
assert_eq!(
BrowserType::from_str("mullvad-browser").unwrap(),
BrowserType::MullvadBrowser
);
assert_eq!(
BrowserType::from_str("firefox").unwrap(),
BrowserType::Firefox
);
assert_eq!(
BrowserType::from_str("firefox-developer").unwrap(),
BrowserType::FirefoxDeveloper
);
assert_eq!(
BrowserType::from_str("chromium").unwrap(),
BrowserType::Chromium
);
assert_eq!(BrowserType::from_str("brave").unwrap(), BrowserType::Brave);
assert_eq!(BrowserType::from_str("zen").unwrap(), BrowserType::Zen);
assert_eq!(BrowserType::from_str("tor-browser").unwrap(), BrowserType::TorBrowser);
assert_eq!(
BrowserType::from_str("tor-browser").unwrap(),
BrowserType::TorBrowser
);
// Test invalid browser type
assert!(BrowserType::from_str("invalid").is_err());
@@ -353,10 +371,10 @@ mod tests {
let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser);
assert_eq!(browser.browser_type(), BrowserType::MullvadBrowser);
let browser = FirefoxBrowser::new(BrowserType::TorBrowser);
assert_eq!(browser.browser_type(), BrowserType::TorBrowser);
let browser = FirefoxBrowser::new(BrowserType::Zen);
assert_eq!(browser.browser_type(), BrowserType::Zen);
}
@@ -381,10 +399,10 @@ mod tests {
let browser = create_browser(BrowserType::Zen);
assert_eq!(browser.browser_type(), BrowserType::Zen);
let browser = create_browser(BrowserType::TorBrowser);
assert_eq!(browser.browser_type(), BrowserType::TorBrowser);
let browser = create_browser(BrowserType::FirefoxDeveloper);
assert_eq!(browser.browser_type(), BrowserType::FirefoxDeveloper);
@@ -400,47 +418,71 @@ mod tests {
fn test_firefox_launch_args() {
// Test regular Firefox (should not use -no-remote)
let browser = FirefoxBrowser::new(BrowserType::Firefox);
let args = browser.create_launch_args("/path/to/profile", None, None).unwrap();
let args = browser
.create_launch_args("/path/to/profile", None, None)
.unwrap();
assert_eq!(args, vec!["-profile", "/path/to/profile"]);
assert!(!args.contains(&"-no-remote".to_string()));
let args = browser.create_launch_args("/path/to/profile", None, Some("https://example.com".to_string())).unwrap();
assert_eq!(args, vec!["-profile", "/path/to/profile", "https://example.com"]);
let args = browser
.create_launch_args(
"/path/to/profile",
None,
Some("https://example.com".to_string()),
)
.unwrap();
assert_eq!(
args,
vec!["-profile", "/path/to/profile", "https://example.com"]
);
// Test Mullvad Browser (should use -no-remote)
let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser);
let args = browser.create_launch_args("/path/to/profile", None, None).unwrap();
let args = browser
.create_launch_args("/path/to/profile", None, None)
.unwrap();
assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]);
// Test Tor Browser (should use -no-remote)
let browser = FirefoxBrowser::new(BrowserType::TorBrowser);
let args = browser.create_launch_args("/path/to/profile", None, None).unwrap();
let args = browser
.create_launch_args("/path/to/profile", None, None)
.unwrap();
assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]);
// Test Zen Browser (should not use -no-remote)
let browser = FirefoxBrowser::new(BrowserType::Zen);
let args = browser.create_launch_args("/path/to/profile", None, None).unwrap();
let args = browser
.create_launch_args("/path/to/profile", None, None)
.unwrap();
assert_eq!(args, vec!["-profile", "/path/to/profile"]);
assert!(!args.contains(&"-no-remote".to_string()));
}
#[test]
fn test_chromium_launch_args() {
let browser = ChromiumBrowser::new(BrowserType::Chromium);
let args = browser.create_launch_args("/path/to/profile", None, None).unwrap();
let args = browser
.create_launch_args("/path/to/profile", None, None)
.unwrap();
// Test that basic required arguments are present
assert!(args.contains(&"--user-data-dir=/path/to/profile".to_string()));
assert!(args.contains(&"--no-default-browser-check".to_string()));
// Test that automatic update disabling arguments are present
assert!(args.contains(&"--disable-background-mode".to_string()));
assert!(args.contains(&"--disable-component-update".to_string()));
let args_with_url = browser.create_launch_args("/path/to/profile", None, Some("https://example.com".to_string())).unwrap();
let args_with_url = browser
.create_launch_args(
"/path/to/profile",
None,
Some("https://example.com".to_string()),
)
.unwrap();
assert!(args_with_url.contains(&"https://example.com".to_string()));
// Verify URL is at the end
assert_eq!(args_with_url.last().unwrap(), "https://example.com");
}
@@ -458,7 +500,7 @@ mod tests {
assert_eq!(proxy.proxy_type, "http");
assert_eq!(proxy.host, "127.0.0.1");
assert_eq!(proxy.port, 8080);
// Test different proxy types
let socks_proxy = ProxySettings {
enabled: true,
@@ -466,13 +508,12 @@ mod tests {
host: "proxy.example.com".to_string(),
port: 1080,
};
assert_eq!(socks_proxy.proxy_type, "socks5");
assert_eq!(socks_proxy.host, "proxy.example.com");
assert_eq!(socks_proxy.port, 1080);
}
#[test]
fn test_version_downloaded_check() {
let temp_dir = TempDir::new().unwrap();
@@ -489,17 +530,20 @@ mod tests {
let browser = FirefoxBrowser::new(BrowserType::Firefox);
assert!(browser.is_version_downloaded("139.0", binaries_dir));
assert!(!browser.is_version_downloaded("140.0", binaries_dir));
// Test with Chromium browser
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");
let executable_path = chromium_app_dir
.join("Contents")
.join("MacOS")
.join("Chromium");
fs::write(&executable_path, "mock executable").unwrap();
let chromium_browser = ChromiumBrowser::new(BrowserType::Chromium);
assert!(chromium_browser.is_version_downloaded("1465660", binaries_dir));
assert!(!chromium_browser.is_version_downloaded("1465661", binaries_dir));
@@ -513,7 +557,7 @@ mod tests {
// Create browser directory but no .app directory
let browser_dir = binaries_dir.join("firefox").join("139.0");
fs::create_dir_all(&browser_dir).unwrap();
// Create some other files but no .app
fs::write(browser_dir.join("readme.txt"), "Some content").unwrap();
@@ -526,7 +570,7 @@ mod tests {
let browser_type = BrowserType::Firefox;
let cloned = browser_type.clone();
assert_eq!(browser_type, cloned);
// Test Debug trait
let debug_str = format!("{:?}", browser_type);
assert!(debug_str.contains("Firefox"));
@@ -540,13 +584,13 @@ mod tests {
host: "127.0.0.1".to_string(),
port: 8080,
};
// Test that it can be serialized (implements Serialize)
let json = serde_json::to_string(&proxy).unwrap();
assert!(json.contains("127.0.0.1"));
assert!(json.contains("8080"));
assert!(json.contains("http"));
// Test that it can be deserialized (implements Deserialize)
let deserialized: ProxySettings = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.enabled, proxy.enabled);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+34 -18
View File
@@ -4,10 +4,7 @@ use tauri::command;
mod macos {
use core_foundation::base::OSStatus;
use core_foundation::string::CFStringRef;
use core_foundation::{
base::TCFType,
string::CFString,
};
use core_foundation::{base::TCFType, string::CFString};
#[link(name = "CoreServices", kind = "framework")]
extern "C" {
@@ -18,7 +15,7 @@ mod macos {
pub fn is_default_browser() -> Result<bool, String> {
let schemes = ["http", "https"];
let bundle_id = "com.donutbrowser";
for scheme in schemes {
let scheme_str = CFString::new(scheme);
unsafe {
@@ -26,10 +23,10 @@ mod macos {
if current_handler.is_null() {
return Ok(false);
}
let current_handler_cf = CFString::wrap_under_create_rule(current_handler);
let current_handler_str = current_handler_cf.to_string();
if current_handler_str != bundle_id {
return Ok(false);
}
@@ -123,14 +120,21 @@ pub async fn set_as_default_browser() -> Result<(), String> {
}
#[tauri::command]
pub async fn open_url_with_profile(app_handle: tauri::AppHandle, profile_name: String, url: String) -> Result<(), String> {
pub async fn open_url_with_profile(
app_handle: tauri::AppHandle,
profile_name: String,
url: String,
) -> Result<(), String> {
use crate::browser_runner::BrowserRunner;
let runner = BrowserRunner::new();
// Get the profile by name
let profiles = runner.list_profiles().map_err(|e| format!("Failed to list profiles: {}", e))?;
let profile = profiles.into_iter()
let profiles = runner
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {}", e))?;
let profile = profiles
.into_iter()
.find(|p| p.name == profile_name)
.ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
@@ -145,25 +149,37 @@ pub async fn open_url_with_profile(app_handle: tauri::AppHandle, profile_name: S
format!("Failed to open URL with profile: {}", e)
})?;
println!("Successfully opened URL '{}' with profile '{}'", url, profile_name);
println!(
"Successfully opened URL '{}' with profile '{}'",
url, profile_name
);
Ok(())
}
#[tauri::command]
pub async fn smart_open_url(_app_handle: tauri::AppHandle, _url: String, _is_startup: Option<bool>) -> Result<String, String> {
pub async fn smart_open_url(
_app_handle: tauri::AppHandle,
_url: String,
_is_startup: Option<bool>,
) -> Result<String, String> {
use crate::browser_runner::BrowserRunner;
let runner = BrowserRunner::new();
// Get all profiles
let profiles = runner.list_profiles().map_err(|e| format!("Failed to list profiles: {}", e))?;
let profiles = runner
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {}", e))?;
if profiles.is_empty() {
return Err("no_profiles".to_string());
}
println!("URL opening - Total profiles: {}, showing profile selector", profiles.len());
println!(
"URL opening - Total profiles: {}, showing profile selector",
profiles.len()
);
// Always show the profile selector so the user can choose
Err("show_selector".to_string())
}
+86 -76
View File
@@ -45,60 +45,67 @@ impl Downloader {
BrowserType::Brave => {
// For Brave, we need to find the actual macOS asset
let releases = self.api_client.fetch_brave_releases().await?;
// Find the release with the matching version
let release = releases
.iter()
.find(|r| r.tag_name == version || r.tag_name == format!("v{}", version.trim_start_matches('v')))
.find(|r| {
r.tag_name == version || r.tag_name == format!("v{}", version.trim_start_matches('v'))
})
.ok_or(format!("Brave version {} not found", version))?;
// Find the universal macOS DMG asset
let asset = release
.assets
.iter()
.find(|asset| {
asset.name.contains(".dmg") && asset.name.contains("universal")
})
.ok_or(format!("No universal macOS DMG asset found for Brave version {}", version))?;
.find(|asset| asset.name.contains(".dmg") && asset.name.contains("universal"))
.ok_or(format!(
"No universal macOS DMG asset found for Brave version {}",
version
))?;
Ok(asset.browser_download_url.clone())
}
BrowserType::Zen => {
// For Zen, verify the asset exists
let releases = self.api_client.fetch_zen_releases().await?;
let release = releases
.iter()
.find(|r| r.tag_name == version)
.ok_or(format!("Zen version {} not found", version))?;
// Find the macOS universal DMG asset
let asset = release
.assets
.iter()
.find(|asset| asset.name == "zen.macos-universal.dmg")
.ok_or(format!("No macOS universal asset found for Zen version {}", version))?;
.ok_or(format!(
"No macOS universal asset found for Zen version {}",
version
))?;
Ok(asset.browser_download_url.clone())
}
BrowserType::MullvadBrowser => {
// For Mullvad, verify the asset exists
let releases = self.api_client.fetch_mullvad_releases().await?;
let release = releases
.iter()
.find(|r| r.tag_name == version)
.ok_or(format!("Mullvad version {} not found", version))?;
// Find the macOS DMG asset
let asset = release
.assets
.iter()
.find(|asset| {
asset.name.contains(".dmg") && asset.name.contains("mac")
})
.ok_or(format!("No macOS asset found for Mullvad version {}", version))?;
.find(|asset| asset.name.contains(".dmg") && asset.name.contains("mac"))
.ok_or(format!(
"No macOS asset found for Mullvad version {}",
version
))?;
Ok(asset.browser_download_url.clone())
}
_ => {
@@ -117,10 +124,12 @@ impl Downloader {
dest_path: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let file_path = dest_path.join(&download_info.filename);
// Resolve the actual download URL
let download_url = self.resolve_download_url(browser_type.clone(), version, download_info).await?;
let download_url = self
.resolve_download_url(browser_type.clone(), version, download_info)
.await?;
// Emit initial progress
let progress = DownloadProgress {
browser: browser_type.as_str().to_string(),
@@ -132,7 +141,7 @@ impl Downloader {
eta_seconds: None,
stage: "downloading".to_string(),
};
let _ = app_handle.emit("download-progress", &progress);
// Start download
@@ -161,7 +170,11 @@ impl Downloader {
// Update progress every 100ms to avoid too many events
if now.duration_since(last_update).as_millis() >= 100 {
let elapsed = start_time.elapsed().as_secs_f64();
let speed = if elapsed > 0.0 { downloaded as f64 / elapsed } else { 0.0 };
let speed = if elapsed > 0.0 {
downloaded as f64 / elapsed
} else {
0.0
};
let percentage = if let Some(total) = total_size {
(downloaded as f64 / total as f64) * 100.0
} else {
@@ -201,20 +214,18 @@ mod tests {
#[tokio::test]
async fn test_resolve_brave_download_url() {
let downloader = Downloader::new();
// Test with a known Brave version
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "brave-test.dmg".to_string(),
is_archive: true,
};
let result = downloader.resolve_download_url(
BrowserType::Brave,
"v1.81.9",
&download_info
).await;
let result = downloader
.resolve_download_url(BrowserType::Brave, "v1.81.9", &download_info)
.await;
match result {
Ok(url) => {
assert!(url.contains("github.com/brave/brave-browser"));
@@ -223,7 +234,10 @@ mod tests {
println!("Brave download URL resolved: {}", url);
}
Err(e) => {
println!("Brave URL resolution failed (expected if version doesn't exist): {}", e);
println!(
"Brave URL resolution failed (expected if version doesn't exist): {}",
e
);
// This might fail if the version doesn't exist, which is okay for testing
}
}
@@ -232,19 +246,17 @@ mod tests {
#[tokio::test]
async fn test_resolve_zen_download_url() {
let downloader = Downloader::new();
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "zen-test.dmg".to_string(),
is_archive: true,
};
let result = downloader.resolve_download_url(
BrowserType::Zen,
"1.11b",
&download_info
).await;
let result = downloader
.resolve_download_url(BrowserType::Zen, "1.11b", &download_info)
.await;
match result {
Ok(url) => {
assert!(url.contains("github.com/zen-browser/desktop"));
@@ -252,7 +264,10 @@ mod tests {
println!("Zen download URL resolved: {}", url);
}
Err(e) => {
println!("Zen URL resolution failed (expected if version doesn't exist): {}", e);
println!(
"Zen URL resolution failed (expected if version doesn't exist): {}",
e
);
}
}
}
@@ -260,19 +275,17 @@ mod tests {
#[tokio::test]
async fn test_resolve_mullvad_download_url() {
let downloader = Downloader::new();
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "mullvad-test.dmg".to_string(),
is_archive: true,
};
let result = downloader.resolve_download_url(
BrowserType::MullvadBrowser,
"14.5a6",
&download_info
).await;
let result = downloader
.resolve_download_url(BrowserType::MullvadBrowser, "14.5a6", &download_info)
.await;
match result {
Ok(url) => {
assert!(url.contains("github.com/mullvad/mullvad-browser"));
@@ -280,7 +293,10 @@ mod tests {
println!("Mullvad download URL resolved: {}", url);
}
Err(e) => {
println!("Mullvad URL resolution failed (expected if version doesn't exist): {}", e);
println!(
"Mullvad URL resolution failed (expected if version doesn't exist): {}",
e
);
}
}
}
@@ -288,19 +304,17 @@ mod tests {
#[tokio::test]
async fn test_resolve_firefox_download_url() {
let downloader = Downloader::new();
let download_info = DownloadInfo {
url: "https://download.mozilla.org/?product=firefox-139.0&os=osx&lang=en-US".to_string(),
filename: "firefox-test.dmg".to_string(),
is_archive: true,
};
let result = downloader.resolve_download_url(
BrowserType::Firefox,
"139.0",
&download_info
).await;
let result = downloader
.resolve_download_url(BrowserType::Firefox, "139.0", &download_info)
.await;
match result {
Ok(url) => {
assert_eq!(url, download_info.url);
@@ -315,19 +329,17 @@ mod tests {
#[tokio::test]
async fn test_resolve_chromium_download_url() {
let downloader = Downloader::new();
let download_info = DownloadInfo {
url: "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac/1465660/chrome-mac.zip".to_string(),
filename: "chromium-test.zip".to_string(),
is_archive: true,
};
let result = downloader.resolve_download_url(
BrowserType::Chromium,
"1465660",
&download_info
).await;
let result = downloader
.resolve_download_url(BrowserType::Chromium, "1465660", &download_info)
.await;
match result {
Ok(url) => {
assert_eq!(url, download_info.url);
@@ -342,19 +354,17 @@ mod tests {
#[tokio::test]
async fn test_resolve_tor_download_url() {
let downloader = Downloader::new();
let download_info = DownloadInfo {
url: "https://archive.torproject.org/tor-package-archive/torbrowser/14.0.4/tor-browser-macos-14.0.4.dmg".to_string(),
filename: "tor-test.dmg".to_string(),
is_archive: true,
};
let result = downloader.resolve_download_url(
BrowserType::TorBrowser,
"14.0.4",
&download_info
).await;
let result = downloader
.resolve_download_url(BrowserType::TorBrowser, "14.0.4", &download_info)
.await;
match result {
Ok(url) => {
assert_eq!(url, download_info.url);
@@ -365,4 +375,4 @@ mod tests {
}
}
}
}
}
+239 -216
View File
@@ -1,258 +1,281 @@
use directories::BaseDirs;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use directories::BaseDirs;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DownloadedBrowserInfo {
pub browser: String,
pub version: String,
pub download_date: u64,
pub file_path: PathBuf,
pub verified: bool,
pub actual_version: Option<String>, // For browsers like Chromium where we track the actual version
pub browser: String,
pub version: String,
pub download_date: u64,
pub file_path: PathBuf,
pub verified: bool,
pub actual_version: Option<String>, // For browsers like Chromium where we track the actual version
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct DownloadedBrowsersRegistry {
pub browsers: HashMap<String, HashMap<String, DownloadedBrowserInfo>>, // browser -> version -> info
pub browsers: HashMap<String, HashMap<String, DownloadedBrowserInfo>>, // browser -> version -> info
}
impl DownloadedBrowsersRegistry {
pub fn new() -> Self {
Self::default()
pub fn new() -> Self {
Self::default()
}
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let registry_path = Self::get_registry_path()?;
if !registry_path.exists() {
return Ok(Self::new());
}
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let registry_path = Self::get_registry_path()?;
if !registry_path.exists() {
return Ok(Self::new());
}
let content = fs::read_to_string(&registry_path)?;
let registry: DownloadedBrowsersRegistry = serde_json::from_str(&content)?;
Ok(registry)
}
let content = fs::read_to_string(&registry_path)?;
let registry: DownloadedBrowsersRegistry = serde_json::from_str(&content)?;
Ok(registry)
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
let registry_path = Self::get_registry_path()?;
// Ensure parent directory exists
if let Some(parent) = registry_path.parent() {
fs::create_dir_all(parent)?;
}
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
let registry_path = Self::get_registry_path()?;
// Ensure parent directory exists
if let Some(parent) = registry_path.parent() {
fs::create_dir_all(parent)?;
}
let content = serde_json::to_string_pretty(self)?;
fs::write(&registry_path, content)?;
Ok(())
}
let content = serde_json::to_string_pretty(self)?;
fs::write(&registry_path, content)?;
Ok(())
fn get_registry_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let mut path = base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
path.push("data");
path.push("downloaded_browsers.json");
Ok(path)
}
pub fn add_browser(&mut self, info: DownloadedBrowserInfo) {
self
.browsers
.entry(info.browser.clone())
.or_insert_with(HashMap::new)
.insert(info.version.clone(), info);
}
pub fn remove_browser(&mut self, browser: &str, version: &str) -> Option<DownloadedBrowserInfo> {
self.browsers.get_mut(browser)?.remove(version)
}
pub fn is_browser_downloaded(&self, browser: &str, version: &str) -> bool {
self
.browsers
.get(browser)
.and_then(|versions| versions.get(version))
.map(|info| info.verified)
.unwrap_or(false)
}
pub fn get_downloaded_versions(&self, browser: &str) -> Vec<String> {
self
.browsers
.get(browser)
.map(|versions| {
versions
.iter()
.filter(|(_, info)| info.verified)
.map(|(version, _)| version.clone())
.collect()
})
.unwrap_or_default()
}
pub fn mark_download_started(&mut self, browser: &str, version: &str, file_path: PathBuf) {
let info = DownloadedBrowserInfo {
browser: browser.to_string(),
version: version.to_string(),
download_date: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
file_path,
verified: false,
actual_version: None,
};
self.add_browser(info);
}
pub fn mark_download_completed_with_actual_version(
&mut self,
browser: &str,
version: &str,
actual_version: Option<String>,
) -> Result<(), String> {
if let Some(info) = self
.browsers
.get_mut(browser)
.and_then(|versions| versions.get_mut(version))
{
info.verified = true;
info.actual_version = actual_version;
Ok(())
} else {
Err(format!(
"Browser {}:{} not found in registry",
browser, version
))
}
}
fn get_registry_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let mut path = base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" });
path.push("data");
path.push("downloaded_browsers.json");
Ok(path)
}
pub fn add_browser(&mut self, info: DownloadedBrowserInfo) {
self.browsers
.entry(info.browser.clone())
.or_insert_with(HashMap::new)
.insert(info.version.clone(), info);
}
pub fn remove_browser(&mut self, browser: &str, version: &str) -> Option<DownloadedBrowserInfo> {
self.browsers
.get_mut(browser)?
.remove(version)
}
pub fn is_browser_downloaded(&self, browser: &str, version: &str) -> bool {
self.browsers
.get(browser)
.and_then(|versions| versions.get(version))
.map(|info| info.verified)
.unwrap_or(false)
}
pub fn get_downloaded_versions(&self, browser: &str) -> Vec<String> {
self.browsers
.get(browser)
.map(|versions| {
versions
.iter()
.filter(|(_, info)| info.verified)
.map(|(version, _)| version.clone())
.collect()
})
.unwrap_or_default()
}
pub fn mark_download_started(&mut self, browser: &str, version: &str, file_path: PathBuf) {
let info = DownloadedBrowserInfo {
browser: browser.to_string(),
version: version.to_string(),
download_date: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
file_path,
verified: false,
actual_version: None,
};
self.add_browser(info);
}
pub fn mark_download_completed_with_actual_version(&mut self, browser: &str, version: &str, actual_version: Option<String>) -> Result<(), String> {
if let Some(info) = self.browsers
.get_mut(browser)
.and_then(|versions| versions.get_mut(version))
{
info.verified = true;
info.actual_version = actual_version;
Ok(())
pub fn cleanup_failed_download(
&mut self,
browser: &str,
version: &str,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(info) = self.remove_browser(browser, version) {
// Clean up any files that might have been left behind
if info.file_path.exists() {
if info.file_path.is_dir() {
fs::remove_dir_all(&info.file_path)?;
} else {
Err(format!("Browser {}:{} not found in registry", browser, version))
fs::remove_file(&info.file_path)?;
}
}
}
pub fn cleanup_failed_download(&mut self, browser: &str, version: &str) -> Result<(), Box<dyn std::error::Error>> {
if let Some(info) = self.remove_browser(browser, version) {
// Clean up any files that might have been left behind
if info.file_path.exists() {
if info.file_path.is_dir() {
fs::remove_dir_all(&info.file_path)?;
} else {
fs::remove_file(&info.file_path)?;
}
}
// Also clean up the browser directory if it exists
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let mut browser_dir = base_dirs.data_local_dir().to_path_buf();
browser_dir.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" });
browser_dir.push("binaries");
browser_dir.push(browser);
browser_dir.push(version);
if browser_dir.exists() {
fs::remove_dir_all(&browser_dir)?;
}
}
Ok(())
// Also clean up the browser directory if it exists
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let mut browser_dir = base_dirs.data_local_dir().to_path_buf();
browser_dir.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
browser_dir.push("binaries");
browser_dir.push(browser);
browser_dir.push(version);
if browser_dir.exists() {
fs::remove_dir_all(&browser_dir)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn test_registry_creation() {
let registry = DownloadedBrowsersRegistry::new();
assert!(registry.browsers.is_empty());
}
#[test]
fn test_registry_creation() {
let registry = DownloadedBrowsersRegistry::new();
assert!(registry.browsers.is_empty());
}
#[test]
fn test_add_and_get_browser() {
let mut registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path"),
verified: true,
actual_version: None,
};
#[test]
fn test_add_and_get_browser() {
let mut registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path"),
verified: true,
actual_version: None,
};
registry.add_browser(info.clone());
registry.add_browser(info.clone());
assert!(registry.is_browser_downloaded("firefox", "139.0"));
assert!(!registry.is_browser_downloaded("firefox", "140.0"));
assert!(!registry.is_browser_downloaded("chrome", "139.0"));
}
assert!(registry.is_browser_downloaded("firefox", "139.0"));
assert!(!registry.is_browser_downloaded("firefox", "140.0"));
assert!(!registry.is_browser_downloaded("chrome", "139.0"));
}
#[test]
fn test_get_downloaded_versions() {
let mut registry = DownloadedBrowsersRegistry::new();
let info1 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path1"),
verified: true,
actual_version: None,
};
let info2 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "140.0".to_string(),
download_date: 1234567891,
file_path: PathBuf::from("/test/path2"),
verified: false, // Not verified, should not be included
actual_version: None,
};
#[test]
fn test_get_downloaded_versions() {
let mut registry = DownloadedBrowsersRegistry::new();
let info3 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "141.0".to_string(),
download_date: 1234567892,
file_path: PathBuf::from("/test/path3"),
verified: true,
actual_version: None,
};
let info1 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path1"),
verified: true,
actual_version: None,
};
registry.add_browser(info1);
registry.add_browser(info2);
registry.add_browser(info3);
let info2 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "140.0".to_string(),
download_date: 1234567891,
file_path: PathBuf::from("/test/path2"),
verified: false, // Not verified, should not be included
actual_version: None,
};
let versions = registry.get_downloaded_versions("firefox");
assert_eq!(versions.len(), 2);
assert!(versions.contains(&"139.0".to_string()));
assert!(versions.contains(&"141.0".to_string()));
assert!(!versions.contains(&"140.0".to_string()));
}
let info3 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "141.0".to_string(),
download_date: 1234567892,
file_path: PathBuf::from("/test/path3"),
verified: true,
actual_version: None,
};
#[test]
fn test_mark_download_lifecycle() {
let mut registry = DownloadedBrowsersRegistry::new();
// Mark download started
registry.mark_download_started("firefox", "139.0", PathBuf::from("/test/path"));
// Should not be considered downloaded yet
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
// Mark as completed
registry.mark_download_completed_with_actual_version("firefox", "139.0", Some("139.0".to_string())).unwrap();
// Now should be considered downloaded
assert!(registry.is_browser_downloaded("firefox", "139.0"));
}
registry.add_browser(info1);
registry.add_browser(info2);
registry.add_browser(info3);
#[test]
fn test_remove_browser() {
let mut registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path"),
verified: true,
actual_version: None,
};
let versions = registry.get_downloaded_versions("firefox");
assert_eq!(versions.len(), 2);
assert!(versions.contains(&"139.0".to_string()));
assert!(versions.contains(&"141.0".to_string()));
assert!(!versions.contains(&"140.0".to_string()));
}
registry.add_browser(info);
assert!(registry.is_browser_downloaded("firefox", "139.0"));
#[test]
fn test_mark_download_lifecycle() {
let mut registry = DownloadedBrowsersRegistry::new();
let removed = registry.remove_browser("firefox", "139.0");
assert!(removed.is_some());
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
}
}
// Mark download started
registry.mark_download_started("firefox", "139.0", PathBuf::from("/test/path"));
// Should not be considered downloaded yet
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
// Mark as completed
registry
.mark_download_completed_with_actual_version("firefox", "139.0", Some("139.0".to_string()))
.unwrap();
// Now should be considered downloaded
assert!(registry.is_browser_downloaded("firefox", "139.0"));
}
#[test]
fn test_remove_browser() {
let mut registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path"),
verified: true,
actual_version: None,
};
registry.add_browser(info);
assert!(registry.is_browser_downloaded("firefox", "139.0"));
let removed = registry.remove_browser("firefox", "139.0");
assert!(removed.is_some());
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
}
}
+22 -22
View File
@@ -3,8 +3,8 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use tauri::Emitter;
use crate::download::DownloadProgress;
use crate::browser::BrowserType;
use crate::download::DownloadProgress;
pub struct Extractor;
@@ -176,7 +176,7 @@ impl Extractor {
// Find the extracted .app directory or Chromium.app specifically
let mut app_path: Option<PathBuf> = None;
// First, try to find any .app file in the destination directory
if let Ok(entries) = fs::read_dir(dest_dir) {
for entry in entries {
@@ -197,7 +197,7 @@ impl Extractor {
let target_path = dest_dir.join(sub_path.file_name().unwrap());
fs::rename(&sub_path, &target_path)?;
app_path = Some(target_path);
// Clean up the now-empty subdirectory
let _ = fs::remove_dir_all(&path);
break;
@@ -247,16 +247,16 @@ mod tests {
let temp_dir = TempDir::new().unwrap();
let fake_archive = temp_dir.path().join("test.rar");
File::create(&fake_archive).unwrap();
// Create a mock app handle (this won't work in real tests without Tauri runtime)
// For now, we'll just test the logic without the actual extraction
// Test that unsupported formats return an error
let extension = fake_archive
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
assert_eq!(extension, "rar");
// We know this would fail with "Unsupported archive format: rar"
}
@@ -265,13 +265,13 @@ mod tests {
fn test_dmg_path_validation() {
let temp_dir = TempDir::new().unwrap();
let dmg_path = temp_dir.path().join("test.dmg");
// Test that we can identify DMG files correctly
let extension = dmg_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
assert_eq!(extension, "dmg");
}
@@ -279,13 +279,13 @@ mod tests {
fn test_zip_path_validation() {
let temp_dir = TempDir::new().unwrap();
let zip_path = temp_dir.path().join("test.zip");
// Test that we can identify ZIP files correctly
let extension = zip_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
assert_eq!(extension, "zip");
}
@@ -299,9 +299,9 @@ mod tests {
.unwrap()
.as_secs()
));
std::thread::sleep(std::time::Duration::from_millis(10));
let mount_point2 = std::env::temp_dir().join(format!(
"donut_mount_{}",
std::time::SystemTime::now()
@@ -309,7 +309,7 @@ mod tests {
.unwrap()
.as_secs()
));
// They should be different (or at least have the potential to be)
assert!(mount_point1.to_string_lossy().contains("donut_mount_"));
assert!(mount_point2.to_string_lossy().contains("donut_mount_"));
@@ -318,18 +318,18 @@ mod tests {
#[test]
fn test_app_path_detection() {
let temp_dir = TempDir::new().unwrap();
// Create a fake .app directory
let app_dir = temp_dir.path().join("TestApp.app");
std::fs::create_dir_all(&app_dir).unwrap();
// Test finding .app directories
let entries: Vec<_> = fs::read_dir(temp_dir.path())
.unwrap()
.filter_map(Result::ok)
.filter(|entry| entry.path().extension().map_or(false, |ext| ext == "app"))
.collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].file_name(), "TestApp.app");
}
@@ -337,17 +337,17 @@ mod tests {
#[test]
fn test_nested_app_detection() {
let temp_dir = TempDir::new().unwrap();
// Create a nested structure like Chromium
let chrome_dir = temp_dir.path().join("chrome-mac");
std::fs::create_dir_all(&chrome_dir).unwrap();
let app_dir = chrome_dir.join("Chromium.app");
std::fs::create_dir_all(&app_dir).unwrap();
// Test finding nested .app directories
let mut found_app = false;
if let Ok(entries) = fs::read_dir(temp_dir.path()) {
for entry in entries {
if let Ok(entry) = entry {
@@ -368,7 +368,7 @@ mod tests {
}
}
}
assert!(found_app);
}
}
}
+40 -31
View File
@@ -1,7 +1,7 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{Manager, Emitter};
use tauri::{Emitter, Manager};
use tauri_plugin_deep_link::DeepLinkExt;
// Store pending URLs that need to be handled when the window is ready
@@ -23,28 +23,33 @@ mod version_updater;
extern crate lazy_static;
use browser_runner::{
check_browser_status, create_browser_profile, create_browser_profile_new, delete_profile,
download_browser, fetch_browser_versions, fetch_browser_versions_cached_first,
fetch_browser_versions_detailed, fetch_browser_versions_with_count, fetch_browser_versions_with_count_cached_first,
get_cached_browser_versions_detailed, get_downloaded_browser_versions, get_saved_mullvad_releases, get_supported_browsers, is_browser_downloaded, check_browser_exists,
kill_browser_profile, launch_browser_profile, list_browser_profiles, rename_profile, should_update_browser_cache, update_profile_proxy,
update_profile_version,
check_browser_exists, check_browser_status, create_browser_profile, create_browser_profile_new,
delete_profile, download_browser, fetch_browser_versions, fetch_browser_versions_cached_first,
fetch_browser_versions_detailed, fetch_browser_versions_with_count,
fetch_browser_versions_with_count_cached_first, get_cached_browser_versions_detailed,
get_downloaded_browser_versions, get_saved_mullvad_releases, get_supported_browsers,
is_browser_downloaded, kill_browser_profile, launch_browser_profile, list_browser_profiles,
rename_profile, should_update_browser_cache, update_profile_proxy, update_profile_version,
};
use settings_manager::{
disable_default_browser_prompt, get_app_settings, save_app_settings,
should_show_settings_on_startup, get_table_sorting_settings, save_table_sorting_settings,
disable_default_browser_prompt, get_app_settings, get_table_sorting_settings, save_app_settings,
save_table_sorting_settings, should_show_settings_on_startup,
};
use default_browser::{is_default_browser, open_url_with_profile, set_as_default_browser, smart_open_url};
use default_browser::{
is_default_browser, open_url_with_profile, set_as_default_browser, smart_open_url,
};
use version_updater::{trigger_manual_version_update, get_version_update_status, get_version_updater, check_version_update_needed, force_version_update_check};
use version_updater::{
check_version_update_needed, force_version_update_check, get_version_update_status,
get_version_updater, trigger_manual_version_update,
};
use auto_updater::{
check_for_browser_updates, start_browser_update, complete_browser_update,
is_browser_disabled_for_update, dismiss_update_notification,
complete_browser_update_with_auto_update,
mark_auto_update_download, remove_auto_update_download, is_auto_update_download,
check_for_browser_updates, complete_browser_update, complete_browser_update_with_auto_update,
dismiss_update_notification, is_auto_update_download, is_browser_disabled_for_update,
mark_auto_update_download, remove_auto_update_download, start_browser_update,
};
#[tauri::command]
@@ -57,13 +62,14 @@ fn greet() -> String {
#[tauri::command]
async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), String> {
println!("handle_url_open called with URL: {}", url);
// Check if the main window exists and is ready
if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or(false) {
// Window is visible, emit event directly
println!("Main window is visible, emitting show-profile-selector event");
app.emit("show-profile-selector", url.clone())
app
.emit("show-profile-selector", url.clone())
.map_err(|e| format!("Failed to emit URL open event: {}", e))?;
let _ = window.show();
let _ = window.set_focus();
@@ -79,7 +85,7 @@ async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), Strin
let mut pending = PENDING_URLS.lock().unwrap();
pending.push(url);
}
Ok(())
}
@@ -91,10 +97,13 @@ async fn check_and_handle_startup_url(app_handle: tauri::AppHandle) -> Result<bo
pending.clear(); // Clear after getting them
urls
};
if !pending_urls.is_empty() {
println!("Handling {} pending URLs from frontend request", pending_urls.len());
println!(
"Handling {} pending URLs from frontend request",
pending_urls.len()
);
for url in pending_urls {
println!("Emitting show-profile-selector event for URL: {}", url);
if let Err(e) = app_handle.emit("show-profile-selector", url.clone()) {
@@ -102,10 +111,10 @@ async fn check_and_handle_startup_url(app_handle: tauri::AppHandle) -> Result<bo
return Err(format!("Failed to emit URL event: {}", e));
}
}
return Ok(true);
}
Ok(false)
}
@@ -119,13 +128,13 @@ pub fn run() {
.setup(|app| {
// Set up deep link handler
let handle = app.handle().clone();
#[cfg(any(windows, target_os = "linux"))]
{
// For Windows and Linux, register all deep links at runtime for development
app.deep_link().register_all()?;
}
// Handle deep links - this works for both scenarios:
// 1. App is running and URL is opened
// 2. App is not running and URL causes app to launch
@@ -136,10 +145,10 @@ pub fn run() {
for url in urls {
let url_string = url.to_string();
println!("Deep link received: {}", url_string);
// Clone the handle for each async task
let handle_clone = handle.clone();
// Handle the URL asynchronously
tauri::async_runtime::spawn(async move {
if let Err(e) = handle_url_open(handle_clone, url_string.clone()).await {
@@ -155,14 +164,14 @@ pub fn run() {
tauri::async_runtime::spawn(async move {
let version_updater = get_version_updater();
let mut updater_guard = version_updater.lock().await;
// Set the app handle
updater_guard.set_app_handle(app_handle).await;
// Start the background updates
updater_guard.start_background_updates().await;
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -173,7 +182,7 @@ pub fn run() {
is_browser_downloaded,
check_browser_exists,
create_browser_profile_new,
create_browser_profile, // Keep for backward compatibility
create_browser_profile, // Keep for backward compatibility
list_browser_profiles,
launch_browser_profile,
fetch_browser_versions,
-2
View File
@@ -174,8 +174,6 @@ impl ProxyManager {
})
}
// Get stored proxy info for a profile
pub fn get_profile_proxy_info(&self, profile_name: &str) -> Option<(String, u16)> {
let profile_proxies = self.profile_proxies.lock().unwrap();
+20 -7
View File
@@ -66,7 +66,11 @@ impl SettingsManager {
pub fn get_settings_dir(&self) -> PathBuf {
let mut path = self.base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" });
path.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
path.push("settings");
path
}
@@ -88,25 +92,31 @@ impl SettingsManager {
}
let content = fs::read_to_string(&settings_file)?;
// Parse the settings file - serde will use default values for missing fields
match serde_json::from_str::<AppSettings>(&content) {
Ok(settings) => {
// Save the settings back to ensure any missing fields are written with defaults
if let Err(e) = self.save_settings(&settings) {
eprintln!("Warning: Failed to update settings file with defaults: {}", e);
eprintln!(
"Warning: Failed to update settings file with defaults: {}",
e
);
}
Ok(settings)
}
Err(e) => {
eprintln!("Warning: Failed to parse settings file, using defaults: {}", e);
eprintln!(
"Warning: Failed to parse settings file, using defaults: {}",
e
);
let default_settings = AppSettings::default();
// Try to save default settings to fix the corrupted file
if let Err(save_error) = self.save_settings(&default_settings) {
eprintln!("Warning: Failed to save default settings: {}", save_error);
}
Ok(default_settings)
}
}
@@ -136,7 +146,10 @@ impl SettingsManager {
Ok(sorting)
}
pub fn save_table_sorting(&self, sorting: &TableSortingSettings) -> Result<(), Box<dyn std::error::Error>> {
pub fn save_table_sorting(
&self,
sorting: &TableSortingSettings,
) -> Result<(), Box<dyn std::error::Error>> {
let settings_dir = self.get_settings_dir();
create_dir_all(&settings_dir)?;
File diff suppressed because it is too large Load Diff
+26 -23
View File
@@ -63,7 +63,7 @@ export default function Home() {
try {
const profileList = await invoke<BrowserProfile[]>(
"list_browser_profiles"
"list_browser_profiles",
);
setProfiles(profileList);
@@ -90,9 +90,12 @@ export default function Home() {
void checkStartupUrls();
// Set up periodic update checks (every 30 minutes)
const updateInterval = setInterval(() => {
void checkForUpdates();
}, 30 * 60 * 1000);
const updateInterval = setInterval(
() => {
void checkForUpdates();
},
30 * 60 * 1000,
);
return () => {
clearInterval(updateInterval);
@@ -104,7 +107,7 @@ export default function Home() {
try {
const shouldShow = await invoke<boolean>(
"should_show_settings_on_startup"
"should_show_settings_on_startup",
);
if (shouldShow) {
setSettingsDialogOpen(true);
@@ -119,7 +122,7 @@ export default function Home() {
try {
const hasStartupUrl = await invoke<boolean>(
"check_and_handle_startup_url"
"check_and_handle_startup_url",
);
if (hasStartupUrl) {
console.log("Handled startup URL successfully");
@@ -152,10 +155,10 @@ export default function Home() {
await listen<string>("show-create-profile-dialog", (event) => {
console.log(
"Received show create profile dialog request:",
event.payload
event.payload,
);
setError(
"No profiles available. Please create a profile first before opening URLs."
"No profiles available. Please create a profile first before opening URLs.",
);
setCreateProfileDialogOpen(true);
});
@@ -177,7 +180,7 @@ export default function Home() {
} catch (error: any) {
console.log(
"Smart URL opening failed or requires profile selection:",
error
error,
);
// Check if it's the special error cases
@@ -187,7 +190,7 @@ export default function Home() {
} else if (error === "no_profiles") {
// No profiles available, show error message
setError(
"No profiles available. Please create a profile first before opening URLs."
"No profiles available. Please create a profile first before opening URLs.",
);
} else {
// Some other error occurred
@@ -225,7 +228,7 @@ export default function Home() {
setError(`Failed to update proxy settings: ${JSON.stringify(err)}`);
}
},
[currentProfileForProxy, loadProfiles]
[currentProfileForProxy, loadProfiles],
);
const handleCreateProfile = useCallback(
@@ -244,7 +247,7 @@ export default function Home() {
name: profileData.name,
browserStr: profileData.browserStr,
version: profileData.version,
}
},
);
// Update proxy if provided
@@ -261,11 +264,11 @@ export default function Home() {
throw error;
}
},
[loadProfiles]
[loadProfiles],
);
const [runningProfiles, setRunningProfiles] = useState<Set<string>>(
new Set()
new Set(),
);
const runningProfilesRef = useRef<Set<string>>(new Set());
@@ -297,7 +300,7 @@ export default function Home() {
console.error("Failed to check browser status:", err);
}
},
[isClient]
[isClient],
);
const launchProfile = useCallback(
@@ -312,12 +315,12 @@ export default function Home() {
"is_browser_disabled_for_update",
{
browser: profile.browser,
}
},
);
if (isDisabled || isUpdating(profile.browser)) {
setError(
`${profile.browser} is currently being updated. Please wait for the update to complete.`
`${profile.browser} is currently being updated. Please wait for the update to complete.`,
);
return;
}
@@ -328,7 +331,7 @@ export default function Home() {
try {
const updatedProfile = await invoke<BrowserProfile>(
"launch_browser_profile",
{ profile }
{ profile },
);
await loadProfiles();
await checkBrowserStatus(updatedProfile);
@@ -337,7 +340,7 @@ export default function Home() {
setError(`Failed to launch browser: ${JSON.stringify(err)}`);
}
},
[loadProfiles, checkBrowserStatus, isUpdating, isClient]
[loadProfiles, checkBrowserStatus, isUpdating, isClient],
);
useEffect(() => {
@@ -376,7 +379,7 @@ export default function Home() {
setError(`Failed to delete profile: ${JSON.stringify(err)}`);
}
},
[loadProfiles]
[loadProfiles],
);
const handleRenameProfile = useCallback(
@@ -391,7 +394,7 @@ export default function Home() {
throw err;
}
},
[loadProfiles]
[loadProfiles],
);
const handleKillProfile = useCallback(
@@ -405,7 +408,7 @@ export default function Home() {
setError(`Failed to kill browser: ${JSON.stringify(err)}`);
}
},
[loadProfiles]
[loadProfiles],
);
// Don't render anything until we're on the client side to prevent hydration issues
@@ -551,7 +554,7 @@ export default function Home() {
isOpen={true}
onClose={() => {
setPendingUrls((prev) =>
prev.filter((u) => u.id !== pendingUrl.id)
prev.filter((u) => u.id !== pendingUrl.id),
);
}}
url={pendingUrl.url}
+2 -2
View File
@@ -60,10 +60,10 @@ export function ChangeVersionDialog({
if (profile && selectedVersion) {
// Check if this is a downgrade
const currentVersionIndex = availableVersions.findIndex(
(v) => v.tag_name === profile.version
(v) => v.tag_name === profile.version,
);
const selectedVersionIndex = availableVersions.findIndex(
(v) => v.tag_name === selectedVersion
(v) => v.tag_name === selectedVersion,
);
// If selected version has a higher index, it's older (downgrade)
+4 -4
View File
@@ -65,7 +65,7 @@ export function CreateProfileDialog({
>([]);
const [isCreating, setIsCreating] = useState(false);
const [existingProfiles, setExistingProfiles] = useState<BrowserProfile[]>(
[]
[],
);
// Proxy settings
@@ -120,7 +120,7 @@ export function CreateProfileDialog({
const loadSupportedBrowsers = async () => {
try {
const browsers = await invoke<BrowserTypeString[]>(
"get_supported_browsers"
"get_supported_browsers",
);
setSupportedBrowsers(browsers);
if (browsers.includes("mullvad-browser")) {
@@ -156,7 +156,7 @@ export function CreateProfileDialog({
// Check for duplicate names (case insensitive)
const isDuplicate = existingProfiles.some(
(profile) => profile.name.toLowerCase() === trimmedName.toLowerCase()
(profile) => profile.name.toLowerCase() === trimmedName.toLowerCase(),
);
if (isDuplicate) {
@@ -271,7 +271,7 @@ export function CreateProfileDialog({
{browser
.split("-")
.map(
(word) => word.charAt(0).toUpperCase() + word.slice(1)
(word) => word.charAt(0).toUpperCase() + word.slice(1),
)
.join(" ")}
</SelectItem>
+10 -10
View File
@@ -298,7 +298,7 @@ export function showLoadingToast(
id?: string;
description?: string;
duration?: number;
}
},
) {
return showToast({
type: "loading",
@@ -312,16 +312,16 @@ export function showDownloadToast(
version: string,
stage: "downloading" | "extracting" | "verifying" | "completed",
progress?: { percentage: number; speed?: string; eta?: string },
options?: { suppressCompletionToast?: boolean }
options?: { suppressCompletionToast?: boolean },
) {
const title =
stage === "completed"
? `${browserName} ${version} downloaded successfully!`
: stage === "downloading"
? `Downloading ${browserName} ${version}`
: stage === "extracting"
? `Extracting ${browserName} ${version}`
: `Verifying ${browserName} ${version}`;
? `Downloading ${browserName} ${version}`
: stage === "extracting"
? `Extracting ${browserName} ${version}`
: `Verifying ${browserName} ${version}`;
// Don't show completion toast if suppressed (for auto-update scenarios)
if (stage === "completed" && options?.suppressCompletionToast) {
@@ -349,7 +349,7 @@ export function showVersionUpdateToast(
found: number;
};
duration?: number;
}
},
) {
return showToast({
type: "version-update",
@@ -364,7 +364,7 @@ export function showFetchingToast(
id?: string;
description?: string;
duration?: number;
}
},
) {
return showToast({
type: "fetching",
@@ -382,7 +382,7 @@ export function showSuccessToast(
id?: string;
description?: string;
duration?: number;
}
},
) {
return showToast({
type: "success",
@@ -397,7 +397,7 @@ export function showErrorToast(
id?: string;
description?: string;
duration?: number;
}
},
) {
return showToast({
type: "error",
+11 -11
View File
@@ -101,7 +101,7 @@ export function ProfilesDataTable({
setSorting(newSorting);
updateSorting(newSorting);
},
[sorting, updateSorting, isClient]
[sorting, updateSorting, isClient],
);
const handleRename = async () => {
@@ -131,7 +131,7 @@ export function ProfilesDataTable({
const anyTorRunning =
isClient &&
data.some(
(p) => p.browser === "tor-browser" && runningProfiles.has(p.name)
(p) => p.browser === "tor-browser" && runningProfiles.has(p.name),
);
const shouldDisableTorStart =
isTorBrowser && !isRunning && anyTorRunning;
@@ -159,12 +159,12 @@ export function ProfilesDataTable({
{!isClient
? "Loading..."
: isRunning
? "Click to forcefully stop the browser"
: isBrowserUpdating
? `${profile.browser} is being updated. Please wait for the update to complete.`
: shouldDisableTorStart
? "Only one TOR browser instance can run at a time. Stop the running TOR browser first."
: "Click to launch the browser"}
? "Click to forcefully stop the browser"
: isBrowserUpdating
? `${profile.browser} is being updated. Please wait for the update to complete.`
: shouldDisableTorStart
? "Only one TOR browser instance can run at a time. Stop the running TOR browser first."
: "Click to launch the browser"}
</TooltipContent>
</Tooltip>
</div>
@@ -392,7 +392,7 @@ export function ProfilesDataTable({
},
},
],
[isClient, runningProfiles, isUpdating, data]
[isClient, runningProfiles, isUpdating, data],
);
const table = useReactTable({
@@ -420,7 +420,7 @@ export function ProfilesDataTable({
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
header.getContext(),
)}
</TableHead>
);
@@ -439,7 +439,7 @@ export function ProfilesDataTable({
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
cell.getContext(),
)}
</TableCell>
))}
+7 -7
View File
@@ -58,7 +58,7 @@ export function ProfileSelectorDialog({
setIsLoading(true);
try {
const profileList = await invoke<BrowserProfile[]>(
"list_browser_profiles"
"list_browser_profiles",
);
// Sort profiles by name
@@ -92,14 +92,14 @@ export function ProfileSelectorDialog({
const canUseProfileForLinks = (
profile: BrowserProfile,
allProfiles: BrowserProfile[],
runningProfiles: Set<string>
runningProfiles: Set<string>,
): boolean => {
const isRunning = runningProfiles.has(profile.name);
// For TOR browser: Check if any TOR browser is running
if (profile.browser === "tor-browser") {
const runningTorProfiles = allProfiles.filter(
(p) => p.browser === "tor-browser" && runningProfiles.has(p.name)
(p) => p.browser === "tor-browser" && runningProfiles.has(p.name),
);
// If no TOR browser is running, allow any TOR profile
@@ -126,7 +126,7 @@ export function ProfileSelectorDialog({
if (profile.browser === "tor-browser") {
const runningTorProfiles = profiles.filter(
(p) => p.browser === "tor-browser" && runningProfiles.has(p.name)
(p) => p.browser === "tor-browser" && runningProfiles.has(p.name),
);
// If another TOR profile is running, this one is not available
@@ -192,7 +192,7 @@ export function ProfileSelectorDialog({
return canUseProfileForLinks(
selectedProfileData,
profiles,
runningProfiles
runningProfiles,
);
};
@@ -261,7 +261,7 @@ export function ProfileSelectorDialog({
const canUseForLinks = canUseProfileForLinks(
profile,
profiles,
runningProfiles
runningProfiles,
);
const tooltipContent = getProfileTooltipContent(profile);
@@ -281,7 +281,7 @@ export function ProfileSelectorDialog({
<div className="flex items-center gap-2">
{(() => {
const IconComponent = getBrowserIcon(
profile.browser
profile.browser,
);
return IconComponent ? (
<IconComponent className="h-4 w-4" />
+2 -2
View File
@@ -17,7 +17,7 @@ interface CustomThemeProviderProps {
function getSystemTheme(): string {
if (typeof window !== "undefined") {
const isDarkMode = window.matchMedia(
"(prefers-color-scheme: dark)"
"(prefers-color-scheme: dark)",
).matches;
return isDarkMode ? "dark" : "light";
}
@@ -39,7 +39,7 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
const systemTheme = getSystemTheme();
console.log(
"First-time user detected, applying system theme:",
systemTheme
systemTheme,
);
// Save the detected theme as the default
+1 -1
View File
@@ -15,7 +15,7 @@ function Checkbox({
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
{...props}
>
+8 -8
View File
@@ -43,7 +43,7 @@ function DropdownMenuContent({
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
className,
)}
{...props}
/>
@@ -75,7 +75,7 @@ function DropdownMenuItem({
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
/>
@@ -93,7 +93,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
checked={checked}
{...props}
@@ -129,7 +129,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
>
@@ -156,7 +156,7 @@ function DropdownMenuLabel({
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
className,
)}
{...props}
/>
@@ -185,7 +185,7 @@ function DropdownMenuShortcut({
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
className,
)}
{...props}
/>
@@ -212,7 +212,7 @@ function DropdownMenuSubTrigger({
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
className,
)}
{...props}
>
@@ -231,7 +231,7 @@ function DropdownMenuSubContent({
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
className,
)}
{...props}
/>
+6 -6
View File
@@ -38,7 +38,7 @@ function SelectTrigger({
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
>
@@ -64,7 +64,7 @@ function SelectContent({
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
className,
)}
position={position}
{...props}
@@ -74,7 +74,7 @@ function SelectContent({
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
@@ -108,7 +108,7 @@ function SelectItem({
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
className,
)}
{...props}
>
@@ -144,7 +144,7 @@ function SelectScrollUpButton({
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
@@ -162,7 +162,7 @@ function SelectScrollDownButton({
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
+9 -9
View File
@@ -112,7 +112,7 @@ function UpdateNotificationComponent({
export function useUpdateNotifications() {
const [notifications, setNotifications] = useState<UpdateNotification[]>([]);
const [updatingBrowsers, setUpdatingBrowsers] = useState<Set<string>>(
new Set()
new Set(),
);
const [isClient, setIsClient] = useState(false);
@@ -126,7 +126,7 @@ export function useUpdateNotifications() {
try {
const updates = await invoke<UpdateNotification[]>(
"check_for_browser_updates"
"check_for_browser_updates",
);
setNotifications(updates);
@@ -145,7 +145,7 @@ export function useUpdateNotifications() {
// Dismiss all notifications for this browser first
const browserNotifications = notifications.filter(
(n) => n.browser === browser
(n) => n.browser === browser,
);
for (const notification of browserNotifications) {
toast.dismiss(notification.id);
@@ -164,7 +164,7 @@ export function useUpdateNotifications() {
if (isDownloaded) {
// Browser already exists, skip download and go straight to profile update
console.log(
`${browserDisplayName} ${newVersion} already exists, skipping download`
`${browserDisplayName} ${newVersion} already exists, skipping download`,
);
} else {
// Mark download as auto-update in the backend for toast suppression
@@ -186,7 +186,7 @@ export function useUpdateNotifications() {
{
browser,
newVersion,
}
},
);
// Show success message based on whether profiles were updated
@@ -252,7 +252,7 @@ export function useUpdateNotifications() {
});
}
},
[notifications, checkForUpdates]
[notifications, checkForUpdates],
);
const handleDismiss = useCallback(
@@ -267,7 +267,7 @@ export function useUpdateNotifications() {
console.error("Failed to dismiss notification:", error);
}
},
[checkForUpdates, isClient]
[checkForUpdates, isClient],
);
// Separate effect to show toasts when notifications change
@@ -288,11 +288,11 @@ export function useUpdateNotifications() {
),
{
id: notification.id,
duration: Infinity, // Persistent until user action
duration: Number.POSITIVE_INFINITY, // Persistent until user action
position: "top-right",
// Remove transparent styling to fix background issue
style: undefined,
}
},
);
});
}, [notifications, updatingBrowsers, handleUpdate, handleDismiss, isClient]);
+3 -3
View File
@@ -91,7 +91,7 @@ export function VersionSelector({
<CommandGroup>
{availableVersions.map((version) => {
const isDownloaded = downloadedVersions.includes(
version.tag_name
version.tag_name,
);
return (
<CommandItem
@@ -101,7 +101,7 @@ export function VersionSelector({
onVersionSelect(
currentValue === selectedVersion
? null
: currentValue
: currentValue,
);
setVersionPopoverOpen(false);
}}
@@ -111,7 +111,7 @@ export function VersionSelector({
"mr-2 h-4 w-4",
selectedVersion === version.tag_name
? "opacity-100"
: "opacity-0"
: "opacity-0",
)}
/>
<div className="flex items-center gap-2">
+4 -5
View File
@@ -115,11 +115,10 @@ export function VersionUpdateSettings() {
<AlertTitle>How it works</AlertTitle>
<AlertDescription className="text-xs">
Version information is checked automatically every 3 hours
<br />
New versions are added to the cache without removing old ones
<br />
When creating profiles or changing versions, you&apos;ll see how
many new versions were found
<br /> New versions are added to the cache without removing old
ones
<br /> When creating profiles or changing versions, you&apos;ll see
how many new versions were found
<br /> This keeps the app responsive while ensuring you have the
latest information
</AlertDescription>
+14 -14
View File
@@ -72,7 +72,7 @@ const isAlphaVersion = (version: string): boolean => {
export function useBrowserDownload() {
const [availableVersions, setAvailableVersions] = useState<GithubRelease[]>(
[]
[],
);
const [downloadedVersions, setDownloadedVersions] = useState<string[]>([]);
const [isDownloading, setIsDownloading] = useState(false);
@@ -128,7 +128,7 @@ export function useBrowserDownload() {
undefined,
{
suppressCompletionToast: isAutoUpdate,
}
},
);
setDownloadProgress(null);
}
@@ -167,7 +167,7 @@ export function useBrowserDownload() {
`Found ${progress.new_versions_found} new browser versions!`,
{
duration: 3000,
}
},
);
}
// Dismiss any update toasts
@@ -179,7 +179,7 @@ export function useBrowserDownload() {
});
toast.dismiss();
}
}
},
);
return () => {
@@ -224,7 +224,7 @@ export function useBrowserDownload() {
try {
const versionInfos = await invoke<BrowserVersionInfo[]>(
"fetch_browser_versions_cached_first",
{ browserStr }
{ browserStr },
);
// Convert BrowserVersionInfo to GithubRelease format for compatibility
@@ -234,7 +234,7 @@ export function useBrowserDownload() {
assets: [],
published_at: versionInfo.date,
is_alpha: versionInfo.is_prerelease,
})
}),
);
setAvailableVersions(githubReleases);
@@ -259,13 +259,13 @@ export function useBrowserDownload() {
// Get versions with new count info and cached detailed info
const result = await invoke<BrowserVersionsResult>(
"fetch_browser_versions_with_count_cached_first",
{ browserStr }
{ browserStr },
);
// Get detailed version info for compatibility
const versionInfos = await invoke<BrowserVersionInfo[]>(
"fetch_browser_versions_cached_first",
{ browserStr }
{ browserStr },
);
// Convert BrowserVersionInfo to GithubRelease format for compatibility
@@ -275,7 +275,7 @@ export function useBrowserDownload() {
assets: [],
published_at: versionInfo.date,
is_alpha: versionInfo.is_prerelease,
})
}),
);
setAvailableVersions(githubReleases);
@@ -287,7 +287,7 @@ export function useBrowserDownload() {
{
duration: 3000,
description: `Total available: ${result.total_versions_count} versions`,
}
},
);
}
@@ -307,7 +307,7 @@ export function useBrowserDownload() {
try {
const downloadedVersions = await invoke<string[]>(
"get_downloaded_browser_versions",
{ browserStr }
{ browserStr },
);
setDownloadedVersions(downloadedVersions);
return downloadedVersions;
@@ -321,7 +321,7 @@ export function useBrowserDownload() {
async (
browserStr: string,
version: string,
suppressNotifications: boolean = false
suppressNotifications = false,
) => {
const browserName = getBrowserDisplayName(browserStr);
setIsDownloading(true);
@@ -345,14 +345,14 @@ export function useBrowserDownload() {
setIsDownloading(false);
}
},
[loadDownloadedVersions]
[loadDownloadedVersions],
);
const isVersionDownloaded = useCallback(
(version: string) => {
return downloadedVersions.includes(version);
},
[downloadedVersions]
[downloadedVersions],
);
return {
+3 -3
View File
@@ -15,7 +15,7 @@ export function useTableSorting() {
const loadSettings = async () => {
try {
const settings = await invoke<TableSortingSettings>(
"get_table_sorting_settings"
"get_table_sorting_settings",
);
setSortingSettings(settings);
} catch (error) {
@@ -39,7 +39,7 @@ export function useTableSorting() {
console.error("Failed to save table sorting settings:", error);
}
},
[]
[],
);
// Convert our settings to tanstack table sorting format
@@ -67,7 +67,7 @@ export function useTableSorting() {
void saveSortingSettings(newSettings);
}
},
[saveSortingSettings, isLoaded]
[saveSortingSettings, isLoaded],
);
return {
+12 -12
View File
@@ -61,7 +61,7 @@ export function useVersionUpdater() {
total: progress.total_browsers,
found: progress.new_versions_found,
},
}
},
);
} else {
showLoadingToast("Starting version update check...", {
@@ -81,7 +81,7 @@ export function useVersionUpdater() {
duration: 4000,
description:
"Version information has been updated in the background",
}
},
);
} else {
toast.success("No new browser versions found", {
@@ -103,7 +103,7 @@ export function useVersionUpdater() {
description: "Check your internet connection and try again",
});
}
}
},
);
return () => {
@@ -130,7 +130,7 @@ export function useVersionUpdater() {
const loadUpdateStatus = useCallback(async () => {
try {
const [lastUpdate, timeUntilNext] = await invoke<[number | null, number]>(
"get_version_update_status"
"get_version_update_status",
);
setLastUpdateTime(lastUpdate);
setTimeUntilNextUpdate(timeUntilNext);
@@ -143,18 +143,18 @@ export function useVersionUpdater() {
try {
setIsUpdating(true);
const results = await invoke<BackgroundUpdateResult[]>(
"trigger_manual_version_update"
"trigger_manual_version_update",
);
const totalNewVersions = results.reduce(
(sum, result) => sum + result.new_versions_count,
0
0,
);
const successfulUpdates = results.filter(
(r) => r.updated_successfully
(r) => r.updated_successfully,
).length;
const failedUpdates = results.filter(
(r) => !r.updated_successfully
(r) => !r.updated_successfully,
).length;
if (failedUpdates > 0) {
@@ -194,7 +194,7 @@ export function useVersionUpdater() {
try {
const result = await invoke<BrowserVersionsResult>(
"fetch_browser_versions_with_count",
{ browserStr }
{ browserStr },
);
// Show notification about new versions if any were found
@@ -205,7 +205,7 @@ export function useVersionUpdater() {
{
duration: 3000,
description: `Total available: ${result.total_versions_count} versions`,
}
},
);
}
@@ -215,7 +215,7 @@ export function useVersionUpdater() {
throw error;
}
},
[]
[],
);
const formatTimeUntilUpdate = useCallback((seconds: number): string => {
@@ -251,7 +251,7 @@ export function useVersionUpdater() {
return "Just now";
}
},
[]
[],
);
return {