From 56c1f9461650991603fd907c7d8b6e9a628c49a1 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Thu, 29 May 2025 10:39:04 +0400 Subject: [PATCH] fix: linter and formatting --- .github/workflows/lint-js.yml | 2 +- .github/workflows/release.yml | 2 +- .husky/pre-commit | 1 + nodecar/src/index.ts | 2 +- nodecar/src/proxy-runner.ts | 2 +- package.json | 20 +- pnpm-lock.yaml | 346 ++++- src-tauri/build.rs | 2 +- src-tauri/src/api_client.rs | 447 +++--- src-tauri/src/auto_updater.rs | 1435 +++++++++++--------- src-tauri/src/browser.rs | 146 +- src-tauri/src/browser_runner.rs | 1218 ++++++++++------- src-tauri/src/browser_version_service.rs | 1296 ++++++++++-------- src-tauri/src/default_browser.rs | 52 +- src-tauri/src/download.rs | 162 +-- src-tauri/src/downloaded_browsers.rs | 455 ++++--- src-tauri/src/extraction.rs | 44 +- src-tauri/src/lib.rs | 71 +- src-tauri/src/proxy_manager.rs | 2 - src-tauri/src/settings_manager.rs | 27 +- src-tauri/src/version_updater.rs | 933 +++++++------ src/app/page.tsx | 49 +- src/components/change-version-dialog.tsx | 4 +- src/components/create-profile-dialog.tsx | 8 +- src/components/custom-toast.tsx | 20 +- src/components/profile-data-table.tsx | 22 +- src/components/profile-selector-dialog.tsx | 14 +- src/components/theme-provider.tsx | 4 +- src/components/ui/checkbox.tsx | 2 +- src/components/ui/dropdown-menu.tsx | 16 +- src/components/ui/select.tsx | 12 +- src/components/update-notification.tsx | 18 +- src/components/version-selector.tsx | 6 +- src/components/version-update-settings.tsx | 9 +- src/hooks/use-browser-download.ts | 28 +- src/hooks/use-table-sorting.ts | 6 +- src/hooks/use-version-updater.ts | 24 +- 37 files changed, 4013 insertions(+), 2894 deletions(-) create mode 100755 .husky/pre-commit diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml index 90432cd..fa16e3d 100644 --- a/.github/workflows/lint-js.yml +++ b/.github/workflows/lint-js.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b4081c2..9910b1a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..cb2c84d --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm lint-staged diff --git a/nodecar/src/index.ts b/nodecar/src/index.ts index 9d63750..1b01162 100644 --- a/nodecar/src/index.ts +++ b/nodecar/src/index.ts @@ -18,7 +18,7 @@ program .option( "-p, --port ", "local port to use (random if not specified)", - parseInt + Number.parseInt ) .option("--ignore-certificate", "ignore certificate errors for HTTPS proxies") .option("--id ", "proxy ID for stop command") diff --git a/nodecar/src/proxy-runner.ts b/nodecar/src/proxy-runner.ts index 57b0b46..5bff9f1 100644 --- a/nodecar/src/proxy-runner.ts +++ b/nodecar/src/proxy-runner.ts @@ -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, diff --git a/package.json b/package.json index 160dfe2..1ee3d27 100644 --- a/package.json +++ b/package.json @@ -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" + ] + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96ae5c3..d301598 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/src-tauri/build.rs b/src-tauri/build.rs index e52f368..37912d5 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -4,6 +4,6 @@ fn main() { println!("cargo:rustc-link-lib=framework=CoreFoundation"); println!("cargo:rustc-link-lib=framework=CoreServices"); } - + tauri_build::build() } diff --git a/src-tauri/src/api_client.rs b/src-tauri/src/api_client.rs index 551ad79..1a7953a 100644 --- a/src-tauri/src/api_client.rs +++ b/src-tauri/src/api_client.rs @@ -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 = 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) { 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 { 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 { 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> { 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> { + pub fn save_cached_versions( + &self, + browser: &str, + versions: &[String], + ) -> Result<(), Box> { 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> { 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> { + fn save_cached_github_releases( + &self, + browser: &str, + releases: &[GithubRelease], + ) -> Result<(), Box> { 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, Box> { + pub async fn fetch_firefox_releases_with_caching( + &self, + no_caching: bool, + ) -> Result, Box> { // 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 = firefox_response .releases @@ -413,7 +446,7 @@ impl ApiClient { // Extract versions for caching let versions: Vec = 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, Box> { + pub async fn fetch_firefox_developer_releases_with_caching( + &self, + no_caching: bool, + ) -> Result, Box> { // 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 = 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, Box> { + pub async fn fetch_mullvad_releases( + &self, + ) -> Result, Box> { self.fetch_mullvad_releases_with_caching(false).await } - pub async fn fetch_mullvad_releases_with_caching(&self, no_caching: bool) -> Result, Box> { + pub async fn fetch_mullvad_releases_with_caching( + &self, + no_caching: bool, + ) -> Result, Box> { // 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, Box> { + pub async fn fetch_zen_releases( + &self, + ) -> Result, Box> { self.fetch_zen_releases_with_caching(false).await } - pub async fn fetch_zen_releases_with_caching(&self, no_caching: bool) -> Result, Box> { + pub async fn fetch_zen_releases_with_caching( + &self, + no_caching: bool, + ) -> Result, Box> { // 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, Box> { + pub async fn fetch_brave_releases( + &self, + ) -> Result, Box> { self.fetch_brave_releases_with_caching(false).await } - pub async fn fetch_brave_releases_with_caching(&self, no_caching: bool) -> Result, Box> { + pub async fn fetch_brave_releases_with_caching( + &self, + no_caching: bool, + ) -> Result, Box> { // 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> { + pub async fn fetch_chromium_latest_version( + &self, + ) -> Result> { // 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, Box> { + pub async fn fetch_chromium_releases_with_caching( + &self, + no_caching: bool, + ) -> Result, Box> { // 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, Box> { + pub async fn fetch_tor_releases_with_caching( + &self, + no_caching: bool, + ) -> Result, Box> { // 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("") { @@ -741,9 +828,12 @@ impl ApiClient { let start = start + 9; // Length of "") { 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> { - let url = format!("https://archive.torproject.org/tor-package-archive/torbrowser/{}/", version); + async fn check_tor_version_has_macos( + &self, + version: &str, + ) -> Result> { + 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 { } } } -} \ No newline at end of file +} diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index 89904b5..c676ab2 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -1,778 +1,869 @@ -use crate::browser_version_service::{BrowserVersionService, BrowserVersionInfo}; use crate::browser_runner::{BrowserProfile, BrowserRunner}; +use crate::browser_version_service::{BrowserVersionInfo, BrowserVersionService}; use crate::settings_manager::SettingsManager; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; -use std::path::PathBuf; use std::fs; +use std::path::PathBuf; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UpdateNotification { - pub id: String, - pub browser: String, - pub current_version: String, - pub new_version: String, - pub affected_profiles: Vec, - pub is_stable_update: bool, - pub timestamp: u64, + pub id: String, + pub browser: String, + pub current_version: String, + pub new_version: String, + pub affected_profiles: Vec, + pub is_stable_update: bool, + pub timestamp: u64, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AutoUpdateState { - pub pending_updates: Vec, - pub disabled_browsers: HashSet, // browsers disabled during update - #[serde(default)] - pub auto_update_downloads: HashSet, // track auto-update downloads for toast suppression - pub last_check_timestamp: u64, + pub pending_updates: Vec, + pub disabled_browsers: HashSet, // browsers disabled during update + #[serde(default)] + pub auto_update_downloads: HashSet, // track auto-update downloads for toast suppression + pub last_check_timestamp: u64, } impl Default for AutoUpdateState { - fn default() -> Self { - Self { - pending_updates: Vec::new(), - disabled_browsers: HashSet::new(), - auto_update_downloads: HashSet::new(), - last_check_timestamp: 0, - } + fn default() -> Self { + Self { + pending_updates: Vec::new(), + disabled_browsers: HashSet::new(), + auto_update_downloads: HashSet::new(), + last_check_timestamp: 0, } + } } pub struct AutoUpdater { - version_service: BrowserVersionService, - browser_runner: BrowserRunner, - settings_manager: SettingsManager, + version_service: BrowserVersionService, + browser_runner: BrowserRunner, + settings_manager: SettingsManager, } impl AutoUpdater { - pub fn new() -> Self { - Self { - version_service: BrowserVersionService::new(), - browser_runner: BrowserRunner::new(), - settings_manager: SettingsManager::new(), - } + pub fn new() -> Self { + Self { + version_service: BrowserVersionService::new(), + browser_runner: BrowserRunner::new(), + settings_manager: SettingsManager::new(), + } + } + + /// Check for updates for all profiles + pub async fn check_for_updates( + &self, + ) -> Result, Box> { + // Check if auto-updates are enabled + let settings = self + .settings_manager + .load_settings() + .map_err(|e| format!("Failed to load settings: {}", e))?; + if !settings.auto_updates_enabled { + return Ok(Vec::new()); } - /// Check for updates for all profiles - pub async fn check_for_updates(&self) -> Result, Box> { - // Check if auto-updates are enabled - let settings = self.settings_manager.load_settings() - .map_err(|e| format!("Failed to load settings: {}", e))?; - if !settings.auto_updates_enabled { - return Ok(Vec::new()); - } + let profiles = self + .browser_runner + .list_profiles() + .map_err(|e| format!("Failed to list profiles: {}", e))?; + let mut notifications = Vec::new(); + let mut browser_versions: HashMap> = HashMap::new(); - let profiles = self.browser_runner.list_profiles() - .map_err(|e| format!("Failed to list profiles: {}", e))?; - let mut notifications = Vec::new(); - let mut browser_versions: HashMap> = HashMap::new(); - - // Group profiles by browser type - let mut browser_profiles: HashMap> = HashMap::new(); - for profile in profiles { - browser_profiles - .entry(profile.browser.clone()) - .or_insert_with(Vec::new) - .push(profile); - } - - // Check each browser type - for (browser, profiles) in browser_profiles { - // Get cached versions first, then try to fetch if needed - let versions = if let Some(cached) = self.version_service.get_cached_browser_versions_detailed(&browser) { - cached - } else if self.version_service.should_update_cache(&browser) { - // Try to fetch fresh versions - match self.version_service.fetch_browser_versions_detailed(&browser, false).await { - Ok(versions) => versions, - Err(_) => continue, // Skip this browser if fetch fails - } - } else { - continue; // No cached versions and cache doesn't need update - }; - - browser_versions.insert(browser.clone(), versions.clone()); - - // Check each profile for updates - for profile in profiles { - if let Some(update) = self.check_profile_update(&profile, &versions)? { - notifications.push(update); - } - } - } - - Ok(notifications) + // Group profiles by browser type + let mut browser_profiles: HashMap> = HashMap::new(); + for profile in profiles { + browser_profiles + .entry(profile.browser.clone()) + .or_insert_with(Vec::new) + .push(profile); } - /// Check if a specific profile has an available update - fn check_profile_update( - &self, - profile: &BrowserProfile, - available_versions: &[BrowserVersionInfo], - ) -> Result, Box> { - let current_version = &profile.version; - let is_current_stable = !self.is_alpha_version(current_version); + // Check each browser type + for (browser, profiles) in browser_profiles { + // Get cached versions first, then try to fetch if needed + let versions = if let Some(cached) = self + .version_service + .get_cached_browser_versions_detailed(&browser) + { + cached + } else if self.version_service.should_update_cache(&browser) { + // Try to fetch fresh versions + match self + .version_service + .fetch_browser_versions_detailed(&browser, false) + .await + { + Ok(versions) => versions, + Err(_) => continue, // Skip this browser if fetch fails + } + } else { + continue; // No cached versions and cache doesn't need update + }; - // Find the best available update - let best_update = available_versions - .iter() - .filter(|v| { - // Only consider versions newer than current - self.is_version_newer(&v.version, current_version) && + browser_versions.insert(browser.clone(), versions.clone()); + + // Check each profile for updates + for profile in profiles { + if let Some(update) = self.check_profile_update(&profile, &versions)? { + notifications.push(update); + } + } + } + + Ok(notifications) + } + + /// Check if a specific profile has an available update + fn check_profile_update( + &self, + profile: &BrowserProfile, + available_versions: &[BrowserVersionInfo], + ) -> Result, Box> { + let current_version = &profile.version; + let is_current_stable = !self.is_alpha_version(current_version); + + // Find the best available update + let best_update = available_versions + .iter() + .filter(|v| { + // Only consider versions newer than current + self.is_version_newer(&v.version, current_version) && // Respect version type preference (is_current_stable == !v.is_prerelease || !is_current_stable) - }) - .max_by(|a, b| self.compare_versions(&a.version, &b.version)); + }) + .max_by(|a, b| self.compare_versions(&a.version, &b.version)); - if let Some(update_version) = best_update { - let notification = UpdateNotification { - id: format!("{}_{}_to_{}", profile.browser, current_version, update_version.version), - browser: profile.browser.clone(), - current_version: current_version.clone(), - new_version: update_version.version.clone(), - affected_profiles: vec![profile.name.clone()], - is_stable_update: !update_version.is_prerelease, - timestamp: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(), - }; - Ok(Some(notification)) - } else { - Ok(None) - } + if let Some(update_version) = best_update { + let notification = UpdateNotification { + id: format!( + "{}_{}_to_{}", + profile.browser, current_version, update_version.version + ), + browser: profile.browser.clone(), + current_version: current_version.clone(), + new_version: update_version.version.clone(), + affected_profiles: vec![profile.name.clone()], + is_stable_update: !update_version.is_prerelease, + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + }; + Ok(Some(notification)) + } else { + Ok(None) + } + } + + /// Group update notifications by browser and version + pub fn group_update_notifications( + &self, + notifications: Vec, + ) -> Vec { + let mut grouped: HashMap = HashMap::new(); + + for notification in notifications { + let key = format!("{}_{}", notification.browser, notification.new_version); + + if let Some(existing) = grouped.get_mut(&key) { + // Merge affected profiles + existing + .affected_profiles + .extend(notification.affected_profiles); + existing.affected_profiles.sort(); + existing.affected_profiles.dedup(); + } else { + grouped.insert(key, notification); + } } - /// Group update notifications by browser and version - pub fn group_update_notifications(&self, notifications: Vec) -> Vec { - let mut grouped: HashMap = HashMap::new(); + let mut result: Vec = grouped.into_values().collect(); - for notification in notifications { - let key = format!("{}_{}", notification.browser, notification.new_version); - - if let Some(existing) = grouped.get_mut(&key) { - // Merge affected profiles - existing.affected_profiles.extend(notification.affected_profiles); - existing.affected_profiles.sort(); - existing.affected_profiles.dedup(); - } else { - grouped.insert(key, notification); + // Sort by priority: stable updates first, then by timestamp + result.sort_by(|a, b| match (a.is_stable_update, b.is_stable_update) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => b.timestamp.cmp(&a.timestamp), + }); + + result + } + + /// Mark download as auto-update + pub fn mark_auto_update_download( + &self, + browser: &str, + version: &str, + ) -> Result<(), Box> { + let mut state = self.load_auto_update_state()?; + let download_key = format!("{}-{}", browser, version); + state.auto_update_downloads.insert(download_key); + self.save_auto_update_state(&state)?; + Ok(()) + } + + /// Remove auto-update download tracking + pub fn remove_auto_update_download( + &self, + browser: &str, + version: &str, + ) -> Result<(), Box> { + let mut state = self.load_auto_update_state()?; + let download_key = format!("{}-{}", browser, version); + state.auto_update_downloads.remove(&download_key); + self.save_auto_update_state(&state)?; + Ok(()) + } + + /// Check if download is marked as auto-update + pub fn is_auto_update_download( + &self, + browser: &str, + version: &str, + ) -> Result> { + let state = self.load_auto_update_state()?; + let download_key = format!("{}-{}", browser, version); + Ok(state.auto_update_downloads.contains(&download_key)) + } + + /// Start browser update process + pub async fn start_browser_update( + &self, + browser: &str, + new_version: &str, + ) -> Result<(), Box> { + // Add browser to disabled list to prevent conflicts during update + let mut state = self.load_auto_update_state()?; + state.disabled_browsers.insert(browser.to_string()); + + // Mark this download as auto-update for toast suppression + let download_key = format!("{}-{}", browser, new_version); + state.auto_update_downloads.insert(download_key); + + self.save_auto_update_state(&state)?; + + // The actual download will be triggered by the frontend + // This function now just marks the browser as updating to prevent conflicts + Ok(()) + } + + /// Complete browser update process + pub async fn complete_browser_update( + &self, + browser: &str, + ) -> Result<(), Box> { + // Remove browser from disabled list + let mut state = self.load_auto_update_state()?; + state.disabled_browsers.remove(browser); + self.save_auto_update_state(&state)?; + + Ok(()) + } + + /// Automatically update all affected profile versions after browser download + pub async fn auto_update_profile_versions( + &self, + browser: &str, + new_version: &str, + ) -> Result, Box> { + let profiles = self + .browser_runner + .list_profiles() + .map_err(|e| format!("Failed to list profiles: {}", e))?; + + let mut updated_profiles = Vec::new(); + + // Find all profiles for this browser that should be updated + for profile in profiles { + if profile.browser == browser { + // Check if profile is currently running + if profile.process_id.is_some() { + continue; // Skip running profiles + } + + // Check if this is an update (newer version) + if self.is_version_newer(new_version, &profile.version) { + // Update the profile version + match self + .browser_runner + .update_profile_version(&profile.name, new_version) + { + Ok(_) => { + updated_profiles.push(profile.name); } - } - - let mut result: Vec = grouped.into_values().collect(); - - // Sort by priority: stable updates first, then by timestamp - result.sort_by(|a, b| { - match (a.is_stable_update, b.is_stable_update) { - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - _ => b.timestamp.cmp(&a.timestamp), - } - }); - - result - } - - /// Mark download as auto-update - pub fn mark_auto_update_download( - &self, - browser: &str, - version: &str, - ) -> Result<(), Box> { - let mut state = self.load_auto_update_state()?; - let download_key = format!("{}-{}", browser, version); - state.auto_update_downloads.insert(download_key); - self.save_auto_update_state(&state)?; - Ok(()) - } - - /// Remove auto-update download tracking - pub fn remove_auto_update_download( - &self, - browser: &str, - version: &str, - ) -> Result<(), Box> { - let mut state = self.load_auto_update_state()?; - let download_key = format!("{}-{}", browser, version); - state.auto_update_downloads.remove(&download_key); - self.save_auto_update_state(&state)?; - Ok(()) - } - - /// Check if download is marked as auto-update - pub fn is_auto_update_download( - &self, - browser: &str, - version: &str, - ) -> Result> { - let state = self.load_auto_update_state()?; - let download_key = format!("{}-{}", browser, version); - Ok(state.auto_update_downloads.contains(&download_key)) - } - - /// Start browser update process - pub async fn start_browser_update( - &self, - browser: &str, - new_version: &str, - ) -> Result<(), Box> { - // Add browser to disabled list to prevent conflicts during update - let mut state = self.load_auto_update_state()?; - state.disabled_browsers.insert(browser.to_string()); - - // Mark this download as auto-update for toast suppression - let download_key = format!("{}-{}", browser, new_version); - state.auto_update_downloads.insert(download_key); - - self.save_auto_update_state(&state)?; - - // The actual download will be triggered by the frontend - // This function now just marks the browser as updating to prevent conflicts - Ok(()) - } - - /// Complete browser update process - pub async fn complete_browser_update( - &self, - browser: &str, - ) -> Result<(), Box> { - // Remove browser from disabled list - let mut state = self.load_auto_update_state()?; - state.disabled_browsers.remove(browser); - self.save_auto_update_state(&state)?; - - Ok(()) - } - - /// Automatically update all affected profile versions after browser download - pub async fn auto_update_profile_versions( - &self, - browser: &str, - new_version: &str, - ) -> Result, Box> { - let profiles = self.browser_runner.list_profiles() - .map_err(|e| format!("Failed to list profiles: {}", e))?; - - let mut updated_profiles = Vec::new(); - - // Find all profiles for this browser that should be updated - for profile in profiles { - if profile.browser == browser { - // Check if profile is currently running - if profile.process_id.is_some() { - continue; // Skip running profiles - } - - // Check if this is an update (newer version) - if self.is_version_newer(new_version, &profile.version) { - // Update the profile version - match self.browser_runner.update_profile_version(&profile.name, new_version) { - Ok(_) => { - updated_profiles.push(profile.name); - } - Err(e) => { - eprintln!("Failed to update profile {}: {}", profile.name, e); - } - } - } + Err(e) => { + eprintln!("Failed to update profile {}: {}", profile.name, e); } + } } - - Ok(updated_profiles) + } } - /// Complete browser update process with auto-update of profile versions - pub async fn complete_browser_update_with_auto_update( - &self, - browser: &str, - new_version: &str, - ) -> Result, Box> { - // Auto-update profile versions first - let updated_profiles = self.auto_update_profile_versions(browser, new_version).await?; - - // Remove browser from disabled list and clean up auto-update tracking - let mut state = self.load_auto_update_state()?; - state.disabled_browsers.remove(browser); - let download_key = format!("{}-{}", browser, new_version); - state.auto_update_downloads.remove(&download_key); - self.save_auto_update_state(&state)?; + Ok(updated_profiles) + } - Ok(updated_profiles) + /// Complete browser update process with auto-update of profile versions + pub async fn complete_browser_update_with_auto_update( + &self, + browser: &str, + new_version: &str, + ) -> Result, Box> { + // Auto-update profile versions first + let updated_profiles = self + .auto_update_profile_versions(browser, new_version) + .await?; + + // Remove browser from disabled list and clean up auto-update tracking + let mut state = self.load_auto_update_state()?; + state.disabled_browsers.remove(browser); + let download_key = format!("{}-{}", browser, new_version); + state.auto_update_downloads.remove(&download_key); + self.save_auto_update_state(&state)?; + + Ok(updated_profiles) + } + + /// Check if browser is disabled due to ongoing update + pub fn is_browser_disabled( + &self, + browser: &str, + ) -> Result> { + let state = self.load_auto_update_state()?; + Ok(state.disabled_browsers.contains(browser)) + } + + /// Dismiss update notification + pub fn dismiss_update_notification( + &self, + notification_id: &str, + ) -> Result<(), Box> { + let mut state = self.load_auto_update_state()?; + state.pending_updates.retain(|n| n.id != notification_id); + self.save_auto_update_state(&state)?; + Ok(()) + } + + // Helper methods + + fn is_alpha_version(&self, version: &str) -> bool { + version.contains("alpha") + || version.contains("beta") + || version.contains("rc") + || version.contains("a") + || version.contains("b") + || version.contains("dev") + } + + fn is_version_newer(&self, version1: &str, version2: &str) -> bool { + self.compare_versions(version1, version2) == std::cmp::Ordering::Greater + } + + fn compare_versions(&self, version1: &str, version2: &str) -> std::cmp::Ordering { + // Basic semantic version comparison + let v1_parts = self.parse_version(version1); + let v2_parts = self.parse_version(version2); + + v1_parts.cmp(&v2_parts) + } + + fn parse_version(&self, version: &str) -> Vec { + version + .split(&['.', 'a', 'b', '-', '_'][..]) + .filter_map(|part| part.parse::().ok()) + .collect() + } + + fn get_auto_update_state_file(&self) -> PathBuf { + self + .settings_manager + .get_settings_dir() + .join("auto_update_state.json") + } + + fn load_auto_update_state( + &self, + ) -> Result> { + let state_file = self.get_auto_update_state_file(); + + if !state_file.exists() { + return Ok(AutoUpdateState::default()); } - /// Check if browser is disabled due to ongoing update - pub fn is_browser_disabled(&self, browser: &str) -> Result> { - let state = self.load_auto_update_state()?; - Ok(state.disabled_browsers.contains(browser)) - } + let content = fs::read_to_string(state_file)?; + let state: AutoUpdateState = serde_json::from_str(&content)?; + Ok(state) + } - /// Dismiss update notification - pub fn dismiss_update_notification(&self, notification_id: &str) -> Result<(), Box> { - let mut state = self.load_auto_update_state()?; - state.pending_updates.retain(|n| n.id != notification_id); - self.save_auto_update_state(&state)?; - Ok(()) - } + fn save_auto_update_state( + &self, + state: &AutoUpdateState, + ) -> Result<(), Box> { + let settings_dir = self.settings_manager.get_settings_dir(); + std::fs::create_dir_all(&settings_dir)?; - // Helper methods + let state_file = self.get_auto_update_state_file(); + let json = serde_json::to_string_pretty(state)?; + fs::write(state_file, json)?; - fn is_alpha_version(&self, version: &str) -> bool { - version.contains("alpha") || version.contains("beta") || version.contains("rc") || - version.contains("a") || version.contains("b") || version.contains("dev") - } - - fn is_version_newer(&self, version1: &str, version2: &str) -> bool { - self.compare_versions(version1, version2) == std::cmp::Ordering::Greater - } - - fn compare_versions(&self, version1: &str, version2: &str) -> std::cmp::Ordering { - // Basic semantic version comparison - let v1_parts = self.parse_version(version1); - let v2_parts = self.parse_version(version2); - - v1_parts.cmp(&v2_parts) - } - - fn parse_version(&self, version: &str) -> Vec { - version - .split(&['.', 'a', 'b', '-', '_'][..]) - .filter_map(|part| part.parse::().ok()) - .collect() - } - - fn get_auto_update_state_file(&self) -> PathBuf { - self.settings_manager.get_settings_dir().join("auto_update_state.json") - } - - fn load_auto_update_state(&self) -> Result> { - let state_file = self.get_auto_update_state_file(); - - if !state_file.exists() { - return Ok(AutoUpdateState::default()); - } - - let content = fs::read_to_string(state_file)?; - let state: AutoUpdateState = serde_json::from_str(&content)?; - Ok(state) - } - - fn save_auto_update_state(&self, state: &AutoUpdateState) -> Result<(), Box> { - let settings_dir = self.settings_manager.get_settings_dir(); - std::fs::create_dir_all(&settings_dir)?; - - let state_file = self.get_auto_update_state_file(); - let json = serde_json::to_string_pretty(state)?; - fs::write(state_file, json)?; - - Ok(()) - } + Ok(()) + } } // Tauri commands #[tauri::command] pub async fn check_for_browser_updates() -> Result, String> { - let updater = AutoUpdater::new(); - let notifications = updater.check_for_updates().await - .map_err(|e| format!("Failed to check for updates: {}", e))?; - let grouped = updater.group_update_notifications(notifications); - Ok(grouped) + let updater = AutoUpdater::new(); + let notifications = updater + .check_for_updates() + .await + .map_err(|e| format!("Failed to check for updates: {}", e))?; + let grouped = updater.group_update_notifications(notifications); + Ok(grouped) } #[tauri::command] pub async fn start_browser_update(browser: String, new_version: String) -> Result<(), String> { - let updater = AutoUpdater::new(); - updater.start_browser_update(&browser, &new_version).await - .map_err(|e| format!("Failed to start browser update: {}", e)) + let updater = AutoUpdater::new(); + updater + .start_browser_update(&browser, &new_version) + .await + .map_err(|e| format!("Failed to start browser update: {}", e)) } #[tauri::command] pub async fn complete_browser_update(browser: String) -> Result<(), String> { - let updater = AutoUpdater::new(); - updater.complete_browser_update(&browser).await - .map_err(|e| format!("Failed to complete browser update: {}", e)) + let updater = AutoUpdater::new(); + updater + .complete_browser_update(&browser) + .await + .map_err(|e| format!("Failed to complete browser update: {}", e)) } #[tauri::command] pub async fn is_browser_disabled_for_update(browser: String) -> Result { - let updater = AutoUpdater::new(); - updater.is_browser_disabled(&browser) - .map_err(|e| format!("Failed to check browser status: {}", e)) + let updater = AutoUpdater::new(); + updater + .is_browser_disabled(&browser) + .map_err(|e| format!("Failed to check browser status: {}", e)) } #[tauri::command] pub async fn dismiss_update_notification(notification_id: String) -> Result<(), String> { - let updater = AutoUpdater::new(); - updater.dismiss_update_notification(¬ification_id) - .map_err(|e| format!("Failed to dismiss notification: {}", e)) + let updater = AutoUpdater::new(); + updater + .dismiss_update_notification(¬ification_id) + .map_err(|e| format!("Failed to dismiss notification: {}", e)) } #[tauri::command] -pub async fn complete_browser_update_with_auto_update(browser: String, new_version: String) -> Result, String> { - let updater = AutoUpdater::new(); - updater.complete_browser_update_with_auto_update(&browser, &new_version).await - .map_err(|e| format!("Failed to complete browser update: {}", e)) +pub async fn complete_browser_update_with_auto_update( + browser: String, + new_version: String, +) -> Result, String> { + let updater = AutoUpdater::new(); + updater + .complete_browser_update_with_auto_update(&browser, &new_version) + .await + .map_err(|e| format!("Failed to complete browser update: {}", e)) } #[tauri::command] pub async fn mark_auto_update_download(browser: String, version: String) -> Result<(), String> { - let updater = AutoUpdater::new(); - updater.mark_auto_update_download(&browser, &version) - .map_err(|e| format!("Failed to mark auto-update download: {}", e)) + let updater = AutoUpdater::new(); + updater + .mark_auto_update_download(&browser, &version) + .map_err(|e| format!("Failed to mark auto-update download: {}", e)) } #[tauri::command] pub async fn remove_auto_update_download(browser: String, version: String) -> Result<(), String> { - let updater = AutoUpdater::new(); - updater.remove_auto_update_download(&browser, &version) - .map_err(|e| format!("Failed to remove auto-update download: {}", e)) + let updater = AutoUpdater::new(); + updater + .remove_auto_update_download(&browser, &version) + .map_err(|e| format!("Failed to remove auto-update download: {}", e)) } #[tauri::command] pub async fn is_auto_update_download(browser: String, version: String) -> Result { - let updater = AutoUpdater::new(); - updater.is_auto_update_download(&browser, &version) - .map_err(|e| format!("Failed to check auto-update download: {}", e)) + let updater = AutoUpdater::new(); + updater + .is_auto_update_download(&browser, &version) + .map_err(|e| format!("Failed to check auto-update download: {}", e)) } #[cfg(test)] mod tests { - use super::*; + use super::*; - fn create_test_profile(name: &str, browser: &str, version: &str) -> BrowserProfile { - BrowserProfile { - name: name.to_string(), - browser: browser.to_string(), - version: version.to_string(), - profile_path: format!("/tmp/{}", name), - process_id: None, - proxy: None, - last_launch: None, - } + fn create_test_profile(name: &str, browser: &str, version: &str) -> BrowserProfile { + BrowserProfile { + name: name.to_string(), + browser: browser.to_string(), + version: version.to_string(), + profile_path: format!("/tmp/{}", name), + process_id: None, + proxy: None, + last_launch: None, + } + } + + fn create_test_version_info(version: &str, is_prerelease: bool) -> BrowserVersionInfo { + BrowserVersionInfo { + version: version.to_string(), + is_prerelease, + date: "2024-01-01".to_string(), + } + } + + #[test] + fn test_is_alpha_version() { + let updater = AutoUpdater::new(); + + assert!(updater.is_alpha_version("1.0.0-alpha")); + assert!(updater.is_alpha_version("1.0.0-beta")); + assert!(updater.is_alpha_version("1.0.0-rc")); + assert!(updater.is_alpha_version("1.0.0a1")); + assert!(updater.is_alpha_version("1.0.0b1")); + assert!(updater.is_alpha_version("1.0.0-dev")); + + assert!(!updater.is_alpha_version("1.0.0")); + assert!(!updater.is_alpha_version("1.2.3")); + } + + #[test] + fn test_compare_versions() { + let updater = AutoUpdater::new(); + + assert_eq!( + updater.compare_versions("1.0.0", "1.0.0"), + std::cmp::Ordering::Equal + ); + assert_eq!( + updater.compare_versions("1.0.1", "1.0.0"), + std::cmp::Ordering::Greater + ); + assert_eq!( + updater.compare_versions("1.0.0", "1.0.1"), + std::cmp::Ordering::Less + ); + assert_eq!( + updater.compare_versions("2.0.0", "1.9.9"), + std::cmp::Ordering::Greater + ); + assert_eq!( + updater.compare_versions("1.10.0", "1.9.0"), + std::cmp::Ordering::Greater + ); + } + + #[test] + fn test_is_version_newer() { + let updater = AutoUpdater::new(); + + assert!(updater.is_version_newer("1.0.1", "1.0.0")); + assert!(updater.is_version_newer("2.0.0", "1.9.9")); + assert!(!updater.is_version_newer("1.0.0", "1.0.1")); + assert!(!updater.is_version_newer("1.0.0", "1.0.0")); + } + + #[test] + fn test_check_profile_update_stable_to_stable() { + let updater = AutoUpdater::new(); + let profile = create_test_profile("test", "firefox", "1.0.0"); + let versions = vec![ + create_test_version_info("1.0.1", false), // stable, newer + create_test_version_info("1.1.0-alpha", true), // alpha, should be ignored + create_test_version_info("0.9.0", false), // stable, older + ]; + + let result = updater.check_profile_update(&profile, &versions).unwrap(); + assert!(result.is_some()); + + let update = result.unwrap(); + assert_eq!(update.new_version, "1.0.1"); + assert!(update.is_stable_update); + } + + #[test] + fn test_check_profile_update_alpha_to_alpha() { + let updater = AutoUpdater::new(); + let profile = create_test_profile("test", "firefox", "1.0.0-alpha"); + let versions = vec![ + create_test_version_info("1.0.1", false), // stable, should be included + create_test_version_info("1.1.0-alpha", true), // alpha, newer + create_test_version_info("0.9.0-alpha", true), // alpha, older + ]; + + let result = updater.check_profile_update(&profile, &versions).unwrap(); + assert!(result.is_some()); + + let update = result.unwrap(); + // Should pick the newest version (alpha user can upgrade to stable or newer alpha) + assert_eq!(update.new_version, "1.1.0-alpha"); + assert!(!update.is_stable_update); + } + + #[test] + fn test_check_profile_update_no_update_available() { + let updater = AutoUpdater::new(); + let profile = create_test_profile("test", "firefox", "1.0.0"); + let versions = vec![ + create_test_version_info("0.9.0", false), // older + create_test_version_info("1.0.0", false), // same version + ]; + + let result = updater.check_profile_update(&profile, &versions).unwrap(); + assert!(result.is_none()); + } + + #[test] + fn test_group_update_notifications() { + let updater = AutoUpdater::new(); + let notifications = vec![ + UpdateNotification { + id: "firefox_1.0.0_to_1.1.0_profile1".to_string(), + browser: "firefox".to_string(), + current_version: "1.0.0".to_string(), + new_version: "1.1.0".to_string(), + affected_profiles: vec!["profile1".to_string()], + is_stable_update: true, + timestamp: 1000, + }, + UpdateNotification { + id: "firefox_1.0.0_to_1.1.0_profile2".to_string(), + browser: "firefox".to_string(), + current_version: "1.0.0".to_string(), + new_version: "1.1.0".to_string(), + affected_profiles: vec!["profile2".to_string()], + is_stable_update: true, + timestamp: 1001, + }, + UpdateNotification { + id: "chrome_1.0.0_to_1.1.0-alpha".to_string(), + browser: "chrome".to_string(), + current_version: "1.0.0".to_string(), + new_version: "1.1.0-alpha".to_string(), + affected_profiles: vec!["profile3".to_string()], + is_stable_update: false, + timestamp: 1002, + }, + ]; + + let grouped = updater.group_update_notifications(notifications); + + assert_eq!(grouped.len(), 2); + + // Find the Firefox notification + let firefox_notification = grouped.iter().find(|n| n.browser == "firefox").unwrap(); + assert_eq!(firefox_notification.affected_profiles.len(), 2); + assert!(firefox_notification + .affected_profiles + .contains(&"profile1".to_string())); + assert!(firefox_notification + .affected_profiles + .contains(&"profile2".to_string())); + + // Stable updates should come first + assert!(grouped[0].is_stable_update); + } + + #[test] + fn test_auto_update_state_persistence() { + use std::sync::Once; + use tempfile::TempDir; + + static INIT: Once = Once::new(); + INIT.call_once(|| { + // Initialize any required static data + }); + + // Create a temporary directory for testing + let temp_dir = TempDir::new().unwrap(); + + // Create a mock settings manager that uses the temp directory + struct TestSettingsManager { + settings_dir: std::path::PathBuf, } - fn create_test_version_info(version: &str, is_prerelease: bool) -> BrowserVersionInfo { - BrowserVersionInfo { - version: version.to_string(), - is_prerelease, - date: "2024-01-01".to_string(), - } + impl TestSettingsManager { + fn new(settings_dir: std::path::PathBuf) -> Self { + Self { settings_dir } + } + + fn get_settings_dir(&self) -> std::path::PathBuf { + self.settings_dir.clone() + } } - #[test] - fn test_is_alpha_version() { - let updater = AutoUpdater::new(); - - assert!(updater.is_alpha_version("1.0.0-alpha")); - assert!(updater.is_alpha_version("1.0.0-beta")); - assert!(updater.is_alpha_version("1.0.0-rc")); - assert!(updater.is_alpha_version("1.0.0a1")); - assert!(updater.is_alpha_version("1.0.0b1")); - assert!(updater.is_alpha_version("1.0.0-dev")); - - assert!(!updater.is_alpha_version("1.0.0")); - assert!(!updater.is_alpha_version("1.2.3")); + let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); + + let mut state = AutoUpdateState::default(); + state.disabled_browsers.insert("firefox".to_string()); + state + .auto_update_downloads + .insert("firefox-1.1.0".to_string()); + state.pending_updates.push(UpdateNotification { + id: "test".to_string(), + browser: "firefox".to_string(), + current_version: "1.0.0".to_string(), + new_version: "1.1.0".to_string(), + affected_profiles: vec!["profile1".to_string()], + is_stable_update: true, + timestamp: 1000, + }); + + // Test save and load + let state_file = test_settings_manager + .get_settings_dir() + .join("auto_update_state.json"); + std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap(); + let json = serde_json::to_string_pretty(&state).unwrap(); + std::fs::write(&state_file, json).unwrap(); + + // Load state + let content = std::fs::read_to_string(&state_file).unwrap(); + let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap(); + + assert_eq!(loaded_state.disabled_browsers.len(), 1); + assert!(loaded_state.disabled_browsers.contains("firefox")); + assert_eq!(loaded_state.auto_update_downloads.len(), 1); + assert!(loaded_state.auto_update_downloads.contains("firefox-1.1.0")); + assert_eq!(loaded_state.pending_updates.len(), 1); + assert_eq!(loaded_state.pending_updates[0].id, "test"); + } + + #[tokio::test] + async fn test_browser_disable_enable_cycle() { + use tempfile::TempDir; + + // Create a temporary directory for testing + let temp_dir = TempDir::new().unwrap(); + + // Create a mock settings manager that uses the temp directory + struct TestSettingsManager { + settings_dir: std::path::PathBuf, } - #[test] - fn test_compare_versions() { - let updater = AutoUpdater::new(); - - assert_eq!(updater.compare_versions("1.0.0", "1.0.0"), std::cmp::Ordering::Equal); - assert_eq!(updater.compare_versions("1.0.1", "1.0.0"), std::cmp::Ordering::Greater); - assert_eq!(updater.compare_versions("1.0.0", "1.0.1"), std::cmp::Ordering::Less); - assert_eq!(updater.compare_versions("2.0.0", "1.9.9"), std::cmp::Ordering::Greater); - assert_eq!(updater.compare_versions("1.10.0", "1.9.0"), std::cmp::Ordering::Greater); + impl TestSettingsManager { + fn new(settings_dir: std::path::PathBuf) -> Self { + Self { settings_dir } + } + + fn get_settings_dir(&self) -> std::path::PathBuf { + self.settings_dir.clone() + } } - #[test] - fn test_is_version_newer() { - let updater = AutoUpdater::new(); - - assert!(updater.is_version_newer("1.0.1", "1.0.0")); - assert!(updater.is_version_newer("2.0.0", "1.9.9")); - assert!(!updater.is_version_newer("1.0.0", "1.0.1")); - assert!(!updater.is_version_newer("1.0.0", "1.0.0")); + let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); + + // Test browser disable/enable cycle with manual state management + let state_file = test_settings_manager + .get_settings_dir() + .join("auto_update_state.json"); + std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap(); + + // Initially not disabled (empty state file means default state) + let state = AutoUpdateState::default(); + assert!(!state.disabled_browsers.contains("firefox")); + + // Start update (should disable) + let mut state = AutoUpdateState::default(); + state.disabled_browsers.insert("firefox".to_string()); + state + .auto_update_downloads + .insert("firefox-1.1.0".to_string()); + let json = serde_json::to_string_pretty(&state).unwrap(); + std::fs::write(&state_file, json).unwrap(); + + // Check that it's disabled + let content = std::fs::read_to_string(&state_file).unwrap(); + let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap(); + assert!(loaded_state.disabled_browsers.contains("firefox")); + assert!(loaded_state.auto_update_downloads.contains("firefox-1.1.0")); + + // Complete update (should enable) + let mut state = loaded_state; + state.disabled_browsers.remove("firefox"); + state.auto_update_downloads.remove("firefox-1.1.0"); + let json = serde_json::to_string_pretty(&state).unwrap(); + std::fs::write(&state_file, json).unwrap(); + + // Check that it's enabled again + let content = std::fs::read_to_string(&state_file).unwrap(); + let final_state: AutoUpdateState = serde_json::from_str(&content).unwrap(); + assert!(!final_state.disabled_browsers.contains("firefox")); + assert!(!final_state.auto_update_downloads.contains("firefox-1.1.0")); + } + + #[test] + fn test_dismiss_update_notification() { + use tempfile::TempDir; + + // Create a temporary directory for testing + let temp_dir = TempDir::new().unwrap(); + + // Create a mock settings manager that uses the temp directory + struct TestSettingsManager { + settings_dir: std::path::PathBuf, } - #[test] - fn test_check_profile_update_stable_to_stable() { - let updater = AutoUpdater::new(); - let profile = create_test_profile("test", "firefox", "1.0.0"); - let versions = vec![ - create_test_version_info("1.0.1", false), // stable, newer - create_test_version_info("1.1.0-alpha", true), // alpha, should be ignored - create_test_version_info("0.9.0", false), // stable, older - ]; + impl TestSettingsManager { + fn new(settings_dir: std::path::PathBuf) -> Self { + Self { settings_dir } + } - let result = updater.check_profile_update(&profile, &versions).unwrap(); - assert!(result.is_some()); - - let update = result.unwrap(); - assert_eq!(update.new_version, "1.0.1"); - assert!(update.is_stable_update); + fn get_settings_dir(&self) -> std::path::PathBuf { + self.settings_dir.clone() + } } - #[test] - fn test_check_profile_update_alpha_to_alpha() { - let updater = AutoUpdater::new(); - let profile = create_test_profile("test", "firefox", "1.0.0-alpha"); - let versions = vec![ - create_test_version_info("1.0.1", false), // stable, should be included - create_test_version_info("1.1.0-alpha", true), // alpha, newer - create_test_version_info("0.9.0-alpha", true), // alpha, older - ]; + let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); - let result = updater.check_profile_update(&profile, &versions).unwrap(); - assert!(result.is_some()); - - let update = result.unwrap(); - // Should pick the newest version (alpha user can upgrade to stable or newer alpha) - assert_eq!(update.new_version, "1.1.0-alpha"); - assert!(!update.is_stable_update); - } + let mut state = AutoUpdateState::default(); + state.pending_updates.push(UpdateNotification { + id: "test_notification".to_string(), + browser: "firefox".to_string(), + current_version: "1.0.0".to_string(), + new_version: "1.1.0".to_string(), + affected_profiles: vec!["profile1".to_string()], + is_stable_update: true, + timestamp: 1000, + }); - #[test] - fn test_check_profile_update_no_update_available() { - let updater = AutoUpdater::new(); - let profile = create_test_profile("test", "firefox", "1.0.0"); - let versions = vec![ - create_test_version_info("0.9.0", false), // older - create_test_version_info("1.0.0", false), // same version - ]; + // Save initial state + let state_file = test_settings_manager + .get_settings_dir() + .join("auto_update_state.json"); + std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap(); + let json = serde_json::to_string_pretty(&state).unwrap(); + std::fs::write(&state_file, json).unwrap(); - let result = updater.check_profile_update(&profile, &versions).unwrap(); - assert!(result.is_none()); - } + // Dismiss notification (remove from pending updates) + let mut state = state; + state + .pending_updates + .retain(|n| n.id != "test_notification"); + let json = serde_json::to_string_pretty(&state).unwrap(); + std::fs::write(&state_file, json).unwrap(); - #[test] - fn test_group_update_notifications() { - let updater = AutoUpdater::new(); - let notifications = vec![ - UpdateNotification { - id: "firefox_1.0.0_to_1.1.0_profile1".to_string(), - browser: "firefox".to_string(), - current_version: "1.0.0".to_string(), - new_version: "1.1.0".to_string(), - affected_profiles: vec!["profile1".to_string()], - is_stable_update: true, - timestamp: 1000, - }, - UpdateNotification { - id: "firefox_1.0.0_to_1.1.0_profile2".to_string(), - browser: "firefox".to_string(), - current_version: "1.0.0".to_string(), - new_version: "1.1.0".to_string(), - affected_profiles: vec!["profile2".to_string()], - is_stable_update: true, - timestamp: 1001, - }, - UpdateNotification { - id: "chrome_1.0.0_to_1.1.0-alpha".to_string(), - browser: "chrome".to_string(), - current_version: "1.0.0".to_string(), - new_version: "1.1.0-alpha".to_string(), - affected_profiles: vec!["profile3".to_string()], - is_stable_update: false, - timestamp: 1002, - }, - ]; + // Check that it's removed + let content = std::fs::read_to_string(&state_file).unwrap(); + let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap(); + assert_eq!(loaded_state.pending_updates.len(), 0); + } - let grouped = updater.group_update_notifications(notifications); - - assert_eq!(grouped.len(), 2); - - // Find the Firefox notification - let firefox_notification = grouped.iter().find(|n| n.browser == "firefox").unwrap(); - assert_eq!(firefox_notification.affected_profiles.len(), 2); - assert!(firefox_notification.affected_profiles.contains(&"profile1".to_string())); - assert!(firefox_notification.affected_profiles.contains(&"profile2".to_string())); - - // Stable updates should come first - assert!(grouped[0].is_stable_update); - } + #[test] + fn test_parse_version() { + let updater = AutoUpdater::new(); - #[test] - fn test_auto_update_state_persistence() { - use tempfile::TempDir; - use std::sync::Once; - - static INIT: Once = Once::new(); - INIT.call_once(|| { - // Initialize any required static data - }); - - // Create a temporary directory for testing - let temp_dir = TempDir::new().unwrap(); - - // Create a mock settings manager that uses the temp directory - struct TestSettingsManager { - settings_dir: std::path::PathBuf, - } - - impl TestSettingsManager { - fn new(settings_dir: std::path::PathBuf) -> Self { - Self { settings_dir } - } - - fn get_settings_dir(&self) -> std::path::PathBuf { - self.settings_dir.clone() - } - } - - let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); - - let mut state = AutoUpdateState::default(); - state.disabled_browsers.insert("firefox".to_string()); - state.auto_update_downloads.insert("firefox-1.1.0".to_string()); - state.pending_updates.push(UpdateNotification { - id: "test".to_string(), - browser: "firefox".to_string(), - current_version: "1.0.0".to_string(), - new_version: "1.1.0".to_string(), - affected_profiles: vec!["profile1".to_string()], - is_stable_update: true, - timestamp: 1000, - }); - - // Test save and load - let state_file = test_settings_manager.get_settings_dir().join("auto_update_state.json"); - std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap(); - let json = serde_json::to_string_pretty(&state).unwrap(); - std::fs::write(&state_file, json).unwrap(); - - // Load state - let content = std::fs::read_to_string(&state_file).unwrap(); - let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap(); - - assert_eq!(loaded_state.disabled_browsers.len(), 1); - assert!(loaded_state.disabled_browsers.contains("firefox")); - assert_eq!(loaded_state.auto_update_downloads.len(), 1); - assert!(loaded_state.auto_update_downloads.contains("firefox-1.1.0")); - assert_eq!(loaded_state.pending_updates.len(), 1); - assert_eq!(loaded_state.pending_updates[0].id, "test"); - } - - #[tokio::test] - async fn test_browser_disable_enable_cycle() { - use tempfile::TempDir; - - // Create a temporary directory for testing - let temp_dir = TempDir::new().unwrap(); - - // Create a mock settings manager that uses the temp directory - struct TestSettingsManager { - settings_dir: std::path::PathBuf, - } - - impl TestSettingsManager { - fn new(settings_dir: std::path::PathBuf) -> Self { - Self { settings_dir } - } - - fn get_settings_dir(&self) -> std::path::PathBuf { - self.settings_dir.clone() - } - } - - let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); - - // Test browser disable/enable cycle with manual state management - let state_file = test_settings_manager.get_settings_dir().join("auto_update_state.json"); - std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap(); - - // Initially not disabled (empty state file means default state) - let state = AutoUpdateState::default(); - assert!(!state.disabled_browsers.contains("firefox")); - - // Start update (should disable) - let mut state = AutoUpdateState::default(); - state.disabled_browsers.insert("firefox".to_string()); - state.auto_update_downloads.insert("firefox-1.1.0".to_string()); - let json = serde_json::to_string_pretty(&state).unwrap(); - std::fs::write(&state_file, json).unwrap(); - - // Check that it's disabled - let content = std::fs::read_to_string(&state_file).unwrap(); - let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap(); - assert!(loaded_state.disabled_browsers.contains("firefox")); - assert!(loaded_state.auto_update_downloads.contains("firefox-1.1.0")); - - // Complete update (should enable) - let mut state = loaded_state; - state.disabled_browsers.remove("firefox"); - state.auto_update_downloads.remove("firefox-1.1.0"); - let json = serde_json::to_string_pretty(&state).unwrap(); - std::fs::write(&state_file, json).unwrap(); - - // Check that it's enabled again - let content = std::fs::read_to_string(&state_file).unwrap(); - let final_state: AutoUpdateState = serde_json::from_str(&content).unwrap(); - assert!(!final_state.disabled_browsers.contains("firefox")); - assert!(!final_state.auto_update_downloads.contains("firefox-1.1.0")); - } - - #[test] - fn test_dismiss_update_notification() { - use tempfile::TempDir; - - // Create a temporary directory for testing - let temp_dir = TempDir::new().unwrap(); - - // Create a mock settings manager that uses the temp directory - struct TestSettingsManager { - settings_dir: std::path::PathBuf, - } - - impl TestSettingsManager { - fn new(settings_dir: std::path::PathBuf) -> Self { - Self { settings_dir } - } - - fn get_settings_dir(&self) -> std::path::PathBuf { - self.settings_dir.clone() - } - } - - let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); - - let mut state = AutoUpdateState::default(); - state.pending_updates.push(UpdateNotification { - id: "test_notification".to_string(), - browser: "firefox".to_string(), - current_version: "1.0.0".to_string(), - new_version: "1.1.0".to_string(), - affected_profiles: vec!["profile1".to_string()], - is_stable_update: true, - timestamp: 1000, - }); - - // Save initial state - let state_file = test_settings_manager.get_settings_dir().join("auto_update_state.json"); - std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap(); - let json = serde_json::to_string_pretty(&state).unwrap(); - std::fs::write(&state_file, json).unwrap(); - - // Dismiss notification (remove from pending updates) - let mut state = state; - state.pending_updates.retain(|n| n.id != "test_notification"); - let json = serde_json::to_string_pretty(&state).unwrap(); - std::fs::write(&state_file, json).unwrap(); - - // Check that it's removed - let content = std::fs::read_to_string(&state_file).unwrap(); - let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap(); - assert_eq!(loaded_state.pending_updates.len(), 0); - } - - #[test] - fn test_parse_version() { - let updater = AutoUpdater::new(); - - assert_eq!(updater.parse_version("1.2.3"), vec![1, 2, 3]); - assert_eq!(updater.parse_version("1.2.3-alpha"), vec![1, 2, 3]); - assert_eq!(updater.parse_version("1.2.3a1"), vec![1, 2, 3, 1]); - assert_eq!(updater.parse_version("1.2.3b2"), vec![1, 2, 3, 2]); - assert_eq!(updater.parse_version("10.0.0"), vec![10, 0, 0]); - } -} \ No newline at end of file + assert_eq!(updater.parse_version("1.2.3"), vec![1, 2, 3]); + assert_eq!(updater.parse_version("1.2.3-alpha"), vec![1, 2, 3]); + assert_eq!(updater.parse_version("1.2.3a1"), vec![1, 2, 3, 1]); + assert_eq!(updater.parse_version("1.2.3b2"), vec![1, 2, 3, 2]); + assert_eq!(updater.parse_version("10.0.0"), vec![10, 0, 0]); + } +} diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index 8451d96..c8bc41d 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -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> { // 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, ) -> Result, Box> { - 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> { // 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 { 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); diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 705551b..040f250 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -9,8 +9,10 @@ use sysinfo::{Pid, System}; use tauri::Emitter; use crate::browser::{create_browser, BrowserType, ProxySettings}; -use crate::browser_version_service::{BrowserVersionService, BrowserVersionInfo, BrowserVersionsResult}; -use crate::download::{Downloader, DownloadProgress}; +use crate::browser_version_service::{ + BrowserVersionInfo, BrowserVersionService, BrowserVersionsResult, +}; +use crate::download::{DownloadProgress, Downloader}; use crate::downloaded_browsers::DownloadedBrowsersRegistry; use crate::extraction::Extractor; @@ -39,7 +41,12 @@ impl BrowserRunner { } // Helper function to check if a process matches TOR/Mullvad browser - fn is_tor_or_mullvad_browser(&self, exe_name: &str, cmd: &[std::ffi::OsString], browser_type: &str) -> bool { + fn is_tor_or_mullvad_browser( + &self, + exe_name: &str, + cmd: &[std::ffi::OsString], + browser_type: &str, + ) -> bool { match browser_type { "mullvad-browser" => { // More specific detection for Mullvad Browser @@ -47,30 +54,30 @@ impl BrowserRunner { let has_firefox_exe = exe_name == "firefox" || exe_name.contains("firefox-bin"); let has_mullvad_in_cmd = cmd.iter().any(|arg| { let arg_str = arg.to_str().unwrap_or(""); - arg_str.contains("Mullvad Browser.app") || - arg_str.contains("mullvad") || - arg_str.contains("Mullvad") || - arg_str.contains("/Applications/Mullvad Browser.app/") || - arg_str.contains("MullvadBrowser") + arg_str.contains("Mullvad Browser.app") + || arg_str.contains("mullvad") + || arg_str.contains("Mullvad") + || arg_str.contains("/Applications/Mullvad Browser.app/") + || arg_str.contains("MullvadBrowser") }); - + has_mullvad_in_exe || (has_firefox_exe && has_mullvad_in_cmd) - }, + } "tor-browser" => { // More specific detection for TOR Browser let has_tor_in_exe = exe_name.contains("tor"); let has_firefox_exe = exe_name == "firefox" || exe_name.contains("firefox-bin"); let has_tor_in_cmd = cmd.iter().any(|arg| { let arg_str = arg.to_str().unwrap_or(""); - arg_str.contains("Tor Browser.app") || - arg_str.contains("tor-browser") || - arg_str.contains("TorBrowser") || - arg_str.contains("/Applications/Tor Browser.app/") || - arg_str.contains("TorBrowser-Data") + arg_str.contains("Tor Browser.app") + || arg_str.contains("tor-browser") + || arg_str.contains("TorBrowser") + || arg_str.contains("/Applications/Tor Browser.app/") + || arg_str.contains("TorBrowser-Data") }); - + has_tor_in_exe || (has_firefox_exe && has_tor_in_cmd) - }, + } _ => false, } } @@ -78,37 +85,49 @@ impl BrowserRunner { // Helper function to validate PID for TOR/Mullvad browsers fn validate_tor_mullvad_pid(&self, profile: &BrowserProfile, pid: u32) -> bool { let system = System::new_all(); - + if let Some(process) = system.process(Pid::from(pid as usize)) { let exe_name = process.name().to_string_lossy().to_lowercase(); let cmd = process.cmd(); - + // Check if this is the correct browser type let is_correct_browser = match profile.browser.as_str() { "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser"), "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "tor-browser"), _ => return false, }; - + if !is_correct_browser { - println!("PID {} is not the correct browser type for {}", pid, profile.browser); + println!( + "PID {} is not the correct browser type for {}", + pid, profile.browser + ); return false; } - + // Check profile path match let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); - arg == &profile.profile_path || - arg == format!("-profile={}", profile.profile_path) || - (arg == "-profile" && cmd.iter().any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path)) + arg == &profile.profile_path + || arg == format!("-profile={}", profile.profile_path) + || (arg == "-profile" + && cmd + .iter() + .any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path)) }); - + if !profile_path_match { - println!("PID {} does not match profile path for {}", pid, profile.name); + println!( + "PID {} does not match profile path for {}", + pid, profile.name + ); return false; } - - println!("PID {} validated successfully for {} profile {}", pid, profile.browser, profile.name); + + println!( + "PID {} validated successfully for {} profile {}", + pid, profile.browser, profile.name + ); return true; } else { println!("PID {} does not exist", pid); @@ -117,14 +136,22 @@ impl BrowserRunner { } pub fn get_binaries_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("binaries"); path } pub fn get_profiles_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("profiles"); path } @@ -244,7 +271,9 @@ impl BrowserRunner { // Check if the browser is currently running if profile.process_id.is_some() { - return Err("Cannot update version while browser is running. Please stop the browser first.".into()); + return Err( + "Cannot update version while browser is running. Please stop the browser first.".into(), + ); } // Verify the new version is downloaded @@ -270,24 +299,20 @@ impl BrowserRunner { vec![ // Disable default browser check "user_pref(\"browser.shell.checkDefaultBrowser\", false);".to_string(), - // Disable automatic updates "user_pref(\"app.update.enabled\", false);".to_string(), "user_pref(\"app.update.auto\", false);".to_string(), "user_pref(\"app.update.mode\", 0);".to_string(), "user_pref(\"app.update.service.enabled\", false);".to_string(), "user_pref(\"app.update.silent\", false);".to_string(), - // Disable update checking entirely "user_pref(\"app.update.checkInstallTime\", false);".to_string(), "user_pref(\"app.update.url\", \"\");".to_string(), "user_pref(\"app.update.url.manual\", \"\");".to_string(), "user_pref(\"app.update.url.details\", \"\");".to_string(), - // Disable background update downloads "user_pref(\"app.update.download.attemptOnce\", false);".to_string(), "user_pref(\"app.update.idletime\", -1);".to_string(), - // Additional update-related preferences for completeness "user_pref(\"security.tls.insecure_fallback_hosts\", \"\");".to_string(), "user_pref(\"app.update.staging.enabled\", false);".to_string(), @@ -436,41 +461,52 @@ impl BrowserRunner { browser_dir.push(&profile.browser); browser_dir.push(&profile.version); - let executable_path = browser.get_executable_path(&browser_dir).expect("Failed to get executable path"); + let executable_path = browser + .get_executable_path(&browser_dir) + .expect("Failed to get executable path"); // Get launch arguments - let browser_args = - browser.create_launch_args(&profile.profile_path, profile.proxy.as_ref(), url).expect("Failed to create launch arguments"); + let browser_args = browser + .create_launch_args(&profile.profile_path, profile.proxy.as_ref(), url) + .expect("Failed to create launch arguments"); // Launch browser let child = Command::new(executable_path).args(&browser_args).spawn()?; let launcher_pid = child.id(); - println!("Launched browser with launcher PID: {} for profile: {}", launcher_pid, profile.name); + println!( + "Launched browser with launcher PID: {} for profile: {}", + launcher_pid, profile.name + ); // For TOR and Mullvad browsers, we need to find the actual browser process // because they use launcher scripts that spawn the real browser process - let actual_pid = if matches!(browser_type, BrowserType::TorBrowser | BrowserType::MullvadBrowser) { + let actual_pid = if matches!( + browser_type, + BrowserType::TorBrowser | BrowserType::MullvadBrowser + ) { println!("Waiting for TOR/Mullvad browser to fully start..."); - + // Wait a bit for the browser to fully start tokio::time::sleep(tokio::time::Duration::from_millis(3000)).await; - + // Search for the actual browser process let system = System::new_all(); let mut found_pid: Option = None; - + // Try multiple times to find the process as it might take time to start for attempt in 1..=5 { println!("Attempt {} to find actual browser process...", attempt); - + for (pid, process) in system.processes() { let cmd = process.cmd(); if cmd.len() >= 2 { // Check if this is the right browser executable let exe_name = process.name().to_string_lossy().to_lowercase(); let is_correct_browser = match profile.browser.as_str() { - "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser"), + "mullvad-browser" => { + self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser") + } "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "tor-browser"), _ => false, }; @@ -482,27 +518,34 @@ impl BrowserRunner { // Check for profile path match let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); - arg == &profile.profile_path || - arg == format!("-profile={}", profile.profile_path) || - (arg == "-profile" && cmd.iter().any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path)) + arg == &profile.profile_path + || arg == format!("-profile={}", profile.profile_path) + || (arg == "-profile" + && cmd + .iter() + .any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path)) }); - + if profile_path_match { found_pid = Some(pid.as_u32()); - println!("Found actual browser process with PID: {} for profile: {}", pid.as_u32(), profile.name); + println!( + "Found actual browser process with PID: {} for profile: {}", + pid.as_u32(), + profile.name + ); break; } } } - + if found_pid.is_some() { break; } - + // Wait before next attempt tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; } - + found_pid.unwrap_or(launcher_pid) } else { // For other browsers, the launcher PID is usually the actual browser PID @@ -515,9 +558,14 @@ impl BrowserRunner { updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()); // Save the updated profile - self.save_process_info(&updated_profile).expect("Failed to save process info"); + self + .save_process_info(&updated_profile) + .expect("Failed to save process info"); - println!("Browser launched successfully with PID: {} for profile: {}", actual_pid, profile.name); + println!( + "Browser launched successfully with PID: {} for profile: {}", + actual_pid, profile.name + ); Ok(updated_profile) } @@ -536,7 +584,8 @@ impl BrowserRunner { // Get the updated profile with current PID let profiles = self.list_profiles().expect("Failed to list profiles"); - let updated_profile = profiles.into_iter() + let updated_profile = profiles + .into_iter() .find(|p| p.name == profile.name) .unwrap_or_else(|| profile.clone()); @@ -554,7 +603,7 @@ impl BrowserRunner { #[cfg(target_os = "macos")] { let pid = updated_profile.process_id.unwrap(); - + // First try: Use Firefox remote command (most reliable for these browsers) println!("Trying Firefox remote command for PID: {}", pid); let mut browser_dir = self.get_binaries_dir(); @@ -570,9 +619,7 @@ impl BrowserRunner { url.to_string(), ]; - let remote_output = Command::new(executable_path) - .args(&remote_args) - .output(); + let remote_output = Command::new(executable_path).args(&remote_args).output(); match remote_output { Ok(output) if output.status.success() => { @@ -581,17 +628,26 @@ impl BrowserRunner { } Ok(output) => { let stderr = String::from_utf8_lossy(&output.stderr); - println!("Firefox remote command failed with stderr: {}, trying AppleScript fallback", stderr); + println!( + "Firefox remote command failed with stderr: {}, trying AppleScript fallback", + stderr + ); } Err(e) => { - println!("Firefox remote command error: {}, trying AppleScript fallback", e); + println!( + "Firefox remote command error: {}, trying AppleScript fallback", + e + ); } } } // Fallback: Use AppleScript if remote command fails - let escaped_url = url.replace("\"", "\\\"").replace("\\", "\\\\").replace("'", "\\'"); - + let escaped_url = url + .replace("\"", "\\\"") + .replace("\\", "\\\\") + .replace("'", "\\'"); + let script = format!( r#" try @@ -660,18 +716,22 @@ end try pid, pid, escaped_url, pid ); - println!("Executing AppleScript fallback for Firefox-based browser (PID: {})...", pid); - let output = Command::new("osascript") - .args(["-e", &script]) - .output()?; + println!( + "Executing AppleScript fallback for Firefox-based browser (PID: {})...", + pid + ); + let output = Command::new("osascript").args(["-e", &script]).output()?; if !output.status.success() { let error_msg = String::from_utf8_lossy(&output.stderr); println!("AppleScript failed: {}", error_msg); - return Err(format!( - "Both Firefox remote command and AppleScript failed. AppleScript error: {}", - error_msg - ).into()); + return Err( + format!( + "Both Firefox remote command and AppleScript failed. AppleScript error: {}", + error_msg + ) + .into(), + ); } else { println!("AppleScript succeeded"); } @@ -688,19 +748,17 @@ end try let executable_path = browser.get_executable_path(&browser_dir)?; let output = Command::new(executable_path) - .args([ - "-profile", - &updated_profile.profile_path, - "-new-tab", - url, - ]) + .args(["-profile", &updated_profile.profile_path, "-new-tab", url]) .output()?; if !output.status.success() { - return Err(format!( - "Failed to open URL in existing browser: {}", - String::from_utf8_lossy(&output.stderr) - ).into()); + return Err( + format!( + "Failed to open URL in existing browser: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into(), + ); } } } @@ -709,25 +767,31 @@ end try #[cfg(target_os = "macos")] { let pid = updated_profile.process_id.unwrap(); - - println!("Opening URL in TOR/Mullvad browser using file-based approach (PID: {})", pid); - + + println!( + "Opening URL in TOR/Mullvad browser using file-based approach (PID: {})", + pid + ); + // Validate that we have the correct PID for this TOR/Mullvad browser if !self.validate_tor_mullvad_pid(&updated_profile, pid) { - return Err(format!( - "PID {} is not valid for {} profile {}. The browser process may have changed.", - pid, updated_profile.browser, updated_profile.name - ).into()); + return Err( + format!( + "PID {} is not valid for {} profile {}. The browser process may have changed.", + pid, updated_profile.browser, updated_profile.name + ) + .into(), + ); } - + // Method 1: Try using a temporary HTML file approach that doesn't require accessibility permissions println!("Attempting file-based URL opening for TOR/Mullvad browser"); - + // Create a temporary HTML file that redirects to the target URL let temp_dir = std::env::temp_dir(); let temp_file_name = format!("donut_browser_url_{}.html", std::process::id()); let temp_file_path = temp_dir.join(&temp_file_name); - + let html_content = format!( r#" @@ -745,78 +809,74 @@ end try "#, url, url, url, url ); - + // Write the HTML file match std::fs::write(&temp_file_path, html_content) { Ok(()) => { println!("Created temporary HTML file: {:?}", temp_file_path); - + // Get the browser executable path to use with 'open' let mut browser_dir = self.get_binaries_dir(); browser_dir.push(&updated_profile.browser); browser_dir.push(&updated_profile.version); - - let browser = create_browser(browser_type.clone()); - if let Ok(executable_path) = browser.get_executable_path(&browser_dir) { - // Use 'open' command to open the HTML file with the specific browser instance - // This approach works because it uses the existing browser process - let open_result = Command::new("open") - .args([ - "-a", - executable_path.to_str().unwrap(), - temp_file_path.to_str().unwrap(), - ]) - .output(); - - // Clean up the temporary file after a short delay - let temp_file_path_clone = temp_file_path.clone(); - tokio::spawn(async move { - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - let _ = std::fs::remove_file(temp_file_path_clone); - }); - - match open_result { - Ok(output) if output.status.success() => { - println!("Successfully opened URL using file-based approach"); - return Ok(()); - } - Ok(output) => { - let stderr = String::from_utf8_lossy(&output.stderr); - println!("File-based approach failed: {}", stderr); - } - Err(e) => { - println!("File-based approach error: {}", e); - } - } - } - - // Clean up temp file if the approach failed - let _ = std::fs::remove_file(&temp_file_path); - } - Err(e) => { - println!("Failed to create temporary HTML file: {}", e); - } - } - - // Method 2: Try using the 'open' command directly with the URL - println!("Attempting direct URL opening with 'open' command"); - - // Get the browser executable path - let mut browser_dir = self.get_binaries_dir(); - browser_dir.push(&updated_profile.browser); - browser_dir.push(&updated_profile.version); - - let browser = create_browser(browser_type.clone()); + + let browser = create_browser(browser_type.clone()); + if let Ok(executable_path) = browser.get_executable_path(&browser_dir) { + // Use 'open' command to open the HTML file with the specific browser instance + // This approach works because it uses the existing browser process + let open_result = Command::new("open") + .args([ + "-a", + executable_path.to_str().unwrap(), + temp_file_path.to_str().unwrap(), + ]) + .output(); + + // Clean up the temporary file after a short delay + let temp_file_path_clone = temp_file_path.clone(); + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + let _ = std::fs::remove_file(temp_file_path_clone); + }); + + match open_result { + Ok(output) if output.status.success() => { + println!("Successfully opened URL using file-based approach"); + return Ok(()); + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + println!("File-based approach failed: {}", stderr); + } + Err(e) => { + println!("File-based approach error: {}", e); + } + } + } + + // Clean up temp file if the approach failed + let _ = std::fs::remove_file(&temp_file_path); + } + Err(e) => { + println!("Failed to create temporary HTML file: {}", e); + } + } + + // Method 2: Try using the 'open' command directly with the URL + println!("Attempting direct URL opening with 'open' command"); + + // Get the browser executable path + let mut browser_dir = self.get_binaries_dir(); + browser_dir.push(&updated_profile.browser); + browser_dir.push(&updated_profile.version); + + let browser = create_browser(browser_type.clone()); if let Ok(executable_path) = browser.get_executable_path(&browser_dir) { // Try to open the URL directly with the browser let direct_open_result = Command::new("open") - .args([ - "-a", - executable_path.to_str().unwrap(), - url, - ]) + .args(["-a", executable_path.to_str().unwrap(), url]) .output(); - + match direct_open_result { Ok(output) if output.status.success() => { println!("Successfully opened URL using direct 'open' command"); @@ -831,10 +891,10 @@ end try } } } - + // Method 3: Try using osascript without accessibility features (just to bring window to front) println!("Attempting minimal AppleScript approach without accessibility features"); - + let minimal_script = format!( r#" try @@ -865,12 +925,10 @@ end try let result = String::from_utf8_lossy(&output.stdout).trim().to_string(); if output.status.success() && result.contains("successfully") { println!("Successfully brought browser to front: {}", result); - + // Now try to use the system's default URL opening mechanism - let system_open_result = Command::new("open") - .args([url]) - .output(); - + let system_open_result = Command::new("open").args([url]).output(); + match system_open_result { Ok(output) if output.status.success() => { println!("Successfully opened URL using system default handler"); @@ -892,7 +950,7 @@ end try println!("Minimal AppleScript execution error: {}", e); } } - + // If all methods fail, return a more helpful error message return Err(format!( "Failed to open URL in existing TOR/Mullvad browser (PID: {}). All methods failed:\n\ @@ -918,7 +976,7 @@ end try #[cfg(target_os = "macos")] { let pid = updated_profile.process_id.unwrap(); - + // First, try using the browser's built-in URL opening capability println!("Trying Chromium URL opening for PID: {}", pid); let mut browser_dir = self.get_binaries_dir(); @@ -942,17 +1000,23 @@ end try } Ok(output) => { let stderr = String::from_utf8_lossy(&output.stderr); - println!("Chromium URL opening failed: {}, trying AppleScript", stderr); + println!( + "Chromium URL opening failed: {}, trying AppleScript", + stderr + ); } Err(e) => { println!("Chromium URL opening error: {}, trying AppleScript", e); } } } - + // Fallback to AppleScript with more precise targeting - let escaped_url = url.replace("\"", "\\\"").replace("\\", "\\\\").replace("'", "\\'"); - + let escaped_url = url + .replace("\"", "\\\"") + .replace("\\", "\\\\") + .replace("'", "\\'"); + let script = format!( r#" try @@ -1021,18 +1085,22 @@ end try pid, pid, escaped_url, pid ); - println!("Executing AppleScript for Chromium-based browser (PID: {})...", pid); - let output = Command::new("osascript") - .args(["-e", &script]) - .output()?; + println!( + "Executing AppleScript for Chromium-based browser (PID: {})...", + pid + ); + let output = Command::new("osascript").args(["-e", &script]).output()?; if !output.status.success() { let error_msg = String::from_utf8_lossy(&output.stderr); println!("AppleScript failed: {}", error_msg); - return Err(format!( - "Failed to open URL in existing Chromium-based browser: {}", - error_msg - ).into()); + return Err( + format!( + "Failed to open URL in existing Chromium-based browser: {}", + error_msg + ) + .into(), + ); } else { println!("AppleScript succeeded"); } @@ -1057,10 +1125,13 @@ end try .output()?; if !output.status.success() { - return Err(format!( - "Failed to open URL in existing Chromium-based browser: {}", - String::from_utf8_lossy(&output.stderr) - ).into()); + return Err( + format!( + "Failed to open URL in existing Chromium-based browser: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into(), + ); } } } @@ -1077,42 +1148,54 @@ end try ) -> Result> { // Get the most up-to-date profile data let profiles = self.list_profiles().expect("Failed to list profiles"); - let updated_profile = profiles.into_iter() + let updated_profile = profiles + .into_iter() .find(|p| p.name == profile.name) .unwrap_or_else(|| profile.clone()); // Check if browser is already running - let is_running = self.check_browser_status(app_handle.clone(), &updated_profile).await?; - + let is_running = self + .check_browser_status(app_handle.clone(), &updated_profile) + .await?; + // Get the updated profile again after status check (PID might have been updated) let profiles = self.list_profiles().expect("Failed to list profiles"); - let final_profile = profiles.into_iter() + let final_profile = profiles + .into_iter() .find(|p| p.name == profile.name) .unwrap_or_else(|| updated_profile.clone()); - - println!("Browser status check - Profile: {}, Running: {}, URL: {:?}, PID: {:?}", - final_profile.name, is_running, url, final_profile.process_id); + + println!( + "Browser status check - Profile: {}, Running: {}, URL: {:?}, PID: {:?}", + final_profile.name, is_running, url, final_profile.process_id + ); if is_running && url.is_some() { // Browser is running and we have a URL to open println!("Opening URL in existing browser: {}", url.as_ref().unwrap()); - + // For TOR/Mullvad browsers, add extra verification - if matches!(final_profile.browser.as_str(), "tor-browser" | "mullvad-browser") { + if matches!( + final_profile.browser.as_str(), + "tor-browser" | "mullvad-browser" + ) { println!("TOR/Mullvad browser detected - ensuring we have correct PID"); if final_profile.process_id.is_none() { println!("ERROR: No PID found for running TOR/Mullvad browser - this should not happen"); return Err("No PID found for running browser".into()); } } - match self.open_url_in_existing_browser(app_handle, &final_profile, url.as_ref().unwrap()).await { + match self + .open_url_in_existing_browser(app_handle, &final_profile, url.as_ref().unwrap()) + .await + { Ok(()) => { println!("Successfully opened URL in existing browser"); Ok(final_profile) } Err(e) => { println!("Failed to open URL in existing browser: {}", e); - + // For Mullvad and Tor browsers, don't fall back to new instance since they use -no-remote // and can't have multiple instances with the same profile match final_profile.browser.as_str() { @@ -1123,7 +1206,10 @@ end try ).into()); } _ => { - println!("Falling back to new instance for browser: {}", final_profile.browser); + println!( + "Falling back to new instance for browser: {}", + final_profile.browser + ); // Fallback to launching a new instance for other browsers self.launch_browser(&final_profile, url).await } @@ -1197,7 +1283,11 @@ end try pub fn get_saved_mullvad_releases(&self) -> Result, Box> { let mut data_path = self.base_dirs.data_local_dir().to_path_buf(); - data_path.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" }); + data_path.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); data_path.push("data"); let releases_file = data_path.join("mullvad_releases.json"); @@ -1207,7 +1297,11 @@ end try let mut versions = Vec::new(); let mut browser_dir = self.base_dirs.data_local_dir().to_path_buf(); - browser_dir.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" }); + browser_dir.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); browser_dir.push("binaries"); browser_dir.push("mullvad-browser"); for entry in fs::read_dir(browser_dir)? { @@ -1275,28 +1369,40 @@ end try let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); // For Firefox-based browsers, check for exact profile path match - if profile.browser == "tor-browser" || profile.browser == "firefox" || - profile.browser == "firefox-developer" || profile.browser == "mullvad-browser" || - profile.browser == "zen" { - arg == &profile.profile_path || - arg == format!("-profile={}", profile.profile_path) || - (arg == "-profile" && cmd.iter().any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path)) + if profile.browser == "tor-browser" + || profile.browser == "firefox" + || profile.browser == "firefox-developer" + || profile.browser == "mullvad-browser" + || profile.browser == "zen" + { + arg == &profile.profile_path + || arg == format!("-profile={}", profile.profile_path) + || (arg == "-profile" + && cmd + .iter() + .any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path)) } else { // For Chromium-based browsers, check for user-data-dir - arg.contains(&format!("--user-data-dir={}", profile.profile_path)) || - arg == &profile.profile_path + arg.contains(&format!("--user-data-dir={}", profile.profile_path)) + || arg == &profile.profile_path } }); - + if profile_path_match { is_running = true; found_pid = Some(pid); - println!("Found existing browser process with PID: {} for profile: {}", pid, profile.name); + println!( + "Found existing browser process with PID: {} for profile: {}", + pid, profile.name + ); } else { println!("PID {} exists but doesn't match our profile path exactly, searching for correct process...", pid); } } else { - println!("Stored PID {} no longer exists, searching for browser process...", pid); + println!( + "Stored PID {} no longer exists, searching for browser process...", + pid + ); } } @@ -1308,7 +1414,12 @@ end try // Check if this is the right browser executable first let exe_name = process.name().to_string_lossy().to_lowercase(); let is_correct_browser = match profile.browser.as_str() { - "firefox" => exe_name.contains("firefox") && !exe_name.contains("developer") && !exe_name.contains("tor") && !exe_name.contains("mullvad"), + "firefox" => { + exe_name.contains("firefox") + && !exe_name.contains("developer") + && !exe_name.contains("tor") + && !exe_name.contains("mullvad") + } "firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"), "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser"), "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "tor-browser"), @@ -1326,24 +1437,34 @@ end try let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); // For Firefox-based browsers, check for exact profile path match - if profile.browser == "tor-browser" || profile.browser == "firefox" || - profile.browser == "firefox-developer" || profile.browser == "mullvad-browser" || - profile.browser == "zen" { - arg == &profile.profile_path || - arg == format!("-profile={}", profile.profile_path) || - (arg == "-profile" && cmd.iter().any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path)) + if profile.browser == "tor-browser" + || profile.browser == "firefox" + || profile.browser == "firefox-developer" + || profile.browser == "mullvad-browser" + || profile.browser == "zen" + { + arg == &profile.profile_path + || arg == format!("-profile={}", profile.profile_path) + || (arg == "-profile" + && cmd + .iter() + .any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path)) } else { // For Chromium-based browsers, check for user-data-dir - arg.contains(&format!("--user-data-dir={}", profile.profile_path)) || - arg == &profile.profile_path + arg.contains(&format!("--user-data-dir={}", profile.profile_path)) + || arg == &profile.profile_path } }); - + if profile_path_match { // Found a matching process found_pid = Some(pid.as_u32()); is_running = true; - println!("Found browser process with PID: {} for profile: {}", pid.as_u32(), profile.name); + println!( + "Found browser process with PID: {} for profile: {}", + pid.as_u32(), + profile.name + ); break; } } @@ -1357,7 +1478,10 @@ end try if let Err(e) = self.save_process_info(&inner_profile) { println!("Warning: Failed to update process info: {}", e); } else { - println!("Updated process ID for profile '{}' to: {}", inner_profile.name, pid); + println!( + "Updated process ID for profile '{}' to: {}", + inner_profile.name, pid + ); } } } else if is_running { @@ -1441,7 +1565,12 @@ end try // Check if this is the right browser executable first let exe_name = process.name().to_string_lossy().to_lowercase(); let is_correct_browser = match profile.browser.as_str() { - "firefox" => exe_name.contains("firefox") && !exe_name.contains("developer") && !exe_name.contains("tor") && !exe_name.contains("mullvad"), + "firefox" => { + exe_name.contains("firefox") + && !exe_name.contains("developer") + && !exe_name.contains("tor") + && !exe_name.contains("mullvad") + } "firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"), "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser"), "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "tor-browser"), @@ -1459,17 +1588,20 @@ end try let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); // For Firefox-based browsers, check for exact profile path match - if profile.browser == "tor-browser" || profile.browser == "firefox" || - profile.browser == "firefox-developer" || profile.browser == "mullvad-browser" || - profile.browser == "zen" { + if profile.browser == "tor-browser" + || profile.browser == "firefox" + || profile.browser == "firefox-developer" + || profile.browser == "mullvad-browser" + || profile.browser == "zen" + { arg == &profile.profile_path || arg == format!("-profile={}", profile.profile_path) } else { // For Chromium-based browsers, check for user-data-dir - arg.contains(&format!("--user-data-dir={}", profile.profile_path)) || - arg == &profile.profile_path + arg.contains(&format!("--user-data-dir={}", profile.profile_path)) + || arg == &profile.profile_path } }); - + if profile_path_match { found_pid = Some(pid.as_u32()); break; @@ -1491,7 +1623,7 @@ end try #[cfg(target_os = "macos")] { use std::process::Command; - + // First try SIGTERM (graceful shutdown) let output = Command::new("kill") .args(["-TERM", &pid.to_string()]) @@ -1505,7 +1637,14 @@ end try .output()?; if !output.status.success() { - return Err(format!("Failed to kill process {}: {}", pid, String::from_utf8_lossy(&output.stderr)).into()); + return Err( + format!( + "Failed to kill process {}: {}", + pid, + String::from_utf8_lossy(&output.stderr) + ) + .into(), + ); } } } @@ -1526,7 +1665,8 @@ end try // Clear the process ID from the profile let mut updated_profile = profile.clone(); updated_profile.process_id = None; - self.save_process_info(&updated_profile) + self + .save_process_info(&updated_profile) .map_err(|e| format!("Failed to update profile: {}", e))?; println!("Successfully killed browser process with PID: {}", pid); @@ -1681,16 +1821,22 @@ pub fn get_supported_browsers() -> Result, String> { } #[tauri::command] -pub async fn fetch_browser_versions_detailed(browser_str: String) -> Result, String> { +pub async fn fetch_browser_versions_detailed( + browser_str: String, +) -> Result, String> { let service = BrowserVersionService::new(); - service.fetch_browser_versions_detailed(&browser_str, false).await + service + .fetch_browser_versions_detailed(&browser_str, false) + .await .map_err(|e| format!("Failed to fetch detailed browser versions: {}", e)) } #[tauri::command] -pub async fn fetch_browser_versions_cached_first(browser_str: String) -> Result, String> { +pub async fn fetch_browser_versions_cached_first( + browser_str: String, +) -> Result, String> { let service = BrowserVersionService::new(); - + // Get cached versions immediately if available if let Some(cached_versions) = service.get_cached_browser_versions_detailed(&browser_str) { // Check if we should update cache in background @@ -1699,23 +1845,33 @@ pub async fn fetch_browser_versions_cached_first(browser_str: String) -> Result< let service_clone = BrowserVersionService::new(); let browser_str_clone = browser_str.clone(); tokio::spawn(async move { - if let Err(e) = service_clone.fetch_browser_versions_detailed(&browser_str_clone, false).await { - eprintln!("Background version update failed for {}: {}", browser_str_clone, e); + if let Err(e) = service_clone + .fetch_browser_versions_detailed(&browser_str_clone, false) + .await + { + eprintln!( + "Background version update failed for {}: {}", + browser_str_clone, e + ); } }); } Ok(cached_versions) } else { // No cache available, fetch fresh - service.fetch_browser_versions_detailed(&browser_str, false).await + service + .fetch_browser_versions_detailed(&browser_str, false) + .await .map_err(|e| format!("Failed to fetch detailed browser versions: {}", e)) } } #[tauri::command] -pub async fn fetch_browser_versions_with_count_cached_first(browser_str: String) -> Result { +pub async fn fetch_browser_versions_with_count_cached_first( + browser_str: String, +) -> Result { let service = BrowserVersionService::new(); - + // Get cached versions immediately if available if let Some(cached_versions) = service.get_cached_browser_versions(&browser_str) { // Check if we should update cache in background @@ -1724,12 +1880,18 @@ pub async fn fetch_browser_versions_with_count_cached_first(browser_str: String) let service_clone = BrowserVersionService::new(); let browser_str_clone = browser_str.clone(); tokio::spawn(async move { - if let Err(e) = service_clone.fetch_browser_versions_with_count(&browser_str_clone, false).await { - eprintln!("Background version update failed for {}: {}", browser_str_clone, e); + if let Err(e) = service_clone + .fetch_browser_versions_with_count(&browser_str_clone, false) + .await + { + eprintln!( + "Background version update failed for {}: {}", + browser_str_clone, e + ); } }); } - + // Return cached data in the expected format Ok(BrowserVersionsResult { versions: cached_versions.clone(), @@ -1738,13 +1900,17 @@ pub async fn fetch_browser_versions_with_count_cached_first(browser_str: String) }) } else { // No cache available, fetch fresh - service.fetch_browser_versions_with_count(&browser_str, false).await + service + .fetch_browser_versions_with_count(&browser_str, false) + .await .map_err(|e| format!("Failed to fetch browser versions: {}", e)) } } #[tauri::command] -pub fn get_cached_browser_versions_detailed(browser_str: String) -> Result>, String> { +pub fn get_cached_browser_versions_detailed( + browser_str: String, +) -> Result>, String> { let service = BrowserVersionService::new(); Ok(service.get_cached_browser_versions_detailed(&browser_str)) } @@ -1794,12 +1960,20 @@ pub async fn download_browser( // Mark download as started in registry registry.mark_download_started(&browser_str, &version, browser_dir.clone()); - registry.save().map_err(|e| format!("Failed to save registry: {}", e))?; + registry + .save() + .map_err(|e| format!("Failed to save registry: {}", e))?; // Use the new download module let downloader = Downloader::new(); let download_path = match downloader - .download_browser(&app_handle, browser_type.clone(), &version, &download_info, &browser_dir) + .download_browser( + &app_handle, + browser_type.clone(), + &version, + &download_info, + &browser_dir, + ) .await { Ok(path) => path, @@ -1815,7 +1989,13 @@ pub async fn download_browser( if download_info.is_archive { let extractor = Extractor::new(); match extractor - .extract_browser(&app_handle, browser_type.clone(), &version, &download_path, &browser_dir) + .extract_browser( + &app_handle, + browser_type.clone(), + &version, + &download_path, + &browser_dir, + ) .await { Ok(_) => { @@ -1850,8 +2030,11 @@ pub async fn download_browser( let _ = app_handle.emit("download-progress", &progress); // Verify the browser was downloaded correctly - println!("Verifying download for browser: {}, version: {}", browser_str, version); - + println!( + "Verifying download for browser: {}, version: {}", + browser_str, version + ); + // Use the browser's own verification method let binaries_dir = browser_runner.get_binaries_dir(); if !browser.is_version_downloaded(&version, &binaries_dir) { @@ -1866,10 +2049,13 @@ pub async fn download_browser( } else { None }; - - registry.mark_download_completed_with_actual_version(&browser_str, &version, actual_version) + + registry + .mark_download_completed_with_actual_version(&browser_str, &version, actual_version) .map_err(|e| format!("Failed to mark download as completed: {}", e))?; - registry.save().map_err(|e| format!("Failed to save registry: {}", e))?; + registry + .save() + .map_err(|e| format!("Failed to save registry: {}", e))?; // Emit completion let progress = DownloadProgress { @@ -1913,7 +2099,9 @@ pub async fn kill_browser_profile( profile: BrowserProfile, ) -> Result<(), String> { let browser_runner = BrowserRunner::new(); - browser_runner.kill_browser_process(app_handle, &profile).await + browser_runner + .kill_browser_process(app_handle, &profile) + .await .map_err(|e| format!("Failed to kill browser: {}", e)) } @@ -1924,22 +2112,28 @@ pub fn create_browser_profile_new( version: String, proxy: Option, ) -> Result { - let browser_type = BrowserType::from_str(&browser_str) - .map_err(|e| format!("Invalid browser type: {}", e))?; + let browser_type = + BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {}", e))?; create_browser_profile(name, browser_type.as_str().to_string(), version, proxy) } #[tauri::command] pub async fn fetch_browser_versions(browser_str: String) -> Result, String> { let service = BrowserVersionService::new(); - service.fetch_browser_versions(&browser_str, false).await + service + .fetch_browser_versions(&browser_str, false) + .await .map_err(|e| format!("Failed to fetch browser versions: {}", e)) } #[tauri::command] -pub async fn fetch_browser_versions_with_count(browser_str: String) -> Result { +pub async fn fetch_browser_versions_with_count( + browser_str: String, +) -> Result { let service = BrowserVersionService::new(); - service.fetch_browser_versions_with_count(&browser_str, false).await + service + .fetch_browser_versions_with_count(&browser_str, false) + .await .map_err(|e| format!("Failed to fetch browser versions: {}", e)) } @@ -1952,310 +2146,294 @@ pub fn get_downloaded_browser_versions(browser_str: String) -> Result (BrowserRunner, TempDir) { - let temp_dir = TempDir::new().unwrap(); - - // Mock the base directories by setting environment variables - std::env::set_var("HOME", temp_dir.path()); - - let browser_runner = BrowserRunner::new(); - (browser_runner, temp_dir) - } + fn create_test_browser_runner() -> (BrowserRunner, TempDir) { + let temp_dir = TempDir::new().unwrap(); - #[test] - fn test_browser_runner_creation() { - let (_runner, _temp_dir) = create_test_browser_runner(); - // If we get here without panicking, the test passes - } + // Mock the base directories by setting environment variables + std::env::set_var("HOME", temp_dir.path()); - #[test] - fn test_get_binaries_dir() { - let (runner, _temp_dir) = create_test_browser_runner(); - let binaries_dir = runner.get_binaries_dir(); - - assert!(binaries_dir.to_string_lossy().contains("DonutBrowser")); - assert!(binaries_dir.to_string_lossy().contains("binaries")); - } + let browser_runner = BrowserRunner::new(); + (browser_runner, temp_dir) + } - #[test] - fn test_get_profiles_dir() { - let (runner, _temp_dir) = create_test_browser_runner(); - let profiles_dir = runner.get_profiles_dir(); - - assert!(profiles_dir.to_string_lossy().contains("DonutBrowser")); - assert!(profiles_dir.to_string_lossy().contains("profiles")); - } + #[test] + fn test_browser_runner_creation() { + let (_runner, _temp_dir) = create_test_browser_runner(); + // If we get here without panicking, the test passes + } - #[test] - fn test_create_profile() { - let (runner, _temp_dir) = create_test_browser_runner(); - - let profile = runner.create_profile( - "Test Profile", - "firefox", - "139.0", - None - ).unwrap(); + #[test] + fn test_get_binaries_dir() { + let (runner, _temp_dir) = create_test_browser_runner(); + let binaries_dir = runner.get_binaries_dir(); - assert_eq!(profile.name, "Test Profile"); - assert_eq!(profile.browser, "firefox"); - assert_eq!(profile.version, "139.0"); - assert!(profile.proxy.is_none()); - assert!(profile.process_id.is_none()); - } + assert!(binaries_dir.to_string_lossy().contains("DonutBrowser")); + assert!(binaries_dir.to_string_lossy().contains("binaries")); + } - #[test] - fn test_create_profile_with_proxy() { - let (runner, _temp_dir) = create_test_browser_runner(); - - let proxy = ProxySettings { - enabled: true, - proxy_type: "http".to_string(), - host: "127.0.0.1".to_string(), - port: 8080, - }; + #[test] + fn test_get_profiles_dir() { + let (runner, _temp_dir) = create_test_browser_runner(); + let profiles_dir = runner.get_profiles_dir(); - let profile = runner.create_profile( - "Test Profile with Proxy", - "firefox", - "139.0", - Some(proxy.clone()) - ).unwrap(); + assert!(profiles_dir.to_string_lossy().contains("DonutBrowser")); + assert!(profiles_dir.to_string_lossy().contains("profiles")); + } - assert_eq!(profile.name, "Test Profile with Proxy"); - assert!(profile.proxy.is_some()); - let profile_proxy = profile.proxy.unwrap(); - assert_eq!(profile_proxy.proxy_type, "http"); - assert_eq!(profile_proxy.host, "127.0.0.1"); - assert_eq!(profile_proxy.port, 8080); - } + #[test] + fn test_create_profile() { + let (runner, _temp_dir) = create_test_browser_runner(); - #[test] - fn test_save_and_load_profile() { - let (runner, _temp_dir) = create_test_browser_runner(); - - let profile = runner.create_profile( - "Test Save Load", - "firefox", - "139.0", - None - ).unwrap(); + let profile = runner + .create_profile("Test Profile", "firefox", "139.0", None) + .unwrap(); - // Save the profile - runner.save_profile(&profile).unwrap(); + assert_eq!(profile.name, "Test Profile"); + assert_eq!(profile.browser, "firefox"); + assert_eq!(profile.version, "139.0"); + assert!(profile.proxy.is_none()); + assert!(profile.process_id.is_none()); + } - // Load profiles and verify - let profiles = runner.list_profiles().unwrap(); - assert_eq!(profiles.len(), 1); - assert_eq!(profiles[0].name, "Test Save Load"); - assert_eq!(profiles[0].browser, "firefox"); - assert_eq!(profiles[0].version, "139.0"); - } + #[test] + fn test_create_profile_with_proxy() { + let (runner, _temp_dir) = create_test_browser_runner(); - #[test] - fn test_update_profile_proxy() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Create profile without proxy - let profile = runner.create_profile( - "Test Update Proxy", - "firefox", - "139.0", - None - ).unwrap(); + let proxy = ProxySettings { + enabled: true, + proxy_type: "http".to_string(), + host: "127.0.0.1".to_string(), + port: 8080, + }; - assert!(profile.proxy.is_none()); + let profile = runner + .create_profile( + "Test Profile with Proxy", + "firefox", + "139.0", + Some(proxy.clone()), + ) + .unwrap(); - // Update with proxy - let proxy = ProxySettings { - enabled: true, - proxy_type: "socks5".to_string(), - host: "192.168.1.1".to_string(), - port: 1080, - }; + assert_eq!(profile.name, "Test Profile with Proxy"); + assert!(profile.proxy.is_some()); + let profile_proxy = profile.proxy.unwrap(); + assert_eq!(profile_proxy.proxy_type, "http"); + assert_eq!(profile_proxy.host, "127.0.0.1"); + assert_eq!(profile_proxy.port, 8080); + } - let updated_profile = runner.update_profile_proxy( - "Test Update Proxy", - Some(proxy.clone()) - ).unwrap(); + #[test] + fn test_save_and_load_profile() { + let (runner, _temp_dir) = create_test_browser_runner(); - assert!(updated_profile.proxy.is_some()); - let profile_proxy = updated_profile.proxy.unwrap(); - assert_eq!(profile_proxy.proxy_type, "socks5"); - assert_eq!(profile_proxy.host, "192.168.1.1"); - assert_eq!(profile_proxy.port, 1080); - } + let profile = runner + .create_profile("Test Save Load", "firefox", "139.0", None) + .unwrap(); - #[test] - fn test_rename_profile() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Create profile - let _profile = runner.create_profile( - "Original Name", - "firefox", - "139.0", - None - ).unwrap(); + // Save the profile + runner.save_profile(&profile).unwrap(); - // Rename profile - let renamed_profile = runner.rename_profile( - "Original Name", - "New Name" - ).unwrap(); + // Load profiles and verify + let profiles = runner.list_profiles().unwrap(); + assert_eq!(profiles.len(), 1); + assert_eq!(profiles[0].name, "Test Save Load"); + assert_eq!(profiles[0].browser, "firefox"); + assert_eq!(profiles[0].version, "139.0"); + } - assert_eq!(renamed_profile.name, "New Name"); + #[test] + fn test_update_profile_proxy() { + let (runner, _temp_dir) = create_test_browser_runner(); - // Verify old profile is gone and new one exists - let profiles = runner.list_profiles().unwrap(); - assert_eq!(profiles.len(), 1); - assert_eq!(profiles[0].name, "New Name"); - } + // Create profile without proxy + let profile = runner + .create_profile("Test Update Proxy", "firefox", "139.0", None) + .unwrap(); - #[test] - fn test_delete_profile() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Create profile - let _profile = runner.create_profile( - "To Delete", - "firefox", - "139.0", - None - ).unwrap(); + assert!(profile.proxy.is_none()); - // Verify profile exists - let profiles = runner.list_profiles().unwrap(); - assert_eq!(profiles.len(), 1); + // Update with proxy + let proxy = ProxySettings { + enabled: true, + proxy_type: "socks5".to_string(), + host: "192.168.1.1".to_string(), + port: 1080, + }; - // Delete profile - runner.delete_profile("To Delete").unwrap(); + let updated_profile = runner + .update_profile_proxy("Test Update Proxy", Some(proxy.clone())) + .unwrap(); - // Verify profile is gone - let profiles = runner.list_profiles().unwrap(); - assert_eq!(profiles.len(), 0); - } + assert!(updated_profile.proxy.is_some()); + let profile_proxy = updated_profile.proxy.unwrap(); + assert_eq!(profile_proxy.proxy_type, "socks5"); + assert_eq!(profile_proxy.host, "192.168.1.1"); + assert_eq!(profile_proxy.port, 1080); + } - #[test] - fn test_profile_name_sanitization() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Create profile with spaces and special characters - let profile = runner.create_profile( - "Test Profile With Spaces", - "firefox", - "139.0", - None - ).unwrap(); + #[test] + fn test_rename_profile() { + let (runner, _temp_dir) = create_test_browser_runner(); - // Profile path should use snake_case - assert!(profile.profile_path.contains("test_profile_with_spaces")); - } + // Create profile + let _profile = runner + .create_profile("Original Name", "firefox", "139.0", None) + .unwrap(); - #[test] - fn test_multiple_profiles() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Create multiple profiles - let _profile1 = runner.create_profile("Profile 1", "firefox", "139.0", None).unwrap(); - let _profile2 = runner.create_profile("Profile 2", "chromium", "1465660", None).unwrap(); - let _profile3 = runner.create_profile("Profile 3", "brave", "v1.81.9", None).unwrap(); + // Rename profile + let renamed_profile = runner.rename_profile("Original Name", "New Name").unwrap(); - // List profiles - let profiles = runner.list_profiles().unwrap(); - assert_eq!(profiles.len(), 3); + assert_eq!(renamed_profile.name, "New Name"); - let profile_names: Vec<&str> = profiles.iter().map(|p| p.name.as_str()).collect(); - assert!(profile_names.contains(&"Profile 1")); - assert!(profile_names.contains(&"Profile 2")); - assert!(profile_names.contains(&"Profile 3")); - } + // Verify old profile is gone and new one exists + let profiles = runner.list_profiles().unwrap(); + assert_eq!(profiles.len(), 1); + assert_eq!(profiles[0].name, "New Name"); + } - #[test] - fn test_profile_validation() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Test that we can't rename to an existing profile name - let _profile1 = runner.create_profile("Profile 1", "firefox", "139.0", None).unwrap(); - let _profile2 = runner.create_profile("Profile 2", "firefox", "139.0", None).unwrap(); + #[test] + fn test_delete_profile() { + let (runner, _temp_dir) = create_test_browser_runner(); - // Try to rename profile2 to profile1's name (should fail) - let result = runner.rename_profile("Profile 2", "Profile 1"); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("already exists")); - } + // Create profile + let _profile = runner + .create_profile("To Delete", "firefox", "139.0", None) + .unwrap(); - #[test] - fn test_error_handling() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Test updating non-existent profile - let result = runner.update_profile_proxy("Non Existent", None); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("not found")); + // Verify profile exists + let profiles = runner.list_profiles().unwrap(); + assert_eq!(profiles.len(), 1); - // Test deleting non-existent profile - let result = runner.delete_profile("Non Existent"); - assert!(result.is_ok()); // Delete should be idempotent + // Delete profile + runner.delete_profile("To Delete").unwrap(); - // Test renaming non-existent profile - let result = runner.rename_profile("Non Existent", "New Name"); - assert!(result.is_err()); - } + // Verify profile is gone + let profiles = runner.list_profiles().unwrap(); + assert_eq!(profiles.len(), 0); + } - #[test] - fn test_firefox_default_browser_preferences() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Create profile without proxy - let profile = runner.create_profile( - "Test Firefox Prefs", - "firefox", - "139.0", - None - ).unwrap(); + #[test] + fn test_profile_name_sanitization() { + let (runner, _temp_dir) = create_test_browser_runner(); - // Check that user.js file was created with default browser preference - let user_js_path = std::path::Path::new(&profile.profile_path).join("user.js"); - assert!(user_js_path.exists()); - - let user_js_content = std::fs::read_to_string(user_js_path).unwrap(); - assert!(user_js_content.contains("browser.shell.checkDefaultBrowser")); - assert!(user_js_content.contains("false")); - - // Verify automatic update disabling preferences are present - assert!(user_js_content.contains("app.update.enabled")); - assert!(user_js_content.contains("app.update.auto")); - - // Create profile with proxy - let proxy = ProxySettings { - enabled: true, - proxy_type: "http".to_string(), - host: "127.0.0.1".to_string(), - port: 8080, - }; + // Create profile with spaces and special characters + let profile = runner + .create_profile("Test Profile With Spaces", "firefox", "139.0", None) + .unwrap(); - let profile_with_proxy = runner.create_profile( - "Test Firefox Prefs Proxy", - "firefox", - "139.0", - Some(proxy) - ).unwrap(); + // Profile path should use snake_case + assert!(profile.profile_path.contains("test_profile_with_spaces")); + } - // Check that user.js file contains both proxy settings and default browser preference - let user_js_path_proxy = std::path::Path::new(&profile_with_proxy.profile_path).join("user.js"); - assert!(user_js_path_proxy.exists()); - - let user_js_content_proxy = std::fs::read_to_string(user_js_path_proxy).unwrap(); - assert!(user_js_content_proxy.contains("browser.shell.checkDefaultBrowser")); - assert!(user_js_content_proxy.contains("network.proxy.type")); - - // Verify automatic update disabling preferences are present even with proxy - assert!(user_js_content_proxy.contains("app.update.enabled")); - assert!(user_js_content_proxy.contains("app.update.auto")); - } + #[test] + fn test_multiple_profiles() { + let (runner, _temp_dir) = create_test_browser_runner(); + + // Create multiple profiles + let _profile1 = runner + .create_profile("Profile 1", "firefox", "139.0", None) + .unwrap(); + let _profile2 = runner + .create_profile("Profile 2", "chromium", "1465660", None) + .unwrap(); + let _profile3 = runner + .create_profile("Profile 3", "brave", "v1.81.9", None) + .unwrap(); + + // List profiles + let profiles = runner.list_profiles().unwrap(); + assert_eq!(profiles.len(), 3); + + let profile_names: Vec<&str> = profiles.iter().map(|p| p.name.as_str()).collect(); + assert!(profile_names.contains(&"Profile 1")); + assert!(profile_names.contains(&"Profile 2")); + assert!(profile_names.contains(&"Profile 3")); + } + + #[test] + fn test_profile_validation() { + let (runner, _temp_dir) = create_test_browser_runner(); + + // Test that we can't rename to an existing profile name + let _profile1 = runner + .create_profile("Profile 1", "firefox", "139.0", None) + .unwrap(); + let _profile2 = runner + .create_profile("Profile 2", "firefox", "139.0", None) + .unwrap(); + + // Try to rename profile2 to profile1's name (should fail) + let result = runner.rename_profile("Profile 2", "Profile 1"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("already exists")); + } + + #[test] + fn test_error_handling() { + let (runner, _temp_dir) = create_test_browser_runner(); + + // Test updating non-existent profile + let result = runner.update_profile_proxy("Non Existent", None); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("not found")); + + // Test deleting non-existent profile + let result = runner.delete_profile("Non Existent"); + assert!(result.is_ok()); // Delete should be idempotent + + // Test renaming non-existent profile + let result = runner.rename_profile("Non Existent", "New Name"); + assert!(result.is_err()); + } + + #[test] + fn test_firefox_default_browser_preferences() { + let (runner, _temp_dir) = create_test_browser_runner(); + + // Create profile without proxy + let profile = runner + .create_profile("Test Firefox Prefs", "firefox", "139.0", None) + .unwrap(); + + // Check that user.js file was created with default browser preference + let user_js_path = std::path::Path::new(&profile.profile_path).join("user.js"); + assert!(user_js_path.exists()); + + let user_js_content = std::fs::read_to_string(user_js_path).unwrap(); + assert!(user_js_content.contains("browser.shell.checkDefaultBrowser")); + assert!(user_js_content.contains("false")); + + // Verify automatic update disabling preferences are present + assert!(user_js_content.contains("app.update.enabled")); + assert!(user_js_content.contains("app.update.auto")); + + // Create profile with proxy + let proxy = ProxySettings { + enabled: true, + proxy_type: "http".to_string(), + host: "127.0.0.1".to_string(), + port: 8080, + }; + + let profile_with_proxy = runner + .create_profile("Test Firefox Prefs Proxy", "firefox", "139.0", Some(proxy)) + .unwrap(); + + // Check that user.js file contains both proxy settings and default browser preference + let user_js_path_proxy = std::path::Path::new(&profile_with_proxy.profile_path).join("user.js"); + assert!(user_js_path_proxy.exists()); + + let user_js_content_proxy = std::fs::read_to_string(user_js_path_proxy).unwrap(); + assert!(user_js_content_proxy.contains("browser.shell.checkDefaultBrowser")); + assert!(user_js_content_proxy.contains("network.proxy.type")); + + // Verify automatic update disabling preferences are present even with proxy + assert!(user_js_content_proxy.contains("app.update.enabled")); + assert!(user_js_content_proxy.contains("app.update.auto")); + } } diff --git a/src-tauri/src/browser_version_service.rs b/src-tauri/src/browser_version_service.rs index a765a27..8447b8f 100644 --- a/src-tauri/src/browser_version_service.rs +++ b/src-tauri/src/browser_version_service.rs @@ -1,309 +1,356 @@ -use crate::api_client::{ApiClient, BrowserRelease, sort_versions}; +use crate::api_client::{sort_versions, ApiClient, BrowserRelease}; use crate::browser::GithubRelease; use serde::{Deserialize, Serialize}; use std::collections::HashSet; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BrowserVersionInfo { - pub version: String, - pub is_prerelease: bool, - pub date: String, + pub version: String, + pub is_prerelease: bool, + pub date: String, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BrowserVersionsResult { - pub versions: Vec, - pub new_versions_count: Option, - pub total_versions_count: usize, + pub versions: Vec, + pub new_versions_count: Option, + pub total_versions_count: usize, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DownloadInfo { - pub url: String, - pub filename: String, - pub is_archive: bool, // true for .dmg, .zip, etc. + pub url: String, + pub filename: String, + pub is_archive: bool, // true for .dmg, .zip, etc. } pub struct BrowserVersionService { - api_client: ApiClient, + api_client: ApiClient, } impl BrowserVersionService { - pub fn new() -> Self { - Self { - api_client: ApiClient::new(), + pub fn new() -> Self { + Self { + api_client: ApiClient::new(), + } + } + + /// Get cached browser versions immediately (returns None if no cache exists) + pub fn get_cached_browser_versions(&self, browser: &str) -> Option> { + self.api_client.load_cached_versions(browser) + } + + /// Get cached detailed browser version information immediately + pub fn get_cached_browser_versions_detailed( + &self, + browser: &str, + ) -> Option> { + let cached_versions = self.api_client.load_cached_versions(browser)?; + + // Convert cached versions to detailed info (without dates since cache doesn't store them) + let detailed_info: Vec = cached_versions + .into_iter() + .map(|version| { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: crate::api_client::is_alpha_version(&version), + date: "".to_string(), // Cache doesn't store dates } + }) + .collect(); + + Some(detailed_info) + } + + /// Check if cache should be updated (expired or doesn't exist) + pub fn should_update_cache(&self, browser: &str) -> bool { + self.api_client.is_cache_expired(browser) + } + + /// Fetch browser versions with optional caching + pub async fn fetch_browser_versions( + &self, + browser: &str, + no_caching: bool, + ) -> Result, Box> { + let result = self + .fetch_browser_versions_with_count(browser, no_caching) + .await?; + Ok(result.versions) + } + + /// Fetch browser versions with new count information and optional caching + pub async fn fetch_browser_versions_with_count( + &self, + browser: &str, + no_caching: bool, + ) -> Result> { + // Get existing cached versions to compare and merge + let existing_versions = self + .api_client + .load_cached_versions(browser) + .unwrap_or_default(); + let existing_set: HashSet = existing_versions.into_iter().collect(); + + // Fetch fresh versions from API + let fresh_versions = match browser { + "firefox" => self.fetch_firefox_versions(true).await?, // Always fetch fresh for merging + "firefox-developer" => self.fetch_firefox_developer_versions(true).await?, + "mullvad-browser" => self.fetch_mullvad_versions(true).await?, + "zen" => self.fetch_zen_versions(true).await?, + "brave" => self.fetch_brave_versions(true).await?, + "chromium" => self.fetch_chromium_versions(true).await?, + "tor-browser" => self.fetch_tor_versions(true).await?, + _ => return Err(format!("Unsupported browser: {}", browser).into()), + }; + + let fresh_set: HashSet = fresh_versions.into_iter().collect(); + + // Find new versions (in fresh but not in existing cache) + let new_versions: Vec = fresh_set.difference(&existing_set).cloned().collect(); + let new_versions_count = if existing_set.is_empty() { + None + } else { + Some(new_versions.len()) + }; + + // Merge existing and fresh versions + let mut merged_versions: Vec = existing_set.union(&fresh_set).cloned().collect(); + + // Sort versions using the existing sorting logic + crate::api_client::sort_versions(&mut merged_versions); + + // Save the merged cache (unless explicitly bypassing cache) + if !no_caching { + if let Err(e) = self + .api_client + .save_cached_versions(browser, &merged_versions) + { + eprintln!("Failed to save merged cache for {}: {}", browser, e); + } } - /// Get cached browser versions immediately (returns None if no cache exists) - pub fn get_cached_browser_versions(&self, browser: &str) -> Option> { - self.api_client.load_cached_versions(browser) - } + let total_versions_count = merged_versions.len(); - /// Get cached detailed browser version information immediately - pub fn get_cached_browser_versions_detailed(&self, browser: &str) -> Option> { - let cached_versions = self.api_client.load_cached_versions(browser)?; - - // Convert cached versions to detailed info (without dates since cache doesn't store them) - let detailed_info: Vec = cached_versions.into_iter().map(|version| { - BrowserVersionInfo { + Ok(BrowserVersionsResult { + versions: merged_versions, + new_versions_count, + total_versions_count, + }) + } + + /// Fetch detailed browser version information with optional caching + pub async fn fetch_browser_versions_detailed( + &self, + browser: &str, + no_caching: bool, + ) -> Result, Box> { + // For detailed versions, we'll use the merged versions from fetch_browser_versions_with_count + // to ensure consistency with the version list + let versions_result = self + .fetch_browser_versions_with_count(browser, no_caching) + .await?; + let merged_versions = versions_result.versions; + + // Convert the version strings to BrowserVersionInfo + // Since we don't have detailed date/prerelease info for cached versions, + // we'll fetch fresh detailed info and map it to our merged versions + let detailed_info: Vec = match browser { + "firefox" => { + let releases = self.fetch_firefox_releases_detailed(true).await?; + merged_versions + .into_iter() + .map(|version| { + // Try to find matching release info, otherwise create basic info + if let Some(release) = releases.iter().find(|r| r.version == version) { + BrowserVersionInfo { + version: release.version.clone(), + is_prerelease: release.is_prerelease, + date: release.date.clone(), + } + } else { + BrowserVersionInfo { version: version.clone(), is_prerelease: crate::api_client::is_alpha_version(&version), - date: "".to_string(), // Cache doesn't store dates + date: "".to_string(), + } } - }).collect(); - - Some(detailed_info) + }) + .collect() + } + "firefox-developer" => { + let releases = self.fetch_firefox_developer_releases_detailed(true).await?; + merged_versions + .into_iter() + .map(|version| { + if let Some(release) = releases.iter().find(|r| r.version == version) { + BrowserVersionInfo { + version: release.version.clone(), + is_prerelease: release.is_prerelease, + date: release.date.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: crate::api_client::is_alpha_version(&version), + date: "".to_string(), + } + } + }) + .collect() + } + "mullvad-browser" => { + let releases = self.fetch_mullvad_releases_detailed(true).await?; + merged_versions + .into_iter() + .map(|version| { + if let Some(release) = releases.iter().find(|r| r.tag_name == version) { + BrowserVersionInfo { + version: release.tag_name.clone(), + is_prerelease: release.is_alpha, + date: release.published_at.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: false, // Mullvad usually stable releases + date: "".to_string(), + } + } + }) + .collect() + } + "zen" => { + let releases = self.fetch_zen_releases_detailed(true).await?; + merged_versions + .into_iter() + .map(|version| { + if let Some(release) = releases.iter().find(|r| r.tag_name == version) { + BrowserVersionInfo { + version: release.tag_name.clone(), + is_prerelease: release.prerelease, + date: release.published_at.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: version.contains("alpha") || version.contains("beta"), + date: "".to_string(), + } + } + }) + .collect() + } + "brave" => { + let releases = self.fetch_brave_releases_detailed(true).await?; + merged_versions + .into_iter() + .map(|version| { + if let Some(release) = releases.iter().find(|r| r.tag_name == version) { + BrowserVersionInfo { + version: release.tag_name.clone(), + is_prerelease: release.prerelease, + date: release.published_at.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: version.contains("beta") || version.contains("dev"), + date: "".to_string(), + } + } + }) + .collect() + } + "chromium" => { + let releases = self.fetch_chromium_releases_detailed(true).await?; + merged_versions + .into_iter() + .map(|version| { + if let Some(release) = releases.iter().find(|r| r.version == version) { + BrowserVersionInfo { + version: release.version.clone(), + is_prerelease: release.is_prerelease, + date: release.date.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: false, // Chromium versions are usually stable + date: "".to_string(), + } + } + }) + .collect() + } + "tor-browser" => { + let releases = self.fetch_tor_releases_detailed(true).await?; + merged_versions + .into_iter() + .map(|version| { + if let Some(release) = releases.iter().find(|r| r.version == version) { + BrowserVersionInfo { + version: release.version.clone(), + is_prerelease: release.is_prerelease, + date: release.date.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: version.contains("alpha") || version.contains("rc"), + date: "".to_string(), + } + } + }) + .collect() + } + _ => return Err(format!("Unsupported browser: {}", browser).into()), + }; + + Ok(detailed_info) + } + + /// Update browser versions incrementally (for background updates) + pub async fn update_browser_versions_incrementally( + &self, + browser: &str, + ) -> Result> { + // Get existing cached versions + let existing_versions = self + .api_client + .load_cached_versions(browser) + .unwrap_or_default(); + let existing_set: HashSet = existing_versions.into_iter().collect(); + + // Fetch new versions (always bypass cache for background updates) + let new_versions = self.fetch_browser_versions(browser, true).await?; + let new_set: HashSet = new_versions.into_iter().collect(); + + // Find truly new versions (not in existing cache) + let really_new_versions: Vec = new_set.difference(&existing_set).cloned().collect(); + let new_versions_count = really_new_versions.len(); + + // Merge existing and new versions + let mut all_versions: Vec = existing_set.union(&new_set).cloned().collect(); + + // Sort versions using the existing sorting logic + sort_versions(&mut all_versions); + + // Save the updated cache + if let Err(e) = self.api_client.save_cached_versions(browser, &all_versions) { + eprintln!("Failed to save updated cache for {}: {}", browser, e); } - /// Check if cache should be updated (expired or doesn't exist) - pub fn should_update_cache(&self, browser: &str) -> bool { - self.api_client.is_cache_expired(browser) - } + Ok(new_versions_count) + } - /// Fetch browser versions with optional caching - pub async fn fetch_browser_versions( - &self, - browser: &str, - no_caching: bool, - ) -> Result, Box> { - let result = self.fetch_browser_versions_with_count(browser, no_caching).await?; - Ok(result.versions) - } - - /// Fetch browser versions with new count information and optional caching - pub async fn fetch_browser_versions_with_count( - &self, - browser: &str, - no_caching: bool, - ) -> Result> { - // Get existing cached versions to compare and merge - let existing_versions = self.api_client.load_cached_versions(browser).unwrap_or_default(); - let existing_set: HashSet = existing_versions.into_iter().collect(); - - // Fetch fresh versions from API - let fresh_versions = match browser { - "firefox" => self.fetch_firefox_versions(true).await?, // Always fetch fresh for merging - "firefox-developer" => self.fetch_firefox_developer_versions(true).await?, - "mullvad-browser" => self.fetch_mullvad_versions(true).await?, - "zen" => self.fetch_zen_versions(true).await?, - "brave" => self.fetch_brave_versions(true).await?, - "chromium" => self.fetch_chromium_versions(true).await?, - "tor-browser" => self.fetch_tor_versions(true).await?, - _ => return Err(format!("Unsupported browser: {}", browser).into()), - }; - - let fresh_set: HashSet = fresh_versions.into_iter().collect(); - - // Find new versions (in fresh but not in existing cache) - let new_versions: Vec = fresh_set.difference(&existing_set).cloned().collect(); - let new_versions_count = if existing_set.is_empty() { None } else { Some(new_versions.len()) }; - - // Merge existing and fresh versions - let mut merged_versions: Vec = existing_set.union(&fresh_set).cloned().collect(); - - // Sort versions using the existing sorting logic - crate::api_client::sort_versions(&mut merged_versions); - - // Save the merged cache (unless explicitly bypassing cache) - if !no_caching { - if let Err(e) = self.api_client.save_cached_versions(browser, &merged_versions) { - eprintln!("Failed to save merged cache for {}: {}", browser, e); - } - } - - let total_versions_count = merged_versions.len(); - - Ok(BrowserVersionsResult { - versions: merged_versions, - new_versions_count, - total_versions_count, - }) - } - - /// Fetch detailed browser version information with optional caching - pub async fn fetch_browser_versions_detailed( - &self, - browser: &str, - no_caching: bool, - ) -> Result, Box> { - // For detailed versions, we'll use the merged versions from fetch_browser_versions_with_count - // to ensure consistency with the version list - let versions_result = self.fetch_browser_versions_with_count(browser, no_caching).await?; - let merged_versions = versions_result.versions; - - // Convert the version strings to BrowserVersionInfo - // Since we don't have detailed date/prerelease info for cached versions, - // we'll fetch fresh detailed info and map it to our merged versions - let detailed_info: Vec = match browser { - "firefox" => { - let releases = self.fetch_firefox_releases_detailed(true).await?; - merged_versions.into_iter().map(|version| { - // Try to find matching release info, otherwise create basic info - if let Some(release) = releases.iter().find(|r| r.version == version) { - BrowserVersionInfo { - version: release.version.clone(), - is_prerelease: release.is_prerelease, - date: release.date.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: crate::api_client::is_alpha_version(&version), - date: "".to_string(), - } - } - }).collect() - } - "firefox-developer" => { - let releases = self.fetch_firefox_developer_releases_detailed(true).await?; - merged_versions.into_iter().map(|version| { - if let Some(release) = releases.iter().find(|r| r.version == version) { - BrowserVersionInfo { - version: release.version.clone(), - is_prerelease: release.is_prerelease, - date: release.date.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: crate::api_client::is_alpha_version(&version), - date: "".to_string(), - } - } - }).collect() - } - "mullvad-browser" => { - let releases = self.fetch_mullvad_releases_detailed(true).await?; - merged_versions.into_iter().map(|version| { - if let Some(release) = releases.iter().find(|r| r.tag_name == version) { - BrowserVersionInfo { - version: release.tag_name.clone(), - is_prerelease: release.is_alpha, - date: release.published_at.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: false, // Mullvad usually stable releases - date: "".to_string(), - } - } - }).collect() - } - "zen" => { - let releases = self.fetch_zen_releases_detailed(true).await?; - merged_versions.into_iter().map(|version| { - if let Some(release) = releases.iter().find(|r| r.tag_name == version) { - BrowserVersionInfo { - version: release.tag_name.clone(), - is_prerelease: release.prerelease, - date: release.published_at.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: version.contains("alpha") || version.contains("beta"), - date: "".to_string(), - } - } - }).collect() - } - "brave" => { - let releases = self.fetch_brave_releases_detailed(true).await?; - merged_versions.into_iter().map(|version| { - if let Some(release) = releases.iter().find(|r| r.tag_name == version) { - BrowserVersionInfo { - version: release.tag_name.clone(), - is_prerelease: release.prerelease, - date: release.published_at.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: version.contains("beta") || version.contains("dev"), - date: "".to_string(), - } - } - }).collect() - } - "chromium" => { - let releases = self.fetch_chromium_releases_detailed(true).await?; - merged_versions.into_iter().map(|version| { - if let Some(release) = releases.iter().find(|r| r.version == version) { - BrowserVersionInfo { - version: release.version.clone(), - is_prerelease: release.is_prerelease, - date: release.date.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: false, // Chromium versions are usually stable - date: "".to_string(), - } - } - }).collect() - } - "tor-browser" => { - let releases = self.fetch_tor_releases_detailed(true).await?; - merged_versions.into_iter().map(|version| { - if let Some(release) = releases.iter().find(|r| r.version == version) { - BrowserVersionInfo { - version: release.version.clone(), - is_prerelease: release.is_prerelease, - date: release.date.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: version.contains("alpha") || version.contains("rc"), - date: "".to_string(), - } - } - }).collect() - } - _ => return Err(format!("Unsupported browser: {}", browser).into()), - }; - - Ok(detailed_info) - } - - /// Update browser versions incrementally (for background updates) - pub async fn update_browser_versions_incrementally( - &self, - browser: &str, - ) -> Result> { - // Get existing cached versions - let existing_versions = self.api_client.load_cached_versions(browser) - .unwrap_or_default(); - let existing_set: HashSet = existing_versions.into_iter().collect(); - - // Fetch new versions (always bypass cache for background updates) - let new_versions = self.fetch_browser_versions(browser, true).await?; - let new_set: HashSet = new_versions.into_iter().collect(); - - // Find truly new versions (not in existing cache) - let really_new_versions: Vec = new_set.difference(&existing_set).cloned().collect(); - let new_versions_count = really_new_versions.len(); - - // Merge existing and new versions - let mut all_versions: Vec = existing_set.union(&new_set).cloned().collect(); - - // Sort versions using the existing sorting logic - sort_versions(&mut all_versions); - - // Save the updated cache - if let Err(e) = self.api_client.save_cached_versions(browser, &all_versions) { - eprintln!("Failed to save updated cache for {}: {}", browser, e); - } - - Ok(new_versions_count) - } - - /// Get download information for a specific browser and version - pub fn get_download_info(&self, browser: &str, version: &str) -> Result> { - match browser { + /// Get download information for a specific browser and version + pub fn get_download_info( + &self, + browser: &str, + version: &str, + ) -> Result> { + match browser { "firefox" => Ok(DownloadInfo { url: format!("https://download.mozilla.org/?product=firefox-{}&os=osx&lang=en-US", version), filename: format!("firefox-{}.dmg", version), @@ -363,307 +410,466 @@ impl BrowserVersionService { }), _ => Err(format!("Unsupported browser: {}", browser).into()), } - } + } - // Private helper methods for each browser type + // Private helper methods for each browser type - async fn fetch_firefox_versions(&self, no_caching: bool) -> Result, Box> { - let releases = self.fetch_firefox_releases_detailed(no_caching).await?; - Ok(releases.into_iter().map(|r| r.version).collect()) - } + async fn fetch_firefox_versions( + &self, + no_caching: bool, + ) -> Result, Box> { + let releases = self.fetch_firefox_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.version).collect()) + } - async fn fetch_firefox_releases_detailed(&self, no_caching: bool) -> Result, Box> { - self.api_client.fetch_firefox_releases_with_caching(no_caching).await - } + async fn fetch_firefox_releases_detailed( + &self, + no_caching: bool, + ) -> Result, Box> { + self + .api_client + .fetch_firefox_releases_with_caching(no_caching) + .await + } - async fn fetch_firefox_developer_versions(&self, no_caching: bool) -> Result, Box> { - let releases = self.fetch_firefox_developer_releases_detailed(no_caching).await?; - Ok(releases.into_iter().map(|r| r.version).collect()) - } + async fn fetch_firefox_developer_versions( + &self, + no_caching: bool, + ) -> Result, Box> { + let releases = self + .fetch_firefox_developer_releases_detailed(no_caching) + .await?; + Ok(releases.into_iter().map(|r| r.version).collect()) + } - async fn fetch_firefox_developer_releases_detailed(&self, no_caching: bool) -> Result, Box> { - self.api_client.fetch_firefox_developer_releases_with_caching(no_caching).await - } + async fn fetch_firefox_developer_releases_detailed( + &self, + no_caching: bool, + ) -> Result, Box> { + self + .api_client + .fetch_firefox_developer_releases_with_caching(no_caching) + .await + } - async fn fetch_mullvad_versions(&self, no_caching: bool) -> Result, Box> { - let releases = self.fetch_mullvad_releases_detailed(no_caching).await?; - Ok(releases.into_iter().map(|r| r.tag_name).collect()) - } + async fn fetch_mullvad_versions( + &self, + no_caching: bool, + ) -> Result, Box> { + let releases = self.fetch_mullvad_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.tag_name).collect()) + } - async fn fetch_mullvad_releases_detailed(&self, no_caching: bool) -> Result, Box> { - self.api_client.fetch_mullvad_releases_with_caching(no_caching).await - } + async fn fetch_mullvad_releases_detailed( + &self, + no_caching: bool, + ) -> Result, Box> { + self + .api_client + .fetch_mullvad_releases_with_caching(no_caching) + .await + } - async fn fetch_zen_versions(&self, no_caching: bool) -> Result, Box> { - let releases = self.fetch_zen_releases_detailed(no_caching).await?; - Ok(releases.into_iter().map(|r| r.tag_name).collect()) - } + async fn fetch_zen_versions( + &self, + no_caching: bool, + ) -> Result, Box> { + let releases = self.fetch_zen_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.tag_name).collect()) + } - async fn fetch_zen_releases_detailed(&self, no_caching: bool) -> Result, Box> { - self.api_client.fetch_zen_releases_with_caching(no_caching).await - } + async fn fetch_zen_releases_detailed( + &self, + no_caching: bool, + ) -> Result, Box> { + self + .api_client + .fetch_zen_releases_with_caching(no_caching) + .await + } - async fn fetch_brave_versions(&self, no_caching: bool) -> Result, Box> { - let releases = self.fetch_brave_releases_detailed(no_caching).await?; - Ok(releases.into_iter().map(|r| r.tag_name).collect()) - } + async fn fetch_brave_versions( + &self, + no_caching: bool, + ) -> Result, Box> { + let releases = self.fetch_brave_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.tag_name).collect()) + } - async fn fetch_brave_releases_detailed(&self, no_caching: bool) -> Result, Box> { - self.api_client.fetch_brave_releases_with_caching(no_caching).await - } + async fn fetch_brave_releases_detailed( + &self, + no_caching: bool, + ) -> Result, Box> { + self + .api_client + .fetch_brave_releases_with_caching(no_caching) + .await + } - async fn fetch_chromium_versions(&self, no_caching: bool) -> Result, Box> { - let releases = self.fetch_chromium_releases_detailed(no_caching).await?; - Ok(releases.into_iter().map(|r| r.version).collect()) - } + async fn fetch_chromium_versions( + &self, + no_caching: bool, + ) -> Result, Box> { + let releases = self.fetch_chromium_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.version).collect()) + } - async fn fetch_chromium_releases_detailed(&self, no_caching: bool) -> Result, Box> { - self.api_client.fetch_chromium_releases_with_caching(no_caching).await - } + async fn fetch_chromium_releases_detailed( + &self, + no_caching: bool, + ) -> Result, Box> { + self + .api_client + .fetch_chromium_releases_with_caching(no_caching) + .await + } - async fn fetch_tor_versions(&self, no_caching: bool) -> Result, Box> { - let releases = self.fetch_tor_releases_detailed(no_caching).await?; - Ok(releases.into_iter().map(|r| r.version).collect()) - } + async fn fetch_tor_versions( + &self, + no_caching: bool, + ) -> Result, Box> { + let releases = self.fetch_tor_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.version).collect()) + } - async fn fetch_tor_releases_detailed(&self, no_caching: bool) -> Result, Box> { - self.api_client.fetch_tor_releases_with_caching(no_caching).await - } + async fn fetch_tor_releases_detailed( + &self, + no_caching: bool, + ) -> Result, Box> { + self + .api_client + .fetch_tor_releases_with_caching(no_caching) + .await + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[tokio::test] - async fn test_browser_version_service_creation() { - let _service = BrowserVersionService::new(); - // Test passes if we can create the service without panicking - assert!(true); + #[tokio::test] + async fn test_browser_version_service_creation() { + let _service = BrowserVersionService::new(); + // Test passes if we can create the service without panicking + assert!(true); + } + + #[tokio::test] + async fn test_fetch_firefox_versions() { + let service = BrowserVersionService::new(); + + // Test with caching + let result_cached = service.fetch_browser_versions("firefox", false).await; + assert!( + result_cached.is_ok(), + "Should fetch Firefox versions with caching" + ); + + if let Ok(versions) = result_cached { + assert!(!versions.is_empty(), "Should have Firefox versions"); + println!( + "Firefox cached test passed. Found {} versions", + versions.len() + ); } - #[tokio::test] - async fn test_fetch_firefox_versions() { - let service = BrowserVersionService::new(); - - // Test with caching - let result_cached = service.fetch_browser_versions("firefox", false).await; - assert!(result_cached.is_ok(), "Should fetch Firefox versions with caching"); - - if let Ok(versions) = result_cached { - assert!(!versions.is_empty(), "Should have Firefox versions"); - println!("Firefox cached test passed. Found {} versions", versions.len()); + // Small delay to avoid rate limiting + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // Test without caching + let result_no_cache = service.fetch_browser_versions("firefox", true).await; + assert!( + result_no_cache.is_ok(), + "Should fetch Firefox versions without caching" + ); + + if let Ok(versions) = result_no_cache { + assert!( + !versions.is_empty(), + "Should have Firefox versions without caching" + ); + println!( + "Firefox no-cache test passed. Found {} versions", + versions.len() + ); + } + } + + #[tokio::test] + async fn test_fetch_browser_versions_with_count() { + let service = BrowserVersionService::new(); + + let result = service + .fetch_browser_versions_with_count("firefox", false) + .await; + assert!(result.is_ok(), "Should fetch Firefox versions with count"); + + if let Ok(result) = result { + assert!(!result.versions.is_empty(), "Should have versions"); + assert_eq!( + result.total_versions_count, + result.versions.len(), + "Total count should match versions length" + ); + println!( + "Firefox count test passed. Found {} versions, new: {:?}", + result.total_versions_count, result.new_versions_count + ); + } + } + + #[tokio::test] + async fn test_fetch_detailed_versions() { + let service = BrowserVersionService::new(); + + let result = service + .fetch_browser_versions_detailed("firefox", false) + .await; + assert!(result.is_ok(), "Should fetch detailed Firefox versions"); + + if let Ok(versions) = result { + assert!(!versions.is_empty(), "Should have detailed versions"); + + // Check that the first version has all required fields + let first_version = &versions[0]; + assert!( + !first_version.version.is_empty(), + "Version should not be empty" + ); + println!( + "Firefox detailed test passed. Found {} detailed versions", + versions.len() + ); + } + } + + #[tokio::test] + async fn test_unsupported_browser() { + let service = BrowserVersionService::new(); + + let result = service.fetch_browser_versions("unsupported", false).await; + assert!( + result.is_err(), + "Should return error for unsupported browser" + ); + + if let Err(e) = result { + assert!( + e.to_string().contains("Unsupported browser"), + "Error should mention unsupported browser" + ); + } + } + + #[tokio::test] + async fn test_incremental_update() { + let service = BrowserVersionService::new(); + + // This test might fail if there are no cached versions yet, which is fine + let result = service + .update_browser_versions_incrementally("firefox") + .await; + + // The test should complete without panicking + match result { + Ok(count) => { + println!( + "Incremental update test passed. Found {} new versions", + count + ); + } + Err(e) => { + println!( + "Incremental update test failed (expected for first run): {}", + e + ); + // Don't fail the test, as this is expected behavior for first run + } + } + } + + #[tokio::test] + async fn test_all_supported_browsers() { + let service = BrowserVersionService::new(); + let browsers = vec![ + "firefox", + "firefox-developer", + "mullvad-browser", + "zen", + "brave", + "chromium", + "tor-browser", + ]; + + for browser in browsers { + // Test that we can at least call the function without panicking + let result = service.fetch_browser_versions(browser, false).await; + + match result { + Ok(versions) => { + println!("{} test passed. Found {} versions", browser, versions.len()); } - - // Small delay to avoid rate limiting - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - - // Test without caching - let result_no_cache = service.fetch_browser_versions("firefox", true).await; - assert!(result_no_cache.is_ok(), "Should fetch Firefox versions without caching"); - - if let Ok(versions) = result_no_cache { - assert!(!versions.is_empty(), "Should have Firefox versions without caching"); - println!("Firefox no-cache test passed. Found {} versions", versions.len()); + Err(e) => { + // Some browsers might fail due to network issues, but shouldn't panic + println!("{} test failed (network issue): {}", browser, e); } - } + } - #[tokio::test] - async fn test_fetch_browser_versions_with_count() { - let service = BrowserVersionService::new(); - - let result = service.fetch_browser_versions_with_count("firefox", false).await; - assert!(result.is_ok(), "Should fetch Firefox versions with count"); - - if let Ok(result) = result { - assert!(!result.versions.is_empty(), "Should have versions"); - assert_eq!(result.total_versions_count, result.versions.len(), "Total count should match versions length"); - println!("Firefox count test passed. Found {} versions, new: {:?}", - result.total_versions_count, result.new_versions_count); - } + // Small delay between requests to avoid rate limiting + tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; } + } - #[tokio::test] - async fn test_fetch_detailed_versions() { - let service = BrowserVersionService::new(); - - let result = service.fetch_browser_versions_detailed("firefox", false).await; - assert!(result.is_ok(), "Should fetch detailed Firefox versions"); - - if let Ok(versions) = result { - assert!(!versions.is_empty(), "Should have detailed versions"); - - // Check that the first version has all required fields - let first_version = &versions[0]; - assert!(!first_version.version.is_empty(), "Version should not be empty"); - println!("Firefox detailed test passed. Found {} detailed versions", versions.len()); - } - } + #[tokio::test] + async fn test_no_caching_parameter() { + let service = BrowserVersionService::new(); - #[tokio::test] - async fn test_unsupported_browser() { - let service = BrowserVersionService::new(); - - let result = service.fetch_browser_versions("unsupported", false).await; - assert!(result.is_err(), "Should return error for unsupported browser"); - - if let Err(e) = result { - assert!(e.to_string().contains("Unsupported browser"), "Error should mention unsupported browser"); - } - } + // Test with caching enabled (default) + let result_cached = service.fetch_browser_versions("firefox", false).await; + assert!( + result_cached.is_ok(), + "Should fetch Firefox versions with caching" + ); - #[tokio::test] - async fn test_incremental_update() { - let service = BrowserVersionService::new(); - - // This test might fail if there are no cached versions yet, which is fine - let result = service.update_browser_versions_incrementally("firefox").await; - - // The test should complete without panicking - match result { - Ok(count) => { - println!("Incremental update test passed. Found {} new versions", count); - } - Err(e) => { - println!("Incremental update test failed (expected for first run): {}", e); - // Don't fail the test, as this is expected behavior for first run - } - } - } + // Small delay to avoid rate limiting + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - #[tokio::test] - async fn test_all_supported_browsers() { - let service = BrowserVersionService::new(); - let browsers = vec![ - "firefox", "firefox-developer", "mullvad-browser", - "zen", "brave", "chromium", "tor-browser" - ]; - - for browser in browsers { - // Test that we can at least call the function without panicking - let result = service.fetch_browser_versions(browser, false).await; - - match result { - Ok(versions) => { - println!("{} test passed. Found {} versions", browser, versions.len()); - } - Err(e) => { - // Some browsers might fail due to network issues, but shouldn't panic - println!("{} test failed (network issue): {}", browser, e); - } - } - - // Small delay between requests to avoid rate limiting - tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; - } - } + // Test with caching disabled (no_caching = true) + let result_no_cache = service.fetch_browser_versions("firefox", true).await; + assert!( + result_no_cache.is_ok(), + "Should fetch Firefox versions without caching" + ); - #[tokio::test] - async fn test_no_caching_parameter() { - let service = BrowserVersionService::new(); - - // Test with caching enabled (default) - let result_cached = service.fetch_browser_versions("firefox", false).await; - assert!(result_cached.is_ok(), "Should fetch Firefox versions with caching"); - - // Small delay to avoid rate limiting - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - - // Test with caching disabled (no_caching = true) - let result_no_cache = service.fetch_browser_versions("firefox", true).await; - assert!(result_no_cache.is_ok(), "Should fetch Firefox versions without caching"); - - // Both should return versions - if let (Ok(cached_versions), Ok(no_cache_versions)) = (result_cached, result_no_cache) { - assert!(!cached_versions.is_empty(), "Cached versions should not be empty"); - assert!(!no_cache_versions.is_empty(), "No-cache versions should not be empty"); - println!("No-caching test passed. Cached: {} versions, No-cache: {} versions", - cached_versions.len(), no_cache_versions.len()); - } + // Both should return versions + if let (Ok(cached_versions), Ok(no_cache_versions)) = (result_cached, result_no_cache) { + assert!( + !cached_versions.is_empty(), + "Cached versions should not be empty" + ); + assert!( + !no_cache_versions.is_empty(), + "No-cache versions should not be empty" + ); + println!( + "No-caching test passed. Cached: {} versions, No-cache: {} versions", + cached_versions.len(), + no_cache_versions.len() + ); } + } - #[tokio::test] - async fn test_detailed_versions_with_no_caching() { - let service = BrowserVersionService::new(); - - // Test detailed versions with caching - let result_cached = service.fetch_browser_versions_detailed("firefox", false).await; - assert!(result_cached.is_ok(), "Should fetch detailed Firefox versions with caching"); - - // Small delay to avoid rate limiting - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - - // Test detailed versions without caching - let result_no_cache = service.fetch_browser_versions_detailed("firefox", true).await; - assert!(result_no_cache.is_ok(), "Should fetch detailed Firefox versions without caching"); - - // Both should return detailed version info - if let (Ok(cached_versions), Ok(no_cache_versions)) = (result_cached, result_no_cache) { - assert!(!cached_versions.is_empty(), "Cached detailed versions should not be empty"); - assert!(!no_cache_versions.is_empty(), "No-cache detailed versions should not be empty"); - - // Check that detailed versions have all required fields - let first_cached = &cached_versions[0]; - let first_no_cache = &no_cache_versions[0]; - - assert!(!first_cached.version.is_empty(), "Cached version should not be empty"); - assert!(!first_no_cache.version.is_empty(), "No-cache version should not be empty"); - - println!("Detailed no-caching test passed. Cached: {} versions, No-cache: {} versions", - cached_versions.len(), no_cache_versions.len()); - } - } + #[tokio::test] + async fn test_detailed_versions_with_no_caching() { + let service = BrowserVersionService::new(); - #[test] - fn test_get_download_info() { - let service = BrowserVersionService::new(); - - // Test Firefox - let firefox_info = service.get_download_info("firefox", "139.0").unwrap(); - assert_eq!(firefox_info.filename, "firefox-139.0.dmg"); - assert!(firefox_info.url.contains("firefox-139.0")); - assert!(firefox_info.is_archive); - - // Test Firefox Developer - let firefox_dev_info = service.get_download_info("firefox-developer", "139.0b1").unwrap(); - assert_eq!(firefox_dev_info.filename, "firefox-developer-139.0b1.dmg"); - assert!(firefox_dev_info.url.contains("devedition-139.0b1")); - assert!(firefox_dev_info.is_archive); - - // Test Mullvad Browser - let mullvad_info = service.get_download_info("mullvad-browser", "14.5a6").unwrap(); - assert_eq!(mullvad_info.filename, "mullvad-browser-14.5a6.dmg"); - assert!(mullvad_info.url.contains("mullvad-browser-macos-14.5a6")); - assert!(mullvad_info.is_archive); - - // Test Zen Browser - let zen_info = service.get_download_info("zen", "1.11b").unwrap(); - assert_eq!(zen_info.filename, "zen-1.11b.dmg"); - assert!(zen_info.url.contains("zen.macos-universal.dmg")); - assert!(zen_info.is_archive); - - // Test Tor Browser - let tor_info = service.get_download_info("tor-browser", "14.0.4").unwrap(); - assert_eq!(tor_info.filename, "tor-browser-14.0.4.dmg"); - assert!(tor_info.url.contains("tor-browser-macos-14.0.4")); - assert!(tor_info.is_archive); - - // Test Chromium - let chromium_info = service.get_download_info("chromium", "1465660").unwrap(); - assert_eq!(chromium_info.filename, "chromium-1465660.zip"); - assert!(chromium_info.url.contains("chrome-mac.zip")); - assert!(chromium_info.is_archive); - - // Test Brave - let brave_info = service.get_download_info("brave", "v1.81.9").unwrap(); - assert_eq!(brave_info.filename, "brave-v1.81.9.dmg"); - assert!(brave_info.url.contains("Brave-Browser")); - assert!(brave_info.is_archive); - - // Test unsupported browser - let unsupported_result = service.get_download_info("unsupported", "1.0.0"); - assert!(unsupported_result.is_err()); - - println!("Download info test passed for all browsers"); + // Test detailed versions with caching + let result_cached = service + .fetch_browser_versions_detailed("firefox", false) + .await; + assert!( + result_cached.is_ok(), + "Should fetch detailed Firefox versions with caching" + ); + + // Small delay to avoid rate limiting + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // Test detailed versions without caching + let result_no_cache = service + .fetch_browser_versions_detailed("firefox", true) + .await; + assert!( + result_no_cache.is_ok(), + "Should fetch detailed Firefox versions without caching" + ); + + // Both should return detailed version info + if let (Ok(cached_versions), Ok(no_cache_versions)) = (result_cached, result_no_cache) { + assert!( + !cached_versions.is_empty(), + "Cached detailed versions should not be empty" + ); + assert!( + !no_cache_versions.is_empty(), + "No-cache detailed versions should not be empty" + ); + + // Check that detailed versions have all required fields + let first_cached = &cached_versions[0]; + let first_no_cache = &no_cache_versions[0]; + + assert!( + !first_cached.version.is_empty(), + "Cached version should not be empty" + ); + assert!( + !first_no_cache.version.is_empty(), + "No-cache version should not be empty" + ); + + println!( + "Detailed no-caching test passed. Cached: {} versions, No-cache: {} versions", + cached_versions.len(), + no_cache_versions.len() + ); } -} \ No newline at end of file + } + + #[test] + fn test_get_download_info() { + let service = BrowserVersionService::new(); + + // Test Firefox + let firefox_info = service.get_download_info("firefox", "139.0").unwrap(); + assert_eq!(firefox_info.filename, "firefox-139.0.dmg"); + assert!(firefox_info.url.contains("firefox-139.0")); + assert!(firefox_info.is_archive); + + // Test Firefox Developer + let firefox_dev_info = service + .get_download_info("firefox-developer", "139.0b1") + .unwrap(); + assert_eq!(firefox_dev_info.filename, "firefox-developer-139.0b1.dmg"); + assert!(firefox_dev_info.url.contains("devedition-139.0b1")); + assert!(firefox_dev_info.is_archive); + + // Test Mullvad Browser + let mullvad_info = service + .get_download_info("mullvad-browser", "14.5a6") + .unwrap(); + assert_eq!(mullvad_info.filename, "mullvad-browser-14.5a6.dmg"); + assert!(mullvad_info.url.contains("mullvad-browser-macos-14.5a6")); + assert!(mullvad_info.is_archive); + + // Test Zen Browser + let zen_info = service.get_download_info("zen", "1.11b").unwrap(); + assert_eq!(zen_info.filename, "zen-1.11b.dmg"); + assert!(zen_info.url.contains("zen.macos-universal.dmg")); + assert!(zen_info.is_archive); + + // Test Tor Browser + let tor_info = service.get_download_info("tor-browser", "14.0.4").unwrap(); + assert_eq!(tor_info.filename, "tor-browser-14.0.4.dmg"); + assert!(tor_info.url.contains("tor-browser-macos-14.0.4")); + assert!(tor_info.is_archive); + + // Test Chromium + let chromium_info = service.get_download_info("chromium", "1465660").unwrap(); + assert_eq!(chromium_info.filename, "chromium-1465660.zip"); + assert!(chromium_info.url.contains("chrome-mac.zip")); + assert!(chromium_info.is_archive); + + // Test Brave + let brave_info = service.get_download_info("brave", "v1.81.9").unwrap(); + assert_eq!(brave_info.filename, "brave-v1.81.9.dmg"); + assert!(brave_info.url.contains("Brave-Browser")); + assert!(brave_info.is_archive); + + // Test unsupported browser + let unsupported_result = service.get_download_info("unsupported", "1.0.0"); + assert!(unsupported_result.is_err()); + + println!("Download info test passed for all browsers"); + } +} diff --git a/src-tauri/src/default_browser.rs b/src-tauri/src/default_browser.rs index ab46787..16edb5a 100644 --- a/src-tauri/src/default_browser.rs +++ b/src-tauri/src/default_browser.rs @@ -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 { 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) -> Result { +pub async fn smart_open_url( + _app_handle: tauri::AppHandle, + _url: String, + _is_startup: Option, +) -> Result { 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()) } diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs index 9515dfb..971b294 100644 --- a/src-tauri/src/download.rs +++ b/src-tauri/src/download.rs @@ -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> { 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 { } } } -} \ No newline at end of file +} diff --git a/src-tauri/src/downloaded_browsers.rs b/src-tauri/src/downloaded_browsers.rs index 21fb855..fd820ae 100644 --- a/src-tauri/src/downloaded_browsers.rs +++ b/src-tauri/src/downloaded_browsers.rs @@ -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, // 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, // For browsers like Chromium where we track the actual version } #[derive(Debug, Serialize, Deserialize, Default)] pub struct DownloadedBrowsersRegistry { - pub browsers: HashMap>, // browser -> version -> info + pub browsers: HashMap>, // browser -> version -> info } impl DownloadedBrowsersRegistry { - pub fn new() -> Self { - Self::default() + pub fn new() -> Self { + Self::default() + } + + pub fn load() -> Result> { + let registry_path = Self::get_registry_path()?; + + if !registry_path.exists() { + return Ok(Self::new()); } - pub fn load() -> Result> { - let registry_path = Self::get_registry_path()?; - - if !registry_path.exists() { - return Ok(Self::new()); - } + let content = fs::read_to_string(®istry_path)?; + let registry: DownloadedBrowsersRegistry = serde_json::from_str(&content)?; + Ok(registry) + } - let content = fs::read_to_string(®istry_path)?; - let registry: DownloadedBrowsersRegistry = serde_json::from_str(&content)?; - Ok(registry) + pub fn save(&self) -> Result<(), Box> { + 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> { - 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(®istry_path, content)?; + Ok(()) + } - let content = serde_json::to_string_pretty(self)?; - fs::write(®istry_path, content)?; - Ok(()) + fn get_registry_path() -> Result> { + 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 { + 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 { + 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, + ) -> 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> { - 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 { - 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 { - 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) -> 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> { + 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> { - 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")); - } -} \ No newline at end of file + // 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")); + } +} diff --git a/src-tauri/src/extraction.rs b/src-tauri/src/extraction.rs index 181eccc..b7ab849 100644 --- a/src-tauri/src/extraction.rs +++ b/src-tauri/src/extraction.rs @@ -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 = 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); } -} \ No newline at end of file +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 469a9d7..f2d41e0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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 Result Option<(String, u16)> { let profile_proxies = self.profile_proxies.lock().unwrap(); diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs index 4b6e7f9..3d5c8ea 100644 --- a/src-tauri/src/settings_manager.rs +++ b/src-tauri/src/settings_manager.rs @@ -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::(&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> { + pub fn save_table_sorting( + &self, + sorting: &TableSortingSettings, + ) -> Result<(), Box> { let settings_dir = self.get_settings_dir(); create_dir_all(&settings_dir)?; diff --git a/src-tauri/src/version_updater.rs b/src-tauri/src/version_updater.rs index d9aa9e3..76f784a 100644 --- a/src-tauri/src/version_updater.rs +++ b/src-tauri/src/version_updater.rs @@ -1,535 +1,568 @@ use crate::browser_version_service::BrowserVersionService; +use directories::BaseDirs; use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; use std::sync::Arc; +use std::sync::OnceLock; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tauri::Emitter; use tokio::sync::Mutex; use tokio::time::{interval, Interval}; -use std::path::PathBuf; -use std::fs; -use directories::BaseDirs; -use std::sync::OnceLock; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct VersionUpdateProgress { - pub current_browser: String, - pub total_browsers: usize, - pub completed_browsers: usize, - pub new_versions_found: usize, - pub browser_new_versions: usize, // New versions found for current browser - pub status: String, // "updating", "completed", "error" + pub current_browser: String, + pub total_browsers: usize, + pub completed_browsers: usize, + pub new_versions_found: usize, + pub browser_new_versions: usize, // New versions found for current browser + pub status: String, // "updating", "completed", "error" } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BackgroundUpdateResult { - pub browser: String, - pub new_versions_count: usize, - pub total_versions_count: usize, - pub updated_successfully: bool, - pub error: Option, + pub browser: String, + pub new_versions_count: usize, + pub total_versions_count: usize, + pub updated_successfully: bool, + pub error: Option, } #[derive(Debug, Serialize, Deserialize)] struct BackgroundUpdateState { - last_update_time: u64, - update_interval_hours: u64, + last_update_time: u64, + update_interval_hours: u64, } impl Default for BackgroundUpdateState { - fn default() -> Self { - Self { - last_update_time: 0, - update_interval_hours: 3, - } + fn default() -> Self { + Self { + last_update_time: 0, + update_interval_hours: 3, } + } } pub struct VersionUpdater { - version_service: BrowserVersionService, - app_handle: Arc>>, - update_interval: Interval, + version_service: BrowserVersionService, + app_handle: Arc>>, + update_interval: Interval, } impl VersionUpdater { - pub fn new() -> Self { - let mut update_interval = interval(Duration::from_secs(5 * 60)); // Check every 5 minutes - update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - Self { - version_service: BrowserVersionService::new(), - app_handle: Arc::new(Mutex::new(None)), - update_interval, - } + pub fn new() -> Self { + let mut update_interval = interval(Duration::from_secs(5 * 60)); // Check every 5 minutes + update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + Self { + version_service: BrowserVersionService::new(), + app_handle: Arc::new(Mutex::new(None)), + update_interval, + } + } + + fn get_cache_dir() -> Result> { + let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?; + let app_name = if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }; + let cache_dir = base_dirs.cache_dir().join(app_name).join("version_cache"); + fs::create_dir_all(&cache_dir)?; + Ok(cache_dir) + } + + fn get_background_update_state_file() -> Result> { + let cache_dir = Self::get_cache_dir()?; + Ok(cache_dir.join("background_update_state.json")) + } + + fn load_background_update_state() -> BackgroundUpdateState { + let state_file = match Self::get_background_update_state_file() { + Ok(file) => file, + Err(_) => return BackgroundUpdateState::default(), + }; + + if !state_file.exists() { + return BackgroundUpdateState::default(); } - fn get_cache_dir() -> Result> { - let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?; - let app_name = if cfg!(debug_assertions) { - "DonutBrowserDev" - } else { - "DonutBrowser" - }; - let cache_dir = base_dirs.cache_dir().join(app_name).join("version_cache"); - fs::create_dir_all(&cache_dir)?; - Ok(cache_dir) + let content = match fs::read_to_string(&state_file) { + Ok(content) => content, + Err(_) => return BackgroundUpdateState::default(), + }; + + serde_json::from_str(&content).unwrap_or_default() + } + + fn save_background_update_state( + state: &BackgroundUpdateState, + ) -> Result<(), Box> { + let state_file = Self::get_background_update_state_file()?; + let content = serde_json::to_string_pretty(state)?; + fs::write(&state_file, content)?; + Ok(()) + } + + fn get_current_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + } + + fn should_run_background_update() -> bool { + let state = Self::load_background_update_state(); + let current_time = Self::get_current_timestamp(); + let elapsed_secs = current_time.saturating_sub(state.last_update_time); + let update_interval_secs = state.update_interval_hours * 60 * 60; + + // Run update if: + // 1. Never updated before (last_update_time == 0) + // 2. More than 3 hours have passed since last update + let should_update = state.last_update_time == 0 || elapsed_secs >= update_interval_secs; + + if should_update { + println!( + "Background update needed: last_update={}, elapsed={}h, required={}h", + state.last_update_time, + elapsed_secs / 3600, + state.update_interval_hours + ); + } else { + println!( + "Background update not needed: last_update={}, elapsed={}h, required={}h", + state.last_update_time, + elapsed_secs / 3600, + state.update_interval_hours + ); } - fn get_background_update_state_file() -> Result> { - let cache_dir = Self::get_cache_dir()?; - Ok(cache_dir.join("background_update_state.json")) + should_update + } + + pub async fn set_app_handle(&self, app_handle: tauri::AppHandle) { + let mut handle = self.app_handle.lock().await; + *handle = Some(app_handle); + } + + pub async fn check_and_run_startup_update( + &self, + ) -> Result<(), Box> { + // Only run if an update is actually needed + if !Self::should_run_background_update() { + println!("No startup version update needed"); + return Ok(()); } - fn load_background_update_state() -> BackgroundUpdateState { - let state_file = match Self::get_background_update_state_file() { - Ok(file) => file, - Err(_) => return BackgroundUpdateState::default(), - }; + let app_handle = { + let handle_guard = self.app_handle.lock().await; + handle_guard.clone() + }; - if !state_file.exists() { - return BackgroundUpdateState::default(); - } + if let Some(handle) = app_handle { + println!("Running startup version update..."); - let content = match fs::read_to_string(&state_file) { - Ok(content) => content, - Err(_) => return BackgroundUpdateState::default(), - }; - - serde_json::from_str(&content).unwrap_or_default() - } - - fn save_background_update_state(state: &BackgroundUpdateState) -> Result<(), Box> { - let state_file = Self::get_background_update_state_file()?; - let content = serde_json::to_string_pretty(state)?; - fs::write(&state_file, content)?; - Ok(()) - } - - fn get_current_timestamp() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs() - } - - fn should_run_background_update() -> bool { - let state = Self::load_background_update_state(); - let current_time = Self::get_current_timestamp(); - let elapsed_secs = current_time.saturating_sub(state.last_update_time); - let update_interval_secs = state.update_interval_hours * 60 * 60; - - // Run update if: - // 1. Never updated before (last_update_time == 0) - // 2. More than 3 hours have passed since last update - let should_update = state.last_update_time == 0 || elapsed_secs >= update_interval_secs; - - if should_update { - println!( - "Background update needed: last_update={}, elapsed={}h, required={}h", - state.last_update_time, - elapsed_secs / 3600, - state.update_interval_hours - ); - } else { - println!( - "Background update not needed: last_update={}, elapsed={}h, required={}h", - state.last_update_time, - elapsed_secs / 3600, - state.update_interval_hours - ); - } - - should_update - } - - pub async fn set_app_handle(&self, app_handle: tauri::AppHandle) { - let mut handle = self.app_handle.lock().await; - *handle = Some(app_handle); - } - - pub async fn check_and_run_startup_update(&self) -> Result<(), Box> { - // Only run if an update is actually needed - if !Self::should_run_background_update() { - println!("No startup version update needed"); - return Ok(()); - } - - let app_handle = { - let handle_guard = self.app_handle.lock().await; - handle_guard.clone() - }; - - if let Some(handle) = app_handle { - println!("Running startup version update..."); - - match self.update_all_browser_versions(&handle).await { - Ok(_) => { - // Update the persistent state after successful update - let state = BackgroundUpdateState { - last_update_time: Self::get_current_timestamp(), - update_interval_hours: 3, - }; - - if let Err(e) = Self::save_background_update_state(&state) { - eprintln!("Failed to save background update state: {}", e); - } else { - println!("Startup version update completed successfully"); - } - } - Err(e) => { - eprintln!("Startup version update failed: {}", e); - return Err(e); - } - } - } else { - return Err("App handle not available for startup update".into()); - } - - Ok(()) - } - - pub async fn start_background_updates(&mut self) { - println!("Starting background version update service (checking every 5 minutes for 3-hour intervals)"); - - // Run initial startup check - if let Err(e) = self.check_and_run_startup_update().await { - eprintln!("Startup version update failed: {}", e); - } - - loop { - self.update_interval.tick().await; - - // Check if we should run an update based on persistent state - if !Self::should_run_background_update() { - continue; - } - - // Check if we have an app handle - let app_handle = { - let handle_guard = self.app_handle.lock().await; - handle_guard.clone() - }; - - if let Some(handle) = app_handle { - println!("Starting background version update..."); - - match self.update_all_browser_versions(&handle).await { - Ok(_) => { - // Update the persistent state after successful update - let state = BackgroundUpdateState { - last_update_time: Self::get_current_timestamp(), - update_interval_hours: 3, - }; - - if let Err(e) = Self::save_background_update_state(&state) { - eprintln!("Failed to save background update state: {}", e); - } else { - println!("Background version update completed successfully"); - } - } - Err(e) => { - eprintln!("Background version update failed: {}", e); - - // Emit error event - let progress = VersionUpdateProgress { - current_browser: "".to_string(), - total_browsers: 0, - completed_browsers: 0, - new_versions_found: 0, - browser_new_versions: 0, - status: "error".to_string(), - }; - let _ = handle.emit("version-update-progress", &progress); - } - } - } else { - println!("App handle not available, skipping background update"); - } - } - } - - async fn update_all_browser_versions(&self, app_handle: &tauri::AppHandle) -> Result, Box> { - println!("Starting background version update for all browsers"); - - let browsers = vec![ - "firefox", - "firefox-developer", - "mullvad-browser", - "zen", - "brave", - "chromium", - "tor-browser", - ]; - - let total_browsers = browsers.len(); - let mut results = Vec::new(); - let mut total_new_versions = 0; - - // Emit start event - let progress = VersionUpdateProgress { - current_browser: "".to_string(), - total_browsers, - completed_browsers: 0, - new_versions_found: 0, - browser_new_versions: 0, - status: "updating".to_string(), - }; - let _ = app_handle.emit("version-update-progress", &progress); - - for (index, browser) in browsers.iter().enumerate() { - // Check if individual browser cache is expired before updating - if !self.version_service.should_update_cache(browser) { - println!("Skipping {} - cache is still fresh", browser); - - let browser_result = BackgroundUpdateResult { - browser: browser.to_string(), - new_versions_count: 0, - total_versions_count: 0, - updated_successfully: true, - error: None, - }; - results.push(browser_result); - continue; - } - - println!("Updating versions for browser: {}", browser); - - // Emit progress for current browser - let progress = VersionUpdateProgress { - current_browser: browser.to_string(), - total_browsers, - completed_browsers: index, - new_versions_found: total_new_versions, - browser_new_versions: 0, - status: "updating".to_string(), - }; - let _ = app_handle.emit("version-update-progress", &progress); - - let result = self.update_browser_versions(browser).await; - - match result { - Ok(new_count) => { - total_new_versions += new_count; - let browser_result = BackgroundUpdateResult { - browser: browser.to_string(), - new_versions_count: new_count, - total_versions_count: 0, // We'll update this if needed - updated_successfully: true, - error: None, - }; - results.push(browser_result); - - println!("Found {} new versions for {}", new_count, browser); - } - Err(e) => { - eprintln!("Failed to update versions for {}: {}", browser, e); - let browser_result = BackgroundUpdateResult { - browser: browser.to_string(), - new_versions_count: 0, - total_versions_count: 0, - updated_successfully: false, - error: Some(e.to_string()), - }; - results.push(browser_result); - } - } - - // Small delay between browsers to avoid overwhelming APIs - tokio::time::sleep(Duration::from_millis(500)).await; - } - - // Emit completion event - let progress = VersionUpdateProgress { - current_browser: "".to_string(), - total_browsers, - completed_browsers: total_browsers, - new_versions_found: total_new_versions, - browser_new_versions: 0, - status: "completed".to_string(), - }; - let _ = app_handle.emit("version-update-progress", &progress); - - println!("Background version update completed. Found {} new versions total", total_new_versions); - - Ok(results) - } - - async fn update_browser_versions(&self, browser: &str) -> Result> { - self.version_service.update_browser_versions_incrementally(browser).await - } - - pub async fn trigger_manual_update(&self, app_handle: &tauri::AppHandle) -> Result, Box> { - let results = self.update_all_browser_versions(app_handle).await?; - - // Update the persistent state after successful manual update - let state = BackgroundUpdateState { + match self.update_all_browser_versions(&handle).await { + Ok(_) => { + // Update the persistent state after successful update + let state = BackgroundUpdateState { last_update_time: Self::get_current_timestamp(), update_interval_hours: 3, - }; - - if let Err(e) = Self::save_background_update_state(&state) { - eprintln!("Failed to save background update state after manual update: {}", e); + }; + + if let Err(e) = Self::save_background_update_state(&state) { + eprintln!("Failed to save background update state: {}", e); + } else { + println!("Startup version update completed successfully"); + } } - - Ok(results) + Err(e) => { + eprintln!("Startup version update failed: {}", e); + return Err(e); + } + } + } else { + return Err("App handle not available for startup update".into()); } - pub async fn get_last_update_time(&self) -> Option { - let state = Self::load_background_update_state(); - if state.last_update_time == 0 { - None - } else { - Some(state.last_update_time) - } + Ok(()) + } + + pub async fn start_background_updates(&mut self) { + println!( + "Starting background version update service (checking every 5 minutes for 3-hour intervals)" + ); + + // Run initial startup check + if let Err(e) = self.check_and_run_startup_update().await { + eprintln!("Startup version update failed: {}", e); } - pub async fn get_time_until_next_update(&self) -> u64 { - let state = Self::load_background_update_state(); - let current_time = Self::get_current_timestamp(); - - if state.last_update_time == 0 { - 0 // No previous update, should update now - } else { - let elapsed = current_time.saturating_sub(state.last_update_time); - let update_interval_secs = state.update_interval_hours * 60 * 60; - - if elapsed >= update_interval_secs { - 0 // Update overdue + loop { + self.update_interval.tick().await; + + // Check if we should run an update based on persistent state + if !Self::should_run_background_update() { + continue; + } + + // Check if we have an app handle + let app_handle = { + let handle_guard = self.app_handle.lock().await; + handle_guard.clone() + }; + + if let Some(handle) = app_handle { + println!("Starting background version update..."); + + match self.update_all_browser_versions(&handle).await { + Ok(_) => { + // Update the persistent state after successful update + let state = BackgroundUpdateState { + last_update_time: Self::get_current_timestamp(), + update_interval_hours: 3, + }; + + if let Err(e) = Self::save_background_update_state(&state) { + eprintln!("Failed to save background update state: {}", e); } else { - update_interval_secs - elapsed + println!("Background version update completed successfully"); } + } + Err(e) => { + eprintln!("Background version update failed: {}", e); + + // Emit error event + let progress = VersionUpdateProgress { + current_browser: "".to_string(), + total_browsers: 0, + completed_browsers: 0, + new_versions_found: 0, + browser_new_versions: 0, + status: "error".to_string(), + }; + let _ = handle.emit("version-update-progress", &progress); + } } + } else { + println!("App handle not available, skipping background update"); + } } + } + + async fn update_all_browser_versions( + &self, + app_handle: &tauri::AppHandle, + ) -> Result, Box> { + println!("Starting background version update for all browsers"); + + let browsers = vec![ + "firefox", + "firefox-developer", + "mullvad-browser", + "zen", + "brave", + "chromium", + "tor-browser", + ]; + + let total_browsers = browsers.len(); + let mut results = Vec::new(); + let mut total_new_versions = 0; + + // Emit start event + let progress = VersionUpdateProgress { + current_browser: "".to_string(), + total_browsers, + completed_browsers: 0, + new_versions_found: 0, + browser_new_versions: 0, + status: "updating".to_string(), + }; + let _ = app_handle.emit("version-update-progress", &progress); + + for (index, browser) in browsers.iter().enumerate() { + // Check if individual browser cache is expired before updating + if !self.version_service.should_update_cache(browser) { + println!("Skipping {} - cache is still fresh", browser); + + let browser_result = BackgroundUpdateResult { + browser: browser.to_string(), + new_versions_count: 0, + total_versions_count: 0, + updated_successfully: true, + error: None, + }; + results.push(browser_result); + continue; + } + + println!("Updating versions for browser: {}", browser); + + // Emit progress for current browser + let progress = VersionUpdateProgress { + current_browser: browser.to_string(), + total_browsers, + completed_browsers: index, + new_versions_found: total_new_versions, + browser_new_versions: 0, + status: "updating".to_string(), + }; + let _ = app_handle.emit("version-update-progress", &progress); + + let result = self.update_browser_versions(browser).await; + + match result { + Ok(new_count) => { + total_new_versions += new_count; + let browser_result = BackgroundUpdateResult { + browser: browser.to_string(), + new_versions_count: new_count, + total_versions_count: 0, // We'll update this if needed + updated_successfully: true, + error: None, + }; + results.push(browser_result); + + println!("Found {} new versions for {}", new_count, browser); + } + Err(e) => { + eprintln!("Failed to update versions for {}: {}", browser, e); + let browser_result = BackgroundUpdateResult { + browser: browser.to_string(), + new_versions_count: 0, + total_versions_count: 0, + updated_successfully: false, + error: Some(e.to_string()), + }; + results.push(browser_result); + } + } + + // Small delay between browsers to avoid overwhelming APIs + tokio::time::sleep(Duration::from_millis(500)).await; + } + + // Emit completion event + let progress = VersionUpdateProgress { + current_browser: "".to_string(), + total_browsers, + completed_browsers: total_browsers, + new_versions_found: total_new_versions, + browser_new_versions: 0, + status: "completed".to_string(), + }; + let _ = app_handle.emit("version-update-progress", &progress); + + println!( + "Background version update completed. Found {} new versions total", + total_new_versions + ); + + Ok(results) + } + + async fn update_browser_versions( + &self, + browser: &str, + ) -> Result> { + self + .version_service + .update_browser_versions_incrementally(browser) + .await + } + + pub async fn trigger_manual_update( + &self, + app_handle: &tauri::AppHandle, + ) -> Result, Box> { + let results = self.update_all_browser_versions(app_handle).await?; + + // Update the persistent state after successful manual update + let state = BackgroundUpdateState { + last_update_time: Self::get_current_timestamp(), + update_interval_hours: 3, + }; + + if let Err(e) = Self::save_background_update_state(&state) { + eprintln!( + "Failed to save background update state after manual update: {}", + e + ); + } + + Ok(results) + } + + pub async fn get_last_update_time(&self) -> Option { + let state = Self::load_background_update_state(); + if state.last_update_time == 0 { + None + } else { + Some(state.last_update_time) + } + } + + pub async fn get_time_until_next_update(&self) -> u64 { + let state = Self::load_background_update_state(); + let current_time = Self::get_current_timestamp(); + + if state.last_update_time == 0 { + 0 // No previous update, should update now + } else { + let elapsed = current_time.saturating_sub(state.last_update_time); + let update_interval_secs = state.update_interval_hours * 60 * 60; + + if elapsed >= update_interval_secs { + 0 // Update overdue + } else { + update_interval_secs - elapsed + } + } + } } // Global instance static VERSION_UPDATER: OnceLock>> = OnceLock::new(); pub fn get_version_updater() -> Arc> { - VERSION_UPDATER.get_or_init(|| { - Arc::new(Mutex::new(VersionUpdater::new())) - }).clone() + VERSION_UPDATER + .get_or_init(|| Arc::new(Mutex::new(VersionUpdater::new()))) + .clone() } #[tauri::command] -pub async fn trigger_manual_version_update(app_handle: tauri::AppHandle) -> Result, String> { - let updater = get_version_updater(); - let updater_guard = updater.lock().await; - - updater_guard.trigger_manual_update(&app_handle) - .await - .map_err(|e| format!("Failed to trigger manual update: {}", e)) +pub async fn trigger_manual_version_update( + app_handle: tauri::AppHandle, +) -> Result, String> { + let updater = get_version_updater(); + let updater_guard = updater.lock().await; + + updater_guard + .trigger_manual_update(&app_handle) + .await + .map_err(|e| format!("Failed to trigger manual update: {}", e)) } #[tauri::command] pub async fn get_version_update_status() -> Result<(Option, u64), String> { - let updater = get_version_updater(); - let updater_guard = updater.lock().await; - - let last_update = updater_guard.get_last_update_time().await; - let time_until_next = updater_guard.get_time_until_next_update().await; - - Ok((last_update, time_until_next)) + let updater = get_version_updater(); + let updater_guard = updater.lock().await; + + let last_update = updater_guard.get_last_update_time().await; + let time_until_next = updater_guard.get_time_until_next_update().await; + + Ok((last_update, time_until_next)) } #[tauri::command] pub async fn check_version_update_needed() -> Result { - Ok(VersionUpdater::should_run_background_update()) + Ok(VersionUpdater::should_run_background_update()) } #[tauri::command] pub async fn force_version_update_check(_app_handle: tauri::AppHandle) -> Result { - let updater = get_version_updater(); - let updater_guard = updater.lock().await; - - match updater_guard.check_and_run_startup_update().await { - Ok(_) => Ok(true), - Err(e) => Err(format!("Failed to run version update check: {}", e)) - } + let updater = get_version_updater(); + let updater_guard = updater.lock().await; + + match updater_guard.check_and_run_startup_update().await { + Ok(_) => Ok(true), + Err(e) => Err(format!("Failed to run version update check: {}", e)), + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - // Helper function to create a unique test state file - fn get_test_state_file(test_name: &str) -> PathBuf { - let cache_dir = VersionUpdater::get_cache_dir().unwrap(); - cache_dir.join(format!("test_{}_state.json", test_name)) + // Helper function to create a unique test state file + fn get_test_state_file(test_name: &str) -> PathBuf { + let cache_dir = VersionUpdater::get_cache_dir().unwrap(); + cache_dir.join(format!("test_{}_state.json", test_name)) + } + + fn save_test_state( + test_name: &str, + state: &BackgroundUpdateState, + ) -> Result<(), Box> { + let state_file = get_test_state_file(test_name); + let content = serde_json::to_string_pretty(state)?; + fs::write(&state_file, content)?; + Ok(()) + } + + fn load_test_state(test_name: &str) -> BackgroundUpdateState { + let state_file = get_test_state_file(test_name); + + if !state_file.exists() { + return BackgroundUpdateState::default(); } - fn save_test_state(test_name: &str, state: &BackgroundUpdateState) -> Result<(), Box> { - let state_file = get_test_state_file(test_name); - let content = serde_json::to_string_pretty(state)?; - fs::write(&state_file, content)?; - Ok(()) - } + let content = match fs::read_to_string(&state_file) { + Ok(content) => content, + Err(_) => return BackgroundUpdateState::default(), + }; - fn load_test_state(test_name: &str) -> BackgroundUpdateState { - let state_file = get_test_state_file(test_name); - - if !state_file.exists() { - return BackgroundUpdateState::default(); - } + serde_json::from_str(&content).unwrap_or_default() + } - let content = match fs::read_to_string(&state_file) { - Ok(content) => content, - Err(_) => return BackgroundUpdateState::default(), - }; + #[test] + fn test_background_update_state_persistence() { + let test_name = "persistence"; - serde_json::from_str(&content).unwrap_or_default() - } + // Create a test state + let test_state = BackgroundUpdateState { + last_update_time: 1609459200, // 2021-01-01 00:00:00 UTC + update_interval_hours: 3, + }; - #[test] - fn test_background_update_state_persistence() { - let test_name = "persistence"; - - // Create a test state - let test_state = BackgroundUpdateState { - last_update_time: 1609459200, // 2021-01-01 00:00:00 UTC - update_interval_hours: 3, - }; + // Save the state + save_test_state(test_name, &test_state).unwrap(); - // Save the state - save_test_state(test_name, &test_state).unwrap(); + // Load the state back + let loaded_state = load_test_state(test_name); - // Load the state back - let loaded_state = load_test_state(test_name); + // Verify the values match + assert_eq!(loaded_state.last_update_time, test_state.last_update_time); + assert_eq!( + loaded_state.update_interval_hours, + test_state.update_interval_hours + ); - // Verify the values match - assert_eq!(loaded_state.last_update_time, test_state.last_update_time); - assert_eq!(loaded_state.update_interval_hours, test_state.update_interval_hours); - - // Clean up - let _ = fs::remove_file(get_test_state_file(test_name)); - } + // Clean up + let _ = fs::remove_file(get_test_state_file(test_name)); + } - #[test] - fn test_should_run_background_update_logic() { - // Note: This test uses the shared state file, so results may vary - // depending on previous test runs. This is expected behavior. - - // Test with recent update (should not update) - let recent_state = BackgroundUpdateState { - last_update_time: VersionUpdater::get_current_timestamp() - 60, // 1 minute ago - update_interval_hours: 3, - }; - VersionUpdater::save_background_update_state(&recent_state).unwrap(); - assert!(!VersionUpdater::should_run_background_update()); + #[test] + fn test_should_run_background_update_logic() { + // Note: This test uses the shared state file, so results may vary + // depending on previous test runs. This is expected behavior. - // Test with old update (should update) - let old_state = BackgroundUpdateState { - last_update_time: VersionUpdater::get_current_timestamp() - (4 * 60 * 60), // 4 hours ago - update_interval_hours: 3, - }; - VersionUpdater::save_background_update_state(&old_state).unwrap(); - assert!(VersionUpdater::should_run_background_update()); - } + // Test with recent update (should not update) + let recent_state = BackgroundUpdateState { + last_update_time: VersionUpdater::get_current_timestamp() - 60, // 1 minute ago + update_interval_hours: 3, + }; + VersionUpdater::save_background_update_state(&recent_state).unwrap(); + assert!(!VersionUpdater::should_run_background_update()); - #[test] - fn test_cache_dir_creation() { - // This should not panic and should create the directory if it doesn't exist - let cache_dir = VersionUpdater::get_cache_dir().unwrap(); - assert!(cache_dir.exists()); - assert!(cache_dir.is_dir()); - } -} \ No newline at end of file + // Test with old update (should update) + let old_state = BackgroundUpdateState { + last_update_time: VersionUpdater::get_current_timestamp() - (4 * 60 * 60), // 4 hours ago + update_interval_hours: 3, + }; + VersionUpdater::save_background_update_state(&old_state).unwrap(); + assert!(VersionUpdater::should_run_background_update()); + } + + #[test] + fn test_cache_dir_creation() { + // This should not panic and should create the directory if it doesn't exist + let cache_dir = VersionUpdater::get_cache_dir().unwrap(); + assert!(cache_dir.exists()); + assert!(cache_dir.is_dir()); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index ff40778..1135a60 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -63,7 +63,7 @@ export default function Home() { try { const profileList = await invoke( - "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( - "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( - "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("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>( - new Set() + new Set(), ); const runningProfilesRef = useRef>(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( "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} diff --git a/src/components/change-version-dialog.tsx b/src/components/change-version-dialog.tsx index d23d8a8..d12dc7f 100644 --- a/src/components/change-version-dialog.tsx +++ b/src/components/change-version-dialog.tsx @@ -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) diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index f8927cc..b23aeb0 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -65,7 +65,7 @@ export function CreateProfileDialog({ >([]); const [isCreating, setIsCreating] = useState(false); const [existingProfiles, setExistingProfiles] = useState( - [] + [], ); // Proxy settings @@ -120,7 +120,7 @@ export function CreateProfileDialog({ const loadSupportedBrowsers = async () => { try { const browsers = await invoke( - "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(" ")} diff --git a/src/components/custom-toast.tsx b/src/components/custom-toast.tsx index 3ce0adf..18bbcdf 100644 --- a/src/components/custom-toast.tsx +++ b/src/components/custom-toast.tsx @@ -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", diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index c742287..ec68c5d 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -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"} @@ -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(), )} ); @@ -439,7 +439,7 @@ export function ProfilesDataTable({ {flexRender( cell.column.columnDef.cell, - cell.getContext() + cell.getContext(), )} ))} diff --git a/src/components/profile-selector-dialog.tsx b/src/components/profile-selector-dialog.tsx index e14782a..d998659 100644 --- a/src/components/profile-selector-dialog.tsx +++ b/src/components/profile-selector-dialog.tsx @@ -58,7 +58,7 @@ export function ProfileSelectorDialog({ setIsLoading(true); try { const profileList = await invoke( - "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 + runningProfiles: Set, ): 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({
{(() => { const IconComponent = getBrowserIcon( - profile.browser + profile.browser, ); return IconComponent ? ( diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx index 59821e5..3826c14 100644 --- a/src/components/theme-provider.tsx +++ b/src/components/theme-provider.tsx @@ -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 diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index 0a0105a..90b0470 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -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} > diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index b446c7e..3a6a712 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -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} /> diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index 5601a89..a74f8fb 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -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} > diff --git a/src/components/update-notification.tsx b/src/components/update-notification.tsx index 102c391..0ae918a 100644 --- a/src/components/update-notification.tsx +++ b/src/components/update-notification.tsx @@ -112,7 +112,7 @@ function UpdateNotificationComponent({ export function useUpdateNotifications() { const [notifications, setNotifications] = useState([]); const [updatingBrowsers, setUpdatingBrowsers] = useState>( - new Set() + new Set(), ); const [isClient, setIsClient] = useState(false); @@ -126,7 +126,7 @@ export function useUpdateNotifications() { try { const updates = await invoke( - "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]); diff --git a/src/components/version-selector.tsx b/src/components/version-selector.tsx index f3ac915..c3a807f 100644 --- a/src/components/version-selector.tsx +++ b/src/components/version-selector.tsx @@ -91,7 +91,7 @@ export function VersionSelector({ {availableVersions.map((version) => { const isDownloaded = downloadedVersions.includes( - version.tag_name + version.tag_name, ); return (
diff --git a/src/components/version-update-settings.tsx b/src/components/version-update-settings.tsx index 08d47c7..284a904 100644 --- a/src/components/version-update-settings.tsx +++ b/src/components/version-update-settings.tsx @@ -115,11 +115,10 @@ export function VersionUpdateSettings() { How it works • Version information is checked automatically every 3 hours -
- • New versions are added to the cache without removing old ones -
- • When creating profiles or changing versions, you'll see how - many new versions were found +
• New versions are added to the cache without removing old + ones +
• When creating profiles or changing versions, you'll see + how many new versions were found
• This keeps the app responsive while ensuring you have the latest information
diff --git a/src/hooks/use-browser-download.ts b/src/hooks/use-browser-download.ts index 8d9f2c5..51e63ca 100644 --- a/src/hooks/use-browser-download.ts +++ b/src/hooks/use-browser-download.ts @@ -72,7 +72,7 @@ const isAlphaVersion = (version: string): boolean => { export function useBrowserDownload() { const [availableVersions, setAvailableVersions] = useState( - [] + [], ); const [downloadedVersions, setDownloadedVersions] = useState([]); 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( "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( "fetch_browser_versions_with_count_cached_first", - { browserStr } + { browserStr }, ); // Get detailed version info for compatibility const versionInfos = await invoke( "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( "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 { diff --git a/src/hooks/use-table-sorting.ts b/src/hooks/use-table-sorting.ts index 7dfeeb8..35b7c35 100644 --- a/src/hooks/use-table-sorting.ts +++ b/src/hooks/use-table-sorting.ts @@ -15,7 +15,7 @@ export function useTableSorting() { const loadSettings = async () => { try { const settings = await invoke( - "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 { diff --git a/src/hooks/use-version-updater.ts b/src/hooks/use-version-updater.ts index e1edbe7..31578fe 100644 --- a/src/hooks/use-version-updater.ts +++ b/src/hooks/use-version-updater.ts @@ -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( - "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( "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 {