mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-09 12:46:13 +02:00
Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb164ce743 | ||
|
|
daa36f008b | ||
|
|
92ef2798d2 | ||
|
|
a9720676ae | ||
|
|
fbcec2cbc1 | ||
|
|
5d395f606e | ||
|
|
6963e07be5 | ||
|
|
c28537d304 | ||
|
|
c303de4a8a | ||
|
|
21a13fb217 | ||
|
|
90d8e782de | ||
|
|
ad2d9b73f2 | ||
|
|
e645e212f2 | ||
|
|
7df92ae8ee | ||
|
|
18a0254ca7 | ||
|
|
b0a58c3131 | ||
|
|
467e82ca93 | ||
|
|
8d9654044a | ||
|
|
711d94c9c1 | ||
|
|
305051d03d | ||
|
|
8695884535 | ||
|
|
bb96936550 | ||
|
|
730621e5a1 | ||
|
|
714102eb25 | ||
|
|
e5378c1bb7 | ||
|
|
b7c8d5672a | ||
|
|
3fb81e2ca0 | ||
|
|
510de96393 | ||
|
|
3688e88d67 | ||
|
|
9646a41788 | ||
|
|
f7679d25ca | ||
|
|
2d42772718 | ||
|
|
3980f835d6 | ||
|
|
d0185dd5ae | ||
|
|
de39fa4555 | ||
|
|
f773eb6f1c | ||
|
|
458c30433d | ||
|
|
1cb9ffa249 | ||
|
|
5c58b5c644 | ||
|
|
f41311a7bb | ||
|
|
c8c09c296e | ||
|
|
ca0c2614f4 | ||
|
|
dca5a2970e | ||
|
|
e0a1dd5a8a | ||
|
|
e48b681215 | ||
|
|
6796912606 | ||
|
|
7105f6544f | ||
|
|
3003f868e7 | ||
|
|
b733d26f10 | ||
|
|
675c2417d7 | ||
|
|
e10a7bf089 | ||
|
|
c8e3cd39ff | ||
|
|
0103150dc7 | ||
|
|
be57ac3219 | ||
|
|
b4067b5e34 | ||
|
|
3fa8822139 | ||
|
|
1e0ef0b497 | ||
|
|
2da832f100 |
@@ -73,7 +73,7 @@ jobs:
|
|||||||
compat-lookup: true
|
compat-lookup: true
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
- name: Auto-merge minor and patch updates
|
- name: Auto-merge minor and patch updates
|
||||||
uses: ridedott/merge-me-action@d288b479e76cb993344ca8b5e0fcaa7d6e667eed #v2.10.123
|
uses: ridedott/merge-me-action@f96a67511b4be051e77760230e6a3fb9cb7b1903 #v2.10.124
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.SECRET_DEPENDABOT_GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.SECRET_DEPENDABOT_GITHUB_TOKEN }}
|
||||||
MERGE_METHOD: SQUASH
|
MERGE_METHOD: SQUASH
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Validate issue with AI
|
- name: Validate issue with AI
|
||||||
id: validate
|
id: validate
|
||||||
uses: actions/ai-inference@9693b137b6566bb66055a713613bf4f0493701eb # v1.2.3
|
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
|
||||||
with:
|
with:
|
||||||
prompt-file: issue_analysis.txt
|
prompt-file: issue_analysis.txt
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
@@ -30,9 +30,8 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest]
|
os: [macos-latest, ubuntu-latest]
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
@@ -87,9 +86,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# TODO: replace with an integration test that fetches everything from rust
|
# TODO: replace with an integration test that fetches everything from rust
|
||||||
- name: Download Camoufox for testing
|
# - name: Download Camoufox for testing
|
||||||
run: npx camoufox-js fetch
|
# run: npx camoufox-js fetch
|
||||||
continue-on-error: true
|
# continue-on-error: true
|
||||||
|
|
||||||
- name: Copy nodecar binary to Tauri binaries
|
- name: Copy nodecar binary to Tauri binaries
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate release notes with AI
|
- name: Generate release notes with AI
|
||||||
id: generate-notes
|
id: generate-notes
|
||||||
uses: actions/ai-inference@9693b137b6566bb66055a713613bf4f0493701eb # v1.2.3
|
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
|
||||||
with:
|
with:
|
||||||
prompt-file: commits.txt
|
prompt-file: commits.txt
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
target: "x86_64-apple-darwin"
|
target: "x86_64-apple-darwin"
|
||||||
pkg_target: "latest-macos-x64"
|
pkg_target: "latest-macos-x64"
|
||||||
nodecar_script: "build:mac-x86_64"
|
nodecar_script: "build:mac-x86_64"
|
||||||
- platform: "ubuntu-22.04"
|
- platform: "ubuntu-latest"
|
||||||
args: "--target x86_64-unknown-linux-gnu"
|
args: "--target x86_64-unknown-linux-gnu"
|
||||||
arch: "x86_64"
|
arch: "x86_64"
|
||||||
target: "x86_64-unknown-linux-gnu"
|
target: "x86_64-unknown-linux-gnu"
|
||||||
@@ -154,9 +154,9 @@ jobs:
|
|||||||
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-${{ matrix.target }}
|
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-${{ matrix.target }}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Download Camoufox for testing
|
# - name: Download Camoufox for testing
|
||||||
run: npx camoufox-js fetch
|
# run: npx camoufox-js fetch
|
||||||
continue-on-error: true
|
# continue-on-error: true
|
||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
@@ -166,6 +166,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||||
|
NO_STRIP: ${{ matrix.platform == 'ubuntu-22.04' && 'true' || '' }}
|
||||||
with:
|
with:
|
||||||
tagName: ${{ github.ref_name }}
|
tagName: ${{ github.ref_name }}
|
||||||
releaseName: "Donut Browser ${{ github.ref_name }}"
|
releaseName: "Donut Browser ${{ github.ref_name }}"
|
||||||
@@ -179,4 +180,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
branch: main
|
branch: main
|
||||||
commit_message: "docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]"
|
commit_message: "docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]"
|
||||||
file_pattern: CHANGELOG.md
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
target: "x86_64-apple-darwin"
|
target: "x86_64-apple-darwin"
|
||||||
pkg_target: "latest-macos-x64"
|
pkg_target: "latest-macos-x64"
|
||||||
nodecar_script: "build:mac-x86_64"
|
nodecar_script: "build:mac-x86_64"
|
||||||
- platform: "ubuntu-22.04"
|
- platform: "ubuntu-latest"
|
||||||
args: "--target x86_64-unknown-linux-gnu"
|
args: "--target x86_64-unknown-linux-gnu"
|
||||||
arch: "x86_64"
|
arch: "x86_64"
|
||||||
target: "x86_64-unknown-linux-gnu"
|
target: "x86_64-unknown-linux-gnu"
|
||||||
@@ -153,9 +153,9 @@ jobs:
|
|||||||
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-${{ matrix.target }}
|
cp nodecar/nodecar-bin src-tauri/binaries/nodecar-${{ matrix.target }}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Download Camoufox for testing
|
# - name: Download Camoufox for testing
|
||||||
run: npx camoufox-js fetch
|
# run: npx camoufox-js fetch
|
||||||
continue-on-error: true
|
# continue-on-error: true
|
||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
@@ -176,6 +176,7 @@ jobs:
|
|||||||
BUILD_TAG: "nightly-${{ steps.timestamp.outputs.timestamp }}"
|
BUILD_TAG: "nightly-${{ steps.timestamp.outputs.timestamp }}"
|
||||||
GITHUB_REF_NAME: "nightly-${{ steps.timestamp.outputs.timestamp }}"
|
GITHUB_REF_NAME: "nightly-${{ steps.timestamp.outputs.timestamp }}"
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
|
NO_STRIP: ${{ matrix.platform == 'ubuntu-22.04' && 'true' || '' }}
|
||||||
with:
|
with:
|
||||||
tagName: "nightly-${{ steps.timestamp.outputs.timestamp }}"
|
tagName: "nightly-${{ steps.timestamp.outputs.timestamp }}"
|
||||||
releaseName: "Donut Browser Nightly (Build ${{ steps.timestamp.outputs.timestamp }})"
|
releaseName: "Donut Browser Nightly (Build ${{ steps.timestamp.outputs.timestamp }})"
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ jobs:
|
|||||||
- name: Checkout Actions Repository
|
- name: Checkout Actions Repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||||
- name: Spell Check Repo
|
- name: Spell Check Repo
|
||||||
uses: crate-ci/typos@392b78fe18a52790c53f42456e46124f77346842 #v1.34.0
|
uses: crate-ci/typos@52bd719c2c91f9d676e2aa359fc8e0db8925e6d8 #v1.35.3
|
||||||
|
|||||||
Vendored
+5
@@ -25,7 +25,9 @@
|
|||||||
"cmdk",
|
"cmdk",
|
||||||
"codegen",
|
"codegen",
|
||||||
"codesign",
|
"codesign",
|
||||||
|
"commitish",
|
||||||
"CTYPE",
|
"CTYPE",
|
||||||
|
"daijro",
|
||||||
"dataclasses",
|
"dataclasses",
|
||||||
"datareporting",
|
"datareporting",
|
||||||
"datas",
|
"datas",
|
||||||
@@ -44,6 +46,7 @@
|
|||||||
"esac",
|
"esac",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"etree",
|
"etree",
|
||||||
|
"flate",
|
||||||
"frontmost",
|
"frontmost",
|
||||||
"geoip",
|
"geoip",
|
||||||
"getcwd",
|
"getcwd",
|
||||||
@@ -78,6 +81,7 @@
|
|||||||
"libxdo",
|
"libxdo",
|
||||||
"localtime",
|
"localtime",
|
||||||
"lxml",
|
"lxml",
|
||||||
|
"lzma",
|
||||||
"mmdb",
|
"mmdb",
|
||||||
"mountpoint",
|
"mountpoint",
|
||||||
"msiexec",
|
"msiexec",
|
||||||
@@ -175,6 +179,7 @@
|
|||||||
"xfconf",
|
"xfconf",
|
||||||
"xsettings",
|
"xsettings",
|
||||||
"zhom",
|
"zhom",
|
||||||
|
"zipball",
|
||||||
"zoneinfo"
|
"zoneinfo"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ Have questions or want to contribute? We'd love to hear from you!
|
|||||||
|
|
||||||
<!-- readme: collaborators,contributors -start -->
|
<!-- readme: collaborators,contributors -start -->
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/zhom">
|
<a href="https://github.com/zhom">
|
||||||
<img src="https://avatars.githubusercontent.com/u/2717306?v=4" width="100;" alt="zhom"/>
|
<img src="https://avatars.githubusercontent.com/u/2717306?v=4" width="100;" alt="zhom"/>
|
||||||
@@ -93,8 +93,8 @@ Have questions or want to contribute? We'd love to hear from you!
|
|||||||
<sub><b>zhom</b></sub>
|
<sub><b>zhom</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tbody>
|
<tbody>
|
||||||
</table>
|
</table>
|
||||||
<!-- readme: collaborators,contributors -end -->
|
<!-- readme: collaborators,contributors -end -->
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^24.2.0",
|
"@types/node": "^24.2.1",
|
||||||
"commander": "^14.0.0",
|
"commander": "^14.0.0",
|
||||||
"donutbrowser-camoufox-js": "^0.6.4",
|
"donutbrowser-camoufox-js": "^0.6.4",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
"playwright-core": "^1.54.2",
|
"playwright-core": "^1.54.2",
|
||||||
"proxy-chain": "^2.5.9",
|
"proxy-chain": "^2.5.9",
|
||||||
"tmp": "^0.2.4",
|
"tmp": "^0.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -344,6 +344,8 @@ interface GenerateConfigOptions {
|
|||||||
proxy?: string;
|
proxy?: string;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
geoip?: string | boolean;
|
geoip?: string | boolean;
|
||||||
blockImages?: boolean;
|
blockImages?: boolean;
|
||||||
blockWebrtc?: boolean;
|
blockWebrtc?: boolean;
|
||||||
@@ -411,11 +413,21 @@ export async function generateCamoufoxConfig(
|
|||||||
} else {
|
} else {
|
||||||
// Use individual options to build configuration
|
// Use individual options to build configuration
|
||||||
|
|
||||||
if (options.maxWidth && options.maxHeight) {
|
// Build screen configuration with min/max dimensions
|
||||||
launchOpts.screen = {
|
const screen: {
|
||||||
maxWidth: options.maxWidth,
|
minWidth?: number;
|
||||||
maxHeight: options.maxHeight,
|
maxWidth?: number;
|
||||||
};
|
minHeight?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
if (options.minWidth) screen.minWidth = options.minWidth;
|
||||||
|
if (options.maxWidth) screen.maxWidth = options.maxWidth;
|
||||||
|
if (options.minHeight) screen.minHeight = options.minHeight;
|
||||||
|
if (options.maxHeight) screen.maxHeight = options.maxHeight;
|
||||||
|
|
||||||
|
if (Object.keys(screen).length > 0) {
|
||||||
|
launchOpts.screen = screen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ program
|
|||||||
.option("--proxy <proxy>", "proxy URL for config generation")
|
.option("--proxy <proxy>", "proxy URL for config generation")
|
||||||
.option("--max-width <width>", "maximum screen width", parseInt)
|
.option("--max-width <width>", "maximum screen width", parseInt)
|
||||||
.option("--max-height <height>", "maximum screen height", parseInt)
|
.option("--max-height <height>", "maximum screen height", parseInt)
|
||||||
|
.option("--min-width <width>", "minimum screen width", parseInt)
|
||||||
|
.option("--min-height <height>", "minimum screen height", parseInt)
|
||||||
.option("--geoip", "enable geoip")
|
.option("--geoip", "enable geoip")
|
||||||
.option("--block-images", "block images")
|
.option("--block-images", "block images")
|
||||||
.option("--block-webrtc", "block WebRTC")
|
.option("--block-webrtc", "block WebRTC")
|
||||||
@@ -384,6 +386,14 @@ program
|
|||||||
typeof options.maxHeight === "number"
|
typeof options.maxHeight === "number"
|
||||||
? options.maxHeight
|
? options.maxHeight
|
||||||
: undefined,
|
: undefined,
|
||||||
|
minWidth:
|
||||||
|
typeof options.minWidth === "number"
|
||||||
|
? options.minWidth
|
||||||
|
: undefined,
|
||||||
|
minHeight:
|
||||||
|
typeof options.minHeight === "number"
|
||||||
|
? options.minHeight
|
||||||
|
: undefined,
|
||||||
geoip: Boolean(options.geoip),
|
geoip: Boolean(options.geoip),
|
||||||
blockImages:
|
blockImages:
|
||||||
typeof options.blockImages === "boolean"
|
typeof options.blockImages === "boolean"
|
||||||
|
|||||||
+6
-5
@@ -46,6 +46,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"motion": "^12.23.12",
|
||||||
"next": "^15.4.6",
|
"next": "^15.4.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
@@ -56,15 +57,15 @@
|
|||||||
"tauri-plugin-macos-permissions-api": "^2.3.0"
|
"tauri-plugin-macos-permissions-api": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.1.3",
|
"@biomejs/biome": "2.1.4",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@tauri-apps/cli": "^2.7.1",
|
"@tauri-apps/cli": "^2.7.1",
|
||||||
"@types/node": "^24.2.0",
|
"@types/node": "^24.2.1",
|
||||||
"@types/react": "^19.1.9",
|
"@types/react": "^19.1.9",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.1.4",
|
"lint-staged": "^16.1.5",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"ts-unused-exports": "^11.0.1",
|
"ts-unused-exports": "^11.0.1",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6",
|
||||||
@@ -72,7 +73,7 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.13.1",
|
"packageManager": "pnpm@10.13.1",
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*.{js,jsx,ts,tsx,json,css,md}": [
|
"**/*.{js,jsx,ts,tsx,json,css}": [
|
||||||
"biome check --fix"
|
"biome check --fix"
|
||||||
],
|
],
|
||||||
"src-tauri/**/*.rs": [
|
"src-tauri/**/*.rs": [
|
||||||
|
|||||||
Generated
+131
-71
@@ -74,6 +74,9 @@ importers:
|
|||||||
cmdk:
|
cmdk:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
version: 1.1.1(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
motion:
|
||||||
|
specifier: ^12.23.12
|
||||||
|
version: 12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
next:
|
next:
|
||||||
specifier: ^15.4.6
|
specifier: ^15.4.6
|
||||||
version: 15.4.6(@babel/core@7.28.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
version: 15.4.6(@babel/core@7.28.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
@@ -100,8 +103,8 @@ importers:
|
|||||||
version: 2.3.0
|
version: 2.3.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@biomejs/biome':
|
'@biomejs/biome':
|
||||||
specifier: 2.1.3
|
specifier: 2.1.4
|
||||||
version: 2.1.3
|
version: 2.1.4
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.1.11
|
specifier: ^4.1.11
|
||||||
version: 4.1.11
|
version: 4.1.11
|
||||||
@@ -109,8 +112,8 @@ importers:
|
|||||||
specifier: ^2.7.1
|
specifier: ^2.7.1
|
||||||
version: 2.7.1
|
version: 2.7.1
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.2.0
|
specifier: ^24.2.1
|
||||||
version: 24.2.0
|
version: 24.2.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.1.9
|
specifier: ^19.1.9
|
||||||
version: 19.1.9
|
version: 19.1.9
|
||||||
@@ -118,14 +121,14 @@ importers:
|
|||||||
specifier: ^19.1.7
|
specifier: ^19.1.7
|
||||||
version: 19.1.7(@types/react@19.1.9)
|
version: 19.1.7(@types/react@19.1.9)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.7.0
|
specifier: ^5.0.0
|
||||||
version: 4.7.0(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))
|
version: 5.0.0(vite@7.0.6(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))
|
||||||
husky:
|
husky:
|
||||||
specifier: ^9.1.7
|
specifier: ^9.1.7
|
||||||
version: 9.1.7
|
version: 9.1.7
|
||||||
lint-staged:
|
lint-staged:
|
||||||
specifier: ^16.1.4
|
specifier: ^16.1.5
|
||||||
version: 16.1.4
|
version: 16.1.5
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.11
|
specifier: ^4.1.11
|
||||||
version: 4.1.11
|
version: 4.1.11
|
||||||
@@ -142,8 +145,8 @@ importers:
|
|||||||
nodecar:
|
nodecar:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.2.0
|
specifier: ^24.2.1
|
||||||
version: 24.2.0
|
version: 24.2.1
|
||||||
commander:
|
commander:
|
||||||
specifier: ^14.0.0
|
specifier: ^14.0.0
|
||||||
version: 14.0.0
|
version: 14.0.0
|
||||||
@@ -169,11 +172,11 @@ importers:
|
|||||||
specifier: ^2.5.9
|
specifier: ^2.5.9
|
||||||
version: 2.5.9
|
version: 2.5.9
|
||||||
tmp:
|
tmp:
|
||||||
specifier: ^0.2.4
|
specifier: ^0.2.5
|
||||||
version: 0.2.4
|
version: 0.2.5
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.2
|
specifier: ^10.9.2
|
||||||
version: 10.9.2(@types/node@24.2.0)(typescript@5.9.2)
|
version: 10.9.2(@types/node@24.2.1)(typescript@5.9.2)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.2
|
specifier: ^5.9.2
|
||||||
version: 5.9.2
|
version: 5.9.2
|
||||||
@@ -279,55 +282,55 @@ packages:
|
|||||||
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@biomejs/biome@2.1.3':
|
'@biomejs/biome@2.1.4':
|
||||||
resolution: {integrity: sha512-KE/tegvJIxTkl7gJbGWSgun7G6X/n2M6C35COT6ctYrAy7SiPyNvi6JtoQERVK/VRbttZfgGq96j2bFmhmnH4w==}
|
resolution: {integrity: sha512-QWlrqyxsU0FCebuMnkvBIkxvPqH89afiJzjMl+z67ybutse590jgeaFdDurE9XYtzpjRGTI1tlUZPGWmbKsElA==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@biomejs/cli-darwin-arm64@2.1.3':
|
'@biomejs/cli-darwin-arm64@2.1.4':
|
||||||
resolution: {integrity: sha512-LFLkSWRoSGS1wVUD/BE6Nlt2dSn0ulH3XImzg2O/36BoToJHKXjSxzPEMAqT9QvwVtk7/9AQhZpTneERU9qaXA==}
|
resolution: {integrity: sha512-sCrNENE74I9MV090Wq/9Dg7EhPudx3+5OiSoQOkIe3DLPzFARuL1dOwCWhKCpA3I5RHmbrsbNSRfZwCabwd8Qg==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@biomejs/cli-darwin-x64@2.1.3':
|
'@biomejs/cli-darwin-x64@2.1.4':
|
||||||
resolution: {integrity: sha512-Q/4OTw8P9No9QeowyxswcWdm0n2MsdCwWcc5NcKQQvzwPjwuPdf8dpPPf4r+x0RWKBtl1FLiAUtJvBlri6DnYw==}
|
resolution: {integrity: sha512-gOEICJbTCy6iruBywBDcG4X5rHMbqCPs3clh3UQ+hRKlgvJTk4NHWQAyHOXvaLe+AxD1/TNX1jbZeffBJzcrOw==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@biomejs/cli-linux-arm64-musl@2.1.3':
|
'@biomejs/cli-linux-arm64-musl@2.1.4':
|
||||||
resolution: {integrity: sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w==}
|
resolution: {integrity: sha512-nYr7H0CyAJPaLupFE2cH16KZmRC5Z9PEftiA2vWxk+CsFkPZQ6dBRdcC6RuS+zJlPc/JOd8xw3uCCt9Pv41WvQ==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@biomejs/cli-linux-arm64@2.1.3':
|
'@biomejs/cli-linux-arm64@2.1.4':
|
||||||
resolution: {integrity: sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA==}
|
resolution: {integrity: sha512-juhEkdkKR4nbUi5k/KRp1ocGPNWLgFRD4NrHZSveYrD6i98pyvuzmS9yFYgOZa5JhaVqo0HPnci0+YuzSwT2fw==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@biomejs/cli-linux-x64-musl@2.1.3':
|
'@biomejs/cli-linux-x64-musl@2.1.4':
|
||||||
resolution: {integrity: sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew==}
|
resolution: {integrity: sha512-lvwvb2SQQHctHUKvBKptR6PLFCM7JfRjpCCrDaTmvB7EeZ5/dQJPhTYBf36BE/B4CRWR2ZiBLRYhK7hhXBCZAg==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@biomejs/cli-linux-x64@2.1.3':
|
'@biomejs/cli-linux-x64@2.1.4':
|
||||||
resolution: {integrity: sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA==}
|
resolution: {integrity: sha512-Eoy9ycbhpJVYuR+LskV9s3uyaIkp89+qqgqhGQsWnp/I02Uqg2fXFblHJOpGZR8AxdB9ADy87oFVxn9MpFKUrw==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@biomejs/cli-win32-arm64@2.1.3':
|
'@biomejs/cli-win32-arm64@2.1.4':
|
||||||
resolution: {integrity: sha512-V9CUZCtWH4u0YwyCYbQ3W5F4ZGPWp2C2TYcsiWFNNyRfmOW1j/TY/jAurl33SaRjgZPO5UUhGyr9m6BN9t84NQ==}
|
resolution: {integrity: sha512-3WRYte7orvyi6TRfIZkDN9Jzoogbv+gSvR+b9VOXUg1We1XrjBg6WljADeVEaKTvOcpVdH0a90TwyOQ6ue4fGw==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@biomejs/cli-win32-x64@2.1.3':
|
'@biomejs/cli-win32-x64@2.1.4':
|
||||||
resolution: {integrity: sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g==}
|
resolution: {integrity: sha512-tBc+W7anBPSFXGAoQW+f/+svkpt8/uXfRwDzN1DvnatkRMt16KIYpEi/iw8u9GahJlFv98kgHcIrSsZHZTR0sw==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -1135,8 +1138,8 @@ packages:
|
|||||||
'@radix-ui/rect@1.1.1':
|
'@radix-ui/rect@1.1.1':
|
||||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.27':
|
'@rolldown/pluginutils@1.0.0-beta.30':
|
||||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
resolution: {integrity: sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw==}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.46.2':
|
'@rollup/rollup-android-arm-eabi@4.46.2':
|
||||||
resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==}
|
resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==}
|
||||||
@@ -1467,8 +1470,8 @@ packages:
|
|||||||
'@types/node-fetch@2.6.13':
|
'@types/node-fetch@2.6.13':
|
||||||
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
|
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
|
||||||
|
|
||||||
'@types/node@24.2.0':
|
'@types/node@24.2.1':
|
||||||
resolution: {integrity: sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==}
|
resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==}
|
||||||
|
|
||||||
'@types/react-dom@19.1.7':
|
'@types/react-dom@19.1.7':
|
||||||
resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==}
|
resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==}
|
||||||
@@ -1481,9 +1484,9 @@ packages:
|
|||||||
'@types/tmp@0.2.6':
|
'@types/tmp@0.2.6':
|
||||||
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
|
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.7.0':
|
'@vitejs/plugin-react@5.0.0':
|
||||||
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
resolution: {integrity: sha512-Jx9JfsTa05bYkS9xo0hkofp2dCmp1blrKjw9JONs5BTHOvJCgLbaPSuZLGSVJW6u2qe0tc4eevY0+gSNNi0YCw==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
|
||||||
@@ -1860,6 +1863,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
framer-motion@12.23.12:
|
||||||
|
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fs-constants@1.0.0:
|
fs-constants@1.0.0:
|
||||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||||
|
|
||||||
@@ -2215,8 +2232,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
lint-staged@16.1.4:
|
lint-staged@16.1.5:
|
||||||
resolution: {integrity: sha512-xy7rnzQrhTVGKMpv6+bmIA3C0yET31x8OhKBYfvGo0/byeZ6E0BjGARrir3Kg/RhhYHutpsi01+2J5IpfVoueA==}
|
resolution: {integrity: sha512-uAeQQwByI6dfV7wpt/gVqg+jAPaSp8WwOA8kKC/dv1qw14oGpnpAisY65ibGHUGDUv0rYaZ8CAJZ/1U8hUvC2A==}
|
||||||
engines: {node: '>=20.17'}
|
engines: {node: '>=20.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -2343,6 +2360,26 @@ packages:
|
|||||||
resolution: {integrity: sha512-DXO4L9W+08T+A7h5+xdT32l7IMot8z7WOH+7C1Maol571PnktQ8un7Ni4CyPFp4H+vht/FDA5/tpjRvWMFQDMw==}
|
resolution: {integrity: sha512-DXO4L9W+08T+A7h5+xdT32l7IMot8z7WOH+7C1Maol571PnktQ8un7Ni4CyPFp4H+vht/FDA5/tpjRvWMFQDMw==}
|
||||||
engines: {node: '>=10', npm: '>=6'}
|
engines: {node: '>=10', npm: '>=6'}
|
||||||
|
|
||||||
|
motion-dom@12.23.12:
|
||||||
|
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
|
||||||
|
|
||||||
|
motion-utils@12.23.6:
|
||||||
|
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
||||||
|
|
||||||
|
motion@12.23.12:
|
||||||
|
resolution: {integrity: sha512-8jCD8uW5GD1csOoqh1WhH1A6j5APHVE15nuBkFeRiMzYBdRwyAHmSP/oXSuW0WJPZRXTFdBoG4hY9TFWNhhwng==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -2782,8 +2819,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
tmp@0.2.4:
|
tmp@0.2.5:
|
||||||
resolution: {integrity: sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==}
|
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
|
||||||
engines: {node: '>=14.14'}
|
engines: {node: '>=14.14'}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
@@ -3100,39 +3137,39 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.27.1
|
'@babel/helper-validator-identifier': 7.27.1
|
||||||
|
|
||||||
'@biomejs/biome@2.1.3':
|
'@biomejs/biome@2.1.4':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@biomejs/cli-darwin-arm64': 2.1.3
|
'@biomejs/cli-darwin-arm64': 2.1.4
|
||||||
'@biomejs/cli-darwin-x64': 2.1.3
|
'@biomejs/cli-darwin-x64': 2.1.4
|
||||||
'@biomejs/cli-linux-arm64': 2.1.3
|
'@biomejs/cli-linux-arm64': 2.1.4
|
||||||
'@biomejs/cli-linux-arm64-musl': 2.1.3
|
'@biomejs/cli-linux-arm64-musl': 2.1.4
|
||||||
'@biomejs/cli-linux-x64': 2.1.3
|
'@biomejs/cli-linux-x64': 2.1.4
|
||||||
'@biomejs/cli-linux-x64-musl': 2.1.3
|
'@biomejs/cli-linux-x64-musl': 2.1.4
|
||||||
'@biomejs/cli-win32-arm64': 2.1.3
|
'@biomejs/cli-win32-arm64': 2.1.4
|
||||||
'@biomejs/cli-win32-x64': 2.1.3
|
'@biomejs/cli-win32-x64': 2.1.4
|
||||||
|
|
||||||
'@biomejs/cli-darwin-arm64@2.1.3':
|
'@biomejs/cli-darwin-arm64@2.1.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-darwin-x64@2.1.3':
|
'@biomejs/cli-darwin-x64@2.1.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-linux-arm64-musl@2.1.3':
|
'@biomejs/cli-linux-arm64-musl@2.1.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-linux-arm64@2.1.3':
|
'@biomejs/cli-linux-arm64@2.1.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-linux-x64-musl@2.1.3':
|
'@biomejs/cli-linux-x64-musl@2.1.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-linux-x64@2.1.3':
|
'@biomejs/cli-linux-x64@2.1.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-win32-arm64@2.1.3':
|
'@biomejs/cli-win32-arm64@2.1.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-win32-x64@2.1.3':
|
'@biomejs/cli-win32-x64@2.1.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@cspotcode/source-map-support@0.8.1':
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
@@ -3826,7 +3863,7 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/rect@1.1.1': {}
|
'@radix-ui/rect@1.1.1': {}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
'@rolldown/pluginutils@1.0.0-beta.30': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.46.2':
|
'@rollup/rollup-android-arm-eabi@4.46.2':
|
||||||
optional: true
|
optional: true
|
||||||
@@ -4077,10 +4114,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/node-fetch@2.6.13':
|
'@types/node-fetch@2.6.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.2.0
|
'@types/node': 24.2.1
|
||||||
form-data: 4.0.4
|
form-data: 4.0.4
|
||||||
|
|
||||||
'@types/node@24.2.0':
|
'@types/node@24.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.10.0
|
undici-types: 7.10.0
|
||||||
|
|
||||||
@@ -4094,15 +4131,15 @@ snapshots:
|
|||||||
|
|
||||||
'@types/tmp@0.2.6': {}
|
'@types/tmp@0.2.6': {}
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.7.0(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))':
|
'@vitejs/plugin-react@5.0.0(vite@7.0.6(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.0
|
'@babel/core': 7.28.0
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0)
|
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0)
|
||||||
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0)
|
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0)
|
||||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
'@rolldown/pluginutils': 1.0.0-beta.30
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.17.0
|
react-refresh: 0.17.0
|
||||||
vite: 7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1)
|
vite: 7.0.6(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -4520,6 +4557,15 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
framer-motion@12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||||
|
dependencies:
|
||||||
|
motion-dom: 12.23.12
|
||||||
|
motion-utils: 12.23.6
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.1.1
|
||||||
|
react-dom: 19.1.1(react@19.1.1)
|
||||||
|
|
||||||
fs-constants@1.0.0: {}
|
fs-constants@1.0.0: {}
|
||||||
|
|
||||||
fs-minipass@2.1.0:
|
fs-minipass@2.1.0:
|
||||||
@@ -4825,7 +4871,7 @@ snapshots:
|
|||||||
|
|
||||||
lilconfig@3.1.3: {}
|
lilconfig@3.1.3: {}
|
||||||
|
|
||||||
lint-staged@16.1.4:
|
lint-staged@16.1.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 5.5.0
|
chalk: 5.5.0
|
||||||
commander: 14.0.0
|
commander: 14.0.0
|
||||||
@@ -4981,6 +5027,20 @@ snapshots:
|
|||||||
|
|
||||||
mmdb-lib@2.2.1: {}
|
mmdb-lib@2.2.1: {}
|
||||||
|
|
||||||
|
motion-dom@12.23.12:
|
||||||
|
dependencies:
|
||||||
|
motion-utils: 12.23.6
|
||||||
|
|
||||||
|
motion-utils@12.23.6: {}
|
||||||
|
|
||||||
|
motion@12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||||
|
dependencies:
|
||||||
|
framer-motion: 12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.1.1
|
||||||
|
react-dom: 19.1.1(react@19.1.1)
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nano-spawn@1.0.2: {}
|
nano-spawn@1.0.2: {}
|
||||||
@@ -5495,7 +5555,7 @@ snapshots:
|
|||||||
fdir: 6.4.6(picomatch@4.0.3)
|
fdir: 6.4.6(picomatch@4.0.3)
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
tmp@0.2.4: {}
|
tmp@0.2.5: {}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5505,14 +5565,14 @@ snapshots:
|
|||||||
|
|
||||||
tr46@0.0.3: {}
|
tr46@0.0.3: {}
|
||||||
|
|
||||||
ts-node@10.9.2(@types/node@24.2.0)(typescript@5.9.2):
|
ts-node@10.9.2(@types/node@24.2.1)(typescript@5.9.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cspotcode/source-map-support': 0.8.1
|
'@cspotcode/source-map-support': 0.8.1
|
||||||
'@tsconfig/node10': 1.0.11
|
'@tsconfig/node10': 1.0.11
|
||||||
'@tsconfig/node12': 1.0.11
|
'@tsconfig/node12': 1.0.11
|
||||||
'@tsconfig/node14': 1.0.3
|
'@tsconfig/node14': 1.0.3
|
||||||
'@tsconfig/node16': 1.0.4
|
'@tsconfig/node16': 1.0.4
|
||||||
'@types/node': 24.2.0
|
'@types/node': 24.2.1
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
acorn-walk: 8.3.4
|
acorn-walk: 8.3.4
|
||||||
arg: 4.1.3
|
arg: 4.1.3
|
||||||
@@ -5599,7 +5659,7 @@ snapshots:
|
|||||||
|
|
||||||
vali-date@1.0.0: {}
|
vali-date@1.0.0: {}
|
||||||
|
|
||||||
vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1):
|
vite@7.0.6(@types/node@24.2.1)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.8
|
esbuild: 0.25.8
|
||||||
fdir: 6.4.6(picomatch@4.0.3)
|
fdir: 6.4.6(picomatch@4.0.3)
|
||||||
@@ -5608,7 +5668,7 @@ snapshots:
|
|||||||
rollup: 4.46.2
|
rollup: 4.46.2
|
||||||
tinyglobby: 0.2.14
|
tinyglobby: 0.2.14
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.2.0
|
'@types/node': 24.2.1
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.5.1
|
jiti: 2.5.1
|
||||||
lightningcss: 1.30.1
|
lightningcss: 1.30.1
|
||||||
|
|||||||
Generated
+132
-16
@@ -401,9 +401,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.23.1"
|
version = "1.23.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
|
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
@@ -429,6 +429,18 @@ dependencies = [
|
|||||||
"libbz2-rs-sys",
|
"libbz2-rs-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cab"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "171228650e6721d5acc0868a462cd864f49ac5f64e4a42cde270406e64e404d2"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"flate2",
|
||||||
|
"lzxd",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cairo-rs"
|
name = "cairo-rs"
|
||||||
version = "0.18.5"
|
version = "0.18.5"
|
||||||
@@ -456,9 +468,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "camino"
|
name = "camino"
|
||||||
version = "1.1.10"
|
version = "1.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
|
checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -498,9 +510,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.31"
|
version = "1.2.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
|
checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jobserver",
|
"jobserver",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -524,6 +536,17 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfb"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8a4f8e55be323b378facfcf1f06aa97f6ec17cf4ac84fb17325093aaf62da41"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"fnv",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-expr"
|
name = "cfg-expr"
|
||||||
version = "0.15.8"
|
version = "0.15.8"
|
||||||
@@ -691,6 +714,21 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
|
||||||
|
dependencies = [
|
||||||
|
"crc-catalog",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc-catalog"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -987,20 +1025,25 @@ version = "0.8.2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
"bzip2",
|
||||||
"chrono",
|
"chrono",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"directories",
|
"directories",
|
||||||
|
"flate2",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"lzma-rs",
|
||||||
|
"msi-extract",
|
||||||
"objc2 0.6.1",
|
"objc2 0.6.1",
|
||||||
"objc2-app-kit 0.3.1",
|
"objc2-app-kit 0.3.1",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
|
"tar",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-deep-link",
|
"tauri-plugin-deep-link",
|
||||||
@@ -1192,6 +1235,18 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"libredox",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -2066,7 +2121,7 @@ version = "0.19.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
|
checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfb",
|
"cfb 0.7.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2330,6 +2385,7 @@ checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"libc",
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2369,6 +2425,22 @@ version = "0.4.27"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lzma-rs"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"crc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lzxd"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mac"
|
name = "mac"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2468,6 +2540,30 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "msi"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a2332f87a064dea9cce571408c879e0da8dc193b3af06a2b3b2604ee4182a32"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"cfb 0.10.0",
|
||||||
|
"encoding_rs",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "msi-extract"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32047fe35aac08636833dcae558307dafc7679726b97f56449c3c126ed4be108"
|
||||||
|
dependencies = [
|
||||||
|
"cab",
|
||||||
|
"cfb 0.10.0",
|
||||||
|
"msi",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "muda"
|
name = "muda"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
@@ -3704,9 +3800,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.21"
|
version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
@@ -4093,9 +4189,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.10"
|
version = "0.4.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
|
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
@@ -4361,6 +4457,17 @@ dependencies = [
|
|||||||
"syn 2.0.104",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tar"
|
||||||
|
version = "0.4.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
|
||||||
|
dependencies = [
|
||||||
|
"filetime",
|
||||||
|
"libc",
|
||||||
|
"xattr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.12.16"
|
version = "0.12.16"
|
||||||
@@ -4720,12 +4827,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-winres"
|
name = "tauri-winres"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c6d9028d41d4de835e3c482c677a8cb88137ac435d6ff9a71f392d4421576c9"
|
checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"embed-resource",
|
"embed-resource",
|
||||||
"indexmap 2.10.0",
|
|
||||||
"toml 0.9.5",
|
"toml 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4739,7 +4845,7 @@ dependencies = [
|
|||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5576,7 +5682,7 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6212,6 +6318,16 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xattr"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xdg-home"
|
name = "xdg-home"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ lazy_static = "1.4"
|
|||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
zip = "4"
|
||||||
|
tar = "0"
|
||||||
|
bzip2 = "0"
|
||||||
|
flate2 = "1"
|
||||||
|
lzma-rs = "0"
|
||||||
|
msi-extract = "0"
|
||||||
|
|
||||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
@@ -44,9 +50,6 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
||||||
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
|
||||||
zip = "4"
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
core-foundation = "0.10"
|
core-foundation = "0.10"
|
||||||
objc2 = "0.6.1"
|
objc2 = "0.6.1"
|
||||||
|
|||||||
@@ -1678,11 +1678,22 @@ mod tests {
|
|||||||
"name": "Release v1.81.9 (Chromium 137.0.7151.104)",
|
"name": "Release v1.81.9 (Chromium 137.0.7151.104)",
|
||||||
"prerelease": false,
|
"prerelease": false,
|
||||||
"published_at": "2024-01-15T10:00:00Z",
|
"published_at": "2024-01-15T10:00:00Z",
|
||||||
|
"draft": false,
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"name": "brave-v1.81.9-universal.dmg",
|
"name": "brave-v1.81.9-universal.dmg",
|
||||||
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
|
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
|
||||||
"size": 200000000
|
"size": 200000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "brave-browser-1.81.9-linux-amd64.zip",
|
||||||
|
"browser_download_url": "https://example.com/brave-1.81.9-linux-amd64.zip",
|
||||||
|
"size": 180000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BraveBrowserStandaloneSetup.exe",
|
||||||
|
"browser_download_url": "https://example.com/brave-1.81.9-setup.exe",
|
||||||
|
"size": 150000000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
use crate::api_client::is_browser_version_nightly;
|
use crate::api_client::is_browser_version_nightly;
|
||||||
use crate::browser_version_service::{BrowserVersionInfo, BrowserVersionService};
|
use crate::browser_version_manager::{BrowserVersionInfo, BrowserVersionManager};
|
||||||
use crate::profile::BrowserProfile;
|
use crate::profile::BrowserProfile;
|
||||||
use crate::settings_manager::SettingsManager;
|
use crate::settings_manager::SettingsManager;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -29,14 +29,14 @@ pub struct AutoUpdateState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct AutoUpdater {
|
pub struct AutoUpdater {
|
||||||
version_service: &'static BrowserVersionService,
|
version_service: &'static BrowserVersionManager,
|
||||||
settings_manager: &'static SettingsManager,
|
settings_manager: &'static SettingsManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AutoUpdater {
|
impl AutoUpdater {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
version_service: BrowserVersionService::instance(),
|
version_service: BrowserVersionManager::instance(),
|
||||||
settings_manager: SettingsManager::instance(),
|
settings_manager: SettingsManager::instance(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -779,13 +779,15 @@ mod tests {
|
|||||||
let state_file = test_settings_manager
|
let state_file = test_settings_manager
|
||||||
.get_settings_dir()
|
.get_settings_dir()
|
||||||
.join("auto_update_state.json");
|
.join("auto_update_state.json");
|
||||||
std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap();
|
std::fs::create_dir_all(test_settings_manager.get_settings_dir())
|
||||||
let json = serde_json::to_string_pretty(&state).unwrap();
|
.expect("Failed to create settings directory");
|
||||||
std::fs::write(&state_file, json).unwrap();
|
let json = serde_json::to_string_pretty(&state).expect("Failed to serialize state");
|
||||||
|
std::fs::write(&state_file, json).expect("Failed to write state file");
|
||||||
|
|
||||||
// Load state
|
// Load state
|
||||||
let content = std::fs::read_to_string(&state_file).unwrap();
|
let content = std::fs::read_to_string(&state_file).expect("Failed to read state file");
|
||||||
let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap();
|
let loaded_state: AutoUpdateState =
|
||||||
|
serde_json::from_str(&content).expect("Failed to deserialize state");
|
||||||
|
|
||||||
assert_eq!(loaded_state.disabled_browsers.len(), 1);
|
assert_eq!(loaded_state.disabled_browsers.len(), 1);
|
||||||
assert!(loaded_state.disabled_browsers.contains("firefox"));
|
assert!(loaded_state.disabled_browsers.contains("firefox"));
|
||||||
@@ -823,11 +825,15 @@ mod tests {
|
|||||||
let state_file = test_settings_manager
|
let state_file = test_settings_manager
|
||||||
.get_settings_dir()
|
.get_settings_dir()
|
||||||
.join("auto_update_state.json");
|
.join("auto_update_state.json");
|
||||||
std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap();
|
std::fs::create_dir_all(test_settings_manager.get_settings_dir())
|
||||||
|
.expect("Failed to create settings directory");
|
||||||
|
|
||||||
// Initially not disabled (empty state file means default state)
|
// Initially not disabled (empty state file means default state)
|
||||||
let state = AutoUpdateState::default();
|
let state = AutoUpdateState::default();
|
||||||
assert!(!state.disabled_browsers.contains("firefox"));
|
assert!(
|
||||||
|
!state.disabled_browsers.contains("firefox"),
|
||||||
|
"Firefox should not be disabled initially"
|
||||||
|
);
|
||||||
|
|
||||||
// Start update (should disable)
|
// Start update (should disable)
|
||||||
let mut state = AutoUpdateState::default();
|
let mut state = AutoUpdateState::default();
|
||||||
@@ -835,27 +841,41 @@ mod tests {
|
|||||||
state
|
state
|
||||||
.auto_update_downloads
|
.auto_update_downloads
|
||||||
.insert("firefox-1.1.0".to_string());
|
.insert("firefox-1.1.0".to_string());
|
||||||
let json = serde_json::to_string_pretty(&state).unwrap();
|
let json = serde_json::to_string_pretty(&state).expect("Failed to serialize state");
|
||||||
std::fs::write(&state_file, json).unwrap();
|
std::fs::write(&state_file, json).expect("Failed to write state file");
|
||||||
|
|
||||||
// Check that it's disabled
|
// Check that it's disabled
|
||||||
let content = std::fs::read_to_string(&state_file).unwrap();
|
let content = std::fs::read_to_string(&state_file).expect("Failed to read state file");
|
||||||
let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap();
|
let loaded_state: AutoUpdateState =
|
||||||
assert!(loaded_state.disabled_browsers.contains("firefox"));
|
serde_json::from_str(&content).expect("Failed to deserialize state");
|
||||||
assert!(loaded_state.auto_update_downloads.contains("firefox-1.1.0"));
|
assert!(
|
||||||
|
loaded_state.disabled_browsers.contains("firefox"),
|
||||||
|
"Firefox should be disabled"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
loaded_state.auto_update_downloads.contains("firefox-1.1.0"),
|
||||||
|
"Firefox download should be tracked"
|
||||||
|
);
|
||||||
|
|
||||||
// Complete update (should enable)
|
// Complete update (should enable)
|
||||||
let mut state = loaded_state;
|
let mut state = loaded_state;
|
||||||
state.disabled_browsers.remove("firefox");
|
state.disabled_browsers.remove("firefox");
|
||||||
state.auto_update_downloads.remove("firefox-1.1.0");
|
state.auto_update_downloads.remove("firefox-1.1.0");
|
||||||
let json = serde_json::to_string_pretty(&state).unwrap();
|
let json = serde_json::to_string_pretty(&state).expect("Failed to serialize final state");
|
||||||
std::fs::write(&state_file, json).unwrap();
|
std::fs::write(&state_file, json).expect("Failed to write final state file");
|
||||||
|
|
||||||
// Check that it's enabled again
|
// Check that it's enabled again
|
||||||
let content = std::fs::read_to_string(&state_file).unwrap();
|
let content = std::fs::read_to_string(&state_file).expect("Failed to read final state file");
|
||||||
let final_state: AutoUpdateState = serde_json::from_str(&content).unwrap();
|
let final_state: AutoUpdateState =
|
||||||
assert!(!final_state.disabled_browsers.contains("firefox"));
|
serde_json::from_str(&content).expect("Failed to deserialize final state");
|
||||||
assert!(!final_state.auto_update_downloads.contains("firefox-1.1.0"));
|
assert!(
|
||||||
|
!final_state.disabled_browsers.contains("firefox"),
|
||||||
|
"Firefox should be enabled again"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!final_state.auto_update_downloads.contains("firefox-1.1.0"),
|
||||||
|
"Firefox download should not be tracked anymore"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -863,7 +883,7 @@ mod tests {
|
|||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
// Create a temporary directory for testing
|
// Create a temporary directory for testing
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||||
|
|
||||||
// Create a mock settings manager that uses the temp directory
|
// Create a mock settings manager that uses the temp directory
|
||||||
struct TestSettingsManager {
|
struct TestSettingsManager {
|
||||||
@@ -897,21 +917,27 @@ mod tests {
|
|||||||
let state_file = test_settings_manager
|
let state_file = test_settings_manager
|
||||||
.get_settings_dir()
|
.get_settings_dir()
|
||||||
.join("auto_update_state.json");
|
.join("auto_update_state.json");
|
||||||
std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap();
|
std::fs::create_dir_all(test_settings_manager.get_settings_dir())
|
||||||
let json = serde_json::to_string_pretty(&state).unwrap();
|
.expect("Failed to create settings directory");
|
||||||
std::fs::write(&state_file, json).unwrap();
|
let json = serde_json::to_string_pretty(&state).expect("Failed to serialize initial state");
|
||||||
|
std::fs::write(&state_file, json).expect("Failed to write initial state file");
|
||||||
|
|
||||||
// Dismiss notification (remove from pending updates)
|
// Dismiss notification (remove from pending updates)
|
||||||
state
|
state
|
||||||
.pending_updates
|
.pending_updates
|
||||||
.retain(|n| n.id != "test_notification");
|
.retain(|n| n.id != "test_notification");
|
||||||
let json = serde_json::to_string_pretty(&state).unwrap();
|
let json = serde_json::to_string_pretty(&state).expect("Failed to serialize updated state");
|
||||||
std::fs::write(&state_file, json).unwrap();
|
std::fs::write(&state_file, json).expect("Failed to write updated state file");
|
||||||
|
|
||||||
// Check that it's removed
|
// Check that it's removed
|
||||||
let content = std::fs::read_to_string(&state_file).unwrap();
|
let content = std::fs::read_to_string(&state_file).expect("Failed to read updated state file");
|
||||||
let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap();
|
let loaded_state: AutoUpdateState =
|
||||||
assert_eq!(loaded_state.pending_updates.len(), 0);
|
serde_json::from_str(&content).expect("Failed to deserialize updated state");
|
||||||
|
assert_eq!(
|
||||||
|
loaded_state.pending_updates.len(),
|
||||||
|
0,
|
||||||
|
"Pending updates should be empty after dismissal"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+179
-69
@@ -168,7 +168,7 @@ mod linux {
|
|||||||
install_dir: &Path,
|
install_dir: &Path,
|
||||||
browser_type: &BrowserType,
|
browser_type: &BrowserType,
|
||||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||||
// Expected structure: install_dir/<browser>/<binary>
|
// Expected structure by default: install_dir/<browser>/<binary>
|
||||||
let browser_subdir = install_dir.join(browser_type.as_str());
|
let browser_subdir = install_dir.join(browser_type.as_str());
|
||||||
|
|
||||||
// Try firefox first (preferred), then firefox-bin
|
// Try firefox first (preferred), then firefox-bin
|
||||||
@@ -198,8 +198,8 @@ mod linux {
|
|||||||
}
|
}
|
||||||
BrowserType::Camoufox => {
|
BrowserType::Camoufox => {
|
||||||
vec![
|
vec![
|
||||||
browser_subdir.join("camoufox-bin"),
|
install_dir.join("camoufox-bin"),
|
||||||
browser_subdir.join("camoufox"),
|
install_dir.join("camoufox"),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
@@ -213,9 +213,9 @@ mod linux {
|
|||||||
|
|
||||||
Err(
|
Err(
|
||||||
format!(
|
format!(
|
||||||
"Firefox executable not found in {}/{}",
|
"Executable not found for {} in {}",
|
||||||
|
browser_type.as_str(),
|
||||||
install_dir.display(),
|
install_dir.display(),
|
||||||
browser_type.as_str()
|
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
@@ -256,10 +256,6 @@ mod linux {
|
|||||||
// Expected structure: install_dir/<browser>/<binary>
|
// Expected structure: install_dir/<browser>/<binary>
|
||||||
let browser_subdir = install_dir.join(browser_type.as_str());
|
let browser_subdir = install_dir.join(browser_type.as_str());
|
||||||
|
|
||||||
if !browser_subdir.exists() || !browser_subdir.is_dir() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let possible_executables = match browser_type {
|
let possible_executables = match browser_type {
|
||||||
BrowserType::Firefox | BrowserType::FirefoxDeveloper => {
|
BrowserType::Firefox | BrowserType::FirefoxDeveloper => {
|
||||||
vec![
|
vec![
|
||||||
@@ -286,8 +282,8 @@ mod linux {
|
|||||||
}
|
}
|
||||||
BrowserType::Camoufox => {
|
BrowserType::Camoufox => {
|
||||||
vec![
|
vec![
|
||||||
browser_subdir.join("camoufox-bin"),
|
install_dir.join("camoufox-bin"),
|
||||||
browser_subdir.join("camoufox"),
|
install_dir.join("camoufox"),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
@@ -893,38 +889,55 @@ mod tests {
|
|||||||
assert_eq!(BrowserType::TorBrowser.as_str(), "tor-browser");
|
assert_eq!(BrowserType::TorBrowser.as_str(), "tor-browser");
|
||||||
assert_eq!(BrowserType::Camoufox.as_str(), "camoufox");
|
assert_eq!(BrowserType::Camoufox.as_str(), "camoufox");
|
||||||
|
|
||||||
// Test from_str
|
// Test from_str - use expect with descriptive messages instead of unwrap
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
BrowserType::from_str("mullvad-browser").unwrap(),
|
BrowserType::from_str("mullvad-browser").expect("mullvad-browser should be valid"),
|
||||||
BrowserType::MullvadBrowser
|
BrowserType::MullvadBrowser
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
BrowserType::from_str("firefox").unwrap(),
|
BrowserType::from_str("firefox").expect("firefox should be valid"),
|
||||||
BrowserType::Firefox
|
BrowserType::Firefox
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
BrowserType::from_str("firefox-developer").unwrap(),
|
BrowserType::from_str("firefox-developer").expect("firefox-developer should be valid"),
|
||||||
BrowserType::FirefoxDeveloper
|
BrowserType::FirefoxDeveloper
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
BrowserType::from_str("chromium").unwrap(),
|
BrowserType::from_str("chromium").expect("chromium should be valid"),
|
||||||
BrowserType::Chromium
|
BrowserType::Chromium
|
||||||
);
|
);
|
||||||
assert_eq!(BrowserType::from_str("brave").unwrap(), BrowserType::Brave);
|
|
||||||
assert_eq!(BrowserType::from_str("zen").unwrap(), BrowserType::Zen);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
BrowserType::from_str("tor-browser").unwrap(),
|
BrowserType::from_str("brave").expect("brave should be valid"),
|
||||||
|
BrowserType::Brave
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
BrowserType::from_str("zen").expect("zen should be valid"),
|
||||||
|
BrowserType::Zen
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
BrowserType::from_str("tor-browser").expect("tor-browser should be valid"),
|
||||||
BrowserType::TorBrowser
|
BrowserType::TorBrowser
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
BrowserType::from_str("camoufox").unwrap(),
|
BrowserType::from_str("camoufox").expect("camoufox should be valid"),
|
||||||
BrowserType::Camoufox
|
BrowserType::Camoufox
|
||||||
);
|
);
|
||||||
|
|
||||||
// Test invalid browser type
|
// Test invalid browser type - these should properly fail
|
||||||
assert!(BrowserType::from_str("invalid").is_err());
|
let invalid_result = BrowserType::from_str("invalid");
|
||||||
assert!(BrowserType::from_str("").is_err());
|
assert!(
|
||||||
assert!(BrowserType::from_str("Firefox").is_err()); // Case sensitive
|
invalid_result.is_err(),
|
||||||
|
"Invalid browser type should return error"
|
||||||
|
);
|
||||||
|
|
||||||
|
let empty_result = BrowserType::from_str("");
|
||||||
|
assert!(empty_result.is_err(), "Empty string should return error");
|
||||||
|
|
||||||
|
let case_sensitive_result = BrowserType::from_str("Firefox");
|
||||||
|
assert!(
|
||||||
|
case_sensitive_result.is_err(),
|
||||||
|
"Case sensitive check should fail"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -933,9 +946,12 @@ mod tests {
|
|||||||
let browser = FirefoxBrowser::new(BrowserType::Firefox);
|
let browser = FirefoxBrowser::new(BrowserType::Firefox);
|
||||||
let args = browser
|
let args = browser
|
||||||
.create_launch_args("/path/to/profile", None, None)
|
.create_launch_args("/path/to/profile", None, None)
|
||||||
.unwrap();
|
.expect("Failed to create launch args for Firefox");
|
||||||
assert_eq!(args, vec!["-profile", "/path/to/profile"]);
|
assert_eq!(args, vec!["-profile", "/path/to/profile"]);
|
||||||
assert!(!args.contains(&"-no-remote".to_string()));
|
assert!(
|
||||||
|
!args.contains(&"-no-remote".to_string()),
|
||||||
|
"Firefox should not use -no-remote"
|
||||||
|
);
|
||||||
|
|
||||||
let args = browser
|
let args = browser
|
||||||
.create_launch_args(
|
.create_launch_args(
|
||||||
@@ -943,7 +959,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
Some("https://example.com".to_string()),
|
Some("https://example.com".to_string()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.expect("Failed to create launch args for Firefox with URL");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
args,
|
args,
|
||||||
vec!["-profile", "/path/to/profile", "https://example.com"]
|
vec!["-profile", "/path/to/profile", "https://example.com"]
|
||||||
@@ -953,23 +969,26 @@ mod tests {
|
|||||||
let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser);
|
let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser);
|
||||||
let args = browser
|
let args = browser
|
||||||
.create_launch_args("/path/to/profile", None, None)
|
.create_launch_args("/path/to/profile", None, None)
|
||||||
.unwrap();
|
.expect("Failed to create launch args for Mullvad Browser");
|
||||||
assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]);
|
assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]);
|
||||||
|
|
||||||
// Test Tor Browser (should use -no-remote)
|
// Test Tor Browser (should use -no-remote)
|
||||||
let browser = FirefoxBrowser::new(BrowserType::TorBrowser);
|
let browser = FirefoxBrowser::new(BrowserType::TorBrowser);
|
||||||
let args = browser
|
let args = browser
|
||||||
.create_launch_args("/path/to/profile", None, None)
|
.create_launch_args("/path/to/profile", None, None)
|
||||||
.unwrap();
|
.expect("Failed to create launch args for Tor Browser");
|
||||||
assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]);
|
assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]);
|
||||||
|
|
||||||
// Test Zen Browser (should not use -no-remote)
|
// Test Zen Browser (should not use -no-remote)
|
||||||
let browser = FirefoxBrowser::new(BrowserType::Zen);
|
let browser = FirefoxBrowser::new(BrowserType::Zen);
|
||||||
let args = browser
|
let args = browser
|
||||||
.create_launch_args("/path/to/profile", None, None)
|
.create_launch_args("/path/to/profile", None, None)
|
||||||
.unwrap();
|
.expect("Failed to create launch args for Zen Browser");
|
||||||
assert_eq!(args, vec!["-profile", "/path/to/profile"]);
|
assert_eq!(args, vec!["-profile", "/path/to/profile"]);
|
||||||
assert!(!args.contains(&"-no-remote".to_string()));
|
assert!(
|
||||||
|
!args.contains(&"-no-remote".to_string()),
|
||||||
|
"Zen Browser should not use -no-remote"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -977,15 +996,27 @@ mod tests {
|
|||||||
let browser = ChromiumBrowser::new(BrowserType::Chromium);
|
let browser = ChromiumBrowser::new(BrowserType::Chromium);
|
||||||
let args = browser
|
let args = browser
|
||||||
.create_launch_args("/path/to/profile", None, None)
|
.create_launch_args("/path/to/profile", None, None)
|
||||||
.unwrap();
|
.expect("Failed to create launch args for Chromium");
|
||||||
|
|
||||||
// Test that basic required arguments are present
|
// Test that basic required arguments are present
|
||||||
assert!(args.contains(&"--user-data-dir=/path/to/profile".to_string()));
|
assert!(
|
||||||
assert!(args.contains(&"--no-default-browser-check".to_string()));
|
args.contains(&"--user-data-dir=/path/to/profile".to_string()),
|
||||||
|
"Chromium args should contain user-data-dir"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
args.contains(&"--no-default-browser-check".to_string()),
|
||||||
|
"Chromium args should contain no-default-browser-check"
|
||||||
|
);
|
||||||
|
|
||||||
// Test that automatic update disabling arguments are present
|
// Test that automatic update disabling arguments are present
|
||||||
assert!(args.contains(&"--disable-background-mode".to_string()));
|
assert!(
|
||||||
assert!(args.contains(&"--disable-component-update".to_string()));
|
args.contains(&"--disable-background-mode".to_string()),
|
||||||
|
"Chromium args should contain disable-background-mode"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
args.contains(&"--disable-component-update".to_string()),
|
||||||
|
"Chromium args should contain disable-component-update"
|
||||||
|
);
|
||||||
|
|
||||||
let args_with_url = browser
|
let args_with_url = browser
|
||||||
.create_launch_args(
|
.create_launch_args(
|
||||||
@@ -993,11 +1024,17 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
Some("https://example.com".to_string()),
|
Some("https://example.com".to_string()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.expect("Failed to create launch args for Chromium with URL");
|
||||||
assert!(args_with_url.contains(&"https://example.com".to_string()));
|
assert!(
|
||||||
|
args_with_url.contains(&"https://example.com".to_string()),
|
||||||
|
"Chromium args should contain the URL"
|
||||||
|
);
|
||||||
|
|
||||||
// Verify URL is at the end
|
// Verify URL is at the end
|
||||||
assert_eq!(args_with_url.last().unwrap(), "https://example.com");
|
assert_eq!(
|
||||||
|
args_with_url.last().expect("Args should not be empty"),
|
||||||
|
"https://example.com"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1030,16 +1067,45 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_version_downloaded_check() {
|
fn test_version_downloaded_check() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||||
let binaries_dir = temp_dir.path();
|
let binaries_dir = temp_dir.path();
|
||||||
|
|
||||||
// Create a mock Firefox browser installation with new path structure: binaries/<browser>/<version>/
|
// Create a mock Firefox browser installation with new path structure: binaries/<browser>/<version>/
|
||||||
let browser_dir = binaries_dir.join("firefox").join("139.0");
|
let browser_dir = binaries_dir.join("firefox").join("139.0");
|
||||||
fs::create_dir_all(&browser_dir).unwrap();
|
fs::create_dir_all(&browser_dir).expect("Failed to create browser directory");
|
||||||
|
|
||||||
// Create a mock .app directory
|
#[cfg(target_os = "macos")]
|
||||||
let app_dir = browser_dir.join("Firefox.app");
|
{
|
||||||
fs::create_dir_all(&app_dir).unwrap();
|
// Create a mock .app directory for macOS
|
||||||
|
let app_dir = browser_dir.join("Firefox.app");
|
||||||
|
fs::create_dir_all(&app_dir).expect("Failed to create Firefox.app directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// Create a mock firefox subdirectory and executable for Linux
|
||||||
|
let firefox_subdir = browser_dir.join("firefox");
|
||||||
|
fs::create_dir_all(&firefox_subdir).expect("Failed to create firefox subdirectory");
|
||||||
|
let executable_path = firefox_subdir.join("firefox");
|
||||||
|
fs::write(&executable_path, "mock executable").expect("Failed to write mock executable");
|
||||||
|
|
||||||
|
// Set executable permissions on Linux
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut permissions = executable_path
|
||||||
|
.metadata()
|
||||||
|
.expect("Failed to get file metadata")
|
||||||
|
.permissions();
|
||||||
|
permissions.set_mode(0o755);
|
||||||
|
fs::set_permissions(&executable_path, permissions)
|
||||||
|
.expect("Failed to set executable permissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// Create a mock firefox.exe for Windows
|
||||||
|
let executable_path = browser_dir.join("firefox.exe");
|
||||||
|
fs::write(&executable_path, "mock executable").expect("Failed to write mock executable");
|
||||||
|
}
|
||||||
|
|
||||||
let browser = FirefoxBrowser::new(BrowserType::Firefox);
|
let browser = FirefoxBrowser::new(BrowserType::Firefox);
|
||||||
assert!(browser.is_version_downloaded("139.0", binaries_dir));
|
assert!(browser.is_version_downloaded("139.0", binaries_dir));
|
||||||
@@ -1047,36 +1113,76 @@ mod tests {
|
|||||||
|
|
||||||
// Test with Chromium browser with new path structure
|
// Test with Chromium browser with new path structure
|
||||||
let chromium_dir = binaries_dir.join("chromium").join("1465660");
|
let chromium_dir = binaries_dir.join("chromium").join("1465660");
|
||||||
fs::create_dir_all(&chromium_dir).unwrap();
|
fs::create_dir_all(&chromium_dir).expect("Failed to create chromium directory");
|
||||||
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
|
#[cfg(target_os = "macos")]
|
||||||
let executable_path = chromium_app_dir
|
{
|
||||||
.join("Contents")
|
let chromium_app_dir = chromium_dir.join("Chromium.app");
|
||||||
.join("MacOS")
|
fs::create_dir_all(chromium_app_dir.join("Contents").join("MacOS"))
|
||||||
.join("Chromium");
|
.expect("Failed to create Chromium.app structure");
|
||||||
fs::write(&executable_path, "mock executable").unwrap();
|
|
||||||
|
// Create a mock executable
|
||||||
|
let executable_path = chromium_app_dir
|
||||||
|
.join("Contents")
|
||||||
|
.join("MacOS")
|
||||||
|
.join("Chromium");
|
||||||
|
fs::write(&executable_path, "mock executable")
|
||||||
|
.expect("Failed to write mock Chromium executable");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// Create a mock chromium executable for Linux
|
||||||
|
let executable_path = chromium_dir.join("chromium");
|
||||||
|
fs::write(&executable_path, "mock executable")
|
||||||
|
.expect("Failed to write mock chromium executable");
|
||||||
|
|
||||||
|
// Set executable permissions on Linux
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut permissions = executable_path
|
||||||
|
.metadata()
|
||||||
|
.expect("Failed to get chromium metadata")
|
||||||
|
.permissions();
|
||||||
|
permissions.set_mode(0o755);
|
||||||
|
fs::set_permissions(&executable_path, permissions)
|
||||||
|
.expect("Failed to set chromium permissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// Create a mock chromium.exe for Windows
|
||||||
|
let executable_path = chromium_dir.join("chromium.exe");
|
||||||
|
fs::write(&executable_path, "mock executable").expect("Failed to write mock chromium.exe");
|
||||||
|
}
|
||||||
|
|
||||||
let chromium_browser = ChromiumBrowser::new(BrowserType::Chromium);
|
let chromium_browser = ChromiumBrowser::new(BrowserType::Chromium);
|
||||||
assert!(chromium_browser.is_version_downloaded("1465660", binaries_dir));
|
assert!(
|
||||||
assert!(!chromium_browser.is_version_downloaded("1465661", binaries_dir));
|
chromium_browser.is_version_downloaded("1465660", binaries_dir),
|
||||||
|
"Chromium version should be detected as downloaded"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!chromium_browser.is_version_downloaded("1465661", binaries_dir),
|
||||||
|
"Non-existent Chromium version should not be detected as downloaded"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_version_downloaded_no_app_directory() {
|
fn test_version_downloaded_no_app_directory() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||||
let binaries_dir = temp_dir.path();
|
let binaries_dir = temp_dir.path();
|
||||||
|
|
||||||
// Create browser directory but no .app directory with new path structure
|
// Create browser directory but no proper executable structure
|
||||||
let browser_dir = binaries_dir.join("firefox").join("139.0");
|
let browser_dir = binaries_dir.join("firefox").join("139.0");
|
||||||
fs::create_dir_all(&browser_dir).unwrap();
|
fs::create_dir_all(&browser_dir).expect("Failed to create browser directory");
|
||||||
|
|
||||||
// Create some other files but no .app
|
// Create some other files but no proper executable structure
|
||||||
fs::write(browser_dir.join("readme.txt"), "Some content").unwrap();
|
fs::write(browser_dir.join("readme.txt"), "Some content").expect("Failed to write readme file");
|
||||||
|
|
||||||
let browser = FirefoxBrowser::new(BrowserType::Firefox);
|
let browser = FirefoxBrowser::new(BrowserType::Firefox);
|
||||||
assert!(!browser.is_version_downloaded("139.0", binaries_dir));
|
assert!(
|
||||||
|
!browser.is_version_downloaded("139.0", binaries_dir),
|
||||||
|
"Firefox version should not be detected without proper executable structure"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1101,16 +1207,20 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Test that it can be serialized (implements Serialize)
|
// Test that it can be serialized (implements Serialize)
|
||||||
let json = serde_json::to_string(&proxy).unwrap();
|
let json = serde_json::to_string(&proxy).expect("Failed to serialize proxy settings");
|
||||||
assert!(json.contains("127.0.0.1"));
|
assert!(json.contains("127.0.0.1"), "JSON should contain host IP");
|
||||||
assert!(json.contains("8080"));
|
assert!(json.contains("8080"), "JSON should contain port number");
|
||||||
assert!(json.contains("http"));
|
assert!(json.contains("http"), "JSON should contain proxy type");
|
||||||
|
|
||||||
// Test that it can be deserialized (implements Deserialize)
|
// Test that it can be deserialized (implements Deserialize)
|
||||||
let deserialized: ProxySettings = serde_json::from_str(&json).unwrap();
|
let deserialized: ProxySettings =
|
||||||
assert_eq!(deserialized.proxy_type, proxy.proxy_type);
|
serde_json::from_str(&json).expect("Failed to deserialize proxy settings");
|
||||||
assert_eq!(deserialized.host, proxy.host);
|
assert_eq!(
|
||||||
assert_eq!(deserialized.port, proxy.port);
|
deserialized.proxy_type, proxy.proxy_type,
|
||||||
|
"Proxy type should match"
|
||||||
|
);
|
||||||
|
assert_eq!(deserialized.host, proxy.host, "Host should match");
|
||||||
|
assert_eq!(deserialized.port, proxy.port, "Port should match");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+151
-34
@@ -11,8 +11,8 @@ use sysinfo::System;
|
|||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
|
|
||||||
use crate::browser::{create_browser, BrowserType, ProxySettings};
|
use crate::browser::{create_browser, BrowserType, ProxySettings};
|
||||||
use crate::browser_version_service::{
|
use crate::browser_version_manager::{
|
||||||
BrowserVersionInfo, BrowserVersionService, BrowserVersionsResult,
|
BrowserVersionInfo, BrowserVersionManager, BrowserVersionsResult,
|
||||||
};
|
};
|
||||||
use crate::camoufox::CamoufoxConfig;
|
use crate::camoufox::CamoufoxConfig;
|
||||||
use crate::download::DownloadProgress;
|
use crate::download::DownloadProgress;
|
||||||
@@ -1044,6 +1044,27 @@ impl BrowserRunner {
|
|||||||
Ok(missing_binaries)
|
Ok(missing_binaries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if GeoIP database is missing for Camoufox profiles
|
||||||
|
pub fn check_missing_geoip_database(
|
||||||
|
&self,
|
||||||
|
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
// Get all profiles
|
||||||
|
let profiles = self
|
||||||
|
.list_profiles()
|
||||||
|
.map_err(|e| format!("Failed to list profiles: {e}"))?;
|
||||||
|
|
||||||
|
// Check if there are any Camoufox profiles
|
||||||
|
let has_camoufox_profiles = profiles.iter().any(|profile| profile.browser == "camoufox");
|
||||||
|
|
||||||
|
if has_camoufox_profiles {
|
||||||
|
// Check if GeoIP database is available
|
||||||
|
use crate::geoip_downloader::GeoIPDownloader;
|
||||||
|
return Ok(!GeoIPDownloader::is_geoip_database_available());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Automatically download missing binaries for all profiles
|
/// Automatically download missing binaries for all profiles
|
||||||
pub async fn ensure_all_binaries_exist(
|
pub async fn ensure_all_binaries_exist(
|
||||||
&self,
|
&self,
|
||||||
@@ -1082,6 +1103,25 @@ impl BrowserRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if GeoIP database is missing for Camoufox profiles
|
||||||
|
if self.check_missing_geoip_database()? {
|
||||||
|
println!("GeoIP database is missing for Camoufox profiles, downloading...");
|
||||||
|
|
||||||
|
use crate::geoip_downloader::GeoIPDownloader;
|
||||||
|
let geoip_downloader = GeoIPDownloader::instance();
|
||||||
|
|
||||||
|
match geoip_downloader.download_geoip_database(app_handle).await {
|
||||||
|
Ok(_) => {
|
||||||
|
downloaded.push("GeoIP database for Camoufox".to_string());
|
||||||
|
println!("GeoIP database downloaded successfully");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to download GeoIP database: {e}");
|
||||||
|
// Don't fail the entire operation if GeoIP download fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(downloaded)
|
Ok(downloaded)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1129,7 +1169,7 @@ impl BrowserRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if browser is supported on current platform before attempting download
|
// Check if browser is supported on current platform before attempting download
|
||||||
let version_service = BrowserVersionService::instance();
|
let version_service = BrowserVersionManager::instance();
|
||||||
|
|
||||||
if !version_service
|
if !version_service
|
||||||
.is_browser_supported(&browser_str)
|
.is_browser_supported(&browser_str)
|
||||||
@@ -1156,11 +1196,6 @@ impl BrowserRunner {
|
|||||||
browser_dir.push(browser_type.as_str());
|
browser_dir.push(browser_type.as_str());
|
||||||
browser_dir.push(&version);
|
browser_dir.push(&version);
|
||||||
|
|
||||||
// Clean up any failed previous download
|
|
||||||
if let Err(e) = registry.cleanup_failed_download(&browser_str, &version) {
|
|
||||||
println!("Warning: Failed to cleanup previous download: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
create_dir_all(&browser_dir).map_err(|e| format!("Failed to create browser directory: {e}"))?;
|
create_dir_all(&browser_dir).map_err(|e| format!("Failed to create browser directory: {e}"))?;
|
||||||
|
|
||||||
// Mark download as started in registry
|
// Mark download as started in registry
|
||||||
@@ -1171,7 +1206,9 @@ impl BrowserRunner {
|
|||||||
|
|
||||||
// Use the download module
|
// Use the download module
|
||||||
let downloader = crate::download::Downloader::instance();
|
let downloader = crate::download::Downloader::instance();
|
||||||
let download_path = match downloader
|
// Attempt to download the archive. If the download fails but an archive with the
|
||||||
|
// expected filename already exists (manual download), continue using that file.
|
||||||
|
let download_path: PathBuf = match downloader
|
||||||
.download_browser(
|
.download_browser(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
browser_type.clone(),
|
browser_type.clone(),
|
||||||
@@ -1183,15 +1220,25 @@ impl BrowserRunner {
|
|||||||
{
|
{
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Clean up failed download
|
// Check if the expected archive is already present (manual download)
|
||||||
let _ = registry.cleanup_failed_download(&browser_str, &version);
|
let expected_archive_path = browser_dir.join(&download_info.filename);
|
||||||
let _ = registry.save();
|
if expected_archive_path.exists() {
|
||||||
// Remove browser-version pair from downloading set on error
|
println!(
|
||||||
{
|
"Download failed, but found existing archive at {}. Continuing with extraction.",
|
||||||
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
|
expected_archive_path.display()
|
||||||
downloading.remove(&download_key);
|
);
|
||||||
|
expected_archive_path
|
||||||
|
} else {
|
||||||
|
// Remove only the registry entry; keep any files (including a partially downloaded archive)
|
||||||
|
let _ = registry.remove_browser(&browser_str, &version);
|
||||||
|
let _ = registry.save();
|
||||||
|
// Remove browser-version pair from downloading set on error
|
||||||
|
{
|
||||||
|
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
|
||||||
|
downloading.remove(&download_key);
|
||||||
|
}
|
||||||
|
return Err(format!("Failed to download browser: {e}").into());
|
||||||
}
|
}
|
||||||
return Err(format!("Failed to download browser: {e}").into());
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1209,14 +1256,12 @@ impl BrowserRunner {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Clean up the downloaded archive
|
// Do not remove the archive here. We keep it until verification succeeds.
|
||||||
if let Err(e) = std::fs::remove_file(&download_path) {
|
|
||||||
println!("Warning: Could not delete archive file: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Clean up failed download
|
// Do not remove the archive or extracted files. Just drop the registry entry
|
||||||
let _ = registry.cleanup_failed_download(&browser_str, &version);
|
// so it won't be reported as downloaded.
|
||||||
|
let _ = registry.remove_browser(&browser_str, &version);
|
||||||
let _ = registry.save();
|
let _ = registry.save();
|
||||||
// Remove browser-version pair from downloading set on error
|
// Remove browser-version pair from downloading set on error
|
||||||
{
|
{
|
||||||
@@ -1250,14 +1295,68 @@ impl BrowserRunner {
|
|||||||
// Use the browser's own verification method
|
// Use the browser's own verification method
|
||||||
let binaries_dir = self.get_binaries_dir();
|
let binaries_dir = self.get_binaries_dir();
|
||||||
if !browser.is_version_downloaded(&version, &binaries_dir) {
|
if !browser.is_version_downloaded(&version, &binaries_dir) {
|
||||||
let _ = registry.cleanup_failed_download(&browser_str, &version);
|
// Provide detailed error information for debugging
|
||||||
|
let browser_dir = binaries_dir.join(&browser_str).join(&version);
|
||||||
|
let mut error_details = format!(
|
||||||
|
"Browser download completed but verification failed for {} {}. Expected directory: {}",
|
||||||
|
browser_str,
|
||||||
|
version,
|
||||||
|
browser_dir.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
// List what files actually exist
|
||||||
|
if browser_dir.exists() {
|
||||||
|
error_details.push_str("\nFiles found in directory:");
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&browser_dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
let file_type = if path.is_dir() { "DIR" } else { "FILE" };
|
||||||
|
error_details.push_str(&format!("\n {} {}", file_type, path.display()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_details.push_str("\n (Could not read directory contents)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_details.push_str("\nDirectory does not exist!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Camoufox on Linux, provide specific expected files
|
||||||
|
if browser_str == "camoufox" && cfg!(target_os = "linux") {
|
||||||
|
let camoufox_subdir = browser_dir.join("camoufox");
|
||||||
|
error_details.push_str("\nExpected Camoufox executable locations:");
|
||||||
|
error_details.push_str(&format!("\n {}/camoufox-bin", camoufox_subdir.display()));
|
||||||
|
error_details.push_str(&format!("\n {}/camoufox", camoufox_subdir.display()));
|
||||||
|
|
||||||
|
if camoufox_subdir.exists() {
|
||||||
|
error_details.push_str(&format!(
|
||||||
|
"\nCamoufox subdirectory exists: {}",
|
||||||
|
camoufox_subdir.display()
|
||||||
|
));
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&camoufox_subdir) {
|
||||||
|
error_details.push_str("\nFiles in camoufox subdirectory:");
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
let file_type = if path.is_dir() { "DIR" } else { "FILE" };
|
||||||
|
error_details.push_str(&format!("\n {} {}", file_type, path.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_details.push_str(&format!(
|
||||||
|
"\nCamoufox subdirectory does not exist: {}",
|
||||||
|
camoufox_subdir.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not delete files on verification failure; keep archive for manual retry.
|
||||||
|
let _ = registry.remove_browser(&browser_str, &version);
|
||||||
let _ = registry.save();
|
let _ = registry.save();
|
||||||
// Remove browser-version pair from downloading set on verification failure
|
// Remove browser-version pair from downloading set on verification failure
|
||||||
{
|
{
|
||||||
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
|
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
|
||||||
downloading.remove(&download_key);
|
downloading.remove(&download_key);
|
||||||
}
|
}
|
||||||
return Err("Browser download completed but verification failed".into());
|
return Err(error_details.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
registry
|
registry
|
||||||
@@ -1267,6 +1366,16 @@ impl BrowserRunner {
|
|||||||
.save()
|
.save()
|
||||||
.map_err(|e| format!("Failed to save registry: {e}"))?;
|
.map_err(|e| format!("Failed to save registry: {e}"))?;
|
||||||
|
|
||||||
|
// Now that verification succeeded, remove the archive file if it exists
|
||||||
|
if download_info.is_archive {
|
||||||
|
let archive_path = browser_dir.join(&download_info.filename);
|
||||||
|
if archive_path.exists() {
|
||||||
|
if let Err(e) = std::fs::remove_file(&archive_path) {
|
||||||
|
println!("Warning: Could not delete archive file after verification: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If this is Camoufox, automatically download GeoIP database
|
// If this is Camoufox, automatically download GeoIP database
|
||||||
if browser_str == "camoufox" {
|
if browser_str == "camoufox" {
|
||||||
use crate::geoip_downloader::GeoIPDownloader;
|
use crate::geoip_downloader::GeoIPDownloader;
|
||||||
@@ -1529,13 +1638,13 @@ pub fn delete_profile(_app_handle: tauri::AppHandle, profile_name: String) -> Re
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_supported_browsers() -> Result<Vec<String>, String> {
|
pub fn get_supported_browsers() -> Result<Vec<String>, String> {
|
||||||
let service = BrowserVersionService::instance();
|
let service = BrowserVersionManager::instance();
|
||||||
Ok(service.get_supported_browsers())
|
Ok(service.get_supported_browsers())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn is_browser_supported_on_platform(browser_str: String) -> Result<bool, String> {
|
pub fn is_browser_supported_on_platform(browser_str: String) -> Result<bool, String> {
|
||||||
let service = BrowserVersionService::instance();
|
let service = BrowserVersionManager::instance();
|
||||||
service
|
service
|
||||||
.is_browser_supported(&browser_str)
|
.is_browser_supported(&browser_str)
|
||||||
.map_err(|e| format!("Failed to check browser support: {e}"))
|
.map_err(|e| format!("Failed to check browser support: {e}"))
|
||||||
@@ -1545,14 +1654,14 @@ pub fn is_browser_supported_on_platform(browser_str: String) -> Result<bool, Str
|
|||||||
pub async fn fetch_browser_versions_cached_first(
|
pub async fn fetch_browser_versions_cached_first(
|
||||||
browser_str: String,
|
browser_str: String,
|
||||||
) -> Result<Vec<BrowserVersionInfo>, String> {
|
) -> Result<Vec<BrowserVersionInfo>, String> {
|
||||||
let service = BrowserVersionService::instance();
|
let service = BrowserVersionManager::instance();
|
||||||
|
|
||||||
// Get cached versions immediately if available
|
// Get cached versions immediately if available
|
||||||
if let Some(cached_versions) = service.get_cached_browser_versions_detailed(&browser_str) {
|
if let Some(cached_versions) = service.get_cached_browser_versions_detailed(&browser_str) {
|
||||||
// Check if we should update cache in background
|
// Check if we should update cache in background
|
||||||
if service.should_update_cache(&browser_str) {
|
if service.should_update_cache(&browser_str) {
|
||||||
// Start background update but return cached data immediately
|
// Start background update but return cached data immediately
|
||||||
let service_clone = BrowserVersionService::instance();
|
let service_clone = BrowserVersionManager::instance();
|
||||||
let browser_str_clone = browser_str.clone();
|
let browser_str_clone = browser_str.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = service_clone
|
if let Err(e) = service_clone
|
||||||
@@ -1577,14 +1686,14 @@ pub async fn fetch_browser_versions_cached_first(
|
|||||||
pub async fn fetch_browser_versions_with_count_cached_first(
|
pub async fn fetch_browser_versions_with_count_cached_first(
|
||||||
browser_str: String,
|
browser_str: String,
|
||||||
) -> Result<BrowserVersionsResult, String> {
|
) -> Result<BrowserVersionsResult, String> {
|
||||||
let service = BrowserVersionService::instance();
|
let service = BrowserVersionManager::instance();
|
||||||
|
|
||||||
// Get cached versions immediately if available
|
// Get cached versions immediately if available
|
||||||
if let Some(cached_versions) = service.get_cached_browser_versions(&browser_str) {
|
if let Some(cached_versions) = service.get_cached_browser_versions(&browser_str) {
|
||||||
// Check if we should update cache in background
|
// Check if we should update cache in background
|
||||||
if service.should_update_cache(&browser_str) {
|
if service.should_update_cache(&browser_str) {
|
||||||
// Start background update but return cached data immediately
|
// Start background update but return cached data immediately
|
||||||
let service_clone = BrowserVersionService::instance();
|
let service_clone = BrowserVersionManager::instance();
|
||||||
let browser_str_clone = browser_str.clone();
|
let browser_str_clone = browser_str.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = service_clone
|
if let Err(e) = service_clone
|
||||||
@@ -1692,7 +1801,7 @@ pub async fn update_camoufox_config(
|
|||||||
pub async fn fetch_browser_versions_with_count(
|
pub async fn fetch_browser_versions_with_count(
|
||||||
browser_str: String,
|
browser_str: String,
|
||||||
) -> Result<BrowserVersionsResult, String> {
|
) -> Result<BrowserVersionsResult, String> {
|
||||||
let service = BrowserVersionService::instance();
|
let service = BrowserVersionManager::instance();
|
||||||
service
|
service
|
||||||
.fetch_browser_versions_with_count(&browser_str, false)
|
.fetch_browser_versions_with_count(&browser_str, false)
|
||||||
.await
|
.await
|
||||||
@@ -1708,8 +1817,8 @@ pub fn get_downloaded_browser_versions(browser_str: String) -> Result<Vec<String
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_browser_release_types(
|
pub async fn get_browser_release_types(
|
||||||
browser_str: String,
|
browser_str: String,
|
||||||
) -> Result<crate::browser_version_service::BrowserReleaseTypes, String> {
|
) -> Result<crate::browser_version_manager::BrowserReleaseTypes, String> {
|
||||||
let service = BrowserVersionService::instance();
|
let service = BrowserVersionManager::instance();
|
||||||
service
|
service
|
||||||
.get_browser_release_types(&browser_str)
|
.get_browser_release_types(&browser_str)
|
||||||
.await
|
.await
|
||||||
@@ -1725,6 +1834,14 @@ pub async fn check_missing_binaries() -> Result<Vec<(String, String, String)>, S
|
|||||||
.map_err(|e| format!("Failed to check missing binaries: {e}"))
|
.map_err(|e| format!("Failed to check missing binaries: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_missing_geoip_database() -> Result<bool, String> {
|
||||||
|
let browser_runner = BrowserRunner::instance();
|
||||||
|
browser_runner
|
||||||
|
.check_missing_geoip_database()
|
||||||
|
.map_err(|e| format!("Failed to check missing GeoIP database: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn ensure_all_binaries_exist(
|
pub async fn ensure_all_binaries_exist(
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
|
|||||||
+167
-28
@@ -30,18 +30,18 @@ pub struct DownloadInfo {
|
|||||||
pub is_archive: bool, // true for .dmg, .zip, etc.
|
pub is_archive: bool, // true for .dmg, .zip, etc.
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BrowserVersionService {
|
pub struct BrowserVersionManager {
|
||||||
api_client: &'static ApiClient,
|
api_client: &'static ApiClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrowserVersionService {
|
impl BrowserVersionManager {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
api_client: ApiClient::instance(),
|
api_client: ApiClient::instance(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn instance() -> &'static BrowserVersionService {
|
pub fn instance() -> &'static BrowserVersionManager {
|
||||||
&BROWSER_VERSION_SERVICE
|
&BROWSER_VERSION_SERVICE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,13 +982,13 @@ mod tests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_test_service(_api_client: ApiClient) -> &'static BrowserVersionService {
|
fn create_test_service(_api_client: ApiClient) -> &'static BrowserVersionManager {
|
||||||
BrowserVersionService::instance()
|
BrowserVersionManager::instance()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_browser_version_service_creation() {
|
async fn test_browser_version_manager_creation() {
|
||||||
let _ = BrowserVersionService::instance();
|
let _ = BrowserVersionManager::instance();
|
||||||
// Test passes if we can create the service without panicking
|
// Test passes if we can create the service without panicking
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1014,61 +1014,200 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_download_info() {
|
fn test_get_download_info() {
|
||||||
let service = BrowserVersionService::instance();
|
let service = BrowserVersionManager::instance();
|
||||||
|
|
||||||
// Test Firefox
|
// Test Firefox - platform-specific expectations
|
||||||
let firefox_info = service.get_download_info("firefox", "139.0").unwrap();
|
let firefox_info = service.get_download_info("firefox", "139.0").unwrap();
|
||||||
assert_eq!(firefox_info.filename, "Firefox 139.0.dmg");
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
assert_eq!(firefox_info.filename, "Firefox 139.0.dmg");
|
||||||
|
assert!(firefox_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
assert_eq!(firefox_info.filename, "firefox-139.0.tar.xz");
|
||||||
|
assert!(firefox_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
assert_eq!(firefox_info.filename, "Firefox Setup 139.0.exe");
|
||||||
|
assert!(!firefox_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
assert!(firefox_info
|
assert!(firefox_info
|
||||||
.url
|
.url
|
||||||
.contains("download-installer.cdn.mozilla.net"));
|
.contains("download-installer.cdn.mozilla.net"));
|
||||||
assert!(firefox_info.url.contains("/pub/firefox/releases/139.0/"));
|
assert!(firefox_info.url.contains("/pub/firefox/releases/139.0/"));
|
||||||
assert!(firefox_info.is_archive);
|
|
||||||
|
|
||||||
// Test Firefox Developer
|
// Test Firefox Developer
|
||||||
let firefox_dev_info = service
|
let firefox_dev_info = service
|
||||||
.get_download_info("firefox-developer", "139.0b1")
|
.get_download_info("firefox-developer", "139.0b1")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(firefox_dev_info.filename, "Firefox 139.0b1.dmg");
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
assert_eq!(firefox_dev_info.filename, "Firefox 139.0b1.dmg");
|
||||||
|
assert!(firefox_dev_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
assert_eq!(firefox_dev_info.filename, "firefox-139.0b1.tar.xz");
|
||||||
|
assert!(firefox_dev_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
assert_eq!(firefox_dev_info.filename, "Firefox Setup 139.0b1.exe");
|
||||||
|
assert!(!firefox_dev_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
assert!(firefox_dev_info
|
assert!(firefox_dev_info
|
||||||
.url
|
.url
|
||||||
.contains("download-installer.cdn.mozilla.net"));
|
.contains("download-installer.cdn.mozilla.net"));
|
||||||
assert!(firefox_dev_info
|
assert!(firefox_dev_info
|
||||||
.url
|
.url
|
||||||
.contains("/pub/devedition/releases/139.0b1/"));
|
.contains("/pub/devedition/releases/139.0b1/"));
|
||||||
assert!(firefox_dev_info.is_archive);
|
|
||||||
|
|
||||||
// Test Mullvad Browser
|
// Test Mullvad Browser
|
||||||
let mullvad_info = service
|
let mullvad_info = service
|
||||||
.get_download_info("mullvad-browser", "14.5a6")
|
.get_download_info("mullvad-browser", "14.5a6")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(mullvad_info.filename, "mullvad-browser-macos-14.5a6.dmg");
|
|
||||||
assert!(mullvad_info.url.contains("mullvad-browser-macos-14.5a6"));
|
#[cfg(target_os = "macos")]
|
||||||
assert!(mullvad_info.is_archive);
|
{
|
||||||
|
assert_eq!(mullvad_info.filename, "mullvad-browser-macos-14.5a6.dmg");
|
||||||
|
assert!(mullvad_info.url.contains("mullvad-browser-macos-14.5a6"));
|
||||||
|
assert!(mullvad_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
mullvad_info.filename,
|
||||||
|
"mullvad-browser-x86_64-14.5a6.tar.xz"
|
||||||
|
);
|
||||||
|
assert!(mullvad_info.url.contains("mullvad-browser-x86_64-14.5a6"));
|
||||||
|
assert!(mullvad_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
mullvad_info.filename,
|
||||||
|
"mullvad-browser-windows-x86_64-14.5a6.exe"
|
||||||
|
);
|
||||||
|
assert!(mullvad_info
|
||||||
|
.url
|
||||||
|
.contains("mullvad-browser-windows-x86_64-14.5a6"));
|
||||||
|
assert!(!mullvad_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
// Test Zen Browser
|
// Test Zen Browser
|
||||||
let zen_info = service.get_download_info("zen", "1.11b").unwrap();
|
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"));
|
#[cfg(target_os = "macos")]
|
||||||
assert!(zen_info.is_archive);
|
{
|
||||||
|
assert_eq!(zen_info.filename, "zen-1.11b.dmg");
|
||||||
|
assert!(zen_info.url.contains("zen.macos-universal.dmg"));
|
||||||
|
assert!(zen_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
assert_eq!(zen_info.filename, "zen-1.11b-x86_64.tar.xz");
|
||||||
|
assert!(zen_info.url.contains("zen.linux-x86_64.tar.xz"));
|
||||||
|
assert!(zen_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
assert_eq!(zen_info.filename, "zen-1.11b.exe");
|
||||||
|
assert!(zen_info.url.contains("zen.installer.exe"));
|
||||||
|
assert!(!zen_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
// Test Tor Browser
|
// Test Tor Browser
|
||||||
let tor_info = service.get_download_info("tor-browser", "14.0.4").unwrap();
|
let tor_info = service.get_download_info("tor-browser", "14.0.4").unwrap();
|
||||||
assert_eq!(tor_info.filename, "tor-browser-macos-14.0.4.dmg");
|
|
||||||
assert!(tor_info.url.contains("tor-browser-macos-14.0.4"));
|
#[cfg(target_os = "macos")]
|
||||||
assert!(tor_info.is_archive);
|
{
|
||||||
|
assert_eq!(tor_info.filename, "tor-browser-macos-14.0.4.dmg");
|
||||||
|
assert!(tor_info.url.contains("tor-browser-macos-14.0.4"));
|
||||||
|
assert!(tor_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
assert_eq!(tor_info.filename, "tor-browser-linux-x86_64-14.0.4.tar.xz");
|
||||||
|
assert!(tor_info.url.contains("tor-browser-linux-x86_64-14.0.4"));
|
||||||
|
assert!(tor_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
tor_info.filename,
|
||||||
|
"tor-browser-windows-x86_64-portable-14.0.4.exe"
|
||||||
|
);
|
||||||
|
assert!(tor_info
|
||||||
|
.url
|
||||||
|
.contains("tor-browser-windows-x86_64-portable-14.0.4"));
|
||||||
|
assert!(!tor_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
// Test Chromium
|
// Test Chromium
|
||||||
let chromium_info = service.get_download_info("chromium", "1465660").unwrap();
|
let chromium_info = service.get_download_info("chromium", "1465660").unwrap();
|
||||||
assert_eq!(chromium_info.filename, "chromium-1465660-mac.zip");
|
|
||||||
assert!(chromium_info.url.contains("chrome-mac.zip"));
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
assert_eq!(chromium_info.filename, "chromium-1465660-mac.zip");
|
||||||
|
assert!(chromium_info.url.contains("chrome-mac.zip"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
assert_eq!(chromium_info.filename, "chromium-1465660-linux.zip");
|
||||||
|
assert!(chromium_info.url.contains("chrome-linux.zip"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
assert_eq!(chromium_info.filename, "chromium-1465660-win.zip");
|
||||||
|
assert!(chromium_info.url.contains("chrome-win.zip"));
|
||||||
|
}
|
||||||
|
|
||||||
assert!(chromium_info.is_archive);
|
assert!(chromium_info.is_archive);
|
||||||
|
|
||||||
// Test Brave - Note: Brave uses dynamic URL resolution, so get_download_info provides a template URL
|
// Test Brave - Note: Brave uses dynamic URL resolution, so get_download_info provides a template URL
|
||||||
let brave_info = service.get_download_info("brave", "v1.81.9").unwrap();
|
let brave_info = service.get_download_info("brave", "v1.81.9").unwrap();
|
||||||
assert_eq!(brave_info.filename, "Brave-Browser-universal.dmg");
|
|
||||||
assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/Brave-Browser-universal.dmg");
|
#[cfg(target_os = "macos")]
|
||||||
assert!(brave_info.is_archive);
|
{
|
||||||
|
assert_eq!(brave_info.filename, "Brave-Browser-universal.dmg");
|
||||||
|
assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/Brave-Browser-universal.dmg");
|
||||||
|
assert!(brave_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
assert_eq!(brave_info.filename, "brave-browser-v1.81.9-linux-amd64.zip");
|
||||||
|
assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/brave-browser-v1.81.9-linux-amd64.zip");
|
||||||
|
assert!(brave_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
assert_eq!(brave_info.filename, "brave-v1.81.9.exe");
|
||||||
|
assert_eq!(
|
||||||
|
brave_info.url,
|
||||||
|
"https://github.com/brave/brave-browser/releases/download/v1.81.9/brave-v1.81.9.exe"
|
||||||
|
);
|
||||||
|
assert!(!brave_info.is_archive);
|
||||||
|
}
|
||||||
|
|
||||||
// Test unsupported browser
|
// Test unsupported browser
|
||||||
let unsupported_result = service.get_download_info("unsupported", "1.0.0");
|
let unsupported_result = service.get_download_info("unsupported", "1.0.0");
|
||||||
@@ -1080,5 +1219,5 @@ mod tests {
|
|||||||
|
|
||||||
// Global singleton instance
|
// Global singleton instance
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref BROWSER_VERSION_SERVICE: BrowserVersionService = BrowserVersionService::new();
|
static ref BROWSER_VERSION_SERVICE: BrowserVersionManager = BrowserVersionManager::new();
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,8 @@ pub struct CamoufoxConfig {
|
|||||||
pub proxy: Option<String>,
|
pub proxy: Option<String>,
|
||||||
pub screen_max_width: Option<u32>,
|
pub screen_max_width: Option<u32>,
|
||||||
pub screen_max_height: Option<u32>,
|
pub screen_max_height: Option<u32>,
|
||||||
|
pub screen_min_width: Option<u32>,
|
||||||
|
pub screen_min_height: Option<u32>,
|
||||||
pub geoip: Option<serde_json::Value>, // Can be String or bool
|
pub geoip: Option<serde_json::Value>, // Can be String or bool
|
||||||
pub block_images: Option<bool>,
|
pub block_images: Option<bool>,
|
||||||
pub block_webrtc: Option<bool>,
|
pub block_webrtc: Option<bool>,
|
||||||
@@ -26,6 +28,8 @@ impl Default for CamoufoxConfig {
|
|||||||
proxy: None,
|
proxy: None,
|
||||||
screen_max_width: None,
|
screen_max_width: None,
|
||||||
screen_max_height: None,
|
screen_max_height: None,
|
||||||
|
screen_min_width: None,
|
||||||
|
screen_min_height: None,
|
||||||
geoip: Some(serde_json::Value::Bool(true)),
|
geoip: Some(serde_json::Value::Bool(true)),
|
||||||
block_images: None,
|
block_images: None,
|
||||||
block_webrtc: None,
|
block_webrtc: None,
|
||||||
@@ -83,6 +87,8 @@ impl CamoufoxNodecarLauncher {
|
|||||||
CamoufoxConfig {
|
CamoufoxConfig {
|
||||||
screen_max_width: Some(1440),
|
screen_max_width: Some(1440),
|
||||||
screen_max_height: Some(900),
|
screen_max_height: Some(900),
|
||||||
|
screen_min_width: Some(800),
|
||||||
|
screen_min_height: Some(600),
|
||||||
geoip: Some(serde_json::Value::Bool(true)),
|
geoip: Some(serde_json::Value::Bool(true)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@@ -134,6 +140,14 @@ impl CamoufoxNodecarLauncher {
|
|||||||
config_args.extend(["--max-height".to_string(), max_height.to_string()]);
|
config_args.extend(["--max-height".to_string(), max_height.to_string()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(min_width) = config.screen_min_width {
|
||||||
|
config_args.extend(["--min-width".to_string(), min_width.to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(min_height) = config.screen_min_height {
|
||||||
|
config_args.extend(["--min-height".to_string(), min_height.to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
// Add block_* options
|
// Add block_* options
|
||||||
if let Some(block_images) = config.block_images {
|
if let Some(block_images) = config.block_images {
|
||||||
if block_images {
|
if block_images {
|
||||||
@@ -477,6 +491,8 @@ mod tests {
|
|||||||
// Verify test config has expected values
|
// Verify test config has expected values
|
||||||
assert_eq!(test_config.screen_max_width, Some(1440));
|
assert_eq!(test_config.screen_max_width, Some(1440));
|
||||||
assert_eq!(test_config.screen_max_height, Some(900));
|
assert_eq!(test_config.screen_max_height, Some(900));
|
||||||
|
assert_eq!(test_config.screen_min_width, Some(800));
|
||||||
|
assert_eq!(test_config.screen_min_height, Some(600));
|
||||||
assert_eq!(test_config.geoip, Some(serde_json::Value::Bool(true)));
|
assert_eq!(test_config.geoip, Some(serde_json::Value::Bool(true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+65
-13
@@ -1,13 +1,12 @@
|
|||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs::File;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
|
|
||||||
use crate::api_client::ApiClient;
|
use crate::api_client::ApiClient;
|
||||||
use crate::browser::BrowserType;
|
use crate::browser::BrowserType;
|
||||||
use crate::browser_version_service::DownloadInfo;
|
use crate::browser_version_manager::DownloadInfo;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct DownloadProgress {
|
pub struct DownloadProgress {
|
||||||
@@ -415,25 +414,74 @@ impl Downloader {
|
|||||||
|
|
||||||
let _ = app_handle.emit("download-progress", &progress);
|
let _ = app_handle.emit("download-progress", &progress);
|
||||||
|
|
||||||
// Start download
|
// Determine if we have a partial file to resume
|
||||||
let response = self
|
let mut existing_size: u64 = 0;
|
||||||
|
if let Ok(meta) = std::fs::metadata(&file_path) {
|
||||||
|
existing_size = meta.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build request, add Range only if we have bytes
|
||||||
|
let mut request = self
|
||||||
.client
|
.client
|
||||||
.get(&download_url)
|
.get(&download_url)
|
||||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
.header(
|
||||||
.send()
|
"User-Agent",
|
||||||
.await?;
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
||||||
|
);
|
||||||
|
|
||||||
|
if existing_size > 0 {
|
||||||
|
request = request.header("Range", format!("bytes={existing_size}-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start download (or resume)
|
||||||
|
let response = request.send().await?;
|
||||||
|
|
||||||
// Check if the response is successful
|
// Check if the response is successful
|
||||||
if !response.status().is_success() {
|
if !(response.status().is_success() || response.status().as_u16() == 206) {
|
||||||
return Err(format!("Download failed with status: {}", response.status()).into());
|
return Err(format!("Download failed with status: {}", response.status()).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_size = response.content_length();
|
// Determine total size
|
||||||
let mut downloaded = 0u64;
|
let mut total_size = response.content_length();
|
||||||
|
|
||||||
|
// If resuming (206) and Content-Range is present, parse total
|
||||||
|
if response.status().as_u16() == 206 {
|
||||||
|
if let Some(content_range) = response.headers().get(reqwest::header::CONTENT_RANGE) {
|
||||||
|
if let Ok(cr) = content_range.to_str() {
|
||||||
|
// Format: bytes start-end/total
|
||||||
|
if let Some((_, total_str)) = cr.split('/').collect::<Vec<_>>().split_first() {
|
||||||
|
if let Some(total_str) = total_str.first() {
|
||||||
|
if let Ok(total) = total_str.parse::<u64>() {
|
||||||
|
total_size = Some(total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(len) = response.headers().get(reqwest::header::CONTENT_LENGTH) {
|
||||||
|
// Fallback: total = existing + incoming length
|
||||||
|
if let Ok(len_str) = len.to_str() {
|
||||||
|
if let Ok(incoming) = len_str.parse::<u64>() {
|
||||||
|
total_size = Some(existing_size + incoming);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if existing_size > 0 && response.status().is_success() {
|
||||||
|
// Server ignored range or we asked from 0; if 200 and existing file has content, start fresh
|
||||||
|
// Truncate existing file so we don't append duplicate bytes
|
||||||
|
let _ = std::fs::remove_file(&file_path);
|
||||||
|
existing_size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut downloaded = existing_size;
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
let mut last_update = start_time;
|
let mut last_update = start_time;
|
||||||
|
|
||||||
let mut file = File::create(&file_path)?;
|
// Open file in append mode (resuming) or create new
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&file_path)?;
|
||||||
let mut stream = response.bytes_stream();
|
let mut stream = response.bytes_stream();
|
||||||
|
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
@@ -452,7 +500,11 @@ impl Downloader {
|
|||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
let percentage = if let Some(total) = total_size {
|
let percentage = if let Some(total) = total_size {
|
||||||
(downloaded as f64 / total as f64) * 100.0
|
if total > 0 {
|
||||||
|
(downloaded as f64 / total as f64) * 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
@@ -493,7 +545,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::api_client::ApiClient;
|
use crate::api_client::ApiClient;
|
||||||
use crate::browser::BrowserType;
|
use crate::browser::BrowserType;
|
||||||
use crate::browser_version_service::DownloadInfo;
|
use crate::browser_version_manager::DownloadInfo;
|
||||||
|
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use wiremock::matchers::{method, path};
|
use wiremock::matchers::{method, path};
|
||||||
|
|||||||
@@ -496,15 +496,21 @@ mod tests {
|
|||||||
registry.mark_download_started("firefox", "139.0", PathBuf::from("/test/path"));
|
registry.mark_download_started("firefox", "139.0", PathBuf::from("/test/path"));
|
||||||
|
|
||||||
// Should be considered downloaded immediately
|
// Should be considered downloaded immediately
|
||||||
assert!(registry.is_browser_downloaded("firefox", "139.0"));
|
assert!(
|
||||||
|
registry.is_browser_downloaded("firefox", "139.0"),
|
||||||
|
"Browser should be considered downloaded after marking as started"
|
||||||
|
);
|
||||||
|
|
||||||
// Mark as completed
|
// Mark as completed
|
||||||
registry
|
registry
|
||||||
.mark_download_completed("firefox", "139.0")
|
.mark_download_completed("firefox", "139.0")
|
||||||
.unwrap();
|
.expect("Failed to mark download as completed");
|
||||||
|
|
||||||
// Should still be considered downloaded
|
// Should still be considered downloaded
|
||||||
assert!(registry.is_browser_downloaded("firefox", "139.0"));
|
assert!(
|
||||||
|
registry.is_browser_downloaded("firefox", "139.0"),
|
||||||
|
"Browser should still be considered downloaded after completion"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -517,11 +523,20 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
registry.add_browser(info);
|
registry.add_browser(info);
|
||||||
assert!(registry.is_browser_downloaded("firefox", "139.0"));
|
assert!(
|
||||||
|
registry.is_browser_downloaded("firefox", "139.0"),
|
||||||
|
"Browser should be downloaded after adding"
|
||||||
|
);
|
||||||
|
|
||||||
let removed = registry.remove_browser("firefox", "139.0");
|
let removed = registry.remove_browser("firefox", "139.0");
|
||||||
assert!(removed.is_some());
|
assert!(
|
||||||
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
|
removed.is_some(),
|
||||||
|
"Remove operation should return the removed browser info"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!registry.is_browser_downloaded("firefox", "139.0"),
|
||||||
|
"Browser should not be downloaded after removal"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -532,6 +547,9 @@ mod tests {
|
|||||||
registry.mark_download_started("zen", "twilight", PathBuf::from("/test/zen-twilight"));
|
registry.mark_download_started("zen", "twilight", PathBuf::from("/test/zen-twilight"));
|
||||||
|
|
||||||
// Check that it's registered
|
// Check that it's registered
|
||||||
assert!(registry.is_browser_downloaded("zen", "twilight"));
|
assert!(
|
||||||
|
registry.is_browser_downloaded("zen", "twilight"),
|
||||||
|
"Zen twilight version should be registered as downloaded"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+803
-724
File diff suppressed because it is too large
Load Diff
@@ -293,12 +293,26 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_is_geoip_database_available() {
|
fn test_is_geoip_database_available() {
|
||||||
// This test will return false unless the database actually exists
|
// Test that the function works correctly regardless of file system state
|
||||||
// In a real environment, this would check the actual file system
|
|
||||||
let is_available = GeoIPDownloader::is_geoip_database_available();
|
let is_available = GeoIPDownloader::is_geoip_database_available();
|
||||||
// We can't assert a specific value since it depends on the system state
|
|
||||||
// But we can verify the function doesn't panic
|
// The function should return a boolean value (either true or false)
|
||||||
println!("GeoIP database available: {is_available}");
|
// The function should return a boolean value - we just verify it doesn't panic
|
||||||
|
// and returns the expected result based on file existence
|
||||||
|
|
||||||
|
// Verify the function logic by checking if the path resolution works
|
||||||
|
let mmdb_path_result = GeoIPDownloader::get_mmdb_file_path();
|
||||||
|
assert!(
|
||||||
|
mmdb_path_result.is_ok(),
|
||||||
|
"Should be able to get MMDB file path"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mmdb_path = mmdb_path_result.unwrap();
|
||||||
|
let expected_available = mmdb_path.exists();
|
||||||
|
assert_eq!(
|
||||||
|
is_available, expected_available,
|
||||||
|
"Function result should match actual file existence"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,258 @@ lazy_static::lazy_static! {
|
|||||||
pub static ref GROUP_MANAGER: Mutex<GroupManager> = Mutex::new(GroupManager::new());
|
pub static ref GROUP_MANAGER: Mutex<GroupManager> = Mutex::new(GroupManager::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::env;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn create_test_group_manager() -> (GroupManager, TempDir) {
|
||||||
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||||
|
|
||||||
|
// Set up a temporary home directory for testing
|
||||||
|
env::set_var("HOME", temp_dir.path());
|
||||||
|
|
||||||
|
let manager = GroupManager::new();
|
||||||
|
(manager, temp_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_group_manager_creation() {
|
||||||
|
let (_manager, _temp_dir) = create_test_group_manager();
|
||||||
|
// Test passes if no panic occurs
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_and_get_groups() {
|
||||||
|
let (manager, _temp_dir) = create_test_group_manager();
|
||||||
|
|
||||||
|
// Initially should have no groups
|
||||||
|
let groups = manager
|
||||||
|
.get_all_groups()
|
||||||
|
.expect("Should be able to get groups");
|
||||||
|
assert!(groups.is_empty(), "Should start with no groups");
|
||||||
|
|
||||||
|
// Create a group
|
||||||
|
let group_name = "Test Group".to_string();
|
||||||
|
let created_group = manager
|
||||||
|
.create_group(group_name.clone())
|
||||||
|
.expect("Should create group successfully");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
created_group.name, group_name,
|
||||||
|
"Created group should have correct name"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!created_group.id.is_empty(),
|
||||||
|
"Created group should have an ID"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify group was saved
|
||||||
|
let groups = manager
|
||||||
|
.get_all_groups()
|
||||||
|
.expect("Should be able to get groups");
|
||||||
|
assert_eq!(groups.len(), 1, "Should have one group");
|
||||||
|
assert_eq!(
|
||||||
|
groups[0].name, group_name,
|
||||||
|
"Retrieved group should have correct name"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
groups[0].id, created_group.id,
|
||||||
|
"Retrieved group should have correct ID"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_duplicate_group_fails() {
|
||||||
|
let (manager, _temp_dir) = create_test_group_manager();
|
||||||
|
|
||||||
|
let group_name = "Duplicate Group".to_string();
|
||||||
|
|
||||||
|
// Create first group
|
||||||
|
let _first_group = manager
|
||||||
|
.create_group(group_name.clone())
|
||||||
|
.expect("Should create first group");
|
||||||
|
|
||||||
|
// Try to create duplicate group
|
||||||
|
let result = manager.create_group(group_name.clone());
|
||||||
|
assert!(result.is_err(), "Should fail to create duplicate group");
|
||||||
|
|
||||||
|
let error_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(
|
||||||
|
error_msg.contains("already exists"),
|
||||||
|
"Error should mention group already exists"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_group() {
|
||||||
|
let (manager, _temp_dir) = create_test_group_manager();
|
||||||
|
|
||||||
|
// Create a group
|
||||||
|
let original_name = "Original Name".to_string();
|
||||||
|
let created_group = manager
|
||||||
|
.create_group(original_name)
|
||||||
|
.expect("Should create group");
|
||||||
|
|
||||||
|
// Update the group
|
||||||
|
let new_name = "Updated Name".to_string();
|
||||||
|
let updated_group = manager
|
||||||
|
.update_group(created_group.id.clone(), new_name.clone())
|
||||||
|
.expect("Should update group successfully");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
updated_group.name, new_name,
|
||||||
|
"Updated group should have new name"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
updated_group.id, created_group.id,
|
||||||
|
"Updated group should keep same ID"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify update was persisted
|
||||||
|
let groups = manager.get_all_groups().expect("Should get groups");
|
||||||
|
assert_eq!(groups.len(), 1, "Should still have one group");
|
||||||
|
assert_eq!(
|
||||||
|
groups[0].name, new_name,
|
||||||
|
"Persisted group should have updated name"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_nonexistent_group_fails() {
|
||||||
|
let (manager, _temp_dir) = create_test_group_manager();
|
||||||
|
|
||||||
|
let result = manager.update_group("nonexistent-id".to_string(), "New Name".to_string());
|
||||||
|
assert!(result.is_err(), "Should fail to update nonexistent group");
|
||||||
|
|
||||||
|
let error_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(
|
||||||
|
error_msg.contains("not found"),
|
||||||
|
"Error should mention group not found"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_group() {
|
||||||
|
let (manager, _temp_dir) = create_test_group_manager();
|
||||||
|
|
||||||
|
// Create a group
|
||||||
|
let group_name = "To Delete".to_string();
|
||||||
|
let created_group = manager
|
||||||
|
.create_group(group_name)
|
||||||
|
.expect("Should create group");
|
||||||
|
|
||||||
|
// Verify group exists
|
||||||
|
let groups = manager.get_all_groups().expect("Should get groups");
|
||||||
|
assert_eq!(groups.len(), 1, "Should have one group");
|
||||||
|
|
||||||
|
// Delete the group
|
||||||
|
manager
|
||||||
|
.delete_group(created_group.id)
|
||||||
|
.expect("Should delete group successfully");
|
||||||
|
|
||||||
|
// Verify group was deleted
|
||||||
|
let groups = manager.get_all_groups().expect("Should get groups");
|
||||||
|
assert!(groups.is_empty(), "Should have no groups after deletion");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_nonexistent_group_fails() {
|
||||||
|
let (manager, _temp_dir) = create_test_group_manager();
|
||||||
|
|
||||||
|
let result = manager.delete_group("nonexistent-id".to_string());
|
||||||
|
assert!(result.is_err(), "Should fail to delete nonexistent group");
|
||||||
|
|
||||||
|
let error_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(
|
||||||
|
error_msg.contains("not found"),
|
||||||
|
"Error should mention group not found"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_groups_with_profile_counts() {
|
||||||
|
let (manager, _temp_dir) = create_test_group_manager();
|
||||||
|
|
||||||
|
// Create test groups
|
||||||
|
let group1 = manager
|
||||||
|
.create_group("Group 1".to_string())
|
||||||
|
.expect("Should create group 1");
|
||||||
|
let _group2 = manager
|
||||||
|
.create_group("Group 2".to_string())
|
||||||
|
.expect("Should create group 2");
|
||||||
|
|
||||||
|
// Create mock profiles
|
||||||
|
let profiles = vec![
|
||||||
|
crate::profile::BrowserProfile {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
name: "Profile 1".to_string(),
|
||||||
|
browser: "firefox".to_string(),
|
||||||
|
version: "1.0".to_string(),
|
||||||
|
proxy_id: None,
|
||||||
|
process_id: None,
|
||||||
|
last_launch: None,
|
||||||
|
release_type: "stable".to_string(),
|
||||||
|
camoufox_config: None,
|
||||||
|
group_id: Some(group1.id.clone()),
|
||||||
|
},
|
||||||
|
crate::profile::BrowserProfile {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
name: "Profile 2".to_string(),
|
||||||
|
browser: "firefox".to_string(),
|
||||||
|
version: "1.0".to_string(),
|
||||||
|
proxy_id: None,
|
||||||
|
process_id: None,
|
||||||
|
last_launch: None,
|
||||||
|
release_type: "stable".to_string(),
|
||||||
|
camoufox_config: None,
|
||||||
|
group_id: Some(group1.id.clone()),
|
||||||
|
},
|
||||||
|
crate::profile::BrowserProfile {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
name: "Profile 3".to_string(),
|
||||||
|
browser: "firefox".to_string(),
|
||||||
|
version: "1.0".to_string(),
|
||||||
|
proxy_id: None,
|
||||||
|
process_id: None,
|
||||||
|
last_launch: None,
|
||||||
|
release_type: "stable".to_string(),
|
||||||
|
camoufox_config: None,
|
||||||
|
group_id: None, // Default group
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let groups_with_counts = manager
|
||||||
|
.get_groups_with_profile_counts(&profiles)
|
||||||
|
.expect("Should get groups with counts");
|
||||||
|
|
||||||
|
// Should have default group + group1 (_group2 has no profiles so shouldn't appear)
|
||||||
|
assert_eq!(
|
||||||
|
groups_with_counts.len(),
|
||||||
|
2,
|
||||||
|
"Should have 2 groups with profiles"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check default group
|
||||||
|
let default_group = groups_with_counts
|
||||||
|
.iter()
|
||||||
|
.find(|g| g.id == "default")
|
||||||
|
.expect("Should have default group");
|
||||||
|
assert_eq!(
|
||||||
|
default_group.count, 1,
|
||||||
|
"Default group should have 1 profile"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check group1
|
||||||
|
let group1_with_count = groups_with_counts
|
||||||
|
.iter()
|
||||||
|
.find(|g| g.id == group1.id)
|
||||||
|
.expect("Should have group1");
|
||||||
|
assert_eq!(group1_with_count.count, 2, "Group1 should have 2 profiles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to get groups with counts
|
// Helper function to get groups with counts
|
||||||
pub fn get_groups_with_counts(profiles: &[crate::profile::BrowserProfile]) -> Vec<GroupWithCount> {
|
pub fn get_groups_with_counts(profiles: &[crate::profile::BrowserProfile]) -> Vec<GroupWithCount> {
|
||||||
let group_manager = GROUP_MANAGER.lock().unwrap();
|
let group_manager = GROUP_MANAGER.lock().unwrap();
|
||||||
|
|||||||
+57
-13
@@ -12,7 +12,7 @@ mod app_auto_updater;
|
|||||||
mod auto_updater;
|
mod auto_updater;
|
||||||
mod browser;
|
mod browser;
|
||||||
mod browser_runner;
|
mod browser_runner;
|
||||||
mod browser_version_service;
|
mod browser_version_manager;
|
||||||
mod camoufox;
|
mod camoufox;
|
||||||
mod default_browser;
|
mod default_browser;
|
||||||
mod download;
|
mod download;
|
||||||
@@ -31,13 +31,13 @@ mod version_updater;
|
|||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
|
|
||||||
use browser_runner::{
|
use browser_runner::{
|
||||||
check_browser_exists, check_browser_status, check_missing_binaries, create_browser_profile_new,
|
check_browser_exists, check_browser_status, check_missing_binaries, check_missing_geoip_database,
|
||||||
delete_profile, download_browser, ensure_all_binaries_exist, fetch_browser_versions_cached_first,
|
create_browser_profile_new, delete_profile, download_browser, ensure_all_binaries_exist,
|
||||||
fetch_browser_versions_with_count, fetch_browser_versions_with_count_cached_first,
|
fetch_browser_versions_cached_first, fetch_browser_versions_with_count,
|
||||||
get_browser_release_types, get_downloaded_browser_versions, get_supported_browsers,
|
fetch_browser_versions_with_count_cached_first, get_browser_release_types,
|
||||||
is_browser_supported_on_platform, kill_browser_profile, launch_browser_profile,
|
get_downloaded_browser_versions, get_supported_browsers, is_browser_supported_on_platform,
|
||||||
list_browser_profiles, rename_profile, update_camoufox_config, update_profile_proxy,
|
kill_browser_profile, launch_browser_profile, list_browser_profiles, rename_profile,
|
||||||
update_profile_version,
|
update_camoufox_config, update_profile_proxy, update_profile_version,
|
||||||
};
|
};
|
||||||
|
|
||||||
use settings_manager::{
|
use settings_manager::{
|
||||||
@@ -69,6 +69,8 @@ use group_manager::{
|
|||||||
get_groups_with_profile_counts, get_profile_groups, update_profile_group,
|
get_groups_with_profile_counts, get_profile_groups, update_profile_group,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use geoip_downloader::GeoIPDownloader;
|
||||||
|
|
||||||
// Trait to extend WebviewWindow with transparent titlebar functionality
|
// Trait to extend WebviewWindow with transparent titlebar functionality
|
||||||
pub trait WindowExt {
|
pub trait WindowExt {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -173,6 +175,20 @@ async fn delete_stored_proxy(proxy_id: String) -> Result<(), String> {
|
|||||||
.map_err(|e| format!("Failed to delete stored proxy: {e}"))
|
.map_err(|e| format!("Failed to delete stored proxy: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn is_geoip_database_available() -> Result<bool, String> {
|
||||||
|
Ok(GeoIPDownloader::is_geoip_database_available())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn download_geoip_database(app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
let downloader = GeoIPDownloader::instance();
|
||||||
|
downloader
|
||||||
|
.download_geoip_database(&app_handle)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to download GeoIP database: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
@@ -386,6 +402,35 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check and download GeoIP database at startup if needed
|
||||||
|
let app_handle_geoip = app.handle().clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
// Wait a bit for the app to fully initialize
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
let browser_runner = crate::browser_runner::BrowserRunner::instance();
|
||||||
|
match browser_runner.check_missing_geoip_database() {
|
||||||
|
Ok(true) => {
|
||||||
|
println!("GeoIP database is missing for Camoufox profiles, downloading at startup...");
|
||||||
|
let geoip_downloader = GeoIPDownloader::instance();
|
||||||
|
if let Err(e) = geoip_downloader
|
||||||
|
.download_geoip_database(&app_handle_geoip)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Failed to download GeoIP database at startup: {e}");
|
||||||
|
} else {
|
||||||
|
println!("GeoIP database downloaded successfully at startup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
// No Camoufox profiles or GeoIP database already available
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to check GeoIP database status at startup: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Start proxy cleanup task for dead browser processes
|
// Start proxy cleanup task for dead browser processes
|
||||||
let app_handle_proxy_cleanup = app.handle().clone();
|
let app_handle_proxy_cleanup = app.handle().clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
@@ -419,11 +464,7 @@ pub fn run() {
|
|||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
// Send a ping request to nodecar to trigger unpacking/warm-up
|
// Send a ping request to nodecar to trigger unpacking/warm-up
|
||||||
match tokio::process::Command::new("nodecar")
|
match tokio::process::Command::new("nodecar").output().await {
|
||||||
.arg("--version")
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
let duration = start_time.elapsed();
|
let duration = start_time.elapsed();
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
@@ -491,6 +532,7 @@ pub fn run() {
|
|||||||
detect_existing_profiles,
|
detect_existing_profiles,
|
||||||
import_browser_profile,
|
import_browser_profile,
|
||||||
check_missing_binaries,
|
check_missing_binaries,
|
||||||
|
check_missing_geoip_database,
|
||||||
ensure_all_binaries_exist,
|
ensure_all_binaries_exist,
|
||||||
create_stored_proxy,
|
create_stored_proxy,
|
||||||
get_stored_proxies,
|
get_stored_proxies,
|
||||||
@@ -504,6 +546,8 @@ pub fn run() {
|
|||||||
delete_profile_group,
|
delete_profile_group,
|
||||||
assign_profiles_to_group,
|
assign_profiles_to_group,
|
||||||
delete_selected_profiles,
|
delete_selected_profiles,
|
||||||
|
is_geoip_database_available,
|
||||||
|
download_geoip_database,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -603,6 +603,11 @@ impl ProfileManager {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit profile update event so frontend UIs can refresh immediately (e.g. proxy manager)
|
||||||
|
if let Err(e) = app_handle.emit("profile-updated", &profile) {
|
||||||
|
println!("Warning: Failed to emit profile update event: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(profile)
|
Ok(profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1025,8 +1030,135 @@ mod tests {
|
|||||||
let (manager, _temp_dir) = create_test_profile_manager();
|
let (manager, _temp_dir) = create_test_profile_manager();
|
||||||
let profiles_dir = manager.get_profiles_dir();
|
let profiles_dir = manager.get_profiles_dir();
|
||||||
|
|
||||||
assert!(profiles_dir.to_string_lossy().contains("DonutBrowser"));
|
assert!(
|
||||||
assert!(profiles_dir.to_string_lossy().contains("profiles"));
|
profiles_dir.to_string_lossy().contains("DonutBrowser"),
|
||||||
|
"Profiles dir should contain DonutBrowser"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
profiles_dir.to_string_lossy().contains("profiles"),
|
||||||
|
"Profiles dir should contain profiles"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_profiles_empty() {
|
||||||
|
let (manager, _temp_dir) = create_test_profile_manager();
|
||||||
|
|
||||||
|
let result = manager.list_profiles();
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Should successfully list profiles even when empty"
|
||||||
|
);
|
||||||
|
|
||||||
|
let profiles = result.unwrap();
|
||||||
|
assert!(
|
||||||
|
profiles.is_empty(),
|
||||||
|
"Should return empty vector when no profiles exist"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_common_firefox_preferences() {
|
||||||
|
let (manager, _temp_dir) = create_test_profile_manager();
|
||||||
|
|
||||||
|
let prefs = manager.get_common_firefox_preferences();
|
||||||
|
assert!(!prefs.is_empty(), "Should return non-empty preferences");
|
||||||
|
|
||||||
|
// Check for some expected preferences
|
||||||
|
let prefs_string = prefs.join("\n");
|
||||||
|
assert!(
|
||||||
|
prefs_string.contains("browser.shell.checkDefaultBrowser"),
|
||||||
|
"Should contain default browser check preference"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
prefs_string.contains("app.update.enabled"),
|
||||||
|
"Should contain update preference"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_binaries_dir() {
|
||||||
|
let (manager, _temp_dir) = create_test_profile_manager();
|
||||||
|
|
||||||
|
let binaries_dir = manager.get_binaries_dir();
|
||||||
|
let path_str = binaries_dir.to_string_lossy();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
path_str.contains("DonutBrowser"),
|
||||||
|
"Binaries dir should contain DonutBrowser"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
path_str.contains("binaries"),
|
||||||
|
"Binaries dir should contain binaries"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_disable_proxy_settings_in_profile() {
|
||||||
|
let (manager, temp_dir) = create_test_profile_manager();
|
||||||
|
|
||||||
|
// Create a test profile directory
|
||||||
|
let profile_dir = temp_dir.path().join("test_profile");
|
||||||
|
fs::create_dir_all(&profile_dir).expect("Should create profile directory");
|
||||||
|
|
||||||
|
let result = manager.disable_proxy_settings_in_profile(&profile_dir);
|
||||||
|
assert!(result.is_ok(), "Should successfully disable proxy settings");
|
||||||
|
|
||||||
|
// Check that user.js was created
|
||||||
|
let user_js_path = profile_dir.join("user.js");
|
||||||
|
assert!(user_js_path.exists(), "user.js should be created");
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&user_js_path).expect("Should read user.js");
|
||||||
|
assert!(
|
||||||
|
content.contains("network.proxy.type"),
|
||||||
|
"Should contain proxy type setting"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
content.contains("0"),
|
||||||
|
"Should set proxy type to 0 (no proxy)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_apply_proxy_settings_to_profile() {
|
||||||
|
let (manager, temp_dir) = create_test_profile_manager();
|
||||||
|
|
||||||
|
// Create a test profile directory structure
|
||||||
|
let uuid_dir = temp_dir.path().join("test_uuid");
|
||||||
|
let profile_dir = uuid_dir.join("profile");
|
||||||
|
fs::create_dir_all(&profile_dir).expect("Should create profile directory");
|
||||||
|
|
||||||
|
let proxy_settings = ProxySettings {
|
||||||
|
proxy_type: "http".to_string(),
|
||||||
|
host: "proxy.example.com".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
username: Some("user".to_string()),
|
||||||
|
password: Some("pass".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = manager.apply_proxy_settings_to_profile(&profile_dir, &proxy_settings, None);
|
||||||
|
assert!(result.is_ok(), "Should successfully apply proxy settings");
|
||||||
|
|
||||||
|
// Check that user.js was created
|
||||||
|
let user_js_path = profile_dir.join("user.js");
|
||||||
|
assert!(user_js_path.exists(), "user.js should be created");
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&user_js_path).expect("Should read user.js");
|
||||||
|
assert!(
|
||||||
|
content.contains("network.proxy.type"),
|
||||||
|
"Should contain proxy type setting"
|
||||||
|
);
|
||||||
|
assert!(content.contains("2"), "Should set proxy type to 2 (PAC)");
|
||||||
|
|
||||||
|
// Check that PAC file was created
|
||||||
|
let pac_path = uuid_dir.join("proxy.pac");
|
||||||
|
assert!(pac_path.exists(), "proxy.pac should be created");
|
||||||
|
|
||||||
|
let pac_content = fs::read_to_string(&pac_path).expect("Should read proxy.pac");
|
||||||
|
assert!(
|
||||||
|
pac_content.contains("FindProxyForURL"),
|
||||||
|
"PAC file should contain FindProxyForURL function"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -778,3 +778,206 @@ pub async fn import_browser_profile(
|
|||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref PROFILE_IMPORTER: ProfileImporter = ProfileImporter::new();
|
static ref PROFILE_IMPORTER: ProfileImporter = ProfileImporter::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::env;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn create_test_profile_importer() -> (ProfileImporter, TempDir) {
|
||||||
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||||
|
|
||||||
|
// Set up a temporary home directory for testing
|
||||||
|
env::set_var("HOME", temp_dir.path());
|
||||||
|
|
||||||
|
let importer = ProfileImporter::new();
|
||||||
|
(importer, temp_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_profile_importer_creation() {
|
||||||
|
let (_importer, _temp_dir) = create_test_profile_importer();
|
||||||
|
// Test passes if no panic occurs
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_browser_display_name() {
|
||||||
|
let (importer, _temp_dir) = create_test_profile_importer();
|
||||||
|
|
||||||
|
assert_eq!(importer.get_browser_display_name("firefox"), "Firefox");
|
||||||
|
assert_eq!(
|
||||||
|
importer.get_browser_display_name("firefox-developer"),
|
||||||
|
"Firefox Developer"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
importer.get_browser_display_name("chromium"),
|
||||||
|
"Chrome/Chromium"
|
||||||
|
);
|
||||||
|
assert_eq!(importer.get_browser_display_name("brave"), "Brave");
|
||||||
|
assert_eq!(
|
||||||
|
importer.get_browser_display_name("mullvad-browser"),
|
||||||
|
"Mullvad Browser"
|
||||||
|
);
|
||||||
|
assert_eq!(importer.get_browser_display_name("zen"), "Zen Browser");
|
||||||
|
assert_eq!(
|
||||||
|
importer.get_browser_display_name("tor-browser"),
|
||||||
|
"Tor Browser"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
importer.get_browser_display_name("unknown"),
|
||||||
|
"Unknown Browser"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_existing_profiles_no_panic() {
|
||||||
|
let (importer, _temp_dir) = create_test_profile_importer();
|
||||||
|
|
||||||
|
// This should not panic even if no browser profiles exist
|
||||||
|
let result = importer.detect_existing_profiles();
|
||||||
|
assert!(result.is_ok(), "detect_existing_profiles should not fail");
|
||||||
|
|
||||||
|
let _profiles = result.unwrap();
|
||||||
|
// We can't assert specific profiles since they depend on the system
|
||||||
|
// but we can verify the result is a valid Vec
|
||||||
|
// We can't assert specific profiles since they depend on the system
|
||||||
|
// but we can verify the result is a valid Vec (length check is always true for Vec, but shows intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_scan_firefox_profiles_dir_nonexistent() {
|
||||||
|
let (importer, temp_dir) = create_test_profile_importer();
|
||||||
|
|
||||||
|
let nonexistent_dir = temp_dir.path().join("nonexistent");
|
||||||
|
let result = importer.scan_firefox_profiles_dir(&nonexistent_dir, "firefox");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Should handle nonexistent directory gracefully"
|
||||||
|
);
|
||||||
|
let profiles = result.unwrap();
|
||||||
|
assert!(
|
||||||
|
profiles.is_empty(),
|
||||||
|
"Should return empty vector for nonexistent directory"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_scan_chrome_profiles_dir_nonexistent() {
|
||||||
|
let (importer, temp_dir) = create_test_profile_importer();
|
||||||
|
|
||||||
|
let nonexistent_dir = temp_dir.path().join("nonexistent");
|
||||||
|
let result = importer.scan_chrome_profiles_dir(&nonexistent_dir, "chromium");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Should handle nonexistent directory gracefully"
|
||||||
|
);
|
||||||
|
let profiles = result.unwrap();
|
||||||
|
assert!(
|
||||||
|
profiles.is_empty(),
|
||||||
|
"Should return empty vector for nonexistent directory"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_firefox_profiles_ini_empty() {
|
||||||
|
let (importer, _temp_dir) = create_test_profile_importer();
|
||||||
|
|
||||||
|
let empty_content = "";
|
||||||
|
let profiles_dir = Path::new("/tmp");
|
||||||
|
let result = importer.parse_firefox_profiles_ini(empty_content, profiles_dir, "firefox");
|
||||||
|
|
||||||
|
assert!(result.is_ok(), "Should handle empty profiles.ini");
|
||||||
|
let profiles = result.unwrap();
|
||||||
|
assert!(
|
||||||
|
profiles.is_empty(),
|
||||||
|
"Should return empty vector for empty content"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_firefox_profiles_ini_valid() {
|
||||||
|
let (importer, temp_dir) = create_test_profile_importer();
|
||||||
|
|
||||||
|
// Create a mock profile directory
|
||||||
|
let profiles_dir = temp_dir.path().join("profiles");
|
||||||
|
let profile_dir = profiles_dir.join("test.profile");
|
||||||
|
fs::create_dir_all(&profile_dir).expect("Should create profile directory");
|
||||||
|
|
||||||
|
// Create a prefs.js file to make it look like a valid profile
|
||||||
|
let prefs_file = profile_dir.join("prefs.js");
|
||||||
|
fs::write(&prefs_file, "// Firefox preferences").expect("Should create prefs.js");
|
||||||
|
|
||||||
|
let profiles_ini_content = r#"
|
||||||
|
[Profile0]
|
||||||
|
Name=Test Profile
|
||||||
|
IsRelative=1
|
||||||
|
Path=test.profile
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let result =
|
||||||
|
importer.parse_firefox_profiles_ini(profiles_ini_content, &profiles_dir, "firefox");
|
||||||
|
|
||||||
|
assert!(result.is_ok(), "Should parse valid profiles.ini");
|
||||||
|
let profiles = result.unwrap();
|
||||||
|
assert_eq!(profiles.len(), 1, "Should find one profile");
|
||||||
|
assert_eq!(profiles[0].name, "Firefox - Test Profile");
|
||||||
|
assert_eq!(profiles[0].browser, "firefox");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_directory_recursive() {
|
||||||
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||||
|
|
||||||
|
// Create source directory structure
|
||||||
|
let source_dir = temp_dir.path().join("source");
|
||||||
|
let source_subdir = source_dir.join("subdir");
|
||||||
|
fs::create_dir_all(&source_subdir).expect("Should create source directories");
|
||||||
|
|
||||||
|
// Create some test files
|
||||||
|
let source_file1 = source_dir.join("file1.txt");
|
||||||
|
let source_file2 = source_subdir.join("file2.txt");
|
||||||
|
fs::write(&source_file1, "content1").expect("Should create file1");
|
||||||
|
fs::write(&source_file2, "content2").expect("Should create file2");
|
||||||
|
|
||||||
|
// Create destination directory
|
||||||
|
let dest_dir = temp_dir.path().join("dest");
|
||||||
|
|
||||||
|
// Copy recursively
|
||||||
|
let result = ProfileImporter::copy_directory_recursive(&source_dir, &dest_dir);
|
||||||
|
assert!(result.is_ok(), "Should copy directory successfully");
|
||||||
|
|
||||||
|
// Verify files were copied
|
||||||
|
let dest_file1 = dest_dir.join("file1.txt");
|
||||||
|
let dest_file2 = dest_dir.join("subdir").join("file2.txt");
|
||||||
|
|
||||||
|
assert!(dest_file1.exists(), "file1.txt should be copied");
|
||||||
|
assert!(dest_file2.exists(), "file2.txt should be copied");
|
||||||
|
|
||||||
|
let content1 = fs::read_to_string(&dest_file1).expect("Should read file1");
|
||||||
|
let content2 = fs::read_to_string(&dest_file2).expect("Should read file2");
|
||||||
|
|
||||||
|
assert_eq!(content1, "content1", "file1 content should match");
|
||||||
|
assert_eq!(content2, "content2", "file2 content should match");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_default_version_for_browser_no_versions() {
|
||||||
|
let (importer, _temp_dir) = create_test_profile_importer();
|
||||||
|
|
||||||
|
// This should fail since no versions are downloaded in test environment
|
||||||
|
let result = importer.get_default_version_for_browser("firefox");
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"Should fail when no versions are available"
|
||||||
|
);
|
||||||
|
|
||||||
|
let error_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(
|
||||||
|
error_msg.contains("No downloaded versions found"),
|
||||||
|
"Error should mention no versions found"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -573,8 +573,23 @@ mod tests {
|
|||||||
password: Some("pass".to_string()),
|
password: Some("pass".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(!valid_settings.host.is_empty());
|
assert!(
|
||||||
assert!(valid_settings.port > 0);
|
!valid_settings.host.is_empty(),
|
||||||
|
"Valid settings should have non-empty host"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
valid_settings.port > 0,
|
||||||
|
"Valid settings should have positive port"
|
||||||
|
);
|
||||||
|
assert_eq!(valid_settings.proxy_type, "http", "Proxy type should match");
|
||||||
|
assert!(
|
||||||
|
valid_settings.username.is_some(),
|
||||||
|
"Username should be present"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
valid_settings.password.is_some(),
|
||||||
|
"Password should be present"
|
||||||
|
);
|
||||||
|
|
||||||
// Test proxy settings with empty values
|
// Test proxy settings with empty values
|
||||||
let empty_settings = ProxySettings {
|
let empty_settings = ProxySettings {
|
||||||
@@ -585,7 +600,16 @@ mod tests {
|
|||||||
password: None,
|
password: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(empty_settings.host.is_empty());
|
assert!(
|
||||||
|
empty_settings.host.is_empty(),
|
||||||
|
"Empty settings should have empty host"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
empty_settings.port, 0,
|
||||||
|
"Empty settings should have zero port"
|
||||||
|
);
|
||||||
|
assert!(empty_settings.username.is_none(), "Username should be None");
|
||||||
|
assert!(empty_settings.password.is_none(), "Password should be None");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -743,7 +767,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Test command arguments match expected format
|
// Test command arguments match expected format
|
||||||
let _expected_args = [
|
let expected_args = [
|
||||||
"proxy",
|
"proxy",
|
||||||
"start",
|
"start",
|
||||||
"--host",
|
"--host",
|
||||||
@@ -759,11 +783,37 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// This test verifies the argument structure without actually running the command
|
// This test verifies the argument structure without actually running the command
|
||||||
assert_eq!(proxy_settings.host, "proxy.example.com");
|
assert_eq!(
|
||||||
assert_eq!(proxy_settings.port, 8080);
|
proxy_settings.host, "proxy.example.com",
|
||||||
assert_eq!(proxy_settings.proxy_type, "http");
|
"Host should match expected value"
|
||||||
assert_eq!(proxy_settings.username.as_ref().unwrap(), "user");
|
);
|
||||||
assert_eq!(proxy_settings.password.as_ref().unwrap(), "pass");
|
assert_eq!(
|
||||||
|
proxy_settings.port, 8080,
|
||||||
|
"Port should match expected value"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
proxy_settings.proxy_type, "http",
|
||||||
|
"Proxy type should match expected value"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
proxy_settings.username.as_ref().unwrap(),
|
||||||
|
"user",
|
||||||
|
"Username should match expected value"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
proxy_settings.password.as_ref().unwrap(),
|
||||||
|
"pass",
|
||||||
|
"Password should match expected value"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify expected args structure
|
||||||
|
assert_eq!(expected_args[0], "proxy", "First arg should be 'proxy'");
|
||||||
|
assert_eq!(expected_args[1], "start", "Second arg should be 'start'");
|
||||||
|
assert_eq!(expected_args[2], "--host", "Third arg should be '--host'");
|
||||||
|
assert_eq!(
|
||||||
|
expected_args[3], "proxy.example.com",
|
||||||
|
"Fourth arg should be host value"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test the CLI detachment specifically - ensure the CLI exits properly
|
// Test the CLI detachment specifically - ensure the CLI exits properly
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ pub async fn clear_all_version_cache_and_refetch(
|
|||||||
// Disable all browsers during the update process
|
// Disable all browsers during the update process
|
||||||
let auto_updater = crate::auto_updater::AutoUpdater::instance();
|
let auto_updater = crate::auto_updater::AutoUpdater::instance();
|
||||||
let supported_browsers =
|
let supported_browsers =
|
||||||
crate::browser_version_service::BrowserVersionService::instance().get_supported_browsers();
|
crate::browser_version_manager::BrowserVersionManager::instance().get_supported_browsers();
|
||||||
|
|
||||||
// Load current state and disable all browsers
|
// Load current state and disable all browsers
|
||||||
let mut state = auto_updater
|
let mut state = auto_updater
|
||||||
@@ -245,3 +245,213 @@ pub async fn clear_all_version_cache_and_refetch(
|
|||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref SETTINGS_MANAGER: SettingsManager = SettingsManager::new();
|
static ref SETTINGS_MANAGER: SettingsManager = SettingsManager::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::env;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn create_test_settings_manager() -> (SettingsManager, TempDir) {
|
||||||
|
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||||
|
|
||||||
|
// Set up a temporary home directory for testing
|
||||||
|
env::set_var("HOME", temp_dir.path());
|
||||||
|
|
||||||
|
let manager = SettingsManager::new();
|
||||||
|
(manager, temp_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_manager_creation() {
|
||||||
|
let (_manager, _temp_dir) = create_test_settings_manager();
|
||||||
|
// Test passes if no panic occurs
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_app_settings() {
|
||||||
|
let default_settings = AppSettings::default();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!default_settings.set_as_default_browser,
|
||||||
|
"Default should not set as default browser"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
default_settings.theme, "system",
|
||||||
|
"Default theme should be system"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_table_sorting_settings() {
|
||||||
|
let default_sorting = TableSortingSettings::default();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
default_sorting.column, "name",
|
||||||
|
"Default sort column should be name"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
default_sorting.direction, "asc",
|
||||||
|
"Default sort direction should be asc"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_settings_nonexistent_file() {
|
||||||
|
let (manager, _temp_dir) = create_test_settings_manager();
|
||||||
|
|
||||||
|
let result = manager.load_settings();
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Should handle nonexistent settings file gracefully"
|
||||||
|
);
|
||||||
|
|
||||||
|
let settings = result.unwrap();
|
||||||
|
assert!(
|
||||||
|
!settings.set_as_default_browser,
|
||||||
|
"Should return default settings"
|
||||||
|
);
|
||||||
|
assert_eq!(settings.theme, "system", "Should return default theme");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_save_and_load_settings() {
|
||||||
|
let (manager, _temp_dir) = create_test_settings_manager();
|
||||||
|
|
||||||
|
let test_settings = AppSettings {
|
||||||
|
set_as_default_browser: true,
|
||||||
|
theme: "dark".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
let save_result = manager.save_settings(&test_settings);
|
||||||
|
assert!(save_result.is_ok(), "Should save settings successfully");
|
||||||
|
|
||||||
|
// Load settings back
|
||||||
|
let load_result = manager.load_settings();
|
||||||
|
assert!(load_result.is_ok(), "Should load settings successfully");
|
||||||
|
|
||||||
|
let loaded_settings = load_result.unwrap();
|
||||||
|
assert!(
|
||||||
|
loaded_settings.set_as_default_browser,
|
||||||
|
"Loaded settings should match saved"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
loaded_settings.theme, "dark",
|
||||||
|
"Loaded theme should match saved"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_table_sorting_nonexistent_file() {
|
||||||
|
let (manager, _temp_dir) = create_test_settings_manager();
|
||||||
|
|
||||||
|
let result = manager.load_table_sorting();
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Should handle nonexistent sorting file gracefully"
|
||||||
|
);
|
||||||
|
|
||||||
|
let sorting = result.unwrap();
|
||||||
|
assert_eq!(sorting.column, "name", "Should return default sorting");
|
||||||
|
assert_eq!(sorting.direction, "asc", "Should return default direction");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_save_and_load_table_sorting() {
|
||||||
|
let (manager, _temp_dir) = create_test_settings_manager();
|
||||||
|
|
||||||
|
let test_sorting = TableSortingSettings {
|
||||||
|
column: "browser".to_string(),
|
||||||
|
direction: "desc".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save sorting
|
||||||
|
let save_result = manager.save_table_sorting(&test_sorting);
|
||||||
|
assert!(save_result.is_ok(), "Should save sorting successfully");
|
||||||
|
|
||||||
|
// Load sorting back
|
||||||
|
let load_result = manager.load_table_sorting();
|
||||||
|
assert!(load_result.is_ok(), "Should load sorting successfully");
|
||||||
|
|
||||||
|
let loaded_sorting = load_result.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
loaded_sorting.column, "browser",
|
||||||
|
"Loaded column should match saved"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
loaded_sorting.direction, "desc",
|
||||||
|
"Loaded direction should match saved"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_show_settings_on_startup() {
|
||||||
|
let (manager, _temp_dir) = create_test_settings_manager();
|
||||||
|
|
||||||
|
let result = manager.should_show_settings_on_startup();
|
||||||
|
assert!(result.is_ok(), "Should not fail");
|
||||||
|
|
||||||
|
let should_show = result.unwrap();
|
||||||
|
assert!(
|
||||||
|
!should_show,
|
||||||
|
"Should always return false as per implementation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_corrupted_settings_file() {
|
||||||
|
let (manager, _temp_dir) = create_test_settings_manager();
|
||||||
|
|
||||||
|
// Create settings directory
|
||||||
|
let settings_dir = manager.get_settings_dir();
|
||||||
|
fs::create_dir_all(&settings_dir).expect("Should create settings directory");
|
||||||
|
|
||||||
|
// Write corrupted JSON
|
||||||
|
let settings_file = manager.get_settings_file();
|
||||||
|
fs::write(&settings_file, "{ invalid json }").expect("Should write corrupted file");
|
||||||
|
|
||||||
|
// Should handle corrupted file gracefully
|
||||||
|
let result = manager.load_settings();
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Should handle corrupted settings file gracefully"
|
||||||
|
);
|
||||||
|
|
||||||
|
let settings = result.unwrap();
|
||||||
|
assert!(
|
||||||
|
!settings.set_as_default_browser,
|
||||||
|
"Should return default settings for corrupted file"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
settings.theme, "system",
|
||||||
|
"Should return default theme for corrupted file"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_file_paths() {
|
||||||
|
let (manager, _temp_dir) = create_test_settings_manager();
|
||||||
|
|
||||||
|
let settings_dir = manager.get_settings_dir();
|
||||||
|
let settings_file = manager.get_settings_file();
|
||||||
|
let sorting_file = manager.get_table_sorting_file();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
settings_dir.to_string_lossy().contains("settings"),
|
||||||
|
"Settings dir should contain 'settings'"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
settings_file
|
||||||
|
.to_string_lossy()
|
||||||
|
.ends_with("app_settings.json"),
|
||||||
|
"Settings file should end with app_settings.json"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
sorting_file
|
||||||
|
.to_string_lossy()
|
||||||
|
.ends_with("table_sorting.json"),
|
||||||
|
"Sorting file should end with table_sorting.json"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ mod linux {
|
|||||||
Err("Could not detect theme from system files".into())
|
Err("Could not detect theme from system files".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_command_available(command: &str) -> bool {
|
pub fn is_command_available(command: &str) -> bool {
|
||||||
Command::new("which")
|
Command::new("which")
|
||||||
.arg(command)
|
.arg(command)
|
||||||
.output()
|
.output()
|
||||||
@@ -529,16 +529,113 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_theme_detector_creation() {
|
fn test_theme_detector_creation() {
|
||||||
let detector = ThemeDetector::instance();
|
let detector = ThemeDetector::instance();
|
||||||
|
|
||||||
|
// Should not panic when creating detector
|
||||||
|
assert!(
|
||||||
|
std::ptr::eq(detector, ThemeDetector::instance()),
|
||||||
|
"Should return same instance (singleton)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_system_theme_returns_valid_value() {
|
||||||
|
let detector = ThemeDetector::instance();
|
||||||
let theme = detector.detect_system_theme();
|
let theme = detector.detect_system_theme();
|
||||||
|
|
||||||
// Should return a valid theme string
|
// Should return a valid theme string
|
||||||
assert!(matches!(theme.theme.as_str(), "light" | "dark" | "unknown"));
|
assert!(
|
||||||
|
matches!(theme.theme.as_str(), "light" | "dark" | "unknown"),
|
||||||
|
"Theme should be one of: light, dark, unknown. Got: {}",
|
||||||
|
theme.theme
|
||||||
|
);
|
||||||
|
|
||||||
|
// Theme string should not be empty
|
||||||
|
assert!(!theme.theme.is_empty(), "Theme string should not be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_system_theme_command() {
|
fn test_get_system_theme_command() {
|
||||||
let theme = get_system_theme();
|
let theme = get_system_theme();
|
||||||
assert!(matches!(theme.theme.as_str(), "light" | "dark" | "unknown"));
|
|
||||||
|
assert!(
|
||||||
|
matches!(theme.theme.as_str(), "light" | "dark" | "unknown"),
|
||||||
|
"Command should return valid theme. Got: {}",
|
||||||
|
theme.theme
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should be consistent with direct detector call
|
||||||
|
let detector = ThemeDetector::instance();
|
||||||
|
let direct_theme = detector.detect_system_theme();
|
||||||
|
assert_eq!(
|
||||||
|
theme.theme, direct_theme.theme,
|
||||||
|
"Command and direct call should return same theme"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_system_theme_serialization() {
|
||||||
|
let theme = SystemTheme {
|
||||||
|
theme: "dark".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test serialization
|
||||||
|
let serialized = serde_json::to_string(&theme);
|
||||||
|
assert!(
|
||||||
|
serialized.is_ok(),
|
||||||
|
"Should serialize SystemTheme successfully"
|
||||||
|
);
|
||||||
|
|
||||||
|
let json_str = serialized.unwrap();
|
||||||
|
assert!(
|
||||||
|
json_str.contains("dark"),
|
||||||
|
"Serialized JSON should contain theme value"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test deserialization
|
||||||
|
let deserialized: Result<SystemTheme, _> = serde_json::from_str(&json_str);
|
||||||
|
assert!(
|
||||||
|
deserialized.is_ok(),
|
||||||
|
"Should deserialize SystemTheme successfully"
|
||||||
|
);
|
||||||
|
|
||||||
|
let theme_back = deserialized.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
theme_back.theme, "dark",
|
||||||
|
"Deserialized theme should match original"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn test_linux_command_availability_check() {
|
||||||
|
use super::linux::is_command_available;
|
||||||
|
|
||||||
|
// Test with a command that should exist on most systems
|
||||||
|
let ls_available = is_command_available("ls");
|
||||||
|
assert!(ls_available, "ls command should be available on Linux");
|
||||||
|
|
||||||
|
// Test with a command that definitely doesn't exist
|
||||||
|
let fake_available = is_command_available("definitely_nonexistent_command_12345");
|
||||||
|
assert!(!fake_available, "Fake command should not be available");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_theme_detector_consistency() {
|
||||||
|
let detector = ThemeDetector::instance();
|
||||||
|
|
||||||
|
// Call detect_system_theme multiple times - should be consistent
|
||||||
|
let theme1 = detector.detect_system_theme();
|
||||||
|
let theme2 = detector.detect_system_theme();
|
||||||
|
let theme3 = detector.detect_system_theme();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
theme1.theme, theme2.theme,
|
||||||
|
"Multiple calls should return consistent results"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
theme2.theme, theme3.theme,
|
||||||
|
"Multiple calls should return consistent results"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use tokio::sync::Mutex;
|
|||||||
use tokio::time::interval;
|
use tokio::time::interval;
|
||||||
|
|
||||||
use crate::auto_updater::AutoUpdater;
|
use crate::auto_updater::AutoUpdater;
|
||||||
use crate::browser_version_service::BrowserVersionService;
|
use crate::browser_version_manager::BrowserVersionManager;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct VersionUpdateProgress {
|
pub struct VersionUpdateProgress {
|
||||||
@@ -47,7 +47,7 @@ impl Default for BackgroundUpdateState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct VersionUpdater {
|
pub struct VersionUpdater {
|
||||||
version_service: &'static BrowserVersionService,
|
version_service: &'static BrowserVersionManager,
|
||||||
auto_updater: &'static AutoUpdater,
|
auto_updater: &'static AutoUpdater,
|
||||||
app_handle: Option<tauri::AppHandle>,
|
app_handle: Option<tauri::AppHandle>,
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ pub struct VersionUpdater {
|
|||||||
impl VersionUpdater {
|
impl VersionUpdater {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
version_service: BrowserVersionService::instance(),
|
version_service: BrowserVersionManager::instance(),
|
||||||
auto_updater: AutoUpdater::instance(),
|
auto_updater: AutoUpdater::instance(),
|
||||||
app_handle: None,
|
app_handle: None,
|
||||||
}
|
}
|
||||||
@@ -519,31 +519,152 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_run_background_update_logic() {
|
fn test_should_run_background_update_logic() {
|
||||||
// Note: This test uses the shared state file, so results may vary
|
// Create isolated test states to avoid interference
|
||||||
// depending on previous test runs. This is expected behavior.
|
let current_time = VersionUpdater::get_current_timestamp();
|
||||||
|
|
||||||
// Test with recent update (should not update)
|
// Test with recent update (should not update)
|
||||||
let recent_state = BackgroundUpdateState {
|
let recent_state = BackgroundUpdateState {
|
||||||
last_update_time: VersionUpdater::get_current_timestamp() - 60, // 1 minute ago
|
last_update_time: current_time - 60, // 1 minute ago
|
||||||
update_interval_hours: 3,
|
update_interval_hours: 3,
|
||||||
};
|
};
|
||||||
VersionUpdater::save_background_update_state(&recent_state).unwrap();
|
|
||||||
assert!(!VersionUpdater::should_run_background_update());
|
// Save and test recent state
|
||||||
|
let save_result = VersionUpdater::save_background_update_state(&recent_state);
|
||||||
|
assert!(save_result.is_ok(), "Should save recent state successfully");
|
||||||
|
|
||||||
|
let should_update_recent = VersionUpdater::should_run_background_update();
|
||||||
|
assert!(
|
||||||
|
!should_update_recent,
|
||||||
|
"Should not update when last update was recent"
|
||||||
|
);
|
||||||
|
|
||||||
// Test with old update (should update)
|
// Test with old update (should update)
|
||||||
let old_state = BackgroundUpdateState {
|
let old_state = BackgroundUpdateState {
|
||||||
last_update_time: VersionUpdater::get_current_timestamp() - (4 * 60 * 60), // 4 hours ago
|
last_update_time: current_time - (4 * 60 * 60), // 4 hours ago
|
||||||
update_interval_hours: 3,
|
update_interval_hours: 3,
|
||||||
};
|
};
|
||||||
VersionUpdater::save_background_update_state(&old_state).unwrap();
|
|
||||||
assert!(VersionUpdater::should_run_background_update());
|
// Save and test old state
|
||||||
|
let save_result = VersionUpdater::save_background_update_state(&old_state);
|
||||||
|
assert!(save_result.is_ok(), "Should save old state successfully");
|
||||||
|
|
||||||
|
let should_update_old = VersionUpdater::should_run_background_update();
|
||||||
|
assert!(should_update_old, "Should update when last update was old");
|
||||||
|
|
||||||
|
// Test with never updated (should update)
|
||||||
|
let never_updated_state = BackgroundUpdateState {
|
||||||
|
last_update_time: 0,
|
||||||
|
update_interval_hours: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
let save_result = VersionUpdater::save_background_update_state(&never_updated_state);
|
||||||
|
assert!(
|
||||||
|
save_result.is_ok(),
|
||||||
|
"Should save never updated state successfully"
|
||||||
|
);
|
||||||
|
|
||||||
|
let should_update_never = VersionUpdater::should_run_background_update();
|
||||||
|
assert!(
|
||||||
|
should_update_never,
|
||||||
|
"Should update when never updated before"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cache_dir_creation() {
|
fn test_cache_dir_creation() {
|
||||||
// This should not panic and should create the directory if it doesn't exist
|
// This should not panic and should create the directory if it doesn't exist
|
||||||
let cache_dir = VersionUpdater::get_cache_dir().unwrap();
|
let cache_dir_result = VersionUpdater::get_cache_dir();
|
||||||
assert!(cache_dir.exists());
|
assert!(
|
||||||
assert!(cache_dir.is_dir());
|
cache_dir_result.is_ok(),
|
||||||
|
"Should successfully get cache directory"
|
||||||
|
);
|
||||||
|
|
||||||
|
let cache_dir = cache_dir_result.unwrap();
|
||||||
|
assert!(
|
||||||
|
cache_dir.exists(),
|
||||||
|
"Cache directory should exist after creation"
|
||||||
|
);
|
||||||
|
assert!(cache_dir.is_dir(), "Cache directory should be a directory");
|
||||||
|
|
||||||
|
// Verify the path contains expected components
|
||||||
|
let path_str = cache_dir.to_string_lossy();
|
||||||
|
assert!(
|
||||||
|
path_str.contains("version_cache"),
|
||||||
|
"Path should contain version_cache"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test that calling it again returns the same directory
|
||||||
|
let cache_dir2 = VersionUpdater::get_cache_dir().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cache_dir, cache_dir2,
|
||||||
|
"Multiple calls should return same directory"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_version_updater_creation() {
|
||||||
|
let updater = VersionUpdater::new();
|
||||||
|
|
||||||
|
// Should have valid references to services
|
||||||
|
assert!(
|
||||||
|
!std::ptr::eq(updater.version_service as *const _, std::ptr::null()),
|
||||||
|
"Version service should not be null"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!std::ptr::eq(updater.auto_updater as *const _, std::ptr::null()),
|
||||||
|
"Auto updater should not be null"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
updater.app_handle.is_none(),
|
||||||
|
"App handle should initially be None"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_current_timestamp() {
|
||||||
|
let timestamp1 = VersionUpdater::get_current_timestamp();
|
||||||
|
|
||||||
|
// Should be a reasonable timestamp (after year 2020)
|
||||||
|
assert!(
|
||||||
|
timestamp1 > 1577836800,
|
||||||
|
"Timestamp should be after 2020-01-01"
|
||||||
|
); // 2020-01-01 00:00:00 UTC
|
||||||
|
|
||||||
|
// Should be before year 2100
|
||||||
|
assert!(
|
||||||
|
timestamp1 < 4102444800,
|
||||||
|
"Timestamp should be before 2100-01-01"
|
||||||
|
); // 2100-01-01 00:00:00 UTC
|
||||||
|
|
||||||
|
// Wait a tiny bit and check it increases
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||||
|
let timestamp2 = VersionUpdater::get_current_timestamp();
|
||||||
|
assert!(timestamp2 >= timestamp1, "Timestamp should not decrease");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_background_update_state_default() {
|
||||||
|
let state = BackgroundUpdateState::default();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
state.last_update_time, 0,
|
||||||
|
"Default last update time should be 0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
state.update_interval_hours, 3,
|
||||||
|
"Default update interval should be 3 hours"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_version_updater_singleton() {
|
||||||
|
let updater1 = get_version_updater();
|
||||||
|
let updater2 = get_version_updater();
|
||||||
|
|
||||||
|
// Should return the same Arc instance
|
||||||
|
assert!(
|
||||||
|
Arc::ptr_eq(&updater1, &updater2),
|
||||||
|
"Should return same singleton instance"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
Hello, World!
|
||||||
Binary file not shown.
+92
-51
@@ -19,12 +19,11 @@ import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
|
|||||||
import { ProxyManagementDialog } from "@/components/proxy-management-dialog";
|
import { ProxyManagementDialog } from "@/components/proxy-management-dialog";
|
||||||
import { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
|
import { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
|
||||||
import { SettingsDialog } from "@/components/settings-dialog";
|
import { SettingsDialog } from "@/components/settings-dialog";
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|
||||||
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
||||||
import type { PermissionType } from "@/hooks/use-permissions";
|
import type { PermissionType } from "@/hooks/use-permissions";
|
||||||
import { usePermissions } from "@/hooks/use-permissions";
|
import { usePermissions } from "@/hooks/use-permissions";
|
||||||
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
|
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
|
||||||
import { showErrorToast } from "@/lib/toast-utils";
|
import { showErrorToast, showToast } from "@/lib/toast-utils";
|
||||||
import type { BrowserProfile, CamoufoxConfig, GroupWithCount } from "@/types";
|
import type { BrowserProfile, CamoufoxConfig, GroupWithCount } from "@/types";
|
||||||
|
|
||||||
type BrowserTypeString =
|
type BrowserTypeString =
|
||||||
@@ -94,8 +93,18 @@ export default function Home() {
|
|||||||
"check_missing_binaries",
|
"check_missing_binaries",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (missingBinaries.length > 0) {
|
// Also check for missing GeoIP database
|
||||||
console.log("Found missing binaries:", missingBinaries);
|
const missingGeoIP = await invoke<boolean>(
|
||||||
|
"check_missing_geoip_database",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missingBinaries.length > 0 || missingGeoIP) {
|
||||||
|
if (missingBinaries.length > 0) {
|
||||||
|
console.log("Found missing binaries:", missingBinaries);
|
||||||
|
}
|
||||||
|
if (missingGeoIP) {
|
||||||
|
console.log("Found missing GeoIP database for Camoufox");
|
||||||
|
}
|
||||||
|
|
||||||
// Group missing binaries by browser type to avoid concurrent downloads
|
// Group missing binaries by browser type to avoid concurrent downloads
|
||||||
const browserMap = new Map<string, string[]>();
|
const browserMap = new Map<string, string[]>();
|
||||||
@@ -110,34 +119,45 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show a toast notification about missing binaries and auto-download them
|
// Show a toast notification about missing binaries and auto-download them
|
||||||
const missingList = Array.from(browserMap.entries())
|
let missingList = Array.from(browserMap.entries())
|
||||||
.map(([browser, versions]) => `${browser}: ${versions.join(", ")}`)
|
.map(([browser, versions]) => `${browser}: ${versions.join(", ")}`)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
console.log(`Downloading missing binaries: ${missingList}`);
|
if (missingGeoIP) {
|
||||||
|
if (missingList) {
|
||||||
|
missingList += ", GeoIP database for Camoufox";
|
||||||
|
} else {
|
||||||
|
missingList = "GeoIP database for Camoufox";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Downloading missing components: ${missingList}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Download missing binaries sequentially by browser type to prevent conflicts
|
// Download missing binaries and GeoIP database sequentially to prevent conflicts
|
||||||
const downloaded = await invoke<string[]>(
|
const downloaded = await invoke<string[]>(
|
||||||
"ensure_all_binaries_exist",
|
"ensure_all_binaries_exist",
|
||||||
);
|
);
|
||||||
if (downloaded.length > 0) {
|
if (downloaded.length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
"Successfully downloaded missing binaries:",
|
"Successfully downloaded missing components:",
|
||||||
downloaded,
|
downloaded,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (downloadError) {
|
} catch (downloadError) {
|
||||||
console.error("Failed to download missing binaries:", downloadError);
|
console.error(
|
||||||
|
"Failed to download missing components:",
|
||||||
|
downloadError,
|
||||||
|
);
|
||||||
setError(
|
setError(
|
||||||
`Failed to download missing binaries: ${JSON.stringify(
|
`Failed to download missing components: ${JSON.stringify(
|
||||||
downloadError,
|
downloadError,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error("Failed to check missing binaries:", err);
|
console.error("Failed to check missing components:", err);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -693,6 +713,29 @@ export default function Home() {
|
|||||||
loadGroups,
|
loadGroups,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Show deprecation warning for unsupported profiles
|
||||||
|
useEffect(() => {
|
||||||
|
if (profiles.length === 0) return;
|
||||||
|
|
||||||
|
const hasDeprecated = profiles.some(
|
||||||
|
(p) =>
|
||||||
|
["tor-browser", "mullvad-browser"].includes(p.browser) ||
|
||||||
|
(p.release_type === "nightly" && p.browser !== "firefox-developer"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasDeprecated) {
|
||||||
|
// Use a stable id to avoid duplicate toasts on re-renders
|
||||||
|
showToast({
|
||||||
|
id: "deprecated-profiles-warning",
|
||||||
|
type: "error",
|
||||||
|
title: "Some profiles will be deprecated soon",
|
||||||
|
description:
|
||||||
|
"Tor, Mullvad Browser and nightly profiles (except Firefox Developers Edition) will be removed in upcoming versions. Please check GitHub for migration instructions which will be available soon.",
|
||||||
|
duration: 15000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [profiles]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profiles.length === 0) return;
|
if (profiles.length === 0) return;
|
||||||
|
|
||||||
@@ -727,46 +770,44 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen gap-8 font-[family-name:var(--font-geist-sans)] bg-white dark:bg-black">
|
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen gap-8 font-[family-name:var(--font-geist-sans)] bg-white dark:bg-black">
|
||||||
<main className="flex flex-col row-start-2 gap-8 items-center w-full max-w-3xl">
|
<main className="flex flex-col row-start-2 gap-6 items-center w-full max-w-3xl">
|
||||||
<Card className="gap-2 w-full">
|
<div className="w-full">
|
||||||
<CardHeader>
|
<HomeHeader
|
||||||
<HomeHeader
|
selectedProfiles={selectedProfiles}
|
||||||
selectedProfiles={selectedProfiles}
|
onBulkDelete={handleBulkDelete}
|
||||||
onBulkDelete={handleBulkDelete}
|
onBulkGroupAssignment={handleBulkGroupAssignment}
|
||||||
onBulkGroupAssignment={handleBulkGroupAssignment}
|
onCreateProfileDialogOpen={setCreateProfileDialogOpen}
|
||||||
onCreateProfileDialogOpen={setCreateProfileDialogOpen}
|
onGroupManagementDialogOpen={setGroupManagementDialogOpen}
|
||||||
onGroupManagementDialogOpen={setGroupManagementDialogOpen}
|
onImportProfileDialogOpen={setImportProfileDialogOpen}
|
||||||
onImportProfileDialogOpen={setImportProfileDialogOpen}
|
onProxyManagementDialogOpen={setProxyManagementDialogOpen}
|
||||||
onProxyManagementDialogOpen={setProxyManagementDialogOpen}
|
onSettingsDialogOpen={setSettingsDialogOpen}
|
||||||
onSettingsDialogOpen={setSettingsDialogOpen}
|
/>
|
||||||
/>
|
</div>
|
||||||
</CardHeader>
|
<div className="space-y-4 w-full">
|
||||||
<CardContent>
|
<GroupBadges
|
||||||
<GroupBadges
|
selectedGroupId={selectedGroupId}
|
||||||
selectedGroupId={selectedGroupId}
|
onGroupSelect={handleSelectGroup}
|
||||||
onGroupSelect={handleSelectGroup}
|
groups={groups}
|
||||||
groups={groups}
|
isLoading={areGroupsLoading}
|
||||||
isLoading={areGroupsLoading}
|
/>
|
||||||
/>
|
<ProfilesDataTable
|
||||||
<ProfilesDataTable
|
data={profiles}
|
||||||
data={profiles}
|
onLaunchProfile={launchProfile}
|
||||||
onLaunchProfile={launchProfile}
|
onKillProfile={handleKillProfile}
|
||||||
onKillProfile={handleKillProfile}
|
onProxySettings={openProxyDialog}
|
||||||
onProxySettings={openProxyDialog}
|
onDeleteProfile={handleDeleteProfile}
|
||||||
onDeleteProfile={handleDeleteProfile}
|
onRenameProfile={handleRenameProfile}
|
||||||
onRenameProfile={handleRenameProfile}
|
onChangeVersion={openChangeVersionDialog}
|
||||||
onChangeVersion={openChangeVersionDialog}
|
onConfigureCamoufox={handleConfigureCamoufox}
|
||||||
onConfigureCamoufox={handleConfigureCamoufox}
|
runningProfiles={runningProfiles}
|
||||||
runningProfiles={runningProfiles}
|
isUpdating={isUpdating}
|
||||||
isUpdating={isUpdating}
|
onDeleteSelectedProfiles={handleDeleteSelectedProfiles}
|
||||||
onDeleteSelectedProfiles={handleDeleteSelectedProfiles}
|
onAssignProfilesToGroup={handleAssignProfilesToGroup}
|
||||||
onAssignProfilesToGroup={handleAssignProfilesToGroup}
|
selectedGroupId={selectedGroupId}
|
||||||
selectedGroupId={selectedGroupId}
|
selectedProfiles={selectedProfiles}
|
||||||
selectedProfiles={selectedProfiles}
|
onSelectedProfilesChange={setSelectedProfiles}
|
||||||
onSelectedProfilesChange={setSelectedProfiles}
|
/>
|
||||||
/>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<ProxySettingsDialog
|
<ProxySettingsDialog
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { LuCheckCheck, LuCog, LuRefreshCw } from "react-icons/lu";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import type { AppUpdateInfo, AppUpdateProgress } from "@/types";
|
import type { AppUpdateInfo, AppUpdateProgress } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface AppUpdateToastProps {
|
interface AppUpdateToastProps {
|
||||||
updateInfo: AppUpdateInfo;
|
updateInfo: AppUpdateInfo;
|
||||||
@@ -78,7 +79,7 @@ export function AppUpdateToast({
|
|||||||
updateProgress.stage === "completed");
|
updateProgress.stage === "completed");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start p-4 w-full max-w-md bg-white rounded-lg border border-gray-200 shadow-lg dark:bg-gray-800 dark:border-gray-700">
|
<div className="flex items-start p-4 w-full max-w-md bg-card rounded-lg border border-border shadow-lg text-card-foreground">
|
||||||
<div className="mr-3 mt-0.5">
|
<div className="mr-3 mt-0.5">
|
||||||
{getStageIcon(updateProgress?.stage, isUpdating)}
|
{getStageIcon(updateProgress?.stage, isUpdating)}
|
||||||
</div>
|
</div>
|
||||||
@@ -133,9 +134,9 @@ export function AppUpdateToast({
|
|||||||
{updateProgress.eta && ` • ${updateProgress.eta} remaining`}
|
{updateProgress.eta && ` • ${updateProgress.eta} remaining`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
<div className="w-full bg-muted rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
|
className="bg-primary h-1.5 rounded-full transition-all duration-300"
|
||||||
style={{ width: `${updateProgress.percentage}%` }}
|
style={{ width: `${updateProgress.percentage}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,12 +147,12 @@ export function AppUpdateToast({
|
|||||||
{showOtherStageProgress && (
|
{showOtherStageProgress && (
|
||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
{/* Progress indicator for non-downloading stages */}
|
{/* Progress indicator for non-downloading stages */}
|
||||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
<div className="w-full bg-muted rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full transition-all duration-500 ${
|
className={`h-1.5 rounded-full transition-all duration-500 ${
|
||||||
updateProgress.stage === "completed"
|
updateProgress.stage === "completed"
|
||||||
? "bg-green-500 w-full"
|
? "bg-green-500 w-full"
|
||||||
: "bg-blue-500 w-full animate-pulse"
|
: "bg-primary w-full animate-pulse"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,22 +161,22 @@ export function AppUpdateToast({
|
|||||||
|
|
||||||
{!isUpdating && (
|
{!isUpdating && (
|
||||||
<div className="flex gap-2 items-center mt-3">
|
<div className="flex gap-2 items-center mt-3">
|
||||||
<Button
|
<RippleButton
|
||||||
onClick={() => void handleUpdateClick()}
|
onClick={() => void handleUpdateClick()}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex gap-2 items-center text-xs"
|
className="flex gap-2 items-center text-xs"
|
||||||
>
|
>
|
||||||
<FaDownload className="w-3 h-3" />
|
<FaDownload className="w-3 h-3" />
|
||||||
Update Now
|
Update Now
|
||||||
</Button>
|
</RippleButton>
|
||||||
<Button
|
<RippleButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onDismiss}
|
onClick={onDismiss}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
>
|
>
|
||||||
Later
|
Later
|
||||||
</Button>
|
</RippleButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
|
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -12,6 +11,8 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import type { BrowserProfile, CamoufoxConfig } from "@/types";
|
import type { BrowserProfile, CamoufoxConfig } from "@/types";
|
||||||
|
import { LoadingButton } from "./loading-button";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface CamoufoxConfigDialogProps {
|
interface CamoufoxConfigDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -106,7 +107,7 @@ export function CamoufoxConfigDialog({
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<ScrollArea className="flex-1 pr-6 h-[400px]">
|
<ScrollArea className="flex-1 h-[400px]">
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<SharedCamoufoxConfigForm
|
<SharedCamoufoxConfigForm
|
||||||
config={config}
|
config={config}
|
||||||
@@ -117,12 +118,16 @@ export function CamoufoxConfigDialog({
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<DialogFooter className="flex-shrink-0 pt-4 border-t">
|
<DialogFooter className="flex-shrink-0 pt-4 border-t">
|
||||||
<Button variant="outline" onClick={handleClose}>
|
<RippleButton variant="outline" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
<Button onClick={handleSave} disabled={isSaving}>
|
<LoadingButton
|
||||||
{isSaving ? "Saving..." : "Save Configuration"}
|
isLoading={isSaving}
|
||||||
</Button>
|
onClick={handleSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</LoadingButton>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -2,12 +2,9 @@
|
|||||||
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { LuTriangleAlert } from "react-icons/lu";
|
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
import { ReleaseTypeSelector } from "@/components/release-type-selector";
|
import { ReleaseTypeSelector } from "@/components/release-type-selector";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -19,6 +16,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { useBrowserDownload } from "@/hooks/use-browser-download";
|
import { useBrowserDownload } from "@/hooks/use-browser-download";
|
||||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||||
import type { BrowserProfile, BrowserReleaseTypes } from "@/types";
|
import type { BrowserProfile, BrowserReleaseTypes } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface ChangeVersionDialogProps {
|
interface ChangeVersionDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -39,8 +37,8 @@ export function ChangeVersionDialog({
|
|||||||
const [releaseTypes, setReleaseTypes] = useState<BrowserReleaseTypes>({});
|
const [releaseTypes, setReleaseTypes] = useState<BrowserReleaseTypes>({});
|
||||||
const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false);
|
const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false);
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [showDowngradeWarning, setShowDowngradeWarning] = useState(false);
|
// Nightly switching is disabled for non-nightly profiles (except Firefox Developer),
|
||||||
const [acknowledgeDowngrade, setAcknowledgeDowngrade] = useState(false);
|
// so downgrade warnings are no longer applicable.
|
||||||
|
|
||||||
const {
|
const {
|
||||||
downloadedVersions,
|
downloadedVersions,
|
||||||
@@ -50,38 +48,36 @@ export function ChangeVersionDialog({
|
|||||||
isVersionDownloaded,
|
isVersionDownloaded,
|
||||||
} = useBrowserDownload();
|
} = useBrowserDownload();
|
||||||
|
|
||||||
const loadReleaseTypes = useCallback(async (browser: string) => {
|
const loadReleaseTypes = useCallback(
|
||||||
setIsLoadingReleaseTypes(true);
|
async (browser: string) => {
|
||||||
try {
|
setIsLoadingReleaseTypes(true);
|
||||||
const releaseTypes = await invoke<BrowserReleaseTypes>(
|
try {
|
||||||
"get_browser_release_types",
|
const releaseTypes = await invoke<BrowserReleaseTypes>(
|
||||||
{ browserStr: browser },
|
"get_browser_release_types",
|
||||||
);
|
{ browserStr: browser },
|
||||||
setReleaseTypes(releaseTypes);
|
);
|
||||||
} catch (error) {
|
// Filter nightly visibility based on rules:
|
||||||
console.error("Failed to load release types:", error);
|
// - Firefox Developer Edition: allow nightly only
|
||||||
} finally {
|
// - If profile is currently nightly: allow both stable and nightly
|
||||||
setIsLoadingReleaseTypes(false);
|
// - Otherwise: allow stable only
|
||||||
}
|
const filtered: BrowserReleaseTypes = {};
|
||||||
}, []);
|
if (profile?.browser === "firefox-developer") {
|
||||||
|
if (releaseTypes.nightly) filtered.nightly = releaseTypes.nightly;
|
||||||
useEffect(() => {
|
} else if (profile?.release_type === "nightly") {
|
||||||
if (
|
if (releaseTypes.stable) filtered.stable = releaseTypes.stable;
|
||||||
profile &&
|
if (releaseTypes.nightly) filtered.nightly = releaseTypes.nightly;
|
||||||
selectedReleaseType &&
|
} else {
|
||||||
selectedReleaseType !== profile.release_type
|
if (releaseTypes.stable) filtered.stable = releaseTypes.stable;
|
||||||
) {
|
}
|
||||||
// For simplicity, we'll show downgrade warning when switching from stable to nightly
|
setReleaseTypes(filtered);
|
||||||
// since nightly versions might be considered "downgrades" in terms of stability
|
} catch (error) {
|
||||||
const isDowngrade =
|
console.error("Failed to load release types:", error);
|
||||||
profile.release_type === "stable" && selectedReleaseType === "nightly";
|
} finally {
|
||||||
setShowDowngradeWarning(isDowngrade);
|
setIsLoadingReleaseTypes(false);
|
||||||
|
|
||||||
if (!isDowngrade) {
|
|
||||||
setAcknowledgeDowngrade(false);
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}, [selectedReleaseType, profile]);
|
[profile?.browser, profile?.release_type],
|
||||||
|
);
|
||||||
|
|
||||||
const handleDownload = useCallback(async () => {
|
const handleDownload = useCallback(async () => {
|
||||||
if (!profile || !selectedReleaseType) return;
|
if (!profile || !selectedReleaseType) return;
|
||||||
@@ -129,14 +125,12 @@ export function ChangeVersionDialog({
|
|||||||
selectedReleaseType &&
|
selectedReleaseType &&
|
||||||
selectedReleaseType !== profile.release_type &&
|
selectedReleaseType !== profile.release_type &&
|
||||||
selectedVersion &&
|
selectedVersion &&
|
||||||
isVersionDownloaded(selectedVersion) &&
|
isVersionDownloaded(selectedVersion);
|
||||||
(!showDowngradeWarning || acknowledgeDowngrade);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && profile) {
|
if (isOpen && profile) {
|
||||||
// Set current release type based on profile
|
// Set current release type based on profile
|
||||||
setSelectedReleaseType(profile.release_type as "stable" | "nightly");
|
setSelectedReleaseType(profile.release_type as "stable" | "nightly");
|
||||||
setAcknowledgeDowngrade(false);
|
|
||||||
void loadReleaseTypes(profile.browser);
|
void loadReleaseTypes(profile.browser);
|
||||||
void loadDownloadedVersions(profile.browser);
|
void loadDownloadedVersions(profile.browser);
|
||||||
}
|
}
|
||||||
@@ -206,7 +200,6 @@ export function ChangeVersionDialog({
|
|||||||
selectedReleaseType={selectedReleaseType}
|
selectedReleaseType={selectedReleaseType}
|
||||||
onReleaseTypeSelect={setSelectedReleaseType}
|
onReleaseTypeSelect={setSelectedReleaseType}
|
||||||
availableReleaseTypes={releaseTypes}
|
availableReleaseTypes={releaseTypes}
|
||||||
browser={profile.browser}
|
|
||||||
isDownloading={isBrowserDownloading(profile.browser)}
|
isDownloading={isBrowserDownloading(profile.browser)}
|
||||||
onDownload={() => {
|
onDownload={() => {
|
||||||
void handleDownload();
|
void handleDownload();
|
||||||
@@ -246,7 +239,6 @@ export function ChangeVersionDialog({
|
|||||||
selectedReleaseType={selectedReleaseType}
|
selectedReleaseType={selectedReleaseType}
|
||||||
onReleaseTypeSelect={setSelectedReleaseType}
|
onReleaseTypeSelect={setSelectedReleaseType}
|
||||||
availableReleaseTypes={releaseTypes}
|
availableReleaseTypes={releaseTypes}
|
||||||
browser={profile.browser}
|
|
||||||
isDownloading={isBrowserDownloading(profile.browser)}
|
isDownloading={isBrowserDownloading(profile.browser)}
|
||||||
onDownload={() => {
|
onDownload={() => {
|
||||||
void handleDownload();
|
void handleDownload();
|
||||||
@@ -259,38 +251,13 @@ export function ChangeVersionDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Downgrade Warning */}
|
{/* Nightly switching disabled in UI; no downgrade warning needed. */}
|
||||||
{showDowngradeWarning && (
|
|
||||||
<Alert className="border-orange-700">
|
|
||||||
<LuTriangleAlert className="w-4 h-4 text-orange-700" />
|
|
||||||
<AlertTitle className="text-orange-700">
|
|
||||||
Stability Warning
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription className="text-orange-700">
|
|
||||||
You are about to switch from stable to nightly releases. Nightly
|
|
||||||
versions may be less stable and could contain bugs or incomplete
|
|
||||||
features.
|
|
||||||
<div className="flex items-center mt-3 space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="acknowledge-downgrade"
|
|
||||||
checked={acknowledgeDowngrade}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
setAcknowledgeDowngrade(checked as boolean);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="acknowledge-downgrade" className="text-sm">
|
|
||||||
I understand the risks and want to proceed
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={onClose}>
|
<RippleButton variant="outline" onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
isLoading={isUpdating}
|
isLoading={isUpdating}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import type { ProfileGroup } from "@/types";
|
import type { ProfileGroup } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface CreateGroupDialogProps {
|
interface CreateGroupDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -98,15 +98,19 @@ export function CreateGroupDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={handleClose} disabled={isCreating}>
|
<RippleButton
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={isCreating}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
isLoading={isCreating}
|
isLoading={isCreating}
|
||||||
onClick={() => void handleCreate()}
|
onClick={() => void handleCreate()}
|
||||||
disabled={!groupName.trim()}
|
disabled={!groupName.trim()}
|
||||||
>
|
>
|
||||||
Create Group
|
Create
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { GoPlus } from "react-icons/go";
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
|
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
|
||||||
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
|
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Combobox } from "@/components/ui/combobox";
|
import { Combobox } from "@/components/ui/combobox";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -27,6 +28,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
import { useBrowserDownload } from "@/hooks/use-browser-download";
|
import { useBrowserDownload } from "@/hooks/use-browser-download";
|
||||||
import { getBrowserIcon } from "@/lib/browser-utils";
|
import { getBrowserIcon } from "@/lib/browser-utils";
|
||||||
import type { BrowserReleaseTypes, CamoufoxConfig, StoredProxy } from "@/types";
|
import type { BrowserReleaseTypes, CamoufoxConfig, StoredProxy } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
type BrowserTypeString =
|
type BrowserTypeString =
|
||||||
| "mullvad-browser"
|
| "mullvad-browser"
|
||||||
@@ -104,7 +106,7 @@ export function CreateProfileDialog({
|
|||||||
selectedGroupId,
|
selectedGroupId,
|
||||||
}: CreateProfileDialogProps) {
|
}: CreateProfileDialogProps) {
|
||||||
const [profileName, setProfileName] = useState("");
|
const [profileName, setProfileName] = useState("");
|
||||||
const [activeTab, setActiveTab] = useState("regular");
|
const [activeTab, setActiveTab] = useState("anti-detect");
|
||||||
|
|
||||||
// Regular browser states
|
// Regular browser states
|
||||||
const [selectedBrowser, setSelectedBrowser] = useState<BrowserTypeString>();
|
const [selectedBrowser, setSelectedBrowser] = useState<BrowserTypeString>();
|
||||||
@@ -132,6 +134,7 @@ export function CreateProfileDialog({
|
|||||||
useState<BrowserReleaseTypes>({});
|
useState<BrowserReleaseTypes>({});
|
||||||
const [supportedBrowsers, setSupportedBrowsers] = useState<string[]>([]);
|
const [supportedBrowsers, setSupportedBrowsers] = useState<string[]>([]);
|
||||||
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
|
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
|
||||||
|
const [showProxyForm, setShowProxyForm] = useState(false);
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const loadingBrowserRef = useRef<string | null>(null);
|
const loadingBrowserRef = useRef<string | null>(null);
|
||||||
|
|
||||||
@@ -161,6 +164,20 @@ export function CreateProfileDialog({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const checkAndDownloadGeoIPDatabase = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const isAvailable = await invoke<boolean>("is_geoip_database_available");
|
||||||
|
if (!isAvailable) {
|
||||||
|
console.log("GeoIP database not available, downloading...");
|
||||||
|
await invoke("download_geoip_database");
|
||||||
|
console.log("GeoIP database downloaded successfully");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check/download GeoIP database:", error);
|
||||||
|
// Don't show error to user as this is not critical for profile creation
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const loadReleaseTypes = useCallback(
|
const loadReleaseTypes = useCallback(
|
||||||
async (browser: string) => {
|
async (browser: string) => {
|
||||||
// Set loading state
|
// Set loading state
|
||||||
@@ -174,10 +191,19 @@ export function CreateProfileDialog({
|
|||||||
|
|
||||||
// Only update state if this browser is still the one we're loading
|
// Only update state if this browser is still the one we're loading
|
||||||
if (loadingBrowserRef.current === browser) {
|
if (loadingBrowserRef.current === browser) {
|
||||||
|
// Filter to enforce stable-only creation, except Firefox Developer (nightly-only)
|
||||||
if (browser === "camoufox") {
|
if (browser === "camoufox") {
|
||||||
setCamoufoxReleaseTypes(releaseTypes);
|
const filtered: BrowserReleaseTypes = {};
|
||||||
|
if (releaseTypes.stable) filtered.stable = releaseTypes.stable;
|
||||||
|
setCamoufoxReleaseTypes(filtered);
|
||||||
|
} else if (browser === "firefox-developer") {
|
||||||
|
const filtered: BrowserReleaseTypes = {};
|
||||||
|
if (releaseTypes.nightly) filtered.nightly = releaseTypes.nightly;
|
||||||
|
setAvailableReleaseTypes(filtered);
|
||||||
} else {
|
} else {
|
||||||
setAvailableReleaseTypes(releaseTypes);
|
const filtered: BrowserReleaseTypes = {};
|
||||||
|
if (releaseTypes.stable) filtered.stable = releaseTypes.stable;
|
||||||
|
setAvailableReleaseTypes(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load downloaded versions for this browser
|
// Load downloaded versions for this browser
|
||||||
@@ -202,8 +228,16 @@ export function CreateProfileDialog({
|
|||||||
void loadStoredProxies();
|
void loadStoredProxies();
|
||||||
// Load camoufox release types when dialog opens
|
// Load camoufox release types when dialog opens
|
||||||
void loadReleaseTypes("camoufox");
|
void loadReleaseTypes("camoufox");
|
||||||
|
// Check and download GeoIP database if needed for Camoufox
|
||||||
|
void checkAndDownloadGeoIPDatabase();
|
||||||
}
|
}
|
||||||
}, [isOpen, loadSupportedBrowsers, loadStoredProxies, loadReleaseTypes]);
|
}, [
|
||||||
|
isOpen,
|
||||||
|
loadSupportedBrowsers,
|
||||||
|
loadStoredProxies,
|
||||||
|
loadReleaseTypes,
|
||||||
|
checkAndDownloadGeoIPDatabase,
|
||||||
|
]);
|
||||||
|
|
||||||
// Load release types when browser selection changes
|
// Load release types when browser selection changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -216,26 +250,20 @@ export function CreateProfileDialog({
|
|||||||
}
|
}
|
||||||
}, [selectedBrowser, loadReleaseTypes]);
|
}, [selectedBrowser, loadReleaseTypes]);
|
||||||
|
|
||||||
// Helper function to get the best available version and release type
|
// Helper function to get the best available version respecting rules
|
||||||
const getBestAvailableVersion = useCallback(
|
const getBestAvailableVersion = useCallback(
|
||||||
(releaseTypes: BrowserReleaseTypes, browserType?: string) => {
|
(releaseTypes: BrowserReleaseTypes, browserType?: string) => {
|
||||||
// For Firefox Developer Edition, prefer nightly over stable
|
// Firefox Developer Edition: nightly-only
|
||||||
if (browserType === "firefox-developer" && releaseTypes.nightly) {
|
if (browserType === "firefox-developer" && releaseTypes.nightly) {
|
||||||
return {
|
return {
|
||||||
version: releaseTypes.nightly,
|
version: releaseTypes.nightly,
|
||||||
releaseType: "nightly" as const,
|
releaseType: "nightly" as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// All others: stable-only
|
||||||
if (releaseTypes.stable) {
|
if (releaseTypes.stable) {
|
||||||
return { version: releaseTypes.stable, releaseType: "stable" as const };
|
return { version: releaseTypes.stable, releaseType: "stable" as const };
|
||||||
}
|
}
|
||||||
if (releaseTypes.nightly) {
|
|
||||||
return {
|
|
||||||
version: releaseTypes.nightly,
|
|
||||||
releaseType: "nightly" as const,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
@@ -334,7 +362,7 @@ export function CreateProfileDialog({
|
|||||||
setCamoufoxConfig({
|
setCamoufoxConfig({
|
||||||
geoip: true, // Reset to automatic geoip
|
geoip: true, // Reset to automatic geoip
|
||||||
});
|
});
|
||||||
setActiveTab("regular");
|
setActiveTab("anti-detect");
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -375,7 +403,7 @@ export function CreateProfileDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||||
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
|
<DialogContent className="w-full max-h-[90vh] flex flex-col">
|
||||||
<DialogHeader className="flex-shrink-0">
|
<DialogHeader className="flex-shrink-0">
|
||||||
<DialogTitle>Create New Profile</DialogTitle>
|
<DialogTitle>Create New Profile</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -385,214 +413,247 @@ export function CreateProfileDialog({
|
|||||||
onValueChange={handleTabChange}
|
onValueChange={handleTabChange}
|
||||||
className="flex flex-col flex-1 w-full min-h-0"
|
className="flex flex-col flex-1 w-full min-h-0"
|
||||||
>
|
>
|
||||||
<TabsList className="grid flex-shrink-0 grid-cols-2 w-full">
|
<TabsList
|
||||||
<TabsTrigger value="regular">Regular Browsers</TabsTrigger>
|
className="grid flex-shrink-0 grid-cols-2 w-full"
|
||||||
|
defaultValue="anti-detect"
|
||||||
|
>
|
||||||
<TabsTrigger value="anti-detect">Anti-Detect</TabsTrigger>
|
<TabsTrigger value="anti-detect">Anti-Detect</TabsTrigger>
|
||||||
|
<TabsTrigger value="regular">Regular</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<ScrollArea className="flex-1 pr-6 h-[320px]">
|
<ScrollArea className="flex-1 h-[330px] overflow-y-hidden">
|
||||||
<div className="py-4 space-y-6">
|
<div className="flex flex-col justify-center items-center w-full">
|
||||||
{/* Profile Name - Common to both tabs */}
|
<div className="py-4 space-y-6 w-full max-w-md">
|
||||||
<div className="space-y-2">
|
{/* Profile Name - Common to both tabs */}
|
||||||
<Label htmlFor="profile-name">Profile Name</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor="profile-name">Profile Name</Label>
|
||||||
id="profile-name"
|
<Input
|
||||||
value={profileName}
|
id="profile-name"
|
||||||
onChange={(e) => setProfileName(e.target.value)}
|
value={profileName}
|
||||||
placeholder="Enter profile name"
|
onChange={(e) => setProfileName(e.target.value)}
|
||||||
/>
|
placeholder="Enter profile name"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TabsContent value="regular" className="mt-0 space-y-6">
|
<TabsContent value="regular" className="mt-0 space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Browser</Label>
|
<Label>Browser</Label>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={browserOptions
|
options={browserOptions
|
||||||
.filter((browser) =>
|
.filter(
|
||||||
supportedBrowsers.includes(browser.value),
|
(browser) =>
|
||||||
)
|
supportedBrowsers.includes(browser.value) &&
|
||||||
.map((browser) => {
|
browser.value !== "mullvad-browser" &&
|
||||||
const IconComponent = getBrowserIcon(browser.value);
|
browser.value !== "tor-browser",
|
||||||
return {
|
)
|
||||||
value: browser.value,
|
.map((browser) => {
|
||||||
label: browser.label,
|
const IconComponent = getBrowserIcon(browser.value);
|
||||||
icon: IconComponent,
|
return {
|
||||||
};
|
value: browser.value,
|
||||||
})}
|
label: browser.label,
|
||||||
value={selectedBrowser || ""}
|
icon: IconComponent,
|
||||||
onValueChange={(value) =>
|
};
|
||||||
setSelectedBrowser(value as BrowserTypeString)
|
})}
|
||||||
}
|
value={selectedBrowser || ""}
|
||||||
placeholder="Select a browser..."
|
onValueChange={(value) =>
|
||||||
searchPlaceholder="Search browsers..."
|
setSelectedBrowser(value as BrowserTypeString)
|
||||||
/>
|
}
|
||||||
</div>
|
placeholder="Select a browser..."
|
||||||
|
searchPlaceholder="Search browsers..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{selectedBrowser && (
|
{selectedBrowser && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{!isBrowserCurrentlyDownloading(selectedBrowser) &&
|
{!isBrowserCurrentlyDownloading(selectedBrowser) &&
|
||||||
!isBrowserVersionAvailable(selectedBrowser) &&
|
!isBrowserVersionAvailable(selectedBrowser) &&
|
||||||
getBestAvailableVersion(
|
getBestAvailableVersion(
|
||||||
availableReleaseTypes,
|
availableReleaseTypes,
|
||||||
selectedBrowser,
|
selectedBrowser,
|
||||||
) && (
|
) && (
|
||||||
<div className="flex gap-3 items-center">
|
<div className="flex gap-3 items-center">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{(() => {
|
||||||
|
const bestVersion = getBestAvailableVersion(
|
||||||
|
availableReleaseTypes,
|
||||||
|
selectedBrowser,
|
||||||
|
);
|
||||||
|
return `Latest version (${bestVersion?.version}) needs to be downloaded`;
|
||||||
|
})()}
|
||||||
|
</p>
|
||||||
|
<LoadingButton
|
||||||
|
onClick={() => handleDownload(selectedBrowser)}
|
||||||
|
isLoading={isBrowserCurrentlyDownloading(
|
||||||
|
selectedBrowser,
|
||||||
|
)}
|
||||||
|
size="sm"
|
||||||
|
disabled={isBrowserCurrentlyDownloading(
|
||||||
|
selectedBrowser,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</LoadingButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isBrowserCurrentlyDownloading(selectedBrowser) &&
|
||||||
|
isBrowserVersionAvailable(selectedBrowser) && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
{(() => {
|
{(() => {
|
||||||
const bestVersion = getBestAvailableVersion(
|
const bestVersion = getBestAvailableVersion(
|
||||||
availableReleaseTypes,
|
availableReleaseTypes,
|
||||||
selectedBrowser,
|
selectedBrowser,
|
||||||
);
|
);
|
||||||
return `${bestVersion?.releaseType === "stable" ? "Latest stable" : "Latest nightly"} version (${bestVersion?.version}) needs to be downloaded`;
|
return `✓ Latest version (${bestVersion?.version}) is available`;
|
||||||
})()}
|
})()}
|
||||||
</p>
|
</div>
|
||||||
<LoadingButton
|
)}
|
||||||
onClick={() => handleDownload(selectedBrowser)}
|
{isBrowserCurrentlyDownloading(selectedBrowser) && (
|
||||||
isLoading={isBrowserCurrentlyDownloading(
|
|
||||||
selectedBrowser,
|
|
||||||
)}
|
|
||||||
size="sm"
|
|
||||||
disabled={isBrowserCurrentlyDownloading(
|
|
||||||
selectedBrowser,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</LoadingButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isBrowserCurrentlyDownloading(selectedBrowser) &&
|
|
||||||
isBrowserVersionAvailable(selectedBrowser) && (
|
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{(() => {
|
{(() => {
|
||||||
const bestVersion = getBestAvailableVersion(
|
const bestVersion = getBestAvailableVersion(
|
||||||
availableReleaseTypes,
|
availableReleaseTypes,
|
||||||
selectedBrowser,
|
selectedBrowser,
|
||||||
);
|
);
|
||||||
return `✓ ${bestVersion?.releaseType === "stable" ? "Latest stable" : "Latest nightly"} version (${bestVersion?.version}) is available`;
|
return `Downloading version (${bestVersion?.version})...`;
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isBrowserCurrentlyDownloading(selectedBrowser) && (
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
)}
|
||||||
{(() => {
|
</div>
|
||||||
const bestVersion = getBestAvailableVersion(
|
</TabsContent>
|
||||||
availableReleaseTypes,
|
|
||||||
selectedBrowser,
|
<TabsContent value="anti-detect" className="mt-0 space-y-6">
|
||||||
);
|
<div className="space-y-6">
|
||||||
return `Downloading ${bestVersion?.releaseType === "stable" ? "stable" : "nightly"} version (${bestVersion?.version})...`;
|
{/* Camoufox Download Status */}
|
||||||
})()}
|
{!isBrowserCurrentlyDownloading("camoufox") &&
|
||||||
|
!isBrowserVersionAvailable("camoufox") &&
|
||||||
|
getBestAvailableVersion(
|
||||||
|
camoufoxReleaseTypes,
|
||||||
|
"camoufox",
|
||||||
|
) && (
|
||||||
|
<div className="flex gap-3 items-center p-3 rounded-md border">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{(() => {
|
||||||
|
const bestVersion = getBestAvailableVersion(
|
||||||
|
camoufoxReleaseTypes,
|
||||||
|
"camoufox",
|
||||||
|
);
|
||||||
|
return `Camoufox version (${bestVersion?.version}) needs to be downloaded`;
|
||||||
|
})()}
|
||||||
|
</p>
|
||||||
|
<LoadingButton
|
||||||
|
onClick={() => handleDownload("camoufox")}
|
||||||
|
isLoading={isBrowserCurrentlyDownloading(
|
||||||
|
"camoufox",
|
||||||
|
)}
|
||||||
|
size="sm"
|
||||||
|
disabled={isBrowserCurrentlyDownloading("camoufox")}
|
||||||
|
>
|
||||||
|
{isBrowserCurrentlyDownloading("camoufox")
|
||||||
|
? "Downloading..."
|
||||||
|
: "Download"}
|
||||||
|
</LoadingButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
{!isBrowserCurrentlyDownloading("camoufox") &&
|
||||||
)}
|
isBrowserVersionAvailable("camoufox") && (
|
||||||
</div>
|
<div className="p-3 text-sm rounded-md border text-muted-foreground">
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="anti-detect" className="mt-0 space-y-6">
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Camoufox Download Status */}
|
|
||||||
{!isBrowserCurrentlyDownloading("camoufox") &&
|
|
||||||
!isBrowserVersionAvailable("camoufox") &&
|
|
||||||
getBestAvailableVersion(
|
|
||||||
camoufoxReleaseTypes,
|
|
||||||
"camoufox",
|
|
||||||
) && (
|
|
||||||
<div className="flex gap-3 items-center p-3 rounded-md border">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const bestVersion = getBestAvailableVersion(
|
const bestVersion = getBestAvailableVersion(
|
||||||
camoufoxReleaseTypes,
|
camoufoxReleaseTypes,
|
||||||
"camoufox",
|
"camoufox",
|
||||||
);
|
);
|
||||||
return `Camoufox ${bestVersion?.releaseType} version (${bestVersion?.version}) needs to be downloaded`;
|
return `✓ Camoufox version (${bestVersion?.version}) is available`;
|
||||||
})()}
|
})()}
|
||||||
</p>
|
</div>
|
||||||
<LoadingButton
|
)}
|
||||||
onClick={() => handleDownload("camoufox")}
|
{isBrowserCurrentlyDownloading("camoufox") && (
|
||||||
isLoading={isBrowserCurrentlyDownloading("camoufox")}
|
|
||||||
size="sm"
|
|
||||||
disabled={isBrowserCurrentlyDownloading("camoufox")}
|
|
||||||
>
|
|
||||||
{isBrowserCurrentlyDownloading("camoufox")
|
|
||||||
? "Downloading..."
|
|
||||||
: "Download"}
|
|
||||||
</LoadingButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isBrowserCurrentlyDownloading("camoufox") &&
|
|
||||||
isBrowserVersionAvailable("camoufox") && (
|
|
||||||
<div className="p-3 text-sm rounded-md border text-muted-foreground">
|
<div className="p-3 text-sm rounded-md border text-muted-foreground">
|
||||||
{(() => {
|
{(() => {
|
||||||
const bestVersion = getBestAvailableVersion(
|
const bestVersion = getBestAvailableVersion(
|
||||||
camoufoxReleaseTypes,
|
camoufoxReleaseTypes,
|
||||||
"camoufox",
|
"camoufox",
|
||||||
);
|
);
|
||||||
return `✓ Camoufox ${bestVersion?.releaseType} version (${bestVersion?.version}) is available`;
|
return `Downloading Camoufox version (${bestVersion?.version})...`;
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isBrowserCurrentlyDownloading("camoufox") && (
|
|
||||||
<div className="p-3 text-sm text-muted-foreground rounded-md border">
|
<SharedCamoufoxConfigForm
|
||||||
{(() => {
|
config={camoufoxConfig}
|
||||||
const bestVersion = getBestAvailableVersion(
|
onConfigChange={updateCamoufoxConfig}
|
||||||
camoufoxReleaseTypes,
|
isCreating
|
||||||
"camoufox",
|
/>
|
||||||
);
|
</div>
|
||||||
return `Downloading Camoufox ${bestVersion?.releaseType} version (${bestVersion?.version})...`;
|
</TabsContent>
|
||||||
})()}
|
|
||||||
|
{/* Proxy Selection - Common to both tabs - Always visible */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Label>Proxy</Label>
|
||||||
|
<RippleButton
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowProxyForm(true)}
|
||||||
|
className="px-2 h-7 text-xs"
|
||||||
|
>
|
||||||
|
<GoPlus className="mr-1 w-3 h-3" /> Add Proxy
|
||||||
|
</RippleButton>
|
||||||
|
</div>
|
||||||
|
{storedProxies.length > 0 ? (
|
||||||
|
<Select
|
||||||
|
value={selectedProxyId || "none"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setSelectedProxyId(value === "none" ? undefined : value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="No proxy" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">No proxy</SelectItem>
|
||||||
|
{storedProxies.map((proxy) => (
|
||||||
|
<SelectItem key={proxy.id} value={proxy.id}>
|
||||||
|
{proxy.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-3 items-center p-3 text-sm rounded-md border text-muted-foreground">
|
||||||
|
No proxies available. Add one to route this profile's
|
||||||
|
traffic.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SharedCamoufoxConfigForm
|
|
||||||
config={camoufoxConfig}
|
|
||||||
onConfigChange={updateCamoufoxConfig}
|
|
||||||
isCreating
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
|
||||||
{/* Proxy Selection - Common to both tabs - Compact without card */}
|
|
||||||
{storedProxies.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label>Proxy</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedProxyId || "none"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setSelectedProxyId(value === "none" ? undefined : value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="No proxy" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">No proxy</SelectItem>
|
|
||||||
{storedProxies.map((proxy) => (
|
|
||||||
<SelectItem key={proxy.id} value={proxy.id}>
|
|
||||||
{proxy.name}{" "}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<DialogFooter className="flex-shrink-0 pt-4 border-t">
|
<DialogFooter className="flex-shrink-0 pt-4 border-t">
|
||||||
<Button variant="outline" onClick={handleClose}>
|
<RippleButton variant="outline" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
isLoading={isCreating}
|
isLoading={isCreating}
|
||||||
disabled={isCreateDisabled()}
|
disabled={isCreateDisabled()}
|
||||||
>
|
>
|
||||||
Create Profile
|
Create
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
<ProxyFormDialog
|
||||||
|
isOpen={showProxyForm}
|
||||||
|
onClose={() => setShowProxyForm(false)}
|
||||||
|
onSave={(proxy) => {
|
||||||
|
setStoredProxies((prev) => [...prev, proxy]);
|
||||||
|
setSelectedProxyId(proxy.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -9,6 +8,8 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { LoadingButton } from "./loading-button";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface DeleteConfirmationDialogProps {
|
interface DeleteConfirmationDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -59,16 +60,20 @@ export function DeleteConfirmationDialog({
|
|||||||
)}
|
)}
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={onClose} disabled={isLoading}>
|
<RippleButton
|
||||||
Cancel
|
variant="outline"
|
||||||
</Button>
|
onClick={onClose}
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => void handleConfirm()}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? "Deleting..." : confirmButtonText}
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
|
<LoadingButton
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => void handleConfirm()}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{confirmButtonText}
|
||||||
|
</LoadingButton>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -17,6 +16,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import type { BrowserProfile, ProfileGroup } from "@/types";
|
import type { BrowserProfile, ProfileGroup } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface DeleteGroupDialogProps {
|
interface DeleteGroupDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -188,9 +188,13 @@ export function DeleteGroupDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={handleClose} disabled={isDeleting}>
|
<RippleButton
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isDeleting}
|
isLoading={isDeleting}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import type { ProfileGroup } from "@/types";
|
import type { ProfileGroup } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface EditGroupDialogProps {
|
interface EditGroupDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -108,9 +108,13 @@ export function EditGroupDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={handleClose} disabled={isUpdating}>
|
<RippleButton
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
isLoading={isUpdating}
|
isLoading={isUpdating}
|
||||||
onClick={() => void handleUpdate()}
|
onClick={() => void handleUpdate()}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { GoPlus } from "react-icons/go";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { CreateGroupDialog } from "@/components/create-group-dialog";
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import type { ProfileGroup } from "@/types";
|
import type { ProfileGroup } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface GroupAssignmentDialogProps {
|
interface GroupAssignmentDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -41,6 +43,7 @@ export function GroupAssignmentDialog({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isAssigning, setIsAssigning] = useState(false);
|
const [isAssigning, setIsAssigning] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||||
|
|
||||||
const loadGroups = useCallback(async () => {
|
const loadGroups = useCallback(async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -126,7 +129,17 @@ export function GroupAssignmentDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="group-select">Assign to Group:</Label>
|
<div className="flex justify-between items-center">
|
||||||
|
<Label htmlFor="group-select">Assign to Group:</Label>
|
||||||
|
<RippleButton
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="h-7 px-2 text-xs"
|
||||||
|
onClick={() => setCreateDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<GoPlus className="mr-1 w-3 h-3" /> Create Group
|
||||||
|
</RippleButton>
|
||||||
|
</div>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Loading groups...
|
Loading groups...
|
||||||
@@ -161,9 +174,13 @@ export function GroupAssignmentDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={onClose} disabled={isAssigning}>
|
<RippleButton
|
||||||
|
variant="outline"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isAssigning}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
isLoading={isAssigning}
|
isLoading={isAssigning}
|
||||||
onClick={() => void handleAssign()}
|
onClick={() => void handleAssign()}
|
||||||
@@ -173,6 +190,15 @@ export function GroupAssignmentDialog({
|
|||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
<CreateGroupDialog
|
||||||
|
isOpen={createDialogOpen}
|
||||||
|
onClose={() => setCreateDialogOpen(false)}
|
||||||
|
onGroupCreated={(group) => {
|
||||||
|
setGroups((prev) => [...prev, group]);
|
||||||
|
setSelectedGroupId(group.id);
|
||||||
|
setCreateDialogOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function GroupBadges({
|
|||||||
<Badge
|
<Badge
|
||||||
key={group.id}
|
key={group.id}
|
||||||
variant={selectedGroupId === group.id ? "default" : "secondary"}
|
variant={selectedGroupId === group.id ? "default" : "secondary"}
|
||||||
className="cursor-pointer hover:bg-primary/80 transition-colors flex items-center gap-2 px-3 py-1"
|
className="flex gap-2 items-center px-3 py-1 transition-colors cursor-pointer dark:hover:bg-primary/60 hover:bg-primary/80"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onGroupSelect(selectedGroupId === group.id ? "default" : group.id);
|
onGroupSelect(selectedGroupId === group.id ? "default" : group.id);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import type { ProfileGroup } from "@/types";
|
import type { ProfileGroup } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface GroupManagementDialogProps {
|
interface GroupManagementDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -119,14 +120,14 @@ export function GroupManagementDialog({
|
|||||||
{/* Create new group button */}
|
{/* Create new group button */}
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<Label>Groups</Label>
|
<Label>Groups</Label>
|
||||||
<Button
|
<RippleButton
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setCreateDialogOpen(true)}
|
onClick={() => setCreateDialogOpen(true)}
|
||||||
className="flex gap-2 items-center"
|
className="flex gap-2 items-center"
|
||||||
>
|
>
|
||||||
<GoPlus className="w-4 h-4" />
|
<GoPlus className="w-4 h-4" />
|
||||||
Create Group
|
Create
|
||||||
</Button>
|
</RippleButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@@ -187,9 +188,9 @@ export function GroupManagementDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={onClose}>
|
<RippleButton variant="outline" onClick={onClose}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</RippleButton>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "./ui/dropdown-menu";
|
} from "./ui/dropdown-menu";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -34,7 +35,7 @@ const HomeHeader = ({
|
|||||||
onImportProfileDialogOpen,
|
onImportProfileDialogOpen,
|
||||||
onCreateProfileDialogOpen,
|
onCreateProfileDialogOpen,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const handleLogoClick = () => {
|
const _handleLogoClick = () => {
|
||||||
// Trigger the same URL handling logic as if the URL came from the system
|
// Trigger the same URL handling logic as if the URL came from the system
|
||||||
const event = new CustomEvent("url-open-request", {
|
const event = new CustomEvent("url-open-request", {
|
||||||
detail: "https://donutbrowser.com",
|
detail: "https://donutbrowser.com",
|
||||||
@@ -46,11 +47,11 @@ const HomeHeader = ({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleLogoClick}
|
|
||||||
className="p-1 cursor-pointer"
|
className="p-1 cursor-pointer"
|
||||||
title="Open donutbrowser.com"
|
title="Open donutbrowser.com"
|
||||||
|
onClick={_handleLogoClick}
|
||||||
>
|
>
|
||||||
<Logo className="w-10 h-10" />
|
<Logo className="w-10 h-10 transition-transform duration-300 ease-out will-change-transform hover:scale-110" />
|
||||||
</button>
|
</button>
|
||||||
{selectedProfiles.length > 0 ? (
|
{selectedProfiles.length > 0 ? (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -59,7 +60,7 @@ const HomeHeader = ({
|
|||||||
{selectedProfiles.length !== 1 ? "s" : ""} selected
|
{selectedProfiles.length !== 1 ? "s" : ""} selected
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<RippleButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onBulkGroupAssignment}
|
onClick={onBulkGroupAssignment}
|
||||||
@@ -67,8 +68,8 @@ const HomeHeader = ({
|
|||||||
>
|
>
|
||||||
<LuUsers className="w-4 h-4" />
|
<LuUsers className="w-4 h-4" />
|
||||||
Assign to Group
|
Assign to Group
|
||||||
</Button>
|
</RippleButton>
|
||||||
<Button
|
<RippleButton
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onBulkDelete}
|
onClick={onBulkDelete}
|
||||||
@@ -76,7 +77,7 @@ const HomeHeader = ({
|
|||||||
>
|
>
|
||||||
<LuTrash2 className="w-4 h-4" />
|
<LuTrash2 className="w-4 h-4" />
|
||||||
Delete Selected
|
Delete Selected
|
||||||
</Button>
|
</RippleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import { useBrowserSupport } from "@/hooks/use-browser-support";
|
import { useBrowserSupport } from "@/hooks/use-browser-support";
|
||||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||||
import type { DetectedProfile } from "@/types";
|
import type { DetectedProfile } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface ImportProfileDialogProps {
|
interface ImportProfileDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -242,7 +243,7 @@ export function ImportProfileDialog({
|
|||||||
);
|
);
|
||||||
if (profile) {
|
if (profile) {
|
||||||
const browserName = getBrowserDisplayName(profile.browser);
|
const browserName = getBrowserDisplayName(profile.browser);
|
||||||
const defaultName = `Imported ${browserName} Profile`;
|
const defaultName = `Old ${browserName}`;
|
||||||
setAutoDetectProfileName(defaultName);
|
setAutoDetectProfileName(defaultName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,7 +269,7 @@ export function ImportProfileDialog({
|
|||||||
<div className="overflow-y-auto flex-1 space-y-6 min-h-0">
|
<div className="overflow-y-auto flex-1 space-y-6 min-h-0">
|
||||||
{/* Mode Selection */}
|
{/* Mode Selection */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<RippleButton
|
||||||
variant={importMode === "auto-detect" ? "default" : "outline"}
|
variant={importMode === "auto-detect" ? "default" : "outline"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setImportMode("auto-detect");
|
setImportMode("auto-detect");
|
||||||
@@ -277,8 +278,8 @@ export function ImportProfileDialog({
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Auto-Detect
|
Auto-Detect
|
||||||
</Button>
|
</RippleButton>
|
||||||
<Button
|
<RippleButton
|
||||||
variant={importMode === "manual" ? "default" : "outline"}
|
variant={importMode === "manual" ? "default" : "outline"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setImportMode("manual");
|
setImportMode("manual");
|
||||||
@@ -287,7 +288,7 @@ export function ImportProfileDialog({
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Manual Import
|
Manual Import
|
||||||
</Button>
|
</RippleButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Auto-Detect Mode */}
|
{/* Auto-Detect Mode */}
|
||||||
@@ -479,9 +480,9 @@ export function ImportProfileDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="flex-shrink-0">
|
<DialogFooter className="flex-shrink-0">
|
||||||
<Button variant="outline" onClick={handleClose}>
|
<RippleButton variant="outline" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
{importMode === "auto-detect" ? (
|
{importMode === "auto-detect" ? (
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
isLoading={isImporting}
|
isLoading={isImporting}
|
||||||
@@ -494,7 +495,7 @@ export function ImportProfileDialog({
|
|||||||
isLoading
|
isLoading
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Import Profile
|
Import
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
) : (
|
) : (
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
@@ -508,7 +509,7 @@ export function ImportProfileDialog({
|
|||||||
!manualProfileName.trim()
|
!manualProfileName.trim()
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Import Profile
|
Import
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
)}
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { LuLoaderCircle } from "react-icons/lu";
|
import { LuLoaderCircle } from "react-icons/lu";
|
||||||
import { type ButtonProps, Button as UIButton } from "./ui/button";
|
import {
|
||||||
|
type RippleButtonProps as ButtonProps,
|
||||||
|
RippleButton as UIButton,
|
||||||
|
} from "./ui/ripple";
|
||||||
|
|
||||||
type Props = ButtonProps & {
|
type Props = ButtonProps & {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -7,7 +10,11 @@ type Props = ButtonProps & {
|
|||||||
};
|
};
|
||||||
export const LoadingButton = ({ isLoading, ...props }: Props) => {
|
export const LoadingButton = ({ isLoading, ...props }: Props) => {
|
||||||
return (
|
return (
|
||||||
<UIButton className="grid place-items-center" {...props}>
|
<UIButton
|
||||||
|
className="grid place-items-center"
|
||||||
|
{...props}
|
||||||
|
disabled={props.disabled || isLoading}
|
||||||
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LuLoaderCircle className="h-4 w-4 animate-spin" />
|
<LuLoaderCircle className="h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { BsCamera, BsMic } from "react-icons/bs";
|
import { BsCamera, BsMic } from "react-icons/bs";
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -15,6 +14,7 @@ import {
|
|||||||
import type { PermissionType } from "@/hooks/use-permissions";
|
import type { PermissionType } from "@/hooks/use-permissions";
|
||||||
import { usePermissions } from "@/hooks/use-permissions";
|
import { usePermissions } from "@/hooks/use-permissions";
|
||||||
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface PermissionDialogProps {
|
interface PermissionDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -148,9 +148,9 @@ export function PermissionDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-2">
|
<DialogFooter className="gap-2">
|
||||||
<Button variant="outline" onClick={onClose}>
|
<RippleButton variant="outline" onClick={onClose}>
|
||||||
{isCurrentPermissionGranted ? "Done" : "Cancel"}
|
{isCurrentPermissionGranted ? "Done" : "Cancel"}
|
||||||
</Button>
|
</RippleButton>
|
||||||
|
|
||||||
{!isCurrentPermissionGranted && (
|
{!isCurrentPermissionGranted && (
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
@@ -57,6 +55,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import type { BrowserProfile, StoredProxy } from "@/types";
|
import type { BrowserProfile, StoredProxy } from "@/types";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface ProfilesDataTableProps {
|
interface ProfilesDataTableProps {
|
||||||
data: BrowserProfile[];
|
data: BrowserProfile[];
|
||||||
@@ -548,7 +547,7 @@ export function ProfilesDataTable({
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="inline-flex">
|
<span className="inline-flex">
|
||||||
<Button
|
<RippleButton
|
||||||
variant={isRunning ? "destructive" : "default"}
|
variant={isRunning ? "destructive" : "default"}
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={!canLaunch || isLaunching || isStopping}
|
disabled={!canLaunch || isLaunching || isStopping}
|
||||||
@@ -567,7 +566,7 @@ export function ProfilesDataTable({
|
|||||||
) : (
|
) : (
|
||||||
"Launch"
|
"Launch"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</RippleButton>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{tooltipContent && (
|
{tooltipContent && (
|
||||||
@@ -667,36 +666,6 @@ export function ProfilesDataTable({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: "release_type",
|
|
||||||
header: "Release",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const releaseType: string = row.getValue("release_type");
|
|
||||||
const isNightly = releaseType === "nightly";
|
|
||||||
return (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
|
||||||
isNightly
|
|
||||||
? "text-yellow-800 bg-yellow-100 dark:bg-yellow-900 dark:text-yellow-200"
|
|
||||||
: "text-green-800 bg-green-100 dark:bg-green-900 dark:text-green-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isNightly ? "Nightly" : "Stable"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
enableSorting: true,
|
|
||||||
sortingFn: (rowA, rowB, columnId) => {
|
|
||||||
const releaseA: string = rowA.getValue(columnId);
|
|
||||||
const releaseB: string = rowB.getValue(columnId);
|
|
||||||
// Sort with "stable" before "nightly"
|
|
||||||
if (releaseA === "stable" && releaseB === "nightly") return -1;
|
|
||||||
if (releaseA === "nightly" && releaseB === "stable") return 1;
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "proxy",
|
id: "proxy",
|
||||||
header: "Proxy",
|
header: "Proxy",
|
||||||
@@ -748,6 +717,10 @@ export function ProfilesDataTable({
|
|||||||
browserState.isClient && runningProfiles.has(profile.name);
|
browserState.isClient && runningProfiles.has(profile.name);
|
||||||
const isBrowserUpdating =
|
const isBrowserUpdating =
|
||||||
browserState.isClient && isUpdating(profile.browser);
|
browserState.isClient && isUpdating(profile.browser);
|
||||||
|
const isLaunching = launchingProfiles.has(profile.name);
|
||||||
|
const isStopping = stoppingProfiles.has(profile.name);
|
||||||
|
const isDisabled =
|
||||||
|
isRunning || isLaunching || isStopping || isBrowserUpdating;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-end items-center">
|
<div className="flex justify-end items-center">
|
||||||
@@ -763,15 +736,11 @@ export function ProfilesDataTable({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onProxySettings(profile);
|
onProxySettings(profile);
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={isDisabled}
|
||||||
!browserState.isClient || isBrowserUpdating || isRunning
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Configure Proxy
|
Configure Proxy
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -781,9 +750,7 @@ export function ProfilesDataTable({
|
|||||||
onAssignProfilesToGroup([profile.name]);
|
onAssignProfilesToGroup([profile.name]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={isDisabled}
|
||||||
!browserState.isClient || isBrowserUpdating || isRunning
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Assign to Group
|
Assign to Group
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -792,9 +759,7 @@ export function ProfilesDataTable({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onConfigureCamoufox(profile);
|
onConfigureCamoufox(profile);
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={isDisabled}
|
||||||
!browserState.isClient || isRunning || isBrowserUpdating
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Configure Camoufox
|
Configure Camoufox
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -806,9 +771,7 @@ export function ProfilesDataTable({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChangeVersion(profile);
|
onChangeVersion(profile);
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={isDisabled}
|
||||||
!browserState.isClient || isRunning || isBrowserUpdating
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Switch Release
|
Switch Release
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -818,9 +781,7 @@ export function ProfilesDataTable({
|
|||||||
setProfileToRename(profile);
|
setProfileToRename(profile);
|
||||||
setNewProfileName(profile.name);
|
setNewProfileName(profile.name);
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={isDisabled}
|
||||||
!browserState.isClient || isRunning || isBrowserUpdating
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Rename
|
Rename
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -828,9 +789,7 @@ export function ProfilesDataTable({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setProfileToDelete(profile);
|
setProfileToDelete(profile);
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={isDisabled}
|
||||||
!browserState.isClient || isRunning || isBrowserUpdating
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -971,15 +930,17 @@ export function ProfilesDataTable({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<RippleButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setProfileToRename(null);
|
setProfileToRename(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
<Button onClick={() => void handleRename()}>Save</Button>
|
<RippleButton onClick={() => void handleRename()}>
|
||||||
|
Save
|
||||||
|
</RippleButton>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { LuCopy } from "react-icons/lu";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -30,6 +29,7 @@ import {
|
|||||||
import { useBrowserState } from "@/hooks/use-browser-state";
|
import { useBrowserState } from "@/hooks/use-browser-state";
|
||||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||||
import type { BrowserProfile, StoredProxy } from "@/types";
|
import type { BrowserProfile, StoredProxy } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface ProfileSelectorDialogProps {
|
interface ProfileSelectorDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -196,7 +196,7 @@ export function ProfileSelectorDialog({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<Label className="text-sm font-medium">Opening URL:</Label>
|
<Label className="text-sm font-medium">Opening URL:</Label>
|
||||||
<Button
|
<RippleButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => void handleCopyUrl()}
|
onClick={() => void handleCopyUrl()}
|
||||||
@@ -204,7 +204,7 @@ export function ProfileSelectorDialog({
|
|||||||
>
|
>
|
||||||
<LuCopy className="w-3 h-3" />
|
<LuCopy className="w-3 h-3" />
|
||||||
Copy
|
Copy
|
||||||
</Button>
|
</RippleButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 text-sm break-all rounded bg-muted">
|
<div className="p-2 text-sm break-all rounded bg-muted">
|
||||||
{url}
|
{url}
|
||||||
@@ -312,9 +312,9 @@ export function ProfileSelectorDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={handleCancel}>
|
<RippleButton variant="outline" onClick={handleCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="inline-flex">
|
<span className="inline-flex">
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -22,6 +21,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import type { StoredProxy } from "@/types";
|
import type { StoredProxy } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface ProxyFormData {
|
interface ProxyFormData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -264,13 +264,13 @@ export function ProxyFormDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<RippleButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { FiEdit2, FiPlus, FiTrash2, FiWifi } from "react-icons/fi";
|
import { FiEdit2, FiPlus, FiTrash2, FiWifi } from "react-icons/fi";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
|
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -20,6 +22,7 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { trimName } from "@/lib/name-utils";
|
import { trimName } from "@/lib/name-utils";
|
||||||
import type { StoredProxy } from "@/types";
|
import type { StoredProxy } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface ProxyManagementDialogProps {
|
interface ProxyManagementDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -34,6 +37,7 @@ export function ProxyManagementDialog({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showProxyForm, setShowProxyForm] = useState(false);
|
const [showProxyForm, setShowProxyForm] = useState(false);
|
||||||
const [editingProxy, setEditingProxy] = useState<StoredProxy | null>(null);
|
const [editingProxy, setEditingProxy] = useState<StoredProxy | null>(null);
|
||||||
|
const [proxyUsage, setProxyUsage] = useState<Record<string, number>>({});
|
||||||
|
|
||||||
const loadStoredProxies = useCallback(async () => {
|
const loadStoredProxies = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -48,11 +52,44 @@ export function ProxyManagementDialog({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const loadProxyUsage = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const profiles = await invoke<Array<{ proxy_id?: string }>>(
|
||||||
|
"list_browser_profiles",
|
||||||
|
);
|
||||||
|
const counts: Record<string, number> = {};
|
||||||
|
for (const p of profiles) {
|
||||||
|
if (p.proxy_id) counts[p.proxy_id] = (counts[p.proxy_id] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
setProxyUsage(counts);
|
||||||
|
} catch (_err) {
|
||||||
|
// ignore non-critical errors
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
loadStoredProxies();
|
loadStoredProxies();
|
||||||
|
void loadProxyUsage();
|
||||||
}
|
}
|
||||||
}, [isOpen, loadStoredProxies]);
|
}, [isOpen, loadStoredProxies, loadProxyUsage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let unlisten: (() => void) | undefined;
|
||||||
|
const setup = async () => {
|
||||||
|
try {
|
||||||
|
unlisten = await listen("profile-updated", () => {
|
||||||
|
void loadProxyUsage();
|
||||||
|
});
|
||||||
|
} catch (_err) {
|
||||||
|
// ignore non-critical errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (isOpen) void setup();
|
||||||
|
return () => {
|
||||||
|
if (unlisten) unlisten();
|
||||||
|
};
|
||||||
|
}, [isOpen, loadProxyUsage]);
|
||||||
|
|
||||||
const handleDeleteProxy = useCallback(async (proxy: StoredProxy) => {
|
const handleDeleteProxy = useCallback(async (proxy: StoredProxy) => {
|
||||||
if (
|
if (
|
||||||
@@ -124,13 +161,13 @@ export function ProxyManagementDialog({
|
|||||||
profiles
|
profiles
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<RippleButton
|
||||||
onClick={handleCreateProxy}
|
onClick={handleCreateProxy}
|
||||||
className="flex gap-2 items-center"
|
className="flex gap-2 items-center"
|
||||||
>
|
>
|
||||||
<FiPlus className="w-4 h-4" />
|
<FiPlus className="w-4 h-4" />
|
||||||
Create Proxy
|
Create Proxy
|
||||||
</Button>
|
</RippleButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Proxy List - Scrollable */}
|
{/* Proxy List - Scrollable */}
|
||||||
@@ -150,10 +187,10 @@ export function ProxyManagementDialog({
|
|||||||
<p className="mb-4 text-sm text-muted-foreground">
|
<p className="mb-4 text-sm text-muted-foreground">
|
||||||
Create your first proxy configuration to get started
|
Create your first proxy configuration to get started
|
||||||
</p>
|
</p>
|
||||||
<Button variant="outline" onClick={handleCreateProxy}>
|
<RippleButton variant="outline" onClick={handleCreateProxy}>
|
||||||
<FiPlus className="mr-2 w-4 h-4" />
|
<FiPlus className="mr-2 w-4 h-4" />
|
||||||
Create First Proxy
|
Create First Proxy
|
||||||
</Button>
|
</RippleButton>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-y-auto pr-2 space-y-2 h-full">
|
<div className="overflow-y-auto pr-2 space-y-2 h-full">
|
||||||
@@ -182,6 +219,11 @@ export function ProxyManagementDialog({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mr-2">
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{proxyUsage[proxy.id] ?? 0}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
<div className="flex flex-shrink-0 gap-1 items-center">
|
<div className="flex flex-shrink-0 gap-1 items-center">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -221,7 +263,7 @@ export function ProxyManagementDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="flex-shrink-0">
|
<DialogFooter className="flex-shrink-0">
|
||||||
<Button onClick={onClose}>Close</Button>
|
<RippleButton onClick={onClose}>Close</RippleButton>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { FiPlus } from "react-icons/fi";
|
import { FiPlus } from "react-icons/fi";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { StoredProxy } from "@/types";
|
import type { StoredProxy } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface ProxySettingsDialogProps {
|
interface ProxySettingsDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -45,6 +47,7 @@ export function ProxySettingsDialog({
|
|||||||
);
|
);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showProxyForm, setShowProxyForm] = useState(false);
|
const [showProxyForm, setShowProxyForm] = useState(false);
|
||||||
|
const [proxyUsage, setProxyUsage] = useState<Record<string, number>>({});
|
||||||
|
|
||||||
// Helper to determine if proxy should be disabled for the selected browser
|
// Helper to determine if proxy should be disabled for the selected browser
|
||||||
const isProxyDisabled = browserType === "tor-browser";
|
const isProxyDisabled = browserType === "tor-browser";
|
||||||
@@ -62,9 +65,28 @@ export function ProxySettingsDialog({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const loadProxyUsage = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const profiles = await invoke<Array<{ proxy_id?: string }>>(
|
||||||
|
"list_browser_profiles",
|
||||||
|
);
|
||||||
|
const counts: Record<string, number> = {};
|
||||||
|
for (const p of profiles) {
|
||||||
|
if (p.proxy_id) {
|
||||||
|
counts[p.proxy_id] = (counts[p.proxy_id] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setProxyUsage(counts);
|
||||||
|
} catch (error) {
|
||||||
|
// Non-fatal
|
||||||
|
console.error("Failed to load proxy usage:", error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
loadStoredProxies();
|
loadStoredProxies();
|
||||||
|
void loadProxyUsage();
|
||||||
if (isProxyDisabled) {
|
if (isProxyDisabled) {
|
||||||
setSelectedProxyId(null);
|
setSelectedProxyId(null);
|
||||||
} else {
|
} else {
|
||||||
@@ -72,7 +94,31 @@ export function ProxySettingsDialog({
|
|||||||
setSelectedProxyId(initialProxyId || null);
|
setSelectedProxyId(initialProxyId || null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isOpen, isProxyDisabled, loadStoredProxies, initialProxyId]);
|
}, [
|
||||||
|
isOpen,
|
||||||
|
isProxyDisabled,
|
||||||
|
loadStoredProxies,
|
||||||
|
initialProxyId,
|
||||||
|
loadProxyUsage,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Refresh usage when profiles change
|
||||||
|
useEffect(() => {
|
||||||
|
let unlisten: (() => void) | undefined;
|
||||||
|
const setup = async () => {
|
||||||
|
try {
|
||||||
|
unlisten = await listen("profile-updated", () => {
|
||||||
|
void loadProxyUsage();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (isOpen) void setup();
|
||||||
|
return () => {
|
||||||
|
if (unlisten) unlisten();
|
||||||
|
};
|
||||||
|
}, [isOpen, loadProxyUsage]);
|
||||||
|
|
||||||
const handleCreateProxy = useCallback(() => {
|
const handleCreateProxy = useCallback(() => {
|
||||||
setShowProxyForm(true);
|
setShowProxyForm(true);
|
||||||
@@ -142,15 +188,15 @@ export function ProxySettingsDialog({
|
|||||||
</Label>
|
</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<RippleButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleCreateProxy}
|
onClick={handleCreateProxy}
|
||||||
className="flex gap-2 items-center"
|
className="flex gap-2 items-center"
|
||||||
>
|
>
|
||||||
<FiPlus className="w-4 h-4" />
|
<FiPlus className="w-4 h-4" />
|
||||||
Create New
|
Create
|
||||||
</Button>
|
</RippleButton>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Create a new proxy configuration</p>
|
<p>Create a new proxy configuration</p>
|
||||||
@@ -233,6 +279,7 @@ export function ProxySettingsDialog({
|
|||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
{proxy.proxy_settings.proxy_type.toUpperCase()}
|
{proxy.proxy_settings.proxy_type.toUpperCase()}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<Badge>{proxyUsage[proxy.id] ?? 0}</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -263,12 +310,12 @@ export function ProxySettingsDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={onClose}>
|
<RippleButton variant="outline" onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
<Button onClick={handleSave} disabled={!hasChanged()}>
|
<RippleButton onClick={handleSave} disabled={!hasChanged()}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</RippleButton>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useState } from "react";
|
|||||||
import { LuCheck, LuChevronsUpDown, LuDownload } from "react-icons/lu";
|
import { LuCheck, LuChevronsUpDown, LuDownload } from "react-icons/lu";
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -19,12 +18,12 @@ import {
|
|||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { BrowserReleaseTypes } from "@/types";
|
import type { BrowserReleaseTypes } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface ReleaseTypeSelectorProps {
|
interface ReleaseTypeSelectorProps {
|
||||||
selectedReleaseType: "stable" | "nightly" | null;
|
selectedReleaseType: "stable" | "nightly" | null;
|
||||||
onReleaseTypeSelect: (releaseType: "stable" | "nightly" | null) => void;
|
onReleaseTypeSelect: (releaseType: "stable" | "nightly" | null) => void;
|
||||||
availableReleaseTypes: BrowserReleaseTypes;
|
availableReleaseTypes: BrowserReleaseTypes;
|
||||||
browser: string;
|
|
||||||
isDownloading: boolean;
|
isDownloading: boolean;
|
||||||
onDownload: () => void;
|
onDownload: () => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -36,7 +35,7 @@ export function ReleaseTypeSelector({
|
|||||||
selectedReleaseType,
|
selectedReleaseType,
|
||||||
onReleaseTypeSelect,
|
onReleaseTypeSelect,
|
||||||
availableReleaseTypes,
|
availableReleaseTypes,
|
||||||
browser,
|
// browser prop removed; callers control availableReleaseTypes
|
||||||
isDownloading,
|
isDownloading,
|
||||||
onDownload,
|
onDownload,
|
||||||
placeholder = "Select release type...",
|
placeholder = "Select release type...",
|
||||||
@@ -45,11 +44,13 @@ export function ReleaseTypeSelector({
|
|||||||
}: ReleaseTypeSelectorProps) {
|
}: ReleaseTypeSelectorProps) {
|
||||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
|
|
||||||
|
// Nightly visibility is controlled by callers. This component will render
|
||||||
|
// whichever options are provided via availableReleaseTypes.
|
||||||
const releaseOptions = [
|
const releaseOptions = [
|
||||||
...(availableReleaseTypes.stable
|
...(availableReleaseTypes.stable
|
||||||
? [{ type: "stable" as const, version: availableReleaseTypes.stable }]
|
? [{ type: "stable" as const, version: availableReleaseTypes.stable }]
|
||||||
: []),
|
: []),
|
||||||
...(availableReleaseTypes.nightly && browser !== "chromium"
|
...(availableReleaseTypes.nightly
|
||||||
? [{ type: "nightly" as const, version: availableReleaseTypes.nightly }]
|
? [{ type: "nightly" as const, version: availableReleaseTypes.nightly }]
|
||||||
: []),
|
: []),
|
||||||
];
|
];
|
||||||
@@ -85,7 +86,7 @@ export function ReleaseTypeSelector({
|
|||||||
{showDropdown ? (
|
{showDropdown ? (
|
||||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen} modal={true}>
|
<Popover open={popoverOpen} onOpenChange={setPopoverOpen} modal={true}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<RippleButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={popoverOpen}
|
aria-expanded={popoverOpen}
|
||||||
@@ -93,7 +94,7 @@ export function ReleaseTypeSelector({
|
|||||||
>
|
>
|
||||||
{selectedDisplayText}
|
{selectedDisplayText}
|
||||||
<LuChevronsUpDown className="ml-2 w-4 h-4 opacity-50 shrink-0" />
|
<LuChevronsUpDown className="ml-2 w-4 h-4 opacity-50 shrink-0" />
|
||||||
</Button>
|
</RippleButton>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0">
|
<PopoverContent className="p-0">
|
||||||
<Command>
|
<Command>
|
||||||
@@ -159,11 +160,6 @@ export function ReleaseTypeSelector({
|
|||||||
<span className="text-sm font-medium capitalize">
|
<span className="text-sm font-medium capitalize">
|
||||||
{releaseOptions[0].type}
|
{releaseOptions[0].type}
|
||||||
</span>
|
</span>
|
||||||
{releaseOptions[0].type === "nightly" && (
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
Nightly
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{releaseOptions[0].version}
|
{releaseOptions[0].version}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useCallback, useEffect, useState } from "react";
|
|||||||
import { BsCamera, BsMic } from "react-icons/bs";
|
import { BsCamera, BsMic } from "react-icons/bs";
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -33,6 +32,7 @@ import {
|
|||||||
showSuccessToast,
|
showSuccessToast,
|
||||||
showUnifiedVersionUpdateToast,
|
showUnifiedVersionUpdateToast,
|
||||||
} from "@/lib/toast-utils";
|
} from "@/lib/toast-utils";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
interface AppSettings {
|
interface AppSettings {
|
||||||
set_as_default_browser: boolean;
|
set_as_default_browser: boolean;
|
||||||
@@ -529,9 +529,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="flex-shrink-0">
|
<DialogFooter className="flex-shrink-0">
|
||||||
<Button variant="outline" onClick={onClose}>
|
<RippleButton variant="outline" onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</RippleButton>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
isLoading={isSaving}
|
isLoading={isSaving}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -874,6 +874,36 @@ export function SharedCamoufoxConfigForm({
|
|||||||
placeholder="e.g., 1080"
|
placeholder="e.g., 1080"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="screen-min-width">Min Width</Label>
|
||||||
|
<Input
|
||||||
|
id="screen-min-width"
|
||||||
|
type="number"
|
||||||
|
value={config.screen_min_width || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onConfigChange(
|
||||||
|
"screen_min_width",
|
||||||
|
e.target.value ? parseInt(e.target.value) : undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="e.g., 800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="screen-min-height">Min Height</Label>
|
||||||
|
<Input
|
||||||
|
id="screen-min-height"
|
||||||
|
type="number"
|
||||||
|
value={config.screen_min_height || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onConfigChange(
|
||||||
|
"screen_min_height",
|
||||||
|
e.target.value ? parseInt(e.target.value) : undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="e.g., 600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const badgeVariants = cva(
|
|||||||
default:
|
default:
|
||||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
secondary:
|
secondary:
|
||||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
"border-transparent bg-secondary dark:bg-secondary/60 text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { type HTMLMotionProps, motion, type Transition } from "motion/react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"relative overflow-hidden cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-9 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-11 px-8 has-[>svg]:px-6",
|
||||||
|
icon: "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const rippleVariants = cva("absolute rounded-full size-5 pointer-events-none", {
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary-foreground",
|
||||||
|
destructive: "bg-destructive",
|
||||||
|
outline: "bg-input",
|
||||||
|
secondary: "bg-secondary",
|
||||||
|
ghost: "bg-accent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type Ripple = {
|
||||||
|
id: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RippleButtonProps = HTMLMotionProps<"button"> & {
|
||||||
|
children: React.ReactNode;
|
||||||
|
rippleClassName?: string;
|
||||||
|
scale?: number;
|
||||||
|
transition?: Transition;
|
||||||
|
} & VariantProps<typeof buttonVariants>;
|
||||||
|
|
||||||
|
function RippleButton({
|
||||||
|
ref,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
rippleClassName,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
scale = 10,
|
||||||
|
transition = { duration: 0.6, ease: "easeOut" },
|
||||||
|
...props
|
||||||
|
}: RippleButtonProps) {
|
||||||
|
const [ripples, setRipples] = React.useState<Ripple[]>([]);
|
||||||
|
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||||
|
React.useImperativeHandle(ref, () => buttonRef.current as HTMLButtonElement);
|
||||||
|
|
||||||
|
const createRipple = React.useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
const button = buttonRef.current;
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
const rect = button.getBoundingClientRect();
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
|
||||||
|
const newRipple: Ripple = {
|
||||||
|
id: Date.now(),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
};
|
||||||
|
|
||||||
|
setRipples((prev) => [...prev, newRipple]);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setRipples((prev) => prev.filter((r) => r.id !== newRipple.id));
|
||||||
|
}, 600);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClick = React.useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
createRipple(event);
|
||||||
|
if (onClick) {
|
||||||
|
onClick(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[createRipple, onClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
ref={buttonRef}
|
||||||
|
data-slot="ripple-button"
|
||||||
|
onClick={handleClick}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{ripples.map((ripple) => (
|
||||||
|
<motion.span
|
||||||
|
key={ripple.id}
|
||||||
|
initial={{ scale: 0, opacity: 0.5 }}
|
||||||
|
animate={{ scale, opacity: 0 }}
|
||||||
|
transition={transition}
|
||||||
|
className={cn(
|
||||||
|
rippleVariants({ variant, className: rippleClassName }),
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
top: ripple.y - 10,
|
||||||
|
left: ripple.x - 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RippleButton, type RippleButtonProps };
|
||||||
@@ -12,8 +12,8 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
className="toaster group"
|
className="toaster group"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--normal-bg": "var(--popover)",
|
"--normal-bg": "var(--card)",
|
||||||
"--normal-text": "var(--popover-foreground)",
|
"--normal-text": "var(--card-foreground)",
|
||||||
"--normal-border": "var(--border)",
|
"--normal-border": "var(--border)",
|
||||||
zIndex: 99999,
|
zIndex: 99999,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
@@ -22,6 +22,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
style: {
|
style: {
|
||||||
zIndex: 99999,
|
zIndex: 99999,
|
||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
|
backdropFilter: "saturate(1.2)",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ export function useAppUpdateNotifications() {
|
|||||||
style: {
|
style: {
|
||||||
zIndex: 99999, // Ensure app updates appear above dialogs
|
zIndex: 99999, // Ensure app updates appear above dialogs
|
||||||
pointerEvents: "auto", // Ensure app updates remain interactive
|
pointerEvents: "auto", // Ensure app updates remain interactive
|
||||||
|
marginTop: "16px", // slightly lower on macOS-like top controls
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -210,9 +210,21 @@ export function useBrowserDownload() {
|
|||||||
if (!suppressNotifications) {
|
if (!suppressNotifications) {
|
||||||
// Dismiss any existing download toast and show error
|
// Dismiss any existing download toast and show error
|
||||||
dismissToast(`download-${browserStr}-${version}`);
|
dismissToast(`download-${browserStr}-${version}`);
|
||||||
|
|
||||||
|
let errorMessage = "Unknown error occurred";
|
||||||
|
if (error instanceof Error) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
} else if (typeof error === "string") {
|
||||||
|
errorMessage = error;
|
||||||
|
} else if (error && typeof error === "object" && "message" in error) {
|
||||||
|
errorMessage = String(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the long-running download toast is dismissed, and show a finite error toast
|
||||||
|
dismissToast(`download-${browserStr}-${version}`);
|
||||||
showErrorToast(`Failed to download ${browserName} ${version}`, {
|
showErrorToast(`Failed to download ${browserName} ${version}`, {
|
||||||
description:
|
description: errorMessage,
|
||||||
error instanceof Error ? error.message : "Unknown error occurred",
|
duration: 8000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -245,11 +245,22 @@ export function useVersionUpdater() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to handle browser auto-update:", error);
|
console.error("Failed to handle browser auto-update:", error);
|
||||||
|
|
||||||
|
let errorMessage = "Unknown error occurred";
|
||||||
|
if (error instanceof Error) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
} else if (typeof error === "string") {
|
||||||
|
errorMessage = error;
|
||||||
|
} else if (
|
||||||
|
error &&
|
||||||
|
typeof error === "object" &&
|
||||||
|
"message" in error
|
||||||
|
) {
|
||||||
|
errorMessage = String(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
showErrorToast(`Failed to auto-update ${browserDisplayName}`, {
|
showErrorToast(`Failed to auto-update ${browserDisplayName}`, {
|
||||||
description:
|
description: errorMessage,
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Unknown error occurred",
|
|
||||||
duration: 8000,
|
duration: 8000,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -336,9 +347,17 @@ export function useVersionUpdater() {
|
|||||||
return results;
|
return results;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to trigger manual update:", error);
|
console.error("Failed to trigger manual update:", error);
|
||||||
|
let errorMessage = "Unknown error occurred";
|
||||||
|
if (error instanceof Error) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
} else if (typeof error === "string") {
|
||||||
|
errorMessage = error;
|
||||||
|
} else if (error && typeof error === "object" && "message" in error) {
|
||||||
|
errorMessage = String(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
showErrorToast("Failed to update browser versions", {
|
showErrorToast("Failed to update browser versions", {
|
||||||
description:
|
description: errorMessage,
|
||||||
error instanceof Error ? error.message : "Unknown error occurred",
|
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ export interface CamoufoxConfig {
|
|||||||
proxy?: string;
|
proxy?: string;
|
||||||
screen_max_width?: number;
|
screen_max_width?: number;
|
||||||
screen_max_height?: number;
|
screen_max_height?: number;
|
||||||
|
screen_min_width?: number;
|
||||||
|
screen_min_height?: number;
|
||||||
geoip?: string | boolean;
|
geoip?: string | boolean;
|
||||||
block_images?: boolean;
|
block_images?: boolean;
|
||||||
block_webrtc?: boolean;
|
block_webrtc?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user