Compare commits

...

22 Commits

Author SHA1 Message Date
zhom 3bec00a2cd chore: reset lock file 2025-07-11 03:43:52 +04:00
zhom 3b78971df8 chore: pnpm update 2025-07-11 03:31:05 +04:00
zhom 5f9a716f62 chore: version bump 2025-07-11 03:22:36 +04:00
zhom 4d07984d99 chore: hide camoufox 2025-07-11 03:22:11 +04:00
zhom 188e14e5b5 style: copy 2025-07-11 03:10:53 +04:00
zhom bc1b9e9757 style: copy 2025-07-11 03:10:00 +04:00
zhom e742e5fdfa style: copy 2025-07-11 03:09:38 +04:00
zhom 9ce7757cb2 chore: version bump 2025-07-08 06:26:39 +04:00
zhom 3ca454a2c5 style: adjust modal height 2025-07-08 04:57:25 +04:00
zhom 689ac8e3ca fix: windows build correct string literal 2025-07-07 07:34:55 +04:00
zhom 0e1c5dcfb6 docs: add feature description 2025-07-07 07:33:41 +04:00
zhom f22a9f3557 style: copy 2025-07-07 07:13:26 +04:00
zhom 5a76fe3221 tests: treat all camoufox versions as stable 2025-07-07 07:13:03 +04:00
zhom 5edad9b97c fix: prevent version downgrade for camoufox 2025-07-07 07:04:49 +04:00
zhom 38556fc504 style: copy and minor self-update modal logic change 2025-07-07 06:44:15 +04:00
zhom 703ca2c50b feat: add anti-detect functionality 2025-07-07 06:19:43 +04:00
zhom 198046fca9 Merge pull request #42 from zhom/dependabot/cargo/src-tauri/rust-dependencies-77d4c5ce85
deps(rust)(deps): bump the rust-dependencies group in /src-tauri with 10 updates
2025-07-05 11:33:20 +00:00
zhom fdcce5c86a Merge pull request #41 from zhom/dependabot/npm_and_yarn/frontend-dependencies-199434007a
deps(deps): bump the frontend-dependencies group with 35 updates
2025-07-05 11:33:00 +00:00
zhom 1cd1c7b59d Merge pull request #40 from zhom/dependabot/github_actions/github-actions-4aaa0eafdc
ci(deps): bump crate-ci/typos from 1.33.1 to 1.34.0 in the github-actions group
2025-07-05 11:32:43 +00:00
dependabot[bot] d803361fca deps(rust)(deps): bump the rust-dependencies group
Bumps the rust-dependencies group in /src-tauri with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [reqwest](https://github.com/seanmonstar/reqwest) | `0.12.20` | `0.12.22` |
| [tokio](https://github.com/tokio-rs/tokio) | `1.45.1` | `1.46.1` |
| [async-channel](https://github.com/smol-rs/async-channel) | `2.3.1` | `2.4.0` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.27` | `1.2.28` |
| [h2](https://github.com/hyperium/h2) | `0.4.10` | `0.4.11` |
| [rust-ini](https://github.com/zonyitoo/rust-ini) | `0.21.1` | `0.21.2` |
| [serde_with](https://github.com/jonasbb/serde_with) | `3.13.0` | `3.14.0` |
| [serde_with_macros](https://github.com/jonasbb/serde_with) | `3.13.0` | `3.14.0` |
| [shared_child](https://github.com/oconnor663/shared_child.rs) | `1.1.0` | `1.1.1` |
| [sigchld](https://github.com/oconnor663/sigchld.rs) | `0.2.3` | `0.2.4` |


Updates `reqwest` from 0.12.20 to 0.12.22
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.20...v0.12.22)

Updates `tokio` from 1.45.1 to 1.46.1
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.45.1...tokio-1.46.1)

Updates `async-channel` from 2.3.1 to 2.4.0
- [Release notes](https://github.com/smol-rs/async-channel/releases)
- [Changelog](https://github.com/smol-rs/async-channel/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/async-channel/compare/v2.3.1...v2.4.0)

Updates `cc` from 1.2.27 to 1.2.28
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.27...cc-v1.2.28)

Updates `h2` from 0.4.10 to 0.4.11
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.4.10...v0.4.11)

Updates `rust-ini` from 0.21.1 to 0.21.2
- [Release notes](https://github.com/zonyitoo/rust-ini/releases)
- [Commits](https://github.com/zonyitoo/rust-ini/compare/v0.21.1...v0.21.2)

Updates `serde_with` from 3.13.0 to 3.14.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.13.0...v3.14.0)

Updates `serde_with_macros` from 3.13.0 to 3.14.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.13.0...v3.14.0)

Updates `shared_child` from 1.1.0 to 1.1.1
- [Commits](https://github.com/oconnor663/shared_child.rs/compare/1.1.0...1.1.1)

Updates `sigchld` from 0.2.3 to 0.2.4
- [Commits](https://github.com/oconnor663/sigchld.rs/compare/0.2.3...0.2.4)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-version: 0.12.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tokio
  dependency-version: 1.46.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: async-channel
  dependency-version: 2.4.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.28
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: h2
  dependency-version: 0.4.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rust-ini
  dependency-version: 0.21.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: serde_with
  dependency-version: 3.14.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_with_macros
  dependency-version: 3.14.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: shared_child
  dependency-version: 1.1.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: sigchld
  dependency-version: 0.2.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-05 09:32:16 +00:00
dependabot[bot] 2f6f20eb29 deps(deps): bump the frontend-dependencies group with 35 updates
---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: sonner
  dependency-version: 2.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@types/node"
  dependency-version: 24.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: tw-animate-css
  dependency-version: 1.3.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: dotenv
  dependency-version: 17.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/env"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-darwin-arm64"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-darwin-x64"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-linux-arm64-gnu"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-linux-arm64-musl"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-linux-x64-gnu"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-linux-x64-musl"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-win32-arm64-msvc"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@next/swc-win32-x64-msvc"
  dependency-version: 15.3.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm-eabi"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-android-arm64"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-darwin-x64"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-arm64"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-freebsd-x64"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-gnueabihf"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm-musleabihf"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-arm64-musl"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-loongarch64-gnu"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-powerpc64le-gnu"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-gnu"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-riscv64-musl"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-s390x-gnu"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-linux-x64-musl"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-arm64-msvc"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-ia32-msvc"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: "@rollup/rollup-win32-x64-msvc"
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
- dependency-name: rollup
  dependency-version: 4.44.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-05 09:18:32 +00:00
dependabot[bot] 59272e0cff ci(deps): bump crate-ci/typos in the github-actions group
Bumps the github-actions group with 1 update: [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `crate-ci/typos` from 1.33.1 to 1.34.0
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/b1ae8d918b6e85bd611117d3d9a3be4f903ee5e4...392b78fe18a52790c53f42456e46124f77346842)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-05 09:12:21 +00:00
40 changed files with 6605 additions and 1358 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
with:
prompt-file: issue_analysis.txt
system-prompt: |
You are an issue validation assistant for Donut Browser, a browser orchestrator application.
You are an issue validation assistant for Donut Browser, an browser orchestrator.
Analyze the provided issue content and determine if it contains sufficient information based on these requirements:
@@ -62,7 +62,7 @@ jobs:
with:
prompt-file: commits.txt
system-prompt: |
You are an expert technical writer tasked with generating comprehensive release notes for Donut Browser, a powerful browser orchestrator application.
You are an expert technical writer tasked with generating comprehensive release notes for Donut Browser, a powerful browser orchestrator.
Analyze the provided commit messages and generate well-structured release notes following this format:
+1 -1
View File
@@ -23,4 +23,4 @@ jobs:
- name: Checkout Actions Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Spell Check Repo
uses: crate-ci/typos@b1ae8d918b6e85bd611117d3d9a3be4f903ee5e4 #v1.33.1
uses: crate-ci/typos@392b78fe18a52790c53f42456e46124f77346842 #v1.34.0
+22 -1
View File
@@ -8,6 +8,7 @@
"autoconfig",
"autologin",
"biomejs",
"camoufox",
"cdylib",
"CFURL",
"checkin",
@@ -15,6 +16,8 @@
"clippy",
"cmdk",
"codegen",
"CTYPE",
"datareporting",
"devedition",
"doesn",
"donutbrowser",
@@ -26,13 +29,18 @@
"esac",
"esbuild",
"frontmost",
"geoip",
"gettimezone",
"gifs",
"gsettings",
"healthreport",
"hkcu",
"icns",
"idletime",
"Inno",
"KHTML",
"launchservices",
"letterboxing",
"libatk",
"libayatana",
"libcairo",
@@ -42,6 +50,8 @@
"librsvg",
"libwebkit",
"libxdo",
"localtime",
"mmdb",
"mountpoint",
"msiexec",
"msvc",
@@ -56,8 +66,10 @@
"objc",
"orhun",
"osascript",
"peerconnection",
"pixbuf",
"plasmohq",
"prefs",
"propertylist",
"reqwest",
"ridedott",
@@ -68,19 +80,25 @@
"shadcn",
"signon",
"sonner",
"splitn",
"sspi",
"staticlib",
"stefanzweifel",
"subdirs",
"subkey",
"SUPPRESSMSGBOXES",
"swatinem",
"sysinfo",
"systempreferences",
"systemsetup",
"taskkill",
"tasklist",
"tauri",
"TERX",
"timedatectl",
"titlebar",
"Torbrowser",
"trackingprotection",
"turbopack",
"udeps",
"unlisten",
@@ -89,9 +107,12 @@
"urlencoding",
"vercel",
"VERYSILENT",
"webgl",
"webrtc",
"winreg",
"wiremock",
"xattr",
"zhom"
"zhom",
"zoneinfo"
]
}
+3 -2
View File
@@ -21,10 +21,11 @@
"author": "",
"license": "AGPL-3.0",
"dependencies": {
"@types/node": "^24.0.7",
"@types/node": "^24.0.10",
"@yao-pkg/pkg": "^6.5.1",
"camoufox-js": "^0.6.0",
"commander": "^14.0.0",
"dotenv": "^17.0.0",
"dotenv": "^17.0.1",
"get-port": "^7.1.0",
"nodemon": "^3.1.10",
"proxy-chain": "^2.5.9",
+503
View File
@@ -0,0 +1,503 @@
import { spawn } from "child_process";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
export interface CamoufoxConfig {
id: string;
pid?: number;
executablePath: string;
profilePath: string;
url?: string;
options: CamoufoxLaunchOptions;
}
export interface CamoufoxLaunchOptions {
// Operating system to use for fingerprint generation
os?: "windows" | "macos" | "linux" | string[];
// Blocking options
block_images?: boolean;
block_webrtc?: boolean;
block_webgl?: boolean;
// Security options
disable_coop?: boolean;
// Geolocation options
geoip?: string | boolean;
// UI behavior
humanize?: boolean | number;
// Localization
locale?: string | string[];
// Extensions and fonts
addons?: string[];
fonts?: string[];
custom_fonts_only?: boolean;
exclude_addons?: string[];
// Screen and window
screen?: {
minWidth?: number;
maxWidth?: number;
minHeight?: number;
maxHeight?: number;
};
window?: [number, number];
// Fingerprint
fingerprint?: any;
// Version and mode
ff_version?: number;
headless?: boolean;
main_world_eval?: boolean;
// Custom executable path
executable_path?: string;
// Firefox preferences
firefox_user_prefs?: Record<string, any>;
// Proxy settings
proxy?:
| string
| {
server: string;
username?: string;
password?: string;
bypass?: string;
};
// Cache and performance
enable_cache?: boolean;
// Additional options
args?: string[];
env?: Record<string, string | number | boolean>;
debug?: boolean;
virtual_display?: string;
webgl_config?: [string, string];
// Custom options
timezone?: string;
country?: string;
geolocation?: {
latitude: number;
longitude: number;
accuracy?: number;
};
}
// Store for active Camoufox processes
const activeCamoufoxProcesses = new Map<string, CamoufoxConfig>();
/**
* Generate a unique ID for the Camoufox instance
*/
function generateCamoufoxId(): string {
return `camoufox_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Save Camoufox configuration to storage
*/
function saveCamoufoxConfig(config: CamoufoxConfig): void {
try {
const configDir = path.join(os.tmpdir(), "nodecar_camoufox");
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
const configFile = path.join(configDir, `${config.id}.json`);
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
activeCamoufoxProcesses.set(config.id, config);
} catch (error) {
console.error(`Failed to save Camoufox config: ${error}`);
}
}
/**
* Load Camoufox configuration from storage
*/
function loadCamoufoxConfig(id: string): CamoufoxConfig | null {
try {
const configFile = path.join(os.tmpdir(), "nodecar_camoufox", `${id}.json`);
if (fs.existsSync(configFile)) {
const config = JSON.parse(fs.readFileSync(configFile, "utf8"));
activeCamoufoxProcesses.set(id, config);
return config;
}
} catch (error) {
console.error(`Failed to load Camoufox config: ${error}`);
}
return null;
}
/**
* Delete Camoufox configuration from storage
*/
function deleteCamoufoxConfig(id: string): boolean {
try {
const configFile = path.join(os.tmpdir(), "nodecar_camoufox", `${id}.json`);
if (fs.existsSync(configFile)) {
fs.unlinkSync(configFile);
}
activeCamoufoxProcesses.delete(id);
return true;
} catch (error) {
console.error(`Failed to delete Camoufox config: ${error}`);
return false;
}
}
/**
* Load all Camoufox configurations on startup
*/
function loadAllCamoufoxConfigs(): void {
try {
const configDir = path.join(os.tmpdir(), "nodecar_camoufox");
if (fs.existsSync(configDir)) {
const files = fs.readdirSync(configDir);
for (const file of files) {
if (file.endsWith(".json")) {
const id = path.basename(file, ".json");
loadCamoufoxConfig(id);
}
}
}
} catch (error) {
console.error(`Failed to load Camoufox configs: ${error}`);
}
}
/**
* Check if a process is still running
*/
function isProcessRunning(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return false;
}
}
/**
* Convert Camoufox options to command line arguments
*/
function buildCamoufoxArgs(
options: CamoufoxLaunchOptions,
profilePath: string,
url?: string,
): string[] {
const args: string[] = [];
// Always use profile
args.push("-profile", profilePath);
// Cache enabled by default as requested
if (options.enable_cache !== false) {
// Cache is enabled by default in Camoufox, no special args needed
}
// Headless mode
if (options.headless) {
args.push("-headless");
}
// No remote for security (anti-detect)
args.push("-no-remote");
// Custom Firefox user preferences will be written to user.js in profile
// Additional custom args
if (options.args) {
args.push(...options.args);
}
// URL to open
if (url) {
args.push(url);
}
return args;
}
/**
* Create user.js file with Camoufox preferences
*/
function createUserJs(
profilePath: string,
options: CamoufoxLaunchOptions,
): void {
const preferences: string[] = [];
// Anti-detect preferences
preferences.push('user_pref("privacy.resistFingerprinting", true);');
preferences.push(
'user_pref("privacy.resistFingerprinting.letterboxing", true);',
);
preferences.push('user_pref("privacy.trackingprotection.enabled", true);');
// Disable telemetry and data collection
preferences.push(
'user_pref("datareporting.healthreport.uploadEnabled", false);',
);
preferences.push(
'user_pref("datareporting.policy.dataSubmissionEnabled", false);',
);
preferences.push('user_pref("toolkit.telemetry.enabled", false);');
preferences.push('user_pref("toolkit.telemetry.unified", false);');
// Block options
if (options.block_images) {
preferences.push('user_pref("permissions.default.image", 2);');
}
if (options.block_webrtc) {
preferences.push('user_pref("media.peerconnection.enabled", false);');
preferences.push('user_pref("media.navigator.enabled", false);');
}
if (options.block_webgl) {
preferences.push('user_pref("webgl.disabled", true);');
preferences.push('user_pref("webgl.disable-extensions", true);');
}
// COOP settings
if (options.disable_coop) {
preferences.push(
'user_pref("browser.tabs.remote.useCrossOriginOpenerPolicy", false);',
);
}
// Locale settings
if (options.locale) {
const localeStr = Array.isArray(options.locale)
? options.locale[0]
: options.locale;
preferences.push(`user_pref("intl.locale.requested", "${localeStr}");`);
preferences.push(`user_pref("general.useragent.locale", "${localeStr}");`);
}
// Timezone
if (options.timezone) {
preferences.push(
`user_pref("privacy.resistFingerprinting.timezone", "${options.timezone}");`,
);
}
// Custom Firefox preferences
if (options.firefox_user_prefs) {
for (const [key, value] of Object.entries(options.firefox_user_prefs)) {
if (typeof value === "string") {
preferences.push(`user_pref("${key}", "${value}");`);
} else if (typeof value === "boolean") {
preferences.push(`user_pref("${key}", ${value});`);
} else if (typeof value === "number") {
preferences.push(`user_pref("${key}", ${value});`);
}
}
}
// Proxy settings
if (options.proxy) {
if (typeof options.proxy === "string") {
// Parse proxy URL
try {
const proxyUrl = new URL(options.proxy);
const port =
parseInt(proxyUrl.port) ||
(proxyUrl.protocol === "https:" ? 443 : 80);
if (proxyUrl.protocol.startsWith("socks")) {
preferences.push('user_pref("network.proxy.type", 1);');
preferences.push(
`user_pref("network.proxy.socks", "${proxyUrl.hostname}");`,
);
preferences.push(`user_pref("network.proxy.socks_port", ${port});`);
if (proxyUrl.protocol === "socks5:") {
preferences.push('user_pref("network.proxy.socks_version", 5);');
} else {
preferences.push('user_pref("network.proxy.socks_version", 4);');
}
} else {
preferences.push('user_pref("network.proxy.type", 1);');
preferences.push(
`user_pref("network.proxy.http", "${proxyUrl.hostname}");`,
);
preferences.push(`user_pref("network.proxy.http_port", ${port});`);
preferences.push(
`user_pref("network.proxy.ssl", "${proxyUrl.hostname}");`,
);
preferences.push(`user_pref("network.proxy.ssl_port", ${port});`);
}
if (proxyUrl.username && proxyUrl.password) {
// Note: Basic auth for proxies is handled differently in modern Firefox
preferences.push(
'user_pref("network.proxy.allow_hijacking_localhost", true);',
);
}
} catch (error) {
console.error(`Invalid proxy URL: ${options.proxy}`);
}
}
}
// Geolocation
if (options.geolocation) {
preferences.push('user_pref("geo.enabled", true);');
preferences.push(
`user_pref("geo.wifi.uri", "data:application/json,{\\"location\\": {\\"lat\\": ${options.geolocation.latitude}, \\"lng\\": ${options.geolocation.longitude}}, \\"accuracy\\": ${options.geolocation.accuracy || 100}}");`,
);
} else {
preferences.push('user_pref("geo.enabled", false);');
}
// Write user.js file
const userJsPath = path.join(profilePath, "user.js");
fs.writeFileSync(userJsPath, preferences.join("\n"));
}
/**
* Launch Camoufox browser with specified options
*/
export async function launchCamoufox(
executablePath: string,
profilePath: string,
options: CamoufoxLaunchOptions = {},
url?: string,
): Promise<CamoufoxConfig> {
const id = generateCamoufoxId();
// Ensure profile directory exists
if (!fs.existsSync(profilePath)) {
fs.mkdirSync(profilePath, { recursive: true });
}
// Create user.js with preferences
createUserJs(profilePath, options);
// Build command line arguments
const args = buildCamoufoxArgs(options, profilePath, url);
// Prepare environment variables
const env = {
...process.env,
...options.env,
};
// Handle virtual display
if (options.virtual_display) {
env.DISPLAY = options.virtual_display;
}
// Launch the process
const child = spawn(executablePath, args, {
env: env as NodeJS.ProcessEnv,
detached: true,
stdio: options.debug ? "inherit" : "ignore",
});
if (!child.pid) {
throw new Error("Failed to launch Camoufox process");
}
const config: CamoufoxConfig = {
id,
pid: child.pid,
executablePath,
profilePath,
url,
options,
};
// Save configuration
saveCamoufoxConfig(config);
// Handle process exit
child.on("exit", (code, signal) => {
console.log(
`Camoufox process ${child.pid} exited with code ${code}, signal ${signal}`,
);
deleteCamoufoxConfig(id);
});
child.on("error", (error) => {
console.error(`Camoufox process error: ${error}`);
deleteCamoufoxConfig(id);
});
// Detach the child process so it can continue running independently
child.unref();
return config;
}
/**
* Stop a Camoufox process by ID
*/
export async function stopCamoufox(id: string): Promise<boolean> {
const config = activeCamoufoxProcesses.get(id) || loadCamoufoxConfig(id);
if (!config || !config.pid) {
return false;
}
try {
if (isProcessRunning(config.pid)) {
process.kill(config.pid, "SIGTERM");
// Wait a moment for graceful shutdown
await new Promise((resolve) => setTimeout(resolve, 2000));
// Force kill if still running
if (isProcessRunning(config.pid)) {
process.kill(config.pid, "SIGKILL");
}
}
deleteCamoufoxConfig(id);
return true;
} catch (error) {
console.error(`Failed to stop Camoufox process: ${error}`);
return false;
}
}
/**
* List all Camoufox processes
*/
export function listCamoufoxProcesses(): any[] {
loadAllCamoufoxConfigs();
// Filter out dead processes
const activeConfigs: any[] = [];
for (const [id, config] of activeCamoufoxProcesses) {
if (config.pid && isProcessRunning(config.pid)) {
// Return in snake_case format for Rust compatibility
activeConfigs.push({
id: config.id,
pid: config.pid,
executable_path: config.executablePath,
profile_path: config.profilePath,
url: config.url,
options: config.options,
});
} else {
// Clean up dead processes
deleteCamoufoxConfig(id);
}
}
return activeConfigs;
}
// Load existing configurations on module initialization
loadAllCamoufoxConfigs();
+278
View File
@@ -1,4 +1,9 @@
import { program } from "commander";
import {
launchCamoufox,
listCamoufoxProcesses,
stopCamoufox,
} from "./camoufox-launcher";
import {
startProxyProcess,
stopAllProxyProcesses,
@@ -149,4 +154,277 @@ program
}
});
// Command for Camoufox browser orchestrator
program
.command("camoufox")
.argument("<action>", "launch, stop, list, or open-url for Camoufox browser")
.requiredOption("--executable-path <path>", "path to Camoufox executable")
.requiredOption("--profile-path <path>", "path to browser profile directory")
.option("--url <url>", "URL to open")
.option("--id <id>", "Camoufox instance ID (for stop/open-url actions)")
// Operating system fingerprinting
.option(
"--os <os>",
"OS to emulate (windows, macos, linux, or comma-separated list)",
)
// Blocking options
.option("--block-images", "block all images")
.option("--block-webrtc", "block WebRTC entirely")
.option("--block-webgl", "block WebGL")
// Security options
.option("--disable-coop", "disable Cross-Origin-Opener-Policy")
// Geolocation and IP
.option(
"--geoip <ip>",
"IP address for geolocation spoofing (or 'auto' for automatic)",
)
.option("--country <country>", "country code for geolocation")
.option("--timezone <timezone>", "timezone to spoof")
.option("--latitude <lat>", "latitude for geolocation", parseFloat)
.option("--longitude <lng>", "longitude for geolocation", parseFloat)
// UI and behavior
.option(
"--humanize [duration]",
"humanize cursor movement (optional max duration in seconds)",
(val) => (val ? parseFloat(val) : true),
)
.option("--headless", "run in headless mode")
// Localization
.option("--locale <locale>", "locale(s) to use (comma-separated)")
// Extensions and fonts
.option("--addons <addons>", "Firefox addons to load (comma-separated paths)")
.option("--fonts <fonts>", "additional fonts to load (comma-separated)")
.option("--custom-fonts-only", "use only custom fonts, exclude OS fonts")
.option(
"--exclude-addons <addons>",
"default addons to exclude (comma-separated)",
)
// Screen and window
.option("--screen-min-width <width>", "minimum screen width", parseInt)
.option("--screen-max-width <width>", "maximum screen width", parseInt)
.option("--screen-min-height <height>", "minimum screen height", parseInt)
.option("--screen-max-height <height>", "maximum screen height", parseInt)
.option("--window-width <width>", "fixed window width", parseInt)
.option("--window-height <height>", "fixed window height", parseInt)
// Advanced options
.option("--ff-version <version>", "Firefox version to emulate", parseInt)
.option("--main-world-eval", "enable main world script evaluation")
.option("--webgl-vendor <vendor>", "WebGL vendor string")
.option("--webgl-renderer <renderer>", "WebGL renderer string")
// Proxy
.option(
"--proxy <proxy>",
"proxy URL (protocol://[username:password@]host:port)",
)
// Cache and performance
.option("--disable-cache", "disable browser cache (cache enabled by default)")
// Environment and debugging
.option("--virtual-display <display>", "virtual display number (e.g., :99)")
.option("--debug", "enable debug output")
.option("--args <args>", "additional browser arguments (comma-separated)")
.option("--env <env>", "environment variables (JSON string)")
// Firefox preferences
.option("--firefox-prefs <prefs>", "Firefox user preferences (JSON string)")
.description("launch and manage Camoufox browser orchestrator instances")
.action(async (action: string, options: any) => {
try {
if (action === "launch") {
// Validate required options
if (!options.executablePath || !options.profilePath) {
console.error(
"Error: --executable-path and --profile-path are required for launch",
);
process.exit(1);
return;
}
// Build Camoufox options
const camoufoxOptions: any = {
enable_cache: !options.disableCache, // Cache enabled by default as requested
};
// OS fingerprinting
if (options.os) {
camoufoxOptions.os = options.os.includes(",")
? options.os.split(",")
: options.os;
}
// Blocking options
if (options.blockImages) camoufoxOptions.block_images = true;
if (options.blockWebrtc) camoufoxOptions.block_webrtc = true;
if (options.blockWebgl) camoufoxOptions.block_webgl = true;
// Security options
if (options.disableCoop) camoufoxOptions.disable_coop = true;
// Geolocation
if (options.geoip) {
camoufoxOptions.geoip =
options.geoip === "auto" ? true : options.geoip;
}
if (options.latitude && options.longitude) {
camoufoxOptions.geolocation = {
latitude: options.latitude,
longitude: options.longitude,
accuracy: 100,
};
}
if (options.country) camoufoxOptions.country = options.country;
if (options.timezone) camoufoxOptions.timezone = options.timezone;
// UI and behavior
if (options.humanize) camoufoxOptions.humanize = options.humanize;
if (options.headless) camoufoxOptions.headless = true;
// Localization
if (options.locale) {
camoufoxOptions.locale = options.locale.includes(",")
? options.locale.split(",")
: options.locale;
}
// Extensions and fonts
if (options.addons) camoufoxOptions.addons = options.addons.split(",");
if (options.fonts) camoufoxOptions.fonts = options.fonts.split(",");
if (options.customFontsOnly) camoufoxOptions.custom_fonts_only = true;
if (options.excludeAddons)
camoufoxOptions.exclude_addons = options.excludeAddons.split(",");
// Screen and window
const screen: any = {};
if (options.screenMinWidth) screen.minWidth = options.screenMinWidth;
if (options.screenMaxWidth) screen.maxWidth = options.screenMaxWidth;
if (options.screenMinHeight) screen.minHeight = options.screenMinHeight;
if (options.screenMaxHeight) screen.maxHeight = options.screenMaxHeight;
if (Object.keys(screen).length > 0) camoufoxOptions.screen = screen;
if (options.windowWidth && options.windowHeight) {
camoufoxOptions.window = [options.windowWidth, options.windowHeight];
}
// Advanced options
if (options.ffVersion) camoufoxOptions.ff_version = options.ffVersion;
if (options.mainWorldEval) camoufoxOptions.main_world_eval = true;
if (options.webglVendor && options.webglRenderer) {
camoufoxOptions.webgl_config = [
options.webglVendor,
options.webglRenderer,
];
}
// Proxy
if (options.proxy) camoufoxOptions.proxy = options.proxy;
// Environment and debugging
if (options.virtualDisplay)
camoufoxOptions.virtual_display = options.virtualDisplay;
if (options.debug) camoufoxOptions.debug = true;
if (options.args) camoufoxOptions.args = options.args.split(",");
if (options.env) {
try {
camoufoxOptions.env = JSON.parse(options.env);
} catch (e) {
console.error("Invalid JSON for --env option");
process.exit(1);
return;
}
}
// Firefox preferences
if (options.firefoxPrefs) {
try {
camoufoxOptions.firefox_user_prefs = JSON.parse(
options.firefoxPrefs,
);
} catch (e) {
console.error("Invalid JSON for --firefox-prefs option");
process.exit(1);
return;
}
}
// Launch Camoufox
const config = await launchCamoufox(
options.executablePath,
options.profilePath,
camoufoxOptions,
options.url,
);
// Output the configuration as JSON for the Rust side to parse
console.log(
JSON.stringify({
id: config.id,
pid: config.pid,
executable_path: config.executablePath,
profile_path: config.profilePath,
url: config.url,
}),
);
process.exit(0);
} else if (action === "stop") {
if (!options.id) {
console.error("Error: --id is required for stop action");
process.exit(1);
return;
}
const success = await stopCamoufox(options.id);
console.log(JSON.stringify({ success }));
process.exit(0);
} else if (action === "list") {
const processes = listCamoufoxProcesses();
// Convert camelCase to snake_case for Rust compatibility
const rustCompatibleProcesses = processes.map((process) => ({
id: process.id,
pid: process.pid,
executable_path: process.executablePath,
profile_path: process.profilePath,
url: process.url,
}));
console.log(JSON.stringify(rustCompatibleProcesses));
process.exit(0);
} else if (action === "open-url") {
if (!options.id || !options.url) {
console.error(
"Error: --id and --url are required for open-url action",
);
process.exit(1);
return;
}
// This would require implementing URL opening in existing instance
// For now, we'll return an error as this feature would need additional implementation
console.error("open-url action is not yet implemented");
process.exit(1);
} else {
console.error(
"Invalid action. Use 'launch', 'stop', 'list', or 'open-url'",
);
process.exit(1);
}
} catch (error: unknown) {
console.error(
`Camoufox command failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`,
);
process.exit(1);
}
});
program.parse();
+6 -5
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.6.1",
"version": "0.7.2",
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
@@ -33,6 +33,7 @@
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-table": "^8.21.3",
"@tauri-apps/api": "^2.6.0",
@@ -44,12 +45,12 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"next": "^15.3.4",
"next": "^15.3.5",
"next-themes": "^0.4.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"sonner": "^2.0.5",
"sonner": "^2.0.6",
"tailwind-merge": "^3.3.1",
"tauri-plugin-macos-permissions-api": "^2.3.0"
},
@@ -57,7 +58,7 @@
"@biomejs/biome": "2.0.6",
"@tailwindcss/postcss": "^4.1.11",
"@tauri-apps/cli": "^2.6.2",
"@types/node": "^24.0.10",
"@types/node": "^24.0.13",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
@@ -65,7 +66,7 @@
"lint-staged": "^16.1.2",
"tailwindcss": "^4.1.11",
"ts-unused-exports": "^11.0.1",
"tw-animate-css": "^1.3.4",
"tw-animate-css": "^1.3.5",
"typescript": "~5.8.3"
},
"packageManager": "pnpm@10.11.1",
+1555 -477
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,9 +1,9 @@
packages:
- "nodecar"
- nodecar
onlyBuiltDependencies:
- "@biomejs/biome"
- "@tailwindcss/oxide"
- '@biomejs/biome'
- '@tailwindcss/oxide'
- esbuild
- sharp
- sqlite3
- unrs-resolver
+51 -32
View File
@@ -124,9 +124,9 @@ dependencies = [
[[package]]
name = "async-channel"
version = "2.3.1"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
checksum = "16c74e56284d2188cabb6ad99603d1ace887a5d7e7b695d01b728155ed9ed427"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
@@ -496,9 +496,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.27"
version = "1.2.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
checksum = "4ad45f4f74e4e20eaa392913b7b33a7091c87e59628f4dd27888205ad888843c"
dependencies = [
"jobserver",
"libc",
@@ -971,7 +971,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.6.1"
version = "0.7.2"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -1123,7 +1123,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -1662,9 +1662,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.4.10"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5"
checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785"
dependencies = [
"atomic-waker",
"bytes",
@@ -2060,6 +2060,17 @@ dependencies = [
"generic-array",
]
[[package]]
name = "io-uring"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"libc",
]
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -3491,9 +3502,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.12.20"
version = "0.12.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -3573,13 +3584,12 @@ dependencies = [
[[package]]
name = "rust-ini"
version = "0.21.1"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
checksum = "e7295b7ce3bf4806b419dc3420745998b447178b7005e2011947b38fc5aa6791"
dependencies = [
"cfg-if",
"ordered-multimap",
"trim-in-place",
]
[[package]]
@@ -3607,7 +3617,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3700,6 +3710,18 @@ dependencies = [
"serde_json",
]
[[package]]
name = "schemars"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1375ba8ef45a6f15d83fa8748f1079428295d403d6ea991d09ab100155fbc06d"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "schemars_derive"
version = "0.8.22"
@@ -3856,9 +3878,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.13.0"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42"
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -3866,6 +3888,7 @@ dependencies = [
"indexmap 1.9.3",
"indexmap 2.9.0",
"schemars 0.9.0",
"schemars 1.0.3",
"serde",
"serde_derive",
"serde_json",
@@ -3875,9 +3898,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.13.0"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77"
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
dependencies = [
"darling",
"proc-macro2",
@@ -3941,9 +3964,9 @@ dependencies = [
[[package]]
name = "shared_child"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2778001df1384cf20b6dc5a5a90f48da35539885edaaefd887f8d744e939c0b"
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
dependencies = [
"libc",
"sigchld",
@@ -3958,9 +3981,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "sigchld"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1219ef50fc0fdb04fcc243e6aa27f855553434ffafe4fa26554efb78b5b4bf89"
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
dependencies = [
"libc",
"os_pipe",
@@ -4652,7 +4675,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4758,17 +4781,19 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.45.1"
version = "1.46.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
dependencies = [
"backtrace",
"bytes",
"io-uring",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2",
"tokio-macros",
"tracing",
@@ -4990,12 +5015,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "trim-in-place"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
[[package]]
name = "try-lock"
version = "0.2.5"
@@ -5412,7 +5431,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.6.1"
version = "0.7.2"
description = "Simple Yet Powerful Browser Orchestrator"
authors = ["zhom@github"]
edition = "2021"
+272 -10
View File
@@ -9,21 +9,21 @@ use std::time::{SystemTime, UNIX_EPOCH};
use crate::browser::GithubRelease;
#[derive(Debug, Clone, PartialEq, Eq)]
struct VersionComponent {
major: u32,
minor: u32,
patch: u32,
pre_release: Option<PreRelease>,
pub struct VersionComponent {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub pre_release: Option<PreRelease>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PreRelease {
kind: PreReleaseKind,
number: Option<u32>,
pub struct PreRelease {
pub kind: PreReleaseKind,
pub number: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum PreReleaseKind {
pub enum PreReleaseKind {
Alpha,
Beta,
RC,
@@ -32,7 +32,7 @@ enum PreReleaseKind {
}
impl VersionComponent {
fn parse(version: &str) -> Self {
pub fn parse(version: &str) -> Self {
let version = version.trim();
// Handle special case for Zen Browser twilight releases
@@ -259,6 +259,10 @@ pub fn is_browser_version_nightly(
// Chromium builds are generally stable snapshots
false
}
"camoufox" => {
// For Camoufox, beta versions are actually the stable releases
false
}
_ => {
// Default fallback
is_nightly_version(version)
@@ -856,6 +860,31 @@ impl ApiClient {
}
/// Check if a Brave release has compatible assets for the given platform and architecture
fn has_compatible_camoufox_asset(
&self,
assets: &[crate::browser::GithubAsset],
os: &str,
arch: &str,
) -> bool {
let (os_name, arch_name) = match (os, arch) {
("windows", "x64") => ("win", "x86_64"),
("windows", "arm64") => ("win", "arm64"),
("linux", "x64") => ("lin", "x86_64"),
("linux", "arm64") => ("lin", "arm64"),
("macos", "x64") => ("mac", "x86_64"),
("macos", "arm64") => ("mac", "arm64"),
_ => return false,
};
// Look for assets matching the pattern: camoufox-{version}-{release}-{os}.{arch}.zip
assets.iter().any(|asset| {
let name = asset.name.to_lowercase();
name.starts_with("camoufox-")
&& name.contains(&format!("-{os_name}.{arch_name}.zip"))
&& name.ends_with(".zip")
})
}
fn has_compatible_brave_asset(
assets: &[crate::browser::GithubAsset],
os: &str,
@@ -996,6 +1025,128 @@ impl ApiClient {
)
}
pub async fn fetch_camoufox_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_releases) = self.load_cached_github_releases("camoufox") {
println!(
"Using cached Camoufox releases, count: {}",
cached_releases.len()
);
return Ok(cached_releases);
}
}
println!("Fetching Camoufox releases from GitHub API...");
let url = format!(
"{}/repos/daijro/camoufox/releases?per_page=100",
self.github_api_base
);
let response = self
.client
.get(url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
.send()
.await?;
if !response.status().is_success() {
return Err(format!("GitHub API returned status: {}", response.status()).into());
}
// Get the response text first for better error reporting
let response_text = response.text().await?;
// Try to parse the JSON with better error handling
let releases: Vec<GithubRelease> = match serde_json::from_str(&response_text) {
Ok(releases) => releases,
Err(e) => {
eprintln!("Failed to parse GitHub API response for Camoufox releases:");
eprintln!("Error: {e}");
eprintln!(
"Response text (first 500 chars): {}",
if response_text.len() > 500 {
&response_text[..500]
} else {
&response_text
}
);
return Err(format!("Failed to parse GitHub API response: {e}").into());
}
};
println!(
"Fetched {} total Camoufox releases from GitHub",
releases.len()
);
// Get platform info to filter appropriate releases
let (os, arch) = Self::get_platform_info();
println!("Filtering for platform: {os}/{arch}");
// Filter releases that have assets compatible with the current platform
let mut compatible_releases: Vec<GithubRelease> = releases
.into_iter()
.enumerate()
.filter_map(|(i, release)| {
let has_compatible = self.has_compatible_camoufox_asset(&release.assets, &os, &arch);
if !has_compatible {
println!(
"Release {} ({}) has no compatible assets for {}/{}",
i, release.tag_name, os, arch
);
println!(
" Available assets: {:?}",
release.assets.iter().map(|a| &a.name).collect::<Vec<_>>()
);
}
if has_compatible {
Some(release)
} else {
None
}
})
.collect();
println!(
"After platform filtering: {} compatible releases",
compatible_releases.len()
);
// Sort by version (latest first) with debugging
println!(
"Before sorting: {:?}",
compatible_releases
.iter()
.map(|r| &r.tag_name)
.take(10)
.collect::<Vec<_>>()
);
sort_github_releases(&mut compatible_releases);
println!(
"After sorting: {:?}",
compatible_releases
.iter()
.map(|r| &r.tag_name)
.take(10)
.collect::<Vec<_>>()
);
// Cache the results (unless bypassing cache)
if !no_caching {
if let Err(e) = self.save_cached_github_releases("camoufox", &compatible_releases) {
eprintln!("Failed to cache Camoufox releases: {e}");
} else {
println!("Cached {} Camoufox releases", compatible_releases.len());
}
}
Ok(compatible_releases)
}
pub async fn fetch_tor_releases_with_caching(
&self,
no_caching: bool,
@@ -1798,4 +1949,115 @@ mod tests {
let result = client.fetch_zen_releases_with_caching(true).await;
assert!(result.is_err());
}
#[test]
fn test_camoufox_beta_version_parsing() {
// Test specific Camoufox beta versions that are causing issues
let v22 = VersionComponent::parse("135.0.5beta22");
let v24 = VersionComponent::parse("135.0.5beta24");
println!("v22: {v22:?}");
println!("v24: {v24:?}");
// v24 should be greater than v22
assert!(
v24 > v22,
"135.0.5beta24 should be greater than 135.0.5beta22"
);
// Test other beta version combinations
let v1 = VersionComponent::parse("135.0.5beta1");
let v2 = VersionComponent::parse("135.0.5beta2");
assert!(v2 > v1, "135.0.5beta2 should be greater than 135.0.5beta1");
// Test sorting of multiple versions
let mut versions = vec![
"135.0.5beta22".to_string(),
"135.0.5beta24".to_string(),
"135.0.5beta23".to_string(),
"135.0.5beta21".to_string(),
];
sort_versions(&mut versions);
println!("Sorted versions: {versions:?}");
// Should be sorted from newest to oldest
assert_eq!(versions[0], "135.0.5beta24");
assert_eq!(versions[1], "135.0.5beta23");
assert_eq!(versions[2], "135.0.5beta22");
assert_eq!(versions[3], "135.0.5beta21");
}
#[test]
fn test_camoufox_user_reported_versions() {
// Test the exact versions reported by the user: 135.0.1beta24 vs 135.0beta22
let v22 = VersionComponent::parse("135.0beta22");
let v24 = VersionComponent::parse("135.0.1beta24");
println!("User reported v22: {v22:?}");
println!("User reported v24: {v24:?}");
// 135.0.1beta24 should be greater than 135.0beta22 (newer patch version)
assert!(
v24 > v22,
"135.0.1beta24 should be greater than 135.0beta22, but got: v24={v24:?} vs v22={v22:?}"
);
// Test sorting of the exact user-reported versions
let mut versions = vec!["135.0beta22".to_string(), "135.0.1beta24".to_string()];
sort_versions(&mut versions);
println!("User reported sorted versions: {versions:?}");
// Should be sorted from newest to oldest
assert_eq!(
versions[0], "135.0.1beta24",
"135.0.1beta24 should be first (newest)"
);
assert_eq!(
versions[1], "135.0beta22",
"135.0beta22 should be second (older)"
);
}
#[test]
fn test_camoufox_version_classification() {
// Test that Camoufox beta versions are now correctly classified as stable (not nightly)
assert!(
!is_browser_version_nightly("camoufox", "135.0beta22", None),
"135.0beta22 should be classified as stable for Camoufox"
);
assert!(
!is_browser_version_nightly("camoufox", "135.0.1beta24", None),
"135.0.1beta24 should be classified as stable for Camoufox"
);
// Test with release names too - beta releases should be stable
assert!(
!is_browser_version_nightly("camoufox", "135.0beta22", Some("Release Beta 22")),
"Release with 'Beta' in name should be classified as stable for Camoufox"
);
// Test that stable versions are not classified as nightly
assert!(
!is_browser_version_nightly("camoufox", "135.0", None),
"135.0 should be classified as stable"
);
assert!(
!is_browser_version_nightly("camoufox", "135.0.1", None),
"135.0.1 should be classified as stable"
);
// Test alpha and RC versions are still considered nightly
assert!(
!is_browser_version_nightly("camoufox", "136.0alpha1", None),
"136.0alpha1 should not be classified as nightly/prerelease"
);
assert!(
!is_browser_version_nightly("camoufox", "136.0rc1", None),
"136.0rc1 should not be classified as nightly/prerelease"
);
}
}
+1 -1
View File
@@ -455,7 +455,7 @@ impl AppAutoUpdater {
percentage: Some(percentage),
speed: Some(format!("{speed:.1}")),
eta: Some(eta),
message: format!("Downloading update... {percentage:.1}%"),
message: "Downloading update...".to_string(),
},
);
+71 -24
View File
@@ -407,22 +407,17 @@ impl AutoUpdater {
}
fn is_version_newer(&self, version1: &str, version2: &str) -> bool {
self.compare_versions(version1, version2) == std::cmp::Ordering::Greater
// Use the proper VersionComponent comparison from api_client.rs
let version_a = crate::api_client::VersionComponent::parse(version1);
let version_b = crate::api_client::VersionComponent::parse(version2);
version_a > version_b
}
fn compare_versions(&self, version1: &str, version2: &str) -> std::cmp::Ordering {
// Basic semantic version comparison
let v1_parts = self.parse_version(version1);
let v2_parts = self.parse_version(version2);
v1_parts.cmp(&v2_parts)
}
fn parse_version(&self, version: &str) -> Vec<u32> {
version
.split(&['.', 'a', 'b', '-', '_'][..])
.filter_map(|part| part.parse::<u32>().ok())
.collect()
// Use the proper VersionComponent comparison from api_client.rs
let version_a = crate::api_client::VersionComponent::parse(version1);
let version_b = crate::api_client::VersionComponent::parse(version2);
version_a.cmp(&version_b)
}
fn get_auto_update_state_file(&self) -> PathBuf {
@@ -522,6 +517,7 @@ mod tests {
proxy_id: None,
last_launch: None,
release_type: "stable".to_string(),
camoufox_config: None,
}
}
@@ -569,6 +565,68 @@ mod tests {
assert!(!updater.is_version_newer("1.0.0", "1.0.0"));
}
#[test]
fn test_camoufox_beta_version_comparison() {
let updater = AutoUpdater::new();
// Test the exact user-reported scenario: 135.0.1beta24 vs 135.0beta22
assert!(
updater.is_version_newer("135.0.1beta24", "135.0beta22"),
"135.0.1beta24 should be newer than 135.0beta22"
);
assert_eq!(
updater.compare_versions("135.0.1beta24", "135.0beta22"),
std::cmp::Ordering::Greater,
"135.0.1beta24 should compare as greater than 135.0beta22"
);
// Test other camoufox beta version combinations
assert!(
updater.is_version_newer("135.0.5beta24", "135.0.5beta22"),
"135.0.5beta24 should be newer than 135.0.5beta22"
);
assert!(
updater.is_version_newer("135.0.1beta1", "135.0beta1"),
"135.0.1beta1 should be newer than 135.0beta1 due to patch version"
);
// Test that older versions are not considered newer
assert!(
!updater.is_version_newer("135.0beta22", "135.0.1beta24"),
"135.0beta22 should NOT be newer than 135.0.1beta24"
);
}
#[test]
fn test_beta_version_ordering_comprehensive() {
let updater = AutoUpdater::new();
// Test various beta version patterns that could appear in camoufox
let test_cases = vec![
("135.0.1beta24", "135.0beta22", true), // User reported case
("135.0.5beta24", "135.0.5beta22", true), // Same patch, different beta
("135.1beta1", "135.0beta99", true), // Higher minor beats beta number
("136.0beta1", "135.9.9beta99", true), // Higher major beats everything
("135.0.1beta1", "135.0beta1", true), // Patch version matters
("135.0beta22", "135.0.1beta24", false), // Reverse of user case
];
for (newer, older, should_be_newer) in test_cases {
let result = updater.is_version_newer(newer, older);
assert_eq!(
result,
should_be_newer,
"Expected {} {} {} but got {}",
newer,
if should_be_newer { ">" } else { "<=" },
older,
if result { "true" } else { "false" }
);
}
}
#[test]
fn test_check_profile_update_stable_to_stable() {
let updater = AutoUpdater::new();
@@ -853,15 +911,4 @@ mod tests {
let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap();
assert_eq!(loaded_state.pending_updates.len(), 0);
}
#[test]
fn test_parse_version() {
let updater = AutoUpdater::new();
assert_eq!(updater.parse_version("1.2.3"), vec![1, 2, 3]);
assert_eq!(updater.parse_version("1.2.3-alpha"), vec![1, 2, 3]);
assert_eq!(updater.parse_version("1.2.3a1"), vec![1, 2, 3, 1]);
assert_eq!(updater.parse_version("1.2.3b2"), vec![1, 2, 3, 2]);
assert_eq!(updater.parse_version("10.0.0"), vec![10, 0, 0]);
}
}
+103 -1
View File
@@ -19,6 +19,7 @@ pub enum BrowserType {
Brave,
Zen,
TorBrowser,
Camoufox,
}
impl BrowserType {
@@ -31,6 +32,7 @@ impl BrowserType {
BrowserType::Brave => "brave",
BrowserType::Zen => "zen",
BrowserType::TorBrowser => "tor-browser",
BrowserType::Camoufox => "camoufox",
}
}
@@ -43,6 +45,7 @@ impl BrowserType {
"brave" => Ok(BrowserType::Brave),
"zen" => Ok(BrowserType::Zen),
"tor-browser" => Ok(BrowserType::TorBrowser),
"camoufox" => Ok(BrowserType::Camoufox),
_ => Err(format!("Unknown browser type: {s}")),
}
}
@@ -89,6 +92,7 @@ mod macos {
|| name.starts_with("mullvad")
|| name.starts_with("zen")
|| name.starts_with("tor")
|| name.starts_with("camoufox")
|| name.contains("Browser")
})
.map(|entry| entry.path())
@@ -192,6 +196,12 @@ mod linux {
browser_subdir.join("firefox-bin"),
]
}
BrowserType::Camoufox => {
vec![
browser_subdir.join("camoufox"),
browser_subdir.join("camoufox-bin"),
]
}
_ => vec![],
};
@@ -274,6 +284,12 @@ mod linux {
browser_subdir.join("firefox"),
]
}
BrowserType::Camoufox => {
vec![
browser_subdir.join("camoufox-bin"),
browser_subdir.join("camoufox"),
]
}
_ => vec![],
};
@@ -358,6 +374,7 @@ mod windows {
|| name.starts_with("mullvad")
|| name.starts_with("zen")
|| name.starts_with("tor")
|| name.starts_with("camoufox")
|| name.contains("browser")
{
return Ok(path);
@@ -436,6 +453,7 @@ mod windows {
|| name.starts_with("mullvad")
|| name.starts_with("zen")
|| name.starts_with("tor")
|| name.starts_with("camoufox")
|| name.contains("browser")
{
return true;
@@ -532,7 +550,10 @@ impl Browser for FirefoxBrowser {
BrowserType::MullvadBrowser | BrowserType::TorBrowser => {
args.push("-no-remote".to_string());
}
BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen => {
BrowserType::Firefox
| BrowserType::FirefoxDeveloper
| BrowserType::Zen
| BrowserType::Camoufox => {
// Don't use -no-remote so we can communicate with existing instances
}
_ => {}
@@ -693,6 +714,81 @@ impl Browser for ChromiumBrowser {
}
}
pub struct CamoufoxBrowser;
impl CamoufoxBrowser {
pub fn new() -> Self {
Self
}
}
impl Browser for CamoufoxBrowser {
fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
#[cfg(target_os = "macos")]
return macos::get_firefox_executable_path(install_dir);
#[cfg(target_os = "linux")]
return linux::get_firefox_executable_path(install_dir, &BrowserType::Camoufox);
#[cfg(target_os = "windows")]
return windows::get_firefox_executable_path(install_dir);
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
Err("Unsupported platform".into())
}
fn create_launch_args(
&self,
profile_path: &str,
_proxy_settings: Option<&ProxySettings>,
url: Option<String>,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
// For Camoufox, we handle launching through the camoufox launcher
// This method won't be used directly, but we provide basic Firefox args as fallback
let mut args = vec![
"-profile".to_string(),
profile_path.to_string(),
"-no-remote".to_string(),
];
if let Some(url) = url {
args.push(url);
}
Ok(args)
}
fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool {
let install_dir = binaries_dir.join("camoufox").join(version);
#[cfg(target_os = "macos")]
return macos::is_firefox_version_downloaded(&install_dir);
#[cfg(target_os = "linux")]
return linux::is_firefox_version_downloaded(&install_dir, &BrowserType::Camoufox);
#[cfg(target_os = "windows")]
return windows::is_firefox_version_downloaded(&install_dir);
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
false
}
fn prepare_executable(&self, executable_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(target_os = "macos")]
return macos::prepare_executable(executable_path);
#[cfg(target_os = "linux")]
return linux::prepare_executable(executable_path);
#[cfg(target_os = "windows")]
return windows::prepare_executable(executable_path);
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
Err("Unsupported platform".into())
}
}
// Factory function to create browser instances
pub fn create_browser(browser_type: BrowserType) -> Box<dyn Browser> {
match browser_type {
@@ -702,6 +798,7 @@ pub fn create_browser(browser_type: BrowserType) -> Box<dyn Browser> {
| BrowserType::Zen
| BrowserType::TorBrowser => Box::new(FirefoxBrowser::new(browser_type)),
BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)),
BrowserType::Camoufox => Box::new(CamoufoxBrowser::new()),
}
}
@@ -778,6 +875,7 @@ mod tests {
assert_eq!(BrowserType::Brave.as_str(), "brave");
assert_eq!(BrowserType::Zen.as_str(), "zen");
assert_eq!(BrowserType::TorBrowser.as_str(), "tor-browser");
assert_eq!(BrowserType::Camoufox.as_str(), "camoufox");
// Test from_str
assert_eq!(
@@ -802,6 +900,10 @@ mod tests {
BrowserType::from_str("tor-browser").unwrap(),
BrowserType::TorBrowser
);
assert_eq!(
BrowserType::from_str("camoufox").unwrap(),
BrowserType::Camoufox
);
// Test invalid browser type
assert!(BrowserType::from_str("invalid").is_err());
+480 -157
View File
@@ -13,6 +13,7 @@ use crate::browser::{create_browser, BrowserType, ProxySettings};
use crate::browser_version_service::{
BrowserVersionInfo, BrowserVersionService, BrowserVersionsResult,
};
use crate::camoufox::CamoufoxConfig;
use crate::download::{DownloadProgress, Downloader};
use crate::downloaded_browsers::DownloadedBrowsersRegistry;
use crate::extraction::Extractor;
@@ -31,13 +32,15 @@ pub struct BrowserProfile {
pub last_launch: Option<u64>,
#[serde(default = "default_release_type")]
pub release_type: String, // "stable" or "nightly"
#[serde(default)]
pub camoufox_config: Option<CamoufoxConfig>, // Camoufox configuration
}
fn default_release_type() -> String {
"stable".to_string()
}
// Global state to track currently downloading browsers
// Global state to track currently downloading browser-version pairs
lazy_static::lazy_static! {
static ref DOWNLOADING_BROWSERS: Arc<Mutex<HashSet<String>>> = Arc::new(Mutex::new(HashSet::new()));
}
@@ -1347,6 +1350,7 @@ impl BrowserRunner {
version: &str,
release_type: &str,
proxy_id: Option<String>,
camoufox_config: Option<CamoufoxConfig>,
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
println!("Attempting to create profile: {name}");
@@ -1379,6 +1383,7 @@ impl BrowserRunner {
process_id: None,
last_launch: None,
release_type: release_type.to_string(),
camoufox_config: camoufox_config.clone(),
};
// Save profile info
@@ -1772,10 +1777,109 @@ impl BrowserRunner {
pub async fn launch_browser(
&self,
app_handle: tauri::AppHandle,
profile: &BrowserProfile,
url: Option<String>,
local_proxy_settings: Option<&ProxySettings>,
) -> Result<BrowserProfile, Box<dyn std::error::Error + Send + Sync>> {
// Handle camoufox profiles specially
if profile.browser == "camoufox" {
if let Some(mut camoufox_config) = profile.camoufox_config.clone() {
// Handle proxy settings for camoufox
if let Some(proxy_id) = &profile.proxy_id {
if let Some(stored_proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) {
println!("Starting proxy for Camoufox profile: {}", profile.name);
// Start the proxy and get local proxy settings
let local_proxy = PROXY_MANAGER
.start_proxy(
app_handle.clone(),
&stored_proxy,
0, // Use 0 as temporary PID, will be updated later
Some(&profile.name),
)
.await
.map_err(|e| format!("Failed to start proxy for Camoufox: {e}"))?;
// Format proxy URL for camoufox
let proxy_url = format!(
"{}://{}:{}",
if stored_proxy.proxy_type == "socks5" || stored_proxy.proxy_type == "socks4" {
&stored_proxy.proxy_type
} else {
"http"
},
local_proxy.host,
local_proxy.port
);
// Add username and password if available
let proxy_url = if let (Some(username), Some(password)) =
(&stored_proxy.username, &stored_proxy.password)
{
format!(
"{}://{}:{}@{}:{}",
if stored_proxy.proxy_type == "socks5" || stored_proxy.proxy_type == "socks4" {
&stored_proxy.proxy_type
} else {
"http"
},
username,
password,
local_proxy.host,
local_proxy.port
)
} else {
proxy_url
};
// Set proxy in camoufox config
camoufox_config.proxy = Some(proxy_url);
println!("Configured proxy for Camoufox: {:?}", camoufox_config.proxy);
}
}
// Use the camoufox launcher
let camoufox_result = crate::camoufox::launch_camoufox_profile(
app_handle.clone(),
profile.clone(),
camoufox_config,
url,
)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
format!("Failed to launch camoufox: {e}").into()
})?;
// Update proxy with actual PID if proxy was started
if let Some(pid) = camoufox_result.pid {
if profile.proxy_id.is_some() {
if let Err(e) = PROXY_MANAGER.update_proxy_pid(0, pid) {
println!("Warning: Failed to update proxy PID: {e}");
}
}
}
// Update profile with the process info from camoufox result
let mut updated_profile = profile.clone();
updated_profile.process_id = camoufox_result.pid;
updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs());
// Save the updated profile
self.save_process_info(&updated_profile)?;
// Emit profile update event to frontend
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
println!("Warning: Failed to emit profile update event: {e}");
}
return Ok(updated_profile);
} else {
return Err("Camoufox profile missing configuration".into());
}
}
// Create browser instance
let browser_type = BrowserType::from_str(&profile.browser)
.map_err(|_| format!("Invalid browser type: {}", profile.browser))?;
@@ -1853,91 +1957,85 @@ impl BrowserRunner {
// For TOR and Mullvad browsers, we need to find the actual browser process
// because they use launcher scripts that spawn the real browser process
let actual_pid = if matches!(
let mut actual_pid = launcher_pid;
if matches!(
browser_type,
BrowserType::TorBrowser | BrowserType::MullvadBrowser
) {
println!("Waiting for TOR/Mullvad browser to fully start...");
// Wait a bit for the browser to fully start
// Wait a moment for the actual browser process to start
tokio::time::sleep(tokio::time::Duration::from_millis(3000)).await;
// Search for the actual browser process
// Find the actual browser process
let system = System::new_all();
let mut found_pid: Option<u32> = None;
for (pid, process) in system.processes() {
let process_name = process.name().to_str().unwrap_or("");
let process_cmd = process.cmd();
let pid_u32 = pid.as_u32();
// Try multiple times to find the process as it might take time to start
for attempt in 1..=5 {
println!("Attempt {attempt} to find actual browser process...");
for (pid, process) in system.processes() {
let cmd = process.cmd();
if cmd.len() >= 2 {
// Check if this is the right browser executable
let exe_name = process.name().to_string_lossy().to_lowercase();
let is_correct_browser = match profile.browser.as_str() {
"mullvad-browser" => {
self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser")
}
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "tor-browser"),
_ => false,
};
if !is_correct_browser {
continue;
}
// Check for profile path match
let profile_path_match = cmd.iter().any(|s| {
let arg = s.to_str().unwrap_or("");
arg == profile_data_path.to_string_lossy()
|| arg == format!("-profile={}", profile_data_path.to_string_lossy())
|| (arg == "-profile"
&& cmd
.iter()
.any(|s2| s2.to_str().unwrap_or("") == profile_data_path.to_string_lossy()))
});
if profile_path_match {
found_pid = Some(pid.as_u32());
println!(
"Found actual browser process with PID: {} for profile: {}",
pid.as_u32(),
profile.name
);
break;
}
}
// Skip if this is the launcher process itself
if pid_u32 == launcher_pid {
continue;
}
if found_pid.is_some() {
if self.is_tor_or_mullvad_browser(process_name, process_cmd, &profile.browser) {
println!(
"Found actual {} browser process: PID {} ({})",
profile.browser, pid_u32, process_name
);
actual_pid = pid_u32;
break;
}
// Wait before next attempt
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
}
}
found_pid.unwrap_or(launcher_pid)
} else {
// For other browsers, the launcher PID is usually the actual browser PID
launcher_pid
};
// Update profile with process info
// Update profile with process info and save
let mut updated_profile = profile.clone();
updated_profile.process_id = Some(actual_pid);
updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs());
// Save the updated profile
self
.save_process_info(&updated_profile)
.expect("Failed to save process info");
self.save_process_info(&updated_profile)?;
// Apply proxy settings if needed (for Firefox-based browsers)
if profile.proxy_id.is_some()
&& matches!(
browser_type,
BrowserType::Firefox
| BrowserType::FirefoxDeveloper
| BrowserType::Zen
| BrowserType::TorBrowser
| BrowserType::MullvadBrowser
)
{
// Proxy settings for Firefox-based browsers are applied via user.js file
// which is already handled in the profile creation process
}
// Start proxy if configured and needed (for Chromium-based browsers)
if let Some(proxy_id) = &profile.proxy_id {
if let Some(stored_proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) {
println!("Starting proxy for profile: {}", profile.name);
match PROXY_MANAGER
.start_proxy(
app_handle.clone(),
&stored_proxy,
actual_pid,
Some(&profile.name),
)
.await
{
Ok(_) => println!("Proxy started successfully for profile: {}", profile.name),
Err(e) => println!("Warning: Failed to start proxy: {e}"),
}
}
}
// Emit profile update event to frontend
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
println!("Warning: Failed to emit profile update event: {e}");
}
println!(
"Browser launched successfully with PID: {} for profile: {}",
actual_pid, profile.name
);
Ok(updated_profile)
}
@@ -1948,8 +2046,43 @@ impl BrowserRunner {
url: &str,
_internal_proxy_settings: Option<&ProxySettings>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Use the comprehensive browser status check
let is_running = self.check_browser_status(app_handle, profile).await?;
// Handle camoufox profiles specially
if profile.browser == "camoufox" {
let camoufox_launcher = crate::camoufox::CamoufoxLauncher::new(app_handle.clone());
// Get the profile path based on the UUID
let profiles_dir = self.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
let profile_path_str = profile_data_path.to_string_lossy();
// Check if the process is running
match camoufox_launcher
.find_camoufox_by_profile(&profile_path_str)
.await
{
Ok(Some(_camoufox_process)) => {
println!(
"Opening URL in existing Camoufox process for profile: {}",
profile.name
);
// For Camoufox, we need to launch a new instance with the URL since nodecar doesn't support
// opening URLs in existing instances. This is a limitation of the anti-detect architecture.
return Err("Camoufox does not support opening URLs in existing instances. Please close the browser and relaunch it with the new URL.".into());
}
Ok(None) => {
return Err("Camoufox browser is not running".into());
}
Err(e) => {
return Err(format!("Error checking Camoufox process: {e}").into());
}
}
}
// Use the comprehensive browser status check for non-camoufox browsers
let is_running = self
.check_browser_status(app_handle.clone(), profile)
.await?;
if !is_running {
return Err("Browser is not running".into());
@@ -2105,6 +2238,10 @@ impl BrowserRunner {
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
return Err("Unsupported platform".into());
}
BrowserType::Camoufox => {
// This should never be reached due to the early return above, but handle it just in case
Err("Camoufox does not support opening URLs in existing instances".into())
}
}
}
@@ -2159,7 +2296,7 @@ impl BrowserRunner {
}
match self
.open_url_in_existing_browser(
app_handle,
app_handle.clone(),
&final_profile,
url_ref,
internal_proxy_settings,
@@ -2188,7 +2325,7 @@ impl BrowserRunner {
final_profile.browser
);
// Fallback to launching a new instance for other browsers
self.launch_browser(&final_profile, url, internal_proxy_settings).await
self.launch_browser(app_handle.clone(), &final_profile, url, internal_proxy_settings).await
}
}
}
@@ -2197,7 +2334,12 @@ impl BrowserRunner {
// This case shouldn't happen since we checked is_some() above, but handle it gracefully
println!("URL was unexpectedly None, launching new browser instance");
self
.launch_browser(&final_profile, url, internal_proxy_settings)
.launch_browser(
app_handle.clone(),
&final_profile,
url,
internal_proxy_settings,
)
.await
}
} else {
@@ -2208,7 +2350,12 @@ impl BrowserRunner {
println!("Launching new browser instance - no URL provided");
}
self
.launch_browser(&final_profile, url, internal_proxy_settings)
.launch_browser(
app_handle.clone(),
&final_profile,
url,
internal_proxy_settings,
)
.await
}
}
@@ -2242,9 +2389,15 @@ impl BrowserRunner {
Ok(profile)
}
fn save_process_info(&self, profile: &BrowserProfile) -> Result<(), Box<dyn std::error::Error>> {
fn save_process_info(
&self,
profile: &BrowserProfile,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Use the regular save_profile method which handles the UUID structure
self.save_profile(profile)
self.save_profile(profile).map_err(|e| {
let error_string = e.to_string();
Box::new(std::io::Error::other(error_string)) as Box<dyn std::error::Error + Send + Sync>
})
}
pub fn delete_profile(&self, profile_name: &str) -> Result<(), Box<dyn std::error::Error>> {
@@ -2294,6 +2447,79 @@ impl BrowserRunner {
app_handle: tauri::AppHandle,
profile: &BrowserProfile,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
// Handle camoufox profiles specially using the camoufox launcher
if profile.browser == "camoufox" {
let camoufox_launcher = crate::camoufox::CamoufoxLauncher::new(app_handle.clone());
// Get the profile path based on the UUID
let profiles_dir = self.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
let profile_path_str = profile_data_path.to_string_lossy();
println!("Checking Camoufox status for profile: {}", profile.name);
println!("Profile UUID: {}", profile.id);
println!("Profile path: {profile_path_str}");
match camoufox_launcher
.find_camoufox_by_profile(&profile_path_str)
.await
{
Ok(Some(camoufox_process)) => {
// Found a running camoufox process for this profile
println!(
"Found running Camoufox process for profile {}: {:?}",
profile.name, camoufox_process
);
// Update the profile with the current PID if it's different
if let Some(pid) = camoufox_process.pid {
if profile.process_id != Some(pid) {
let mut updated_profile = profile.clone();
updated_profile.process_id = Some(pid);
if let Err(e) = self.save_profile(&updated_profile) {
println!("Warning: Failed to update profile PID: {e}");
} else {
// Emit profile update event to frontend
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
println!("Warning: Failed to emit profile update event: {e}");
}
}
}
}
return Ok(true);
}
Ok(None) => {
// No running camoufox process found for this profile
println!(
"No running Camoufox process found for profile: {}",
profile.name
);
// Clear the PID if one was stored
if profile.process_id.is_some() {
let mut updated_profile = profile.clone();
updated_profile.process_id = None;
if let Err(e) = self.save_profile(&updated_profile) {
println!("Warning: Failed to clear profile PID: {e}");
} else {
// Emit profile update event to frontend
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
println!("Warning: Failed to emit profile update event: {e}");
}
}
}
return Ok(false);
}
Err(e) => {
println!("Error checking Camoufox status: {e}");
return Ok(false);
}
}
}
// For non-camoufox browsers, use the existing logic
let mut inner_profile = profile.clone();
let system = System::new_all();
let mut is_running = false;
@@ -2416,67 +2642,21 @@ impl BrowserRunner {
if let Some(pid) = found_pid {
if inner_profile.process_id != Some(pid) {
inner_profile.process_id = Some(pid);
if let Err(e) = self.save_process_info(&inner_profile) {
println!("Warning: Failed to update process info: {e}");
} else {
println!(
"Updated process ID for profile '{}' to: {}",
inner_profile.name, pid
);
if let Err(e) = self.save_profile(&inner_profile) {
println!("Warning: Failed to update profile with new PID: {e}");
}
}
} else if is_running {
println!("Browser is running but no PID found - this shouldn't happen");
} else {
// Browser is not running, clear the PID if it was set
if inner_profile.process_id.is_some() {
inner_profile.process_id = None;
if let Err(e) = self.save_process_info(&inner_profile) {
println!("Warning: Failed to clear process info: {e}");
} else {
println!("Cleared process ID for profile '{}'", inner_profile.name);
}
} else if inner_profile.process_id.is_some() {
// Clear the PID if no process found
inner_profile.process_id = None;
if let Err(e) = self.save_profile(&inner_profile) {
println!("Warning: Failed to clear profile PID: {e}");
}
}
// Handle proxy management based on browser status
if let Some(proxy_id) = &inner_profile.proxy_id {
if let Some(proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) {
if is_running {
// Browser is running, check if proxy is active
let proxy_active = PROXY_MANAGER
.get_proxy_settings(inner_profile.process_id.unwrap_or(0))
.is_some();
if !proxy_active {
// Browser is running but proxy is not - restart the proxy
match PROXY_MANAGER
.start_proxy(
app_handle,
&proxy,
inner_profile.process_id.unwrap(),
Some(&inner_profile.name),
)
.await
{
Ok(_) => {
println!("Restarted proxy for profile {}", inner_profile.name);
}
Err(e) => {
eprintln!(
"Failed to restart proxy for profile {}: {}",
inner_profile.name, e
);
}
}
}
} else {
// Browser is not running, stop the proxy if it exists
if let Some(pid) = profile.process_id {
let _ = PROXY_MANAGER.stop_proxy(app_handle, pid).await;
}
}
}
// Emit profile update event to frontend
if let Err(e) = app_handle.emit("profile-updated", &inner_profile) {
println!("Warning: Failed to emit profile update event: {e}");
}
Ok(is_running)
@@ -2487,7 +2667,87 @@ impl BrowserRunner {
app_handle: tauri::AppHandle,
profile: &BrowserProfile,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Get the current process ID
// Handle camoufox profiles specially
if profile.browser == "camoufox" {
let camoufox_launcher = crate::camoufox::CamoufoxLauncher::new(app_handle.clone());
// Get the profile path based on the UUID
let profiles_dir = self.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
let profile_path_str = profile_data_path.to_string_lossy();
println!(
"Attempting to kill Camoufox process for profile: {}",
profile.name
);
println!("Profile UUID: {}", profile.id);
println!("Profile path: {profile_path_str}");
match camoufox_launcher
.find_camoufox_by_profile(&profile_path_str)
.await
{
Ok(Some(camoufox_process)) => {
println!(
"Found running Camoufox process for profile {}: {:?}",
profile.name, camoufox_process
);
// Stop the camoufox process using the launcher
match camoufox_launcher.stop_camoufox(&camoufox_process.id).await {
Ok(stopped) => {
if stopped {
println!(
"Successfully stopped Camoufox process: {}",
camoufox_process.id
);
} else {
println!("Failed to stop Camoufox process: {}", camoufox_process.id);
return Err("Failed to stop Camoufox process".into());
}
}
Err(e) => {
println!("Error stopping Camoufox process: {e}");
return Err(format!("Error stopping Camoufox process: {e}").into());
}
}
}
Ok(None) => {
println!(
"No running Camoufox process found for profile: {}",
profile.name
);
// Process might already be stopped, just clear the PID
}
Err(e) => {
println!("Error finding Camoufox process: {e}");
return Err(format!("Error finding Camoufox process: {e}").into());
}
}
// Stop proxy if one was running for this profile
if let Some(pid) = profile.process_id {
if let Err(e) = PROXY_MANAGER.stop_proxy(app_handle.clone(), pid).await {
println!("Warning: Failed to stop proxy for Camoufox profile: {e}");
}
}
// Clear the process ID from the profile
let mut updated_profile = profile.clone();
updated_profile.process_id = None;
self
.save_process_info(&updated_profile)
.map_err(|e| format!("Failed to update profile: {e}"))?;
// Emit profile update event to frontend
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
println!("Warning: Failed to emit profile update event: {e}");
}
return Ok(());
}
// For non-camoufox browsers, use the existing logic
let pid = if let Some(pid) = profile.process_id {
pid
} else {
@@ -2554,7 +2814,7 @@ impl BrowserRunner {
println!("Attempting to kill browser process with PID: {pid}");
// Stop any associated proxy first
if let Err(e) = PROXY_MANAGER.stop_proxy(app_handle, pid).await {
if let Err(e) = PROXY_MANAGER.stop_proxy(app_handle.clone(), pid).await {
println!("Warning: Failed to stop proxy for PID {pid}: {e}");
}
@@ -2667,16 +2927,17 @@ impl BrowserRunner {
browser_str: String,
version: String,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// Check if this browser type is already being downloaded
// Check if this browser-version pair is already being downloaded
let download_key = format!("{browser_str}-{version}");
{
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
if downloading.contains(&browser_str) {
if downloading.contains(&download_key) {
return Err(format!(
"Browser '{browser_str}' is already being downloaded. Please wait for the current download to complete."
"Browser '{browser_str}' version '{version}' is already being downloaded. Please wait for the current download to complete."
).into());
}
// Mark this browser as being downloaded
downloading.insert(browser_str.clone());
// Mark this browser-version pair as being downloaded
downloading.insert(download_key.clone());
}
let browser_type =
@@ -2762,10 +3023,10 @@ impl BrowserRunner {
// Clean up failed download
let _ = registry.cleanup_failed_download(&browser_str, &version);
let _ = registry.save();
// Remove browser from downloading set on error
// Remove browser-version pair from downloading set on error
{
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
downloading.remove(&browser_str);
downloading.remove(&download_key);
}
return Err(format!("Failed to download browser: {e}").into());
}
@@ -2794,10 +3055,10 @@ impl BrowserRunner {
// Clean up failed download
let _ = registry.cleanup_failed_download(&browser_str, &version);
let _ = registry.save();
// Remove browser from downloading set on error
// Remove browser-version pair from downloading set on error
{
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
downloading.remove(&browser_str);
downloading.remove(&download_key);
}
return Err(format!("Failed to extract browser: {e}").into());
}
@@ -2828,10 +3089,10 @@ impl BrowserRunner {
if !browser.is_version_downloaded(&version, &binaries_dir) {
let _ = registry.cleanup_failed_download(&browser_str, &version);
let _ = registry.save();
// Remove browser from downloading set on verification failure
// Remove browser-version pair from downloading set on verification failure
{
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
downloading.remove(&browser_str);
downloading.remove(&download_key);
}
return Err("Browser download completed but verification failed".into());
}
@@ -2850,6 +3111,26 @@ impl BrowserRunner {
.save()
.map_err(|e| format!("Failed to save registry: {e}"))?;
// If this is Camoufox, automatically download GeoIP database
if browser_str == "camoufox" {
use crate::geoip_downloader::GeoIPDownloader;
// Check if GeoIP database is already available
if !GeoIPDownloader::is_geoip_database_available() {
println!("Downloading GeoIP database for Camoufox...");
let geoip_downloader = GeoIPDownloader::new();
if let Err(e) = geoip_downloader.download_geoip_database(&app_handle).await {
eprintln!("Warning: Failed to download GeoIP database: {e}");
// Don't fail the browser download if GeoIP download fails
} else {
println!("GeoIP database downloaded successfully");
}
} else {
println!("GeoIP database already available");
}
}
// Emit completion
let progress = DownloadProgress {
browser: browser_str.clone(),
@@ -2863,10 +3144,10 @@ impl BrowserRunner {
};
let _ = app_handle.emit("download-progress", &progress);
// Remove browser from downloading set
// Remove browser-version pair from downloading set
{
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
downloading.remove(&browser_str);
downloading.remove(&download_key);
}
Ok(version)
@@ -2899,6 +3180,34 @@ impl BrowserRunner {
files_exist
}
/// Update camoufox configuration for a profile
pub fn update_camoufox_config(
&self,
profile_name: &str,
config: CamoufoxConfig,
) -> Result<(), Box<dyn std::error::Error>> {
let mut profiles = self.list_profiles()?;
// Find the profile to update
let profile = profiles
.iter_mut()
.find(|p| p.name == profile_name)
.ok_or_else(|| format!("Profile '{profile_name}' not found"))?;
// Ensure the profile is a camoufox profile
if profile.browser != "camoufox" {
return Err(format!("Profile '{profile_name}' is not a camoufox profile").into());
}
// Update the camoufox configuration
profile.camoufox_config = Some(config);
// Save the updated profile
self.save_profile(profile)?;
Ok(())
}
}
impl BrowserProfile {
@@ -2915,10 +3224,18 @@ pub fn create_browser_profile(
version: String,
release_type: String,
proxy_id: Option<String>,
camoufox_config: Option<CamoufoxConfig>,
) -> Result<BrowserProfile, String> {
let browser_runner = BrowserRunner::new();
browser_runner
.create_profile(&name, &browser, &version, &release_type, proxy_id)
.create_profile(
&name,
&browser,
&version,
&release_type,
proxy_id,
camoufox_config,
)
.map_err(|e| format!("Failed to create profile: {e}"))
}
@@ -3207,6 +3524,7 @@ pub fn create_browser_profile_new(
version: String,
release_type: String,
proxy_id: Option<String>,
camoufox_config: Option<CamoufoxConfig>,
) -> Result<BrowserProfile, String> {
let browser_type =
BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?;
@@ -3216,6 +3534,7 @@ pub fn create_browser_profile_new(
version,
release_type,
proxy_id,
camoufox_config,
)
}
@@ -3313,7 +3632,7 @@ mod tests {
let (runner, _temp_dir) = create_test_browser_runner();
let profile = runner
.create_profile("Test Profile", "firefox", "139.0", "stable", None)
.create_profile("Test Profile", "firefox", "139.0", "stable", None, None)
.unwrap();
assert_eq!(profile.name, "Test Profile");
@@ -3342,6 +3661,7 @@ mod tests {
"139.0",
"stable",
None, // Tests now use separate proxy storage system
None, // No camoufox config for this test
)
.unwrap();
@@ -3355,7 +3675,7 @@ mod tests {
let (runner, _temp_dir) = create_test_browser_runner();
let profile = runner
.create_profile("Test Save Load", "firefox", "139.0", "stable", None)
.create_profile("Test Save Load", "firefox", "139.0", "stable", None, None)
.unwrap();
// Save the profile
@@ -3375,7 +3695,7 @@ mod tests {
// Create profile
let _ = runner
.create_profile("Original Name", "firefox", "139.0", "stable", None)
.create_profile("Original Name", "firefox", "139.0", "stable", None, None)
.unwrap();
// Rename profile
@@ -3395,7 +3715,7 @@ mod tests {
// Create profile
let _ = runner
.create_profile("To Delete", "firefox", "139.0", "stable", None)
.create_profile("To Delete", "firefox", "139.0", "stable", None, None)
.unwrap();
// Verify profile exists
@@ -3422,6 +3742,7 @@ mod tests {
"139.0",
"stable",
None,
None,
)
.unwrap();
@@ -3444,13 +3765,13 @@ mod tests {
// Create multiple profiles
let _ = runner
.create_profile("Profile 1", "firefox", "139.0", "stable", None)
.create_profile("Profile 1", "firefox", "139.0", "stable", None, None)
.unwrap();
let _ = runner
.create_profile("Profile 2", "chromium", "1465660", "stable", None)
.create_profile("Profile 2", "chromium", "1465660", "stable", None, None)
.unwrap();
let _ = runner
.create_profile("Profile 3", "brave", "v1.81.9", "stable", None)
.create_profile("Profile 3", "brave", "v1.81.9", "stable", None, None)
.unwrap();
// List profiles
@@ -3469,10 +3790,10 @@ mod tests {
// Test that we can't rename to an existing profile name
let _ = runner
.create_profile("Profile 1", "firefox", "139.0", "stable", None)
.create_profile("Profile 1", "firefox", "139.0", "stable", None, None)
.unwrap();
let _ = runner
.create_profile("Profile 2", "firefox", "139.0", "stable", None)
.create_profile("Profile 2", "firefox", "139.0", "stable", None, None)
.unwrap();
// Try to rename profile2 to profile1's name (should fail)
@@ -3493,6 +3814,7 @@ mod tests {
"139.0",
"stable",
None,
None,
)
.unwrap();
@@ -3526,6 +3848,7 @@ mod tests {
"139.0",
"stable",
None, // Tests now use separate proxy storage system
None, // No camoufox config for this test
)
.unwrap();
+71
View File
@@ -87,6 +87,10 @@ impl BrowserVersionService {
Ok(true)
}
}
"camoufox" => {
// Camoufox supports all platforms and architectures according to the JS code
Ok(true)
}
_ => Err(format!("Unknown browser: {browser}").into()),
}
}
@@ -101,6 +105,7 @@ impl BrowserVersionService {
"brave",
"chromium",
"tor-browser",
"camoufox",
];
all_browsers
@@ -237,6 +242,7 @@ impl BrowserVersionService {
"brave" => self.fetch_brave_versions(true).await?,
"chromium" => self.fetch_chromium_versions(true).await?,
"tor-browser" => self.fetch_tor_versions(true).await?,
"camoufox" => self.fetch_camoufox_versions(true).await?,
_ => return Err(format!("Unsupported browser: {browser}").into()),
};
@@ -454,6 +460,27 @@ impl BrowserVersionService {
})
.collect()
}
"camoufox" => {
let releases = self.fetch_camoufox_releases_detailed(true).await?;
merged_versions
.into_iter()
.map(|version| {
if let Some(release) = releases.iter().find(|r| r.tag_name == version) {
BrowserVersionInfo {
version: release.tag_name.clone(),
is_prerelease: release.is_nightly,
date: release.published_at.clone(),
}
} else {
BrowserVersionInfo {
version: version.clone(),
is_prerelease: false, // Camoufox usually stable releases
date: "".to_string(),
}
}
})
.collect()
}
_ => {
return Err(format!("Unsupported browser: {browser}").into());
}
@@ -727,6 +754,32 @@ impl BrowserVersionService {
is_archive,
})
}
"camoufox" => {
// Camoufox downloads from GitHub releases with pattern: camoufox-{version}-{release}-{os}.{arch}.zip
let (os_name, arch_name) = match (&os[..], &arch[..]) {
("windows", "x64") => ("win", "x86_64"),
("windows", "arm64") => ("win", "arm64"),
("linux", "x64") => ("lin", "x86_64"),
("linux", "arm64") => ("lin", "arm64"),
("macos", "x64") => ("mac", "x86_64"),
("macos", "arm64") => ("mac", "arm64"),
_ => {
return Err(
format!("Unsupported platform/architecture for Camoufox: {os}/{arch}").into(),
)
}
};
// Note: We provide a placeholder URL here since Camoufox requires dynamic resolution
// The actual URL will be resolved in download.rs resolve_download_url
Ok(DownloadInfo {
url: format!(
"https://github.com/daijro/camoufox/releases/download/{version}/camoufox-{{version}}-{{release}}-{os_name}.{arch_name}.zip"
),
filename: format!("camoufox-{version}-{os_name}.{arch_name}.zip"),
is_archive: true,
})
}
_ => Err(format!("Unsupported browser: {browser}").into()),
}
}
@@ -889,6 +942,24 @@ impl BrowserVersionService {
.fetch_tor_releases_with_caching(no_caching)
.await
}
async fn fetch_camoufox_versions(
&self,
no_caching: bool,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let releases = self.fetch_camoufox_releases_detailed(no_caching).await?;
Ok(releases.into_iter().map(|r| r.tag_name).collect())
}
async fn fetch_camoufox_releases_detailed(
&self,
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client
.fetch_camoufox_releases_with_caching(no_caching)
.await
}
}
#[cfg(test)]
+607
View File
@@ -0,0 +1,607 @@
use crate::browser_runner::BrowserProfile;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tauri::AppHandle;
use tauri_plugin_shell::ShellExt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CamoufoxConfig {
pub os: Option<Vec<String>>,
pub block_images: Option<bool>,
pub block_webrtc: Option<bool>,
pub block_webgl: Option<bool>,
pub disable_coop: Option<bool>,
pub geoip: Option<serde_json::Value>, // Can be String or bool
pub country: Option<String>,
pub timezone: Option<String>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub humanize: Option<bool>,
pub humanize_duration: Option<f64>,
pub headless: Option<bool>,
pub locale: Option<Vec<String>>,
pub addons: Option<Vec<String>>,
pub fonts: Option<Vec<String>>,
pub custom_fonts_only: Option<bool>,
pub exclude_addons: Option<Vec<String>>,
pub screen_min_width: Option<u32>,
pub screen_max_width: Option<u32>,
pub screen_min_height: Option<u32>,
pub screen_max_height: Option<u32>,
pub window_width: Option<u32>,
pub window_height: Option<u32>,
pub ff_version: Option<u32>,
pub main_world_eval: Option<bool>,
pub webgl_vendor: Option<String>,
pub webgl_renderer: Option<String>,
pub proxy: Option<String>,
pub enable_cache: Option<bool>,
pub virtual_display: Option<String>,
pub debug: Option<bool>,
pub additional_args: Option<Vec<String>>,
pub env_vars: Option<HashMap<String, String>>,
pub firefox_prefs: Option<HashMap<String, serde_json::Value>>,
}
impl Default for CamoufoxConfig {
fn default() -> Self {
Self {
os: None,
block_images: None,
block_webrtc: None,
block_webgl: None,
disable_coop: None,
geoip: None,
country: None,
timezone: None,
latitude: None,
longitude: None,
humanize: None,
humanize_duration: None,
headless: None,
locale: None,
addons: None,
fonts: None,
custom_fonts_only: None,
exclude_addons: None,
screen_min_width: None,
screen_max_width: None,
screen_min_height: None,
screen_max_height: None,
window_width: None,
window_height: None,
ff_version: None,
main_world_eval: None,
webgl_vendor: None,
webgl_renderer: None,
proxy: None,
enable_cache: Some(true), // Cache enabled by default
virtual_display: None,
debug: None,
additional_args: None,
env_vars: None,
firefox_prefs: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct CamoufoxLaunchResult {
pub id: String,
pub pid: Option<u32>,
#[serde(alias = "executable_path")]
pub executablePath: String,
#[serde(alias = "profile_path")]
pub profilePath: String,
pub url: Option<String>,
}
pub struct CamoufoxLauncher {
app_handle: AppHandle,
}
impl CamoufoxLauncher {
pub fn new(app_handle: AppHandle) -> Self {
Self { app_handle }
}
/// Launch Camoufox browser with the specified configuration
pub async fn launch_camoufox(
&self,
executable_path: &str,
profile_path: &str,
config: &CamoufoxConfig,
url: Option<&str>,
) -> Result<CamoufoxLaunchResult, Box<dyn std::error::Error + Send + Sync>> {
println!("Launching Camoufox with executable: {executable_path}");
println!("Profile path: {profile_path}");
println!("URL: {url:?}");
// Use Tauri's sidecar to call nodecar
let mut sidecar = self
.app_handle
.shell()
.sidecar("nodecar")
.map_err(|e| format!("Failed to create nodecar sidecar: {e}"))?
.arg("camoufox")
.arg("launch")
.arg("--executable-path")
.arg(executable_path)
.arg("--profile-path")
.arg(profile_path);
// Add URL if provided
if let Some(url) = url {
sidecar = sidecar.arg("--url").arg(url);
}
// Add configuration options
if let Some(os_list) = &config.os {
sidecar = sidecar.arg("--os").arg(os_list.join(","));
}
if config.block_images.unwrap_or(false) {
sidecar = sidecar.arg("--block-images");
}
if config.block_webrtc.unwrap_or(false) {
sidecar = sidecar.arg("--block-webrtc");
}
if config.block_webgl.unwrap_or(false) {
sidecar = sidecar.arg("--block-webgl");
}
if config.disable_coop.unwrap_or(false) {
sidecar = sidecar.arg("--disable-coop");
}
if let Some(geoip) = &config.geoip {
match geoip {
serde_json::Value::String(s) => {
sidecar = sidecar.arg("--geoip").arg(s);
}
serde_json::Value::Bool(b) => {
sidecar = sidecar
.arg("--geoip")
.arg(if *b { "auto" } else { "false" });
}
_ => {
sidecar = sidecar.arg("--geoip").arg(geoip.to_string());
}
}
}
if let Some(country) = &config.country {
sidecar = sidecar.arg("--country").arg(country);
}
if let Some(timezone) = &config.timezone {
sidecar = sidecar.arg("--timezone").arg(timezone);
}
if let Some(latitude) = config.latitude {
if let Some(longitude) = config.longitude {
sidecar = sidecar.arg("--latitude").arg(latitude.to_string());
sidecar = sidecar.arg("--longitude").arg(longitude.to_string());
}
}
if let Some(humanize) = config.humanize {
if humanize {
if let Some(duration) = config.humanize_duration {
sidecar = sidecar.arg("--humanize").arg(duration.to_string());
} else {
sidecar = sidecar.arg("--humanize");
}
}
}
if config.headless.unwrap_or(false) {
sidecar = sidecar.arg("--headless");
}
if let Some(locale_list) = &config.locale {
sidecar = sidecar.arg("--locale").arg(locale_list.join(","));
}
if let Some(addons_list) = &config.addons {
sidecar = sidecar.arg("--addons").arg(addons_list.join(","));
}
if let Some(fonts_list) = &config.fonts {
sidecar = sidecar.arg("--fonts").arg(fonts_list.join(","));
}
if config.custom_fonts_only.unwrap_or(false) {
sidecar = sidecar.arg("--custom-fonts-only");
}
if let Some(exclude_addons_list) = &config.exclude_addons {
sidecar = sidecar
.arg("--exclude-addons")
.arg(exclude_addons_list.join(","));
}
// Screen size configuration
if let Some(width) = config.screen_min_width {
sidecar = sidecar.arg("--screen-min-width").arg(width.to_string());
}
if let Some(width) = config.screen_max_width {
sidecar = sidecar.arg("--screen-max-width").arg(width.to_string());
}
if let Some(height) = config.screen_min_height {
sidecar = sidecar.arg("--screen-min-height").arg(height.to_string());
}
if let Some(height) = config.screen_max_height {
sidecar = sidecar.arg("--screen-max-height").arg(height.to_string());
}
if let Some(width) = config.window_width {
sidecar = sidecar.arg("--window-width").arg(width.to_string());
}
if let Some(height) = config.window_height {
sidecar = sidecar.arg("--window-height").arg(height.to_string());
}
// Advanced options
if let Some(ff_version) = config.ff_version {
sidecar = sidecar.arg("--ff-version").arg(ff_version.to_string());
}
if config.main_world_eval.unwrap_or(false) {
sidecar = sidecar.arg("--main-world-eval");
}
if let Some(vendor) = &config.webgl_vendor {
if let Some(renderer) = &config.webgl_renderer {
sidecar = sidecar.arg("--webgl-vendor").arg(vendor);
sidecar = sidecar.arg("--webgl-renderer").arg(renderer);
}
}
if let Some(proxy) = &config.proxy {
sidecar = sidecar.arg("--proxy").arg(proxy);
}
// Cache is enabled by default, only add flag if disabled
if !config.enable_cache.unwrap_or(true) {
sidecar = sidecar.arg("--disable-cache");
}
if let Some(virtual_display) = &config.virtual_display {
sidecar = sidecar.arg("--virtual-display").arg(virtual_display);
}
if config.debug.unwrap_or(false) {
sidecar = sidecar.arg("--debug");
}
if let Some(args) = &config.additional_args {
sidecar = sidecar.arg("--args").arg(args.join(","));
}
if let Some(env_vars) = &config.env_vars {
let env_json = serde_json::to_string(env_vars)
.map_err(|e| format!("Failed to serialize environment variables: {e}"))?;
sidecar = sidecar.arg("--env").arg(env_json);
}
if let Some(firefox_prefs) = &config.firefox_prefs {
let prefs_json = serde_json::to_string(firefox_prefs)
.map_err(|e| format!("Failed to serialize Firefox preferences: {e}"))?;
sidecar = sidecar.arg("--firefox-prefs").arg(prefs_json);
}
// Execute the command
println!("Executing nodecar command...");
let output = sidecar
.output()
.await
.map_err(|e| format!("Failed to execute nodecar command: {e}"))?;
// Check the command status first
if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr);
let stdout_msg = String::from_utf8_lossy(&output.stdout);
return Err(
format!(
"Failed to launch Camoufox: Command failed with status {:?}\nstderr: {}\nstdout: {}",
output.status, error_msg, stdout_msg
)
.into(),
);
}
// Parse the JSON response
let stdout = String::from_utf8_lossy(&output.stdout);
println!("Nodecar stdout: {stdout}");
// Try to parse the JSON response
let result: CamoufoxLaunchResult = serde_json::from_str(&stdout)
.map_err(|e| format!("Failed to parse nodecar response as JSON: {e}\nResponse: {stdout}"))?;
println!("Successfully launched Camoufox with ID: {}", result.id);
Ok(result)
}
/// Stop a Camoufox process by ID
pub async fn stop_camoufox(
&self,
id: &str,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
println!("Stopping Camoufox process with ID: {id}");
// First, we need to find the process to get its executable and profile paths
let processes = self.list_camoufox_processes().await?;
let target_process = processes.iter().find(|p| p.id == id);
if let Some(process) = target_process {
println!(
"Found process to stop: executable={}, profile={}",
process.executablePath, process.profilePath
);
let sidecar = self
.app_handle
.shell()
.sidecar("nodecar")
.map_err(|e| format!("Failed to create nodecar sidecar: {e}"))?
.arg("camoufox")
.arg("stop")
.arg("--executable-path")
.arg(&process.executablePath)
.arg("--profile-path")
.arg(&process.profilePath)
.arg("--id")
.arg(id);
let output = sidecar
.output()
.await
.map_err(|e| format!("Failed to execute nodecar stop command: {e}"))?;
if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr);
let stdout_msg = String::from_utf8_lossy(&output.stdout);
println!("Failed to stop Camoufox process - stderr: {error_msg}, stdout: {stdout_msg}");
return Err(format!("Failed to stop Camoufox process: {error_msg}").into());
}
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
println!("Stop command result: {stdout}");
// Parse the JSON response which contains a "success" field
let response: serde_json::Value = serde_json::from_str(&stdout)
.map_err(|e| format!("Failed to parse stop response as JSON: {e}\nResponse: {stdout}"))?;
let success = response
.get("success")
.and_then(|v| v.as_bool())
.ok_or_else(|| {
format!("Invalid response format - missing or invalid 'success' field: {stdout}")
})?;
if success {
println!("Successfully stopped Camoufox process: {id}");
} else {
println!("Failed to stop Camoufox process: {id} (process may not exist)");
}
Ok(success)
} else {
println!("Camoufox process with ID {id} not found in running processes");
// If we can't find the process, it might already be stopped
Ok(false)
}
}
/// List all Camoufox processes
pub async fn list_camoufox_processes(
&self,
) -> Result<Vec<CamoufoxLaunchResult>, Box<dyn std::error::Error + Send + Sync>> {
println!("Listing Camoufox processes...");
// For the list command, we need to provide dummy executable-path and profile-path
// even though they're not used by the list action
let sidecar = self
.app_handle
.shell()
.sidecar("nodecar")
.map_err(|e| format!("Failed to create nodecar sidecar: {e}"))?
.arg("camoufox")
.arg("list")
.arg("--executable-path")
.arg("/dummy/path") // Dummy path since list doesn't use it
.arg("--profile-path")
.arg("/dummy/profile"); // Dummy path since list doesn't use it
let output = sidecar
.output()
.await
.map_err(|e| format!("Failed to execute nodecar list command: {e}"))?;
if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr);
return Err(format!("Failed to list Camoufox processes: {error_msg}").into());
}
let stdout = String::from_utf8_lossy(&output.stdout);
println!("List command result: {stdout}");
// Parse the response as an array of process info
let processes: Vec<serde_json::Value> =
serde_json::from_str(&stdout).map_err(|e| format!("Failed to parse list response: {e}"))?;
// Convert to CamoufoxLaunchResult format
let mut results = Vec::new();
for process in processes {
// Handle both camelCase and snake_case formats from nodecar
let id = process.get("id").and_then(|v| v.as_str());
// Try both formats for executable path
let executable_path = process
.get("executable_path")
.and_then(|v| v.as_str())
.or_else(|| process.get("executablePath").and_then(|v| v.as_str()));
// Try both formats for profile path
let profile_path = process
.get("profile_path")
.and_then(|v| v.as_str())
.or_else(|| process.get("profilePath").and_then(|v| v.as_str()));
if let (Some(id), Some(executable_path), Some(profile_path)) =
(id, executable_path, profile_path)
{
let pid = process
.get("pid")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
let url = process
.get("url")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
results.push(CamoufoxLaunchResult {
id: id.to_string(),
pid,
executablePath: executable_path.to_string(),
profilePath: profile_path.to_string(),
url,
});
} else {
println!("Skipping malformed process entry: {process:?}");
}
}
println!("Parsed {} valid Camoufox processes", results.len());
Ok(results)
}
/// Find Camoufox process by profile path (for integration with browser_runner)
pub async fn find_camoufox_by_profile(
&self,
profile_path: &str,
) -> Result<Option<CamoufoxLaunchResult>, Box<dyn std::error::Error + Send + Sync>> {
println!("Looking for Camoufox process with profile path: {profile_path}");
let processes = self.list_camoufox_processes().await?;
println!("Found {} running Camoufox processes", processes.len());
for process in &processes {
println!(
"Checking process with profile path: {}",
process.profilePath
);
}
// Convert both paths to canonical form for comparison
let target_path = std::path::Path::new(profile_path)
.canonicalize()
.unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf());
for process in &processes {
println!(
"Comparing target path: {} with process path: {}",
target_path.display(),
process.profilePath
);
// Try multiple comparison methods
let process_path = std::path::Path::new(&process.profilePath)
.canonicalize()
.unwrap_or_else(|_| std::path::Path::new(&process.profilePath).to_path_buf());
// Method 1: Canonical path comparison
if process_path == target_path {
println!("Found match using canonical path comparison");
return Ok(Some(process.clone()));
}
// Method 2: Direct string comparison
if process.profilePath == profile_path {
println!("Found match using direct string comparison");
return Ok(Some(process.clone()));
}
// Method 3: Compare as strings after canonicalization
if process_path.to_string_lossy() == target_path.to_string_lossy() {
println!("Found match using canonical string comparison");
return Ok(Some(process.clone()));
}
// Method 4: Compare file names if full paths don't match
if let (Some(process_file), Some(target_file)) =
(process_path.file_name(), target_path.file_name())
{
if process_file == target_file {
// If the parent directories also match, it's likely the same profile
if let (Some(process_parent), Some(target_parent)) =
(process_path.parent(), target_path.parent())
{
if process_parent == target_parent {
println!("Found match using parent directory and file name comparison");
return Ok(Some(process.clone()));
}
}
}
}
// Method 5: Check if either path contains the other (for symlinks or different representations)
let process_path_str = process_path.to_string_lossy();
let target_path_str = target_path.to_string_lossy();
if process_path_str.contains(target_path_str.as_ref())
|| target_path_str.contains(process_path_str.as_ref())
{
println!("Found match using path containment check");
return Ok(Some(process.clone()));
}
}
println!("No matching Camoufox process found for profile path: {profile_path}");
Ok(None)
}
}
pub async fn launch_camoufox_profile(
app_handle: AppHandle,
profile: BrowserProfile,
config: CamoufoxConfig,
url: Option<String>,
) -> Result<CamoufoxLaunchResult, String> {
let launcher = CamoufoxLauncher::new(app_handle);
// Get the executable path for Camoufox
let browser_runner = crate::browser_runner::BrowserRunner::new();
let binaries_dir = browser_runner.get_binaries_dir();
let browser_dir = binaries_dir.join("camoufox").join(&profile.version);
// Get executable path
let browser = crate::browser::create_browser(crate::browser::BrowserType::Camoufox);
let executable_path = browser
.get_executable_path(&browser_dir)
.map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?;
// Get profile path
let profiles_dir = browser_runner.get_profiles_dir();
let profile_path = profile.get_profile_data_path(&profiles_dir);
launcher
.launch_camoufox(
&executable_path.to_string_lossy(),
&profile_path.to_string_lossy(),
&config,
url.as_deref(),
)
.await
.map_err(|e| format!("Failed to launch Camoufox: {e}"))
}
+53
View File
@@ -147,6 +147,30 @@ impl Downloader {
Ok(asset_url)
}
BrowserType::Camoufox => {
// For Camoufox, verify the asset exists and find the correct download URL
let releases = self
.api_client
.fetch_camoufox_releases_with_caching(true)
.await?;
let release = releases
.iter()
.find(|r| r.tag_name == version)
.ok_or(format!("Camoufox version {version} not found"))?;
// Get platform and architecture info
let (os, arch) = Self::get_platform_info();
// Find the appropriate asset
let asset_url = self
.find_camoufox_asset(&release.assets, &os, &arch)
.ok_or(format!(
"No compatible asset found for Camoufox version {version} on {os}/{arch}"
))?;
Ok(asset_url)
}
_ => {
// For other browsers, use the provided URL
Ok(download_info.url.clone())
@@ -321,6 +345,35 @@ impl Downloader {
asset.map(|a| a.browser_download_url.clone())
}
/// Find the appropriate Camoufox asset for the current platform and architecture
fn find_camoufox_asset(
&self,
assets: &[crate::browser::GithubAsset],
os: &str,
arch: &str,
) -> Option<String> {
// Camoufox asset naming pattern: camoufox-{version}-{release}-{os}.{arch}.zip
let (os_name, arch_name) = match (os, arch) {
("windows", "x64") => ("win", "x86_64"),
("windows", "arm64") => ("win", "arm64"),
("linux", "x64") => ("lin", "x86_64"),
("linux", "arm64") => ("lin", "arm64"),
("macos", "x64") => ("mac", "x86_64"),
("macos", "arm64") => ("mac", "arm64"),
_ => return None,
};
// Look for assets matching the pattern
let asset = assets.iter().find(|asset| {
let name = asset.name.to_lowercase();
name.starts_with("camoufox-")
&& name.contains(&format!("-{os_name}.{arch_name}.zip"))
&& name.ends_with(".zip")
});
asset.map(|a| a.browser_download_url.clone())
}
pub async fn download_browser<R: tauri::Runtime>(
&self,
app_handle: &tauri::AppHandle<R>,
+171
View File
@@ -0,0 +1,171 @@
use crate::browser::GithubRelease;
use directories::BaseDirs;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tauri::Emitter;
use tokio::fs;
use tokio::io::AsyncWriteExt;
const MMDB_REPO: &str = "P3TERX/GeoLite.mmdb";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeoIPDownloadProgress {
pub stage: String, // "downloading", "extracting", "completed"
pub percentage: f64,
pub message: String,
}
pub struct GeoIPDownloader {
client: Client,
}
impl GeoIPDownloader {
pub fn new() -> Self {
Self {
client: Client::new(),
}
}
fn get_cache_dir() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let base_dirs = BaseDirs::new().ok_or("Failed to determine base directories")?;
#[cfg(target_os = "windows")]
let cache_dir = base_dirs
.data_local_dir()
.join("camoufox")
.join("camoufox")
.join("Cache");
#[cfg(target_os = "macos")]
let cache_dir = base_dirs.cache_dir().join("camoufox");
#[cfg(target_os = "linux")]
let cache_dir = base_dirs.cache_dir().join("camoufox");
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
let cache_dir = base_dirs.cache_dir().join("camoufox");
Ok(cache_dir)
}
fn get_mmdb_file_path() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
Ok(Self::get_cache_dir()?.join("GeoLite2-City.mmdb"))
}
pub fn is_geoip_database_available() -> bool {
if let Ok(mmdb_path) = Self::get_mmdb_file_path() {
mmdb_path.exists()
} else {
false
}
}
fn find_city_mmdb_asset(&self, release: &GithubRelease) -> Option<String> {
for asset in &release.assets {
if asset.name.ends_with("-City.mmdb") {
return Some(asset.browser_download_url.clone());
}
}
None
}
pub async fn download_geoip_database(
&self,
app_handle: &tauri::AppHandle,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Emit initial progress
let _ = app_handle.emit(
"geoip-download-progress",
GeoIPDownloadProgress {
stage: "downloading".to_string(),
percentage: 0.0,
message: "Starting GeoIP database download".to_string(),
},
);
// Fetch latest release from GitHub
let releases = self.fetch_geoip_releases().await?;
let latest_release = releases.first().ok_or("No GeoIP database releases found")?;
let download_url = self
.find_city_mmdb_asset(latest_release)
.ok_or("No compatible GeoIP database asset found")?;
// Create cache directory
let cache_dir = Self::get_cache_dir()?;
fs::create_dir_all(&cache_dir).await?;
let mmdb_path = Self::get_mmdb_file_path()?;
// Download the file
let response = self.client.get(&download_url).send().await?;
if !response.status().is_success() {
return Err(
format!(
"Failed to download GeoIP database: HTTP {}",
response.status()
)
.into(),
);
}
let total_size = response.content_length().unwrap_or(0);
let mut downloaded = 0;
let mut file = fs::File::create(&mmdb_path).await?;
let mut stream = response.bytes_stream();
use futures_util::StreamExt;
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
downloaded += chunk.len() as u64;
file.write_all(&chunk).await?;
if total_size > 0 {
let percentage = (downloaded as f64 / total_size as f64) * 100.0;
let _ = app_handle.emit(
"geoip-download-progress",
GeoIPDownloadProgress {
stage: "downloading".to_string(),
percentage,
message: format!("Downloaded {downloaded} / {total_size} bytes"),
},
);
}
}
file.flush().await?;
// Emit completion
let _ = app_handle.emit(
"geoip-download-progress",
GeoIPDownloadProgress {
stage: "completed".to_string(),
percentage: 100.0,
message: "GeoIP database download completed".to_string(),
},
);
Ok(())
}
async fn fetch_geoip_releases(
&self,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://api.github.com/repos/{MMDB_REPO}/releases");
let response = self
.client
.get(&url)
.header("User-Agent", "Mozilla/5.0 (compatible; donutbrowser)")
.send()
.await?;
if !response.status().is_success() {
return Err(format!("Failed to fetch releases: HTTP {}", response.status()).into());
}
let releases: Vec<GithubRelease> = response.json().await?;
Ok(releases)
}
}
+20
View File
@@ -13,13 +13,17 @@ mod auto_updater;
mod browser;
mod browser_runner;
mod browser_version_service;
mod camoufox;
mod default_browser;
mod download;
mod downloaded_browsers;
mod extraction;
mod geoip_downloader;
mod profile_importer;
mod proxy_manager;
mod settings_manager;
mod system_utils;
mod theme_detector;
mod version_updater;
@@ -60,6 +64,8 @@ use profile_importer::{detect_existing_profiles, import_browser_profile};
use theme_detector::get_system_theme;
use system_utils::{get_system_locale, get_system_timezone};
// Trait to extend WebviewWindow with transparent titlebar functionality
pub trait WindowExt {
#[cfg(target_os = "macos")]
@@ -207,6 +213,17 @@ async fn delete_stored_proxy(proxy_id: String) -> Result<(), String> {
.map_err(|e| format!("Failed to delete stored proxy: {e}"))
}
#[tauri::command]
async fn update_camoufox_config(
profile_name: String,
config: crate::camoufox::CamoufoxConfig,
) -> Result<(), String> {
let browser_runner = browser_runner::BrowserRunner::new();
browser_runner
.update_camoufox_config(&profile_name, config)
.map_err(|e| format!("Failed to update Camoufox config: {e}"))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let args: Vec<String> = env::args().collect();
@@ -433,6 +450,9 @@ pub fn run() {
get_stored_proxies,
update_stored_proxy,
delete_stored_proxy,
update_camoufox_config,
get_system_locale,
get_system_timezone,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
+1
View File
@@ -689,6 +689,7 @@ impl ProfileImporter {
process_id: None,
last_launch: None,
release_type: "stable".to_string(),
camoufox_config: None,
};
// Save the profile metadata
+1 -5
View File
@@ -173,11 +173,6 @@ impl ProxyManager {
}
// Get a stored proxy by ID
#[allow(dead_code)]
pub fn get_stored_proxy(&self, proxy_id: &str) -> Option<StoredProxy> {
let stored_proxies = self.stored_proxies.lock().unwrap();
stored_proxies.get(proxy_id).cloned()
}
// Update a stored proxy
pub fn update_stored_proxy(
@@ -418,6 +413,7 @@ impl ProxyManager {
}
// Get proxy settings for a browser process ID
#[allow(dead_code)]
pub fn get_proxy_settings(&self, browser_pid: u32) -> Option<ProxySettings> {
let proxies = self.active_proxies.lock().unwrap();
proxies.get(&browser_pid).map(|proxy| ProxySettings {
+331
View File
@@ -0,0 +1,331 @@
use serde::{Deserialize, Serialize};
use std::process::Command;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SystemLocale {
pub locale: String,
pub language: String,
pub country: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SystemTimezone {
pub timezone: String,
pub offset: String,
}
pub struct SystemUtils;
impl SystemUtils {
pub fn new() -> Self {
Self
}
/// Detect the system's locale settings
pub fn detect_system_locale(&self) -> SystemLocale {
#[cfg(target_os = "macos")]
return macos::detect_system_locale();
#[cfg(target_os = "linux")]
return linux::detect_system_locale();
#[cfg(target_os = "windows")]
return windows::detect_system_locale();
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
return SystemLocale {
locale: "en-US".to_string(),
language: "en".to_string(),
country: "US".to_string(),
};
}
/// Detect the system's timezone settings
pub fn detect_system_timezone(&self) -> SystemTimezone {
#[cfg(target_os = "macos")]
return macos::detect_system_timezone();
#[cfg(target_os = "linux")]
return linux::detect_system_timezone();
#[cfg(target_os = "windows")]
return windows::detect_system_timezone();
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
return SystemTimezone {
timezone: "UTC".to_string(),
offset: "+00:00".to_string(),
};
}
}
#[cfg(target_os = "macos")]
mod macos {
use super::*;
pub fn detect_system_locale() -> SystemLocale {
// Try to get the system locale from macOS
if let Ok(output) = Command::new("defaults")
.args(["read", "-g", "AppleLocale"])
.output()
{
if output.status.success() {
let locale_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
return parse_locale(&locale_str);
}
}
// Fallback to environment variables
detect_locale_from_env()
}
pub fn detect_system_timezone() -> SystemTimezone {
// Try to get timezone from macOS system
if let Ok(output) = Command::new("date").arg("+%Z").output() {
if output.status.success() {
let tz_abbr = String::from_utf8_lossy(&output.stdout).trim().to_string();
// Get the full timezone name
if let Ok(tz_output) = Command::new("systemsetup").args(["-gettimezone"]).output() {
if tz_output.status.success() {
let tz_full = String::from_utf8_lossy(&tz_output.stdout);
if let Some(tz_name) = tz_full.strip_prefix("Time Zone: ") {
let tz_clean = tz_name.trim().to_string();
if !tz_clean.is_empty() {
return SystemTimezone {
timezone: tz_clean,
offset: tz_abbr,
};
}
}
}
}
}
}
// Fallback to reading /etc/localtime link
detect_timezone_from_files()
}
}
#[cfg(target_os = "linux")]
mod linux {
use super::*;
pub fn detect_system_locale() -> SystemLocale {
// Try to get locale from locale command
if let Ok(output) = Command::new("locale").output() {
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
if line.starts_with("LANG=") {
let locale_value = line.strip_prefix("LANG=").unwrap_or("");
let locale_clean = locale_value.trim_matches('"');
return parse_locale(locale_clean);
}
}
}
}
// Fallback to environment variables
detect_locale_from_env()
}
pub fn detect_system_timezone() -> SystemTimezone {
// Try to read /etc/timezone first (Debian/Ubuntu)
if let Ok(tz_content) = std::fs::read_to_string("/etc/timezone") {
let tz_name = tz_content.trim().to_string();
if !tz_name.is_empty() {
return SystemTimezone {
timezone: tz_name,
offset: get_timezone_offset(),
};
}
}
// Try timedatectl (systemd systems)
if let Ok(output) = Command::new("timedatectl")
.args(["show", "--property=Timezone", "--value"])
.output()
{
if output.status.success() {
let tz_name = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !tz_name.is_empty() {
return SystemTimezone {
timezone: tz_name,
offset: get_timezone_offset(),
};
}
}
}
// Fallback to reading /etc/localtime symlink
detect_timezone_from_files()
}
fn get_timezone_offset() -> String {
if let Ok(output) = Command::new("date").arg("+%z").output() {
if output.status.success() {
return String::from_utf8_lossy(&output.stdout).trim().to_string();
}
}
"+00:00".to_string()
}
}
#[cfg(target_os = "windows")]
mod windows {
use super::*;
pub fn detect_system_locale() -> SystemLocale {
// Try to get locale from Windows registry/powershell
if let Ok(output) = Command::new("powershell")
.args([
"-Command",
"Get-Culture | Select-Object -ExpandProperty Name",
])
.output()
{
if output.status.success() {
let locale_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
return parse_locale(&locale_str);
}
}
// Fallback to environment variables
detect_locale_from_env()
}
pub fn detect_system_timezone() -> SystemTimezone {
// Try to get timezone from Windows
if let Ok(output) = Command::new("powershell")
.args([
"-Command",
"Get-TimeZone | Select-Object -ExpandProperty Id",
])
.output()
{
if output.status.success() {
let tz_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !tz_id.is_empty() {
return SystemTimezone {
timezone: tz_id,
offset: get_windows_timezone_offset(),
};
}
}
}
// Fallback
SystemTimezone {
timezone: "UTC".to_string(),
offset: "+00:00".to_string(),
}
}
fn get_windows_timezone_offset() -> String {
if let Ok(output) = Command::new("powershell")
.args([
"-Command",
"Get-TimeZone | Select-Object -ExpandProperty BaseUtcOffset",
])
.output()
{
if output.status.success() {
let offset_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
// Convert Windows offset format to standard format
if let Some(colon_pos) = offset_str.find(':') {
let hours = &offset_str[..colon_pos];
let minutes = &offset_str[colon_pos + 1..];
if let (Ok(h), Ok(m)) = (hours.parse::<i32>(), minutes.parse::<i32>()) {
return format!("{:+03}:{:02}", h, m);
}
}
}
}
"+00:00".to_string()
}
}
// Helper functions used across platforms
fn parse_locale(locale_str: &str) -> SystemLocale {
// Remove encoding suffix if present (e.g., "en_US.UTF-8" -> "en_US")
let locale_base = locale_str.split('.').next().unwrap_or(locale_str);
// Split language and country (e.g., "en_US" -> ["en", "US"])
let parts: Vec<&str> = locale_base.split(&['_', '-']).collect();
let language = parts.first().unwrap_or(&"en").to_string();
let country = parts.get(1).unwrap_or(&"US").to_string();
// Convert to standard format (e.g., "en-US")
let standard_locale = if parts.len() >= 2 {
format!("{}-{}", language, country.to_uppercase())
} else {
format!("{language}-US")
};
SystemLocale {
locale: standard_locale,
language,
country: country.to_uppercase(),
}
}
fn detect_locale_from_env() -> SystemLocale {
// Check environment variables in order of preference
let env_vars = ["LANG", "LC_ALL", "LC_CTYPE", "LANGUAGE"];
for var in &env_vars {
if let Ok(value) = std::env::var(var) {
if !value.is_empty() {
return parse_locale(&value);
}
}
}
// Default fallback
SystemLocale {
locale: "en-US".to_string(),
language: "en".to_string(),
country: "US".to_string(),
}
}
fn detect_timezone_from_files() -> SystemTimezone {
// Try to read timezone from /etc/localtime symlink
if let Ok(link_target) = std::fs::read_link("/etc/localtime") {
if let Some(tz_path) = link_target.to_str() {
// Extract timezone name from path like /usr/share/zoneinfo/America/New_York
if let Some(zoneinfo_pos) = tz_path.find("zoneinfo/") {
let tz_name = &tz_path[zoneinfo_pos + 9..];
if !tz_name.is_empty() {
return SystemTimezone {
timezone: tz_name.to_string(),
offset: "+00:00".to_string(), // Could be improved with actual offset calculation
};
}
}
}
}
// Default fallback
SystemTimezone {
timezone: "UTC".to_string(),
offset: "+00:00".to_string(),
}
}
/// Tauri command to get system locale
#[tauri::command]
pub async fn get_system_locale() -> Result<SystemLocale, String> {
let utils = SystemUtils::new();
Ok(utils.detect_system_locale())
}
/// Tauri command to get system timezone
#[tauri::command]
pub async fn get_system_timezone() -> Result<SystemTimezone, String> {
let utils = SystemUtils::new();
Ok(utils.detect_system_timezone())
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut Browser",
"version": "0.6.1",
"version": "0.7.2",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm dev",
+53 -5
View File
@@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { FaDownload } from "react-icons/fa";
import { FiWifi } from "react-icons/fi";
import { GoGear, GoKebabHorizontal, GoPlus } from "react-icons/go";
import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog";
import { ChangeVersionDialog } from "@/components/change-version-dialog";
import { CreateProfileDialog } from "@/components/create-profile-dialog";
import { ImportProfileDialog } from "@/components/import-profile-dialog";
@@ -36,7 +37,7 @@ import { useUpdateNotifications } from "@/hooks/use-update-notifications";
import { useVersionUpdater } from "@/hooks/use-version-updater";
import { showErrorToast } from "@/lib/toast-utils";
import { sleep } from "@/lib/utils";
import type { BrowserProfile } from "@/types";
import type { BrowserProfile, CamoufoxConfig } from "@/types";
type BrowserTypeString =
| "mullvad-browser"
@@ -45,7 +46,8 @@ type BrowserTypeString =
| "chromium"
| "brave"
| "zen"
| "tor-browser";
| "tor-browser"
| "camoufox";
interface PendingUrl {
id: string;
@@ -62,11 +64,15 @@ export default function Home() {
const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false);
const [proxyManagementDialogOpen, setProxyManagementDialogOpen] =
useState(false);
const [camoufoxConfigDialogOpen, setCamoufoxConfigDialogOpen] =
useState(false);
const [pendingUrls, setPendingUrls] = useState<PendingUrl[]>([]);
const [currentProfileForProxy, setCurrentProfileForProxy] =
useState<BrowserProfile | null>(null);
const [currentProfileForVersionChange, setCurrentProfileForVersionChange] =
useState<BrowserProfile | null>(null);
const [currentProfileForCamoufoxConfig, setCurrentProfileForCamoufoxConfig] =
useState<BrowserProfile | null>(null);
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
const [currentPermissionType, setCurrentPermissionType] =
@@ -135,7 +141,9 @@ export default function Home() {
const profileList = await invoke<BrowserProfile[]>(
"list_browser_profiles",
);
setProfiles(profileList);
setProfiles(
profileList.filter((profile) => profile.browser !== "camoufox"),
);
// Check for missing binaries after loading profiles
await checkMissingBinaries();
@@ -183,7 +191,9 @@ export default function Home() {
const profileList = await invoke<BrowserProfile[]>(
"list_browser_profiles",
);
setProfiles(profileList);
setProfiles(
profileList.filter((profile) => profile.browser !== "camoufox"),
);
// TODO: remove after a few version bumps, needed to properly display migrated profiles
setTimeout(async () => {
@@ -191,7 +201,9 @@ export default function Home() {
const profiles = await invoke<BrowserProfile[]>(
"list_browser_profiles",
);
setProfiles(profiles);
setProfiles(
profiles.filter((profile) => profile.browser !== "camoufox"),
);
}
await sleep(500);
}, 0);
@@ -326,6 +338,30 @@ export default function Home() {
setChangeVersionDialogOpen(true);
}, []);
const handleConfigureCamoufox = useCallback((profile: BrowserProfile) => {
setCurrentProfileForCamoufoxConfig(profile);
setCamoufoxConfigDialogOpen(true);
}, []);
const handleSaveCamoufoxConfig = useCallback(
async (profile: BrowserProfile, config: CamoufoxConfig) => {
setError(null);
try {
await invoke("update_camoufox_config", {
profileName: profile.name,
config,
});
await loadProfiles();
setCamoufoxConfigDialogOpen(false);
} catch (err: unknown) {
console.error("Failed to update camoufox config:", err);
setError(`Failed to update camoufox config: ${JSON.stringify(err)}`);
throw err;
}
},
[loadProfiles],
);
const handleSaveProxy = useCallback(
async (proxyId: string | null) => {
setProxyDialogOpen(false);
@@ -356,6 +392,7 @@ export default function Home() {
version: string;
releaseType: string;
proxyId?: string;
camoufoxConfig?: CamoufoxConfig;
}) => {
setError(null);
@@ -368,6 +405,7 @@ export default function Home() {
version: profileData.version,
releaseType: profileData.releaseType,
proxyId: profileData.proxyId,
camoufoxConfig: profileData.camoufoxConfig,
},
);
@@ -658,6 +696,7 @@ export default function Home() {
onDeleteProfile={handleDeleteProfile}
onRenameProfile={handleRenameProfile}
onChangeVersion={openChangeVersionDialog}
onConfigureCamoufox={handleConfigureCamoufox}
runningProfiles={runningProfiles}
isUpdating={isUpdating}
onReloadProxyData={
@@ -739,6 +778,15 @@ export default function Home() {
permissionType={currentPermissionType}
onPermissionGranted={checkNextPermission}
/>
<CamoufoxConfigDialog
isOpen={camoufoxConfigDialogOpen}
onClose={() => {
setCamoufoxConfigDialogOpen(false);
}}
profile={currentProfileForCamoufoxConfig}
onSave={handleSaveCamoufoxConfig}
/>
</div>
);
}
+43 -26
View File
@@ -65,11 +65,18 @@ export function AppUpdateToast({
await onUpdate(updateInfo);
};
const showProgress =
const showDownloadProgress =
isUpdating &&
updateProgress?.stage === "downloading" &&
updateProgress.percentage !== undefined;
const showOtherStageProgress =
isUpdating &&
updateProgress &&
(updateProgress.stage === "extracting" ||
updateProgress.stage === "installing" ||
updateProgress.stage === "completed");
return (
<div className="flex items-start p-4 w-full max-w-md bg-white rounded-lg border border-gray-200 shadow-lg dark:bg-gray-800 dark:border-gray-700">
<div className="mr-3 mt-0.5">
@@ -117,7 +124,7 @@ export function AppUpdateToast({
</div>
{/* Download progress */}
{showProgress && updateProgress && (
{showDownloadProgress && updateProgress && (
<div className="mt-2 space-y-1">
<div className="flex justify-between items-center">
<p className="flex-1 min-w-0 text-xs text-muted-foreground">
@@ -135,31 +142,41 @@ export function AppUpdateToast({
</div>
)}
{/* Other stage progress (without percentage) */}
{isUpdating &&
updateProgress &&
updateProgress.stage !== "downloading" && (
<div className="mt-2">
<p className="text-xs text-muted-foreground">
{updateProgress.message}
</p>
{updateProgress.stage === "extracting" && (
<p className="mt-1 text-xs text-muted-foreground">
Preparing update files...
</p>
)}
{updateProgress.stage === "installing" && (
<p className="mt-1 text-xs text-muted-foreground">
Installing new version...
</p>
)}
{updateProgress.stage === "completed" && (
<p className="mt-1 text-xs text-green-600 dark:text-green-400">
Update completed! Restarting application...
</p>
)}
{/* Other stage progress (with visual indicators) */}
{showOtherStageProgress && (
<div className="mt-2 space-y-2">
<p className="text-xs text-muted-foreground">
{updateProgress.message}
</p>
{/* Progress indicator for non-downloading stages */}
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all duration-500 ${
updateProgress.stage === "completed"
? "bg-green-500 w-full"
: "bg-blue-500 w-full animate-pulse"
}`}
/>
</div>
)}
{updateProgress.stage === "extracting" && (
<p className="text-xs text-muted-foreground">
Preparing update files...
</p>
)}
{updateProgress.stage === "installing" && (
<p className="text-xs text-muted-foreground">
Installing new version...
</p>
)}
{updateProgress.stage === "completed" && (
<p className="text-xs text-green-600 dark:text-green-400">
Update completed! Restarting application...
</p>
)}
</div>
)}
{!isUpdating && (
<div className="flex gap-2 items-center mt-3">
+502
View File
@@ -0,0 +1,502 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { BrowserProfile, CamoufoxConfig } from "@/types";
interface CamoufoxConfigDialogProps {
isOpen: boolean;
onClose: () => void;
profile: BrowserProfile | null;
onSave: (profile: BrowserProfile, config: CamoufoxConfig) => Promise<void>;
}
const osOptions = [
{ value: "windows", label: "Windows" },
{ value: "macos", label: "macOS" },
{ value: "linux", label: "Linux" },
];
const timezoneOptions = [
{ value: "America/New_York", label: "America/New_York" },
{ value: "America/Los_Angeles", label: "America/Los_Angeles" },
{ value: "Europe/London", label: "Europe/London" },
{ value: "Europe/Paris", label: "Europe/Paris" },
{ value: "Asia/Tokyo", label: "Asia/Tokyo" },
{ value: "Asia/Shanghai", label: "Asia/Shanghai" },
{ value: "Australia/Sydney", label: "Australia/Sydney" },
];
const localeOptions = [
{ value: "en-US", label: "English (US)" },
{ value: "en-GB", label: "English (UK)" },
{ value: "fr-FR", label: "French" },
{ value: "de-DE", label: "German" },
{ value: "es-ES", label: "Spanish" },
{ value: "it-IT", label: "Italian" },
{ value: "ja-JP", label: "Japanese" },
{ value: "zh-CN", label: "Chinese (Simplified)" },
];
const getCurrentOS = () => {
if (typeof window !== "undefined") {
const userAgent = window.navigator.userAgent;
if (userAgent.includes("Win")) return "windows";
if (userAgent.includes("Mac")) return "macos";
if (userAgent.includes("Linux")) return "linux";
}
return "unknown";
};
export function CamoufoxConfigDialog({
isOpen,
onClose,
profile,
onSave,
}: CamoufoxConfigDialogProps) {
const [config, setConfig] = useState<CamoufoxConfig>({
enable_cache: true,
os: [getCurrentOS()],
});
const [isSaving, setIsSaving] = useState(false);
// Initialize config when profile changes
useEffect(() => {
if (profile && profile.browser === "camoufox") {
setConfig(
profile.camoufox_config || {
enable_cache: true,
os: [getCurrentOS()],
},
);
}
}, [profile]);
const updateConfig = (key: keyof CamoufoxConfig, value: unknown) => {
setConfig((prev) => ({ ...prev, [key]: value }));
};
const handleSave = async () => {
if (!profile) return;
setIsSaving(true);
try {
await onSave(profile, config);
onClose();
} catch (error) {
console.error("Failed to save camoufox config:", error);
} finally {
setIsSaving(false);
}
};
const handleClose = () => {
// Reset config to original when closing without saving
if (profile && profile.browser === "camoufox") {
setConfig(
profile.camoufox_config || {
enable_cache: true,
os: [getCurrentOS()],
},
);
}
onClose();
};
if (!profile || profile.browser !== "camoufox") {
return null;
}
// Get the selected OS for warning
const selectedOS = config.os?.[0];
const currentOS = getCurrentOS();
const showOSWarning =
selectedOS && selectedOS !== currentOS && currentOS !== "unknown";
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-3xl max-h-[90vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>
Configure Camoufox Settings - {profile.name}
</DialogTitle>
</DialogHeader>
<ScrollArea className="flex-1 pr-6 h-[320px]">
<div className="py-4 space-y-6">
{/* Operating System */}
<div className="space-y-3">
<Label>Operating System Fingerprint</Label>
<Select
value={selectedOS || ""}
onValueChange={(value: string) => updateConfig("os", [value])}
>
<SelectTrigger>
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
{osOptions.map((os) => (
<SelectItem key={os.value} value={os.value}>
{os.label}
</SelectItem>
))}
</SelectContent>
</Select>
{showOSWarning && (
<div className="p-3 bg-amber-50 rounded-md border border-amber-200">
<p className="text-sm text-amber-800">
Warning: Spoofing OS features is detectable by advanced
anti-bot systems. Some platform-specific APIs and behaviors
cannot be fully replicated.
</p>
</div>
)}
</div>
{/* Blocking Options */}
<div className="space-y-3">
<Label>Privacy & Blocking</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="block-images"
checked={config.block_images || false}
onCheckedChange={(checked) =>
updateConfig("block_images", checked)
}
/>
<Label htmlFor="block-images">Block Images</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="block-webrtc"
checked={config.block_webrtc || false}
onCheckedChange={(checked) =>
updateConfig("block_webrtc", checked)
}
/>
<Label htmlFor="block-webrtc">Block WebRTC</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="block-webgl"
checked={config.block_webgl || false}
onCheckedChange={(checked) =>
updateConfig("block_webgl", checked)
}
/>
<Label htmlFor="block-webgl">Block WebGL</Label>
</div>
</div>
</div>
{/* Geolocation */}
<div className="space-y-3">
<Label>Geolocation</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="country">Country</Label>
<Input
id="country"
value={config.country || ""}
onChange={(e) =>
updateConfig("country", e.target.value || undefined)
}
placeholder="e.g., US, GB, DE"
/>
</div>
<div className="space-y-2">
<Label>Timezone</Label>
<Select
value={config.timezone || "auto"}
onValueChange={(value) =>
updateConfig(
"timezone",
value === "auto" ? undefined : value,
)
}
>
<SelectTrigger>
<SelectValue placeholder="Select timezone" />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto</SelectItem>
{timezoneOptions.map((tz) => (
<SelectItem key={tz.value} value={tz.value}>
{tz.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="latitude">Latitude</Label>
<Input
id="latitude"
type="number"
step="any"
value={config.latitude || ""}
onChange={(e) =>
updateConfig(
"latitude",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
placeholder="e.g., 40.7128"
/>
</div>
<div className="space-y-2">
<Label htmlFor="longitude">Longitude</Label>
<Input
id="longitude"
type="number"
step="any"
value={config.longitude || ""}
onChange={(e) =>
updateConfig(
"longitude",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
placeholder="e.g., -74.0060"
/>
</div>
</div>
</div>
{/* Localization */}
<div className="space-y-3">
<Label>Locale</Label>
<Select
value={config.locale?.[0] || ""}
onValueChange={(value) =>
updateConfig("locale", value ? [value] : undefined)
}
>
<SelectTrigger>
<SelectValue placeholder="Select locale" />
</SelectTrigger>
<SelectContent>
{localeOptions.map((locale) => (
<SelectItem key={locale.value} value={locale.value}>
{locale.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Screen Resolution */}
<div className="space-y-3">
<Label>Screen Resolution</Label>
<div className="grid grid-cols-2 gap-4">
<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) =>
updateConfig(
"screen_min_width",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1024"
/>
</div>
<div className="space-y-2">
<Label htmlFor="screen-max-width">Max Width</Label>
<Input
id="screen-max-width"
type="number"
value={config.screen_max_width || ""}
onChange={(e) =>
updateConfig(
"screen_max_width",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1920"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<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) =>
updateConfig(
"screen_min_height",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 768"
/>
</div>
<div className="space-y-2">
<Label htmlFor="screen-max-height">Max Height</Label>
<Input
id="screen-max-height"
type="number"
value={config.screen_max_height || ""}
onChange={(e) =>
updateConfig(
"screen_max_height",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1080"
/>
</div>
</div>
</div>
{/* Window Size */}
<div className="space-y-3">
<Label>Window Size</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="window-width">Width</Label>
<Input
id="window-width"
type="number"
value={config.window_width || ""}
onChange={(e) =>
updateConfig(
"window_width",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1366"
/>
</div>
<div className="space-y-2">
<Label htmlFor="window-height">Height</Label>
<Input
id="window-height"
type="number"
value={config.window_height || ""}
onChange={(e) =>
updateConfig(
"window_height",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 768"
/>
</div>
</div>
</div>
{/* Advanced Options */}
<div className="space-y-3">
<Label>Advanced Options</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="enable-cache"
checked={config.enable_cache || false}
onCheckedChange={(checked) =>
updateConfig("enable_cache", checked)
}
/>
<Label htmlFor="enable-cache">Enable Browser Cache</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="main-world-eval"
checked={config.main_world_eval || false}
onCheckedChange={(checked) =>
updateConfig("main_world_eval", checked)
}
/>
<Label htmlFor="main-world-eval">
Enable Main World Evaluation
</Label>
</div>
</div>
</div>
{/* WebGL Settings */}
<div className="space-y-3">
<Label>WebGL Settings</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="webgl-vendor">WebGL Vendor</Label>
<Input
id="webgl-vendor"
value={config.webgl_vendor || ""}
onChange={(e) =>
updateConfig("webgl_vendor", e.target.value || undefined)
}
placeholder="e.g., Intel Inc."
/>
</div>
<div className="space-y-2">
<Label htmlFor="webgl-renderer">WebGL Renderer</Label>
<Input
id="webgl-renderer"
value={config.webgl_renderer || ""}
onChange={(e) =>
updateConfig(
"webgl_renderer",
e.target.value || undefined,
)
}
placeholder="e.g., Intel Iris OpenGL Engine"
/>
</div>
</div>
</div>
{/* Debug Options */}
<div className="space-y-3">
<Label>Debug Options</Label>
<div className="flex items-center space-x-2">
<Checkbox
id="debug"
checked={config.debug || false}
onCheckedChange={(checked) => updateConfig("debug", checked)}
/>
<Label htmlFor="debug">Enable Debug Mode</Label>
</div>
</div>
</div>
</ScrollArea>
<DialogFooter className="flex-shrink-0 pt-4 border-t">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? "Saving..." : "Save Configuration"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+347 -417
View File
@@ -2,12 +2,10 @@
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useEffect, useState } from "react";
import { FiPlus } from "react-icons/fi";
import { toast } from "sonner";
import { LoadingButton } from "@/components/loading-button";
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
import { ReleaseTypeSelector } from "@/components/release-type-selector";
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import {
Dialog,
DialogContent,
@@ -17,6 +15,7 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
@@ -24,16 +23,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useBrowserDownload } from "@/hooks/use-browser-download";
import { useBrowserSupport } from "@/hooks/use-browser-support";
import { getBrowserDisplayName } from "@/lib/browser-utils";
import type { BrowserProfile, BrowserReleaseTypes, StoredProxy } from "@/types";
import { Alert, AlertDescription } from "./ui/alert";
import { getBrowserIcon, getCurrentOS } from "@/lib/browser-utils";
import type { BrowserReleaseTypes, CamoufoxConfig, StoredProxy } from "@/types";
type BrowserTypeString =
| "mullvad-browser"
@@ -42,7 +35,8 @@ type BrowserTypeString =
| "chromium"
| "brave"
| "zen"
| "tor-browser";
| "tor-browser"
| "camoufox";
interface CreateProfileDialogProps {
isOpen: boolean;
@@ -53,503 +47,439 @@ interface CreateProfileDialogProps {
version: string;
releaseType: string;
proxyId?: string;
camoufoxConfig?: CamoufoxConfig;
}) => Promise<void>;
}
interface BrowserOption {
value: BrowserTypeString;
label: string;
description: string;
}
const browserOptions: BrowserOption[] = [
{
value: "firefox",
label: "Firefox",
description: "Mozilla's main web browser",
},
{
value: "firefox-developer",
label: "Firefox Developer Edition",
description: "Browser for developers with cutting-edge features",
},
{
value: "chromium",
label: "Chromium",
description: "Open-source version of Chrome",
},
{
value: "brave",
label: "Brave",
description: "Privacy-focused browser with ad blocking",
},
{
value: "zen",
label: "Zen Browser",
description: "Beautiful, customizable Firefox-based browser",
},
{
value: "mullvad-browser",
label: "Mullvad Browser",
description: "Privacy browser by Mullvad VPN",
},
{
value: "tor-browser",
label: "Tor Browser",
description: "Browse anonymously through the Tor network",
},
];
const IS_ANTI_DETECT_SUPPORTED = false;
export function CreateProfileDialog({
isOpen,
onClose,
onCreateProfile,
}: CreateProfileDialogProps) {
const [profileName, setProfileName] = useState("");
const [selectedBrowser, setSelectedBrowser] =
useState<BrowserTypeString | null>("mullvad-browser");
const [selectedReleaseType, setSelectedReleaseType] = useState<
"stable" | "nightly" | null
>(null);
const [releaseTypes, setReleaseTypes] = useState<BrowserReleaseTypes>({
stable: undefined,
nightly: undefined,
const [activeTab, setActiveTab] = useState("regular");
// Regular browser states
const [selectedBrowser, setSelectedBrowser] = useState<BrowserTypeString>();
const [selectedProxyId, setSelectedProxyId] = useState<string>();
// Camoufox anti-detect states
const [camoufoxConfig, setCamoufoxConfig] = useState<CamoufoxConfig>({
enable_cache: true, // Cache enabled by default
os: [getCurrentOS()], // Default to current OS
});
const [isCreating, setIsCreating] = useState(false);
const [existingProfiles, setExistingProfiles] = useState<BrowserProfile[]>(
[],
);
const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false);
// Proxy settings - now using stored proxy selection
const [selectedProxyId, setSelectedProxyId] = useState<string | null>(null);
// Common states
const [availableReleaseTypes, setAvailableReleaseTypes] =
useState<BrowserReleaseTypes>({});
const [camoufoxReleaseTypes, setCamoufoxReleaseTypes] =
useState<BrowserReleaseTypes>({});
const [supportedBrowsers, setSupportedBrowsers] = useState<string[]>([]);
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
const [isLoadingProxies, setIsLoadingProxies] = useState(false);
const [showProxyForm, setShowProxyForm] = useState(false);
const [isCreating, setIsCreating] = useState(false);
// Use the browser download hook
const {
isBrowserDownloading,
downloadBrowser,
isDownloading,
downloadedVersions,
loadDownloadedVersions,
isVersionDownloaded,
} = useBrowserDownload();
const {
supportedBrowsers,
isLoading: isLoadingSupport,
isBrowserSupported,
} = useBrowserSupport();
useEffect(() => {
if (supportedBrowsers.length > 0) {
// Set default browser to first supported browser
if (supportedBrowsers.includes("mullvad-browser")) {
setSelectedBrowser("mullvad-browser");
} else if (supportedBrowsers.length > 0) {
setSelectedBrowser(supportedBrowsers[0] as BrowserTypeString);
}
}
}, [supportedBrowsers]);
// Set default release type when release types are loaded
useEffect(() => {
if (!selectedReleaseType && Object.keys(releaseTypes).length > 0) {
// First try to set stable if it exists
if (releaseTypes.stable) {
setSelectedReleaseType("stable");
}
// If stable doesn't exist but nightly does, set nightly as default
else if (releaseTypes.nightly && selectedBrowser !== "chromium") {
setSelectedReleaseType("nightly");
}
}
}, [releaseTypes, selectedReleaseType, selectedBrowser]);
const loadExistingProfiles = useCallback(async () => {
const loadSupportedBrowsers = useCallback(async () => {
try {
const profiles = await invoke<BrowserProfile[]>("list_browser_profiles");
setExistingProfiles(profiles);
const browsers = await invoke<string[]>("get_supported_browsers");
setSupportedBrowsers(browsers);
} catch (error) {
console.error("Failed to load existing profiles:", error);
console.error("Failed to load supported browsers:", error);
}
}, []);
const loadStoredProxies = useCallback(async () => {
try {
setIsLoadingProxies(true);
const proxies = await invoke<StoredProxy[]>("get_stored_proxies");
setStoredProxies(proxies);
} catch (error) {
console.error("Failed to load stored proxies:", error);
toast.error("Failed to load available proxies");
} finally {
setIsLoadingProxies(false);
}
}, []);
const loadReleaseTypes = useCallback(async (browser: string) => {
try {
setIsLoadingReleaseTypes(true);
const types = await invoke<BrowserReleaseTypes>(
"get_browser_release_types",
{
browserStr: browser,
},
);
setReleaseTypes(types);
} catch (error) {
console.error("Failed to load release types:", error);
toast.error("Failed to load available versions");
} finally {
setIsLoadingReleaseTypes(false);
}
}, []);
const loadReleaseTypes = useCallback(
async (browser: string) => {
try {
const releaseTypes = await invoke<BrowserReleaseTypes>(
"get_browser_release_types",
{ browserStr: browser },
);
const handleDownload = useCallback(async () => {
if (!selectedBrowser || !selectedReleaseType) return;
if (browser === "camoufox") {
setCamoufoxReleaseTypes(releaseTypes);
} else {
setAvailableReleaseTypes(releaseTypes);
}
const version =
selectedReleaseType === "stable"
? releaseTypes.stable
: releaseTypes.nightly;
if (!version) return;
await downloadBrowser(selectedBrowser, version);
}, [selectedBrowser, selectedReleaseType, downloadBrowser, releaseTypes]);
const validateProfileName = useCallback(
(name: string): string | null => {
const trimmedName = name.trim();
if (!trimmedName) {
return "Profile name cannot be empty";
// Load downloaded versions for this browser
await loadDownloadedVersions(browser);
} catch (error) {
console.error(`Failed to load release types for ${browser}:`, error);
}
// Check for duplicate names (case insensitive)
const isDuplicate = existingProfiles.some(
(profile) => profile.name.toLowerCase() === trimmedName.toLowerCase(),
);
if (isDuplicate) {
return "A profile with this name already exists";
}
return null;
},
[existingProfiles],
[loadDownloadedVersions],
);
// Helper to determine if proxy should be disabled for the selected browser
const isProxyDisabled = selectedBrowser === "tor-browser";
// Update proxy selection when browser changes to tor-browser
// Load data when dialog opens
useEffect(() => {
if (selectedBrowser === "tor-browser" && selectedProxyId) {
setSelectedProxyId(null);
if (isOpen) {
void loadSupportedBrowsers();
void loadStoredProxies();
// Load camoufox release types when dialog opens
void loadReleaseTypes("camoufox");
}
}, [selectedBrowser, selectedProxyId]);
}, [isOpen, loadSupportedBrowsers, loadStoredProxies, loadReleaseTypes]);
const handleCreateProxy = useCallback(() => {
setShowProxyForm(true);
}, []);
// Load release types when browser selection changes
useEffect(() => {
if (selectedBrowser) {
void loadReleaseTypes(selectedBrowser);
}
}, [selectedBrowser, loadReleaseTypes]);
const handleProxySaved = useCallback((savedProxy: StoredProxy) => {
setStoredProxies((prev) => {
const existingIndex = prev.findIndex((p) => p.id === savedProxy.id);
if (existingIndex >= 0) {
// Update existing proxy
const updated = [...prev];
updated[existingIndex] = savedProxy;
return updated;
} else {
// Add new proxy
return [...prev, savedProxy];
}
});
setSelectedProxyId(savedProxy.id);
setShowProxyForm(false);
}, []);
const handleDownload = async (browserStr: string) => {
const releaseTypes =
browserStr === "camoufox" ? camoufoxReleaseTypes : availableReleaseTypes;
const latestStableVersion = releaseTypes.stable;
const handleProxyFormClose = useCallback(() => {
setShowProxyForm(false);
}, []);
const handleCreate = useCallback(async () => {
if (!profileName.trim() || !selectedBrowser || !selectedReleaseType) return;
// Validate profile name
const nameError = validateProfileName(profileName);
if (nameError) {
toast.error(nameError);
if (!latestStableVersion) {
console.error("No stable version available for download");
return;
}
const version =
selectedReleaseType === "stable"
? releaseTypes.stable
: releaseTypes.nightly;
if (!version) {
toast.error("Selected release type is not available");
return;
try {
await downloadBrowser(browserStr, latestStableVersion);
} catch (error) {
console.error("Failed to download browser:", error);
}
};
const handleCreate = async () => {
if (!profileName.trim()) return;
setIsCreating(true);
try {
await onCreateProfile({
name: profileName.trim(),
browserStr: selectedBrowser,
version,
releaseType: selectedReleaseType,
proxyId: isProxyDisabled ? undefined : (selectedProxyId ?? undefined),
});
if (activeTab === "regular") {
if (!selectedBrowser) {
console.error("Missing required browser selection");
return;
}
// Reset form
setProfileName("");
setSelectedReleaseType(null);
setSelectedProxyId(null);
onClose();
// Use the latest stable version by default
const latestStableVersion = availableReleaseTypes.stable;
if (!latestStableVersion) {
console.error("No stable version available");
return;
}
await onCreateProfile({
name: profileName.trim(),
browserStr: selectedBrowser,
version: latestStableVersion,
releaseType: "stable",
proxyId: selectedProxyId,
});
} else {
// Anti-detect tab - always use Camoufox with latest version
const latestCamoufoxVersion = camoufoxReleaseTypes.stable;
if (!latestCamoufoxVersion) {
console.error("No Camoufox version available");
return;
}
await onCreateProfile({
name: profileName.trim(),
browserStr: "camoufox" as BrowserTypeString,
version: latestCamoufoxVersion,
releaseType: "stable",
proxyId: selectedProxyId,
camoufoxConfig,
});
}
handleClose();
} catch (error) {
console.error("Failed to create profile:", error);
} finally {
setIsCreating(false);
}
}, [
profileName,
selectedBrowser,
selectedReleaseType,
onCreateProfile,
isProxyDisabled,
selectedProxyId,
onClose,
releaseTypes.nightly,
releaseTypes.stable,
validateProfileName,
]);
};
const nameError = profileName.trim()
? validateProfileName(profileName)
: null;
const handleClose = () => {
// Reset all states
setProfileName("");
setSelectedBrowser(undefined);
setSelectedProxyId(undefined);
setCamoufoxConfig({
enable_cache: true,
os: [getCurrentOS()], // Reset to current OS
});
setActiveTab("regular");
onClose();
};
const selectedVersion =
selectedReleaseType === "stable"
? releaseTypes.stable
: releaseTypes.nightly;
const isCreateDisabled = () => {
if (!profileName.trim()) return true;
const canCreate =
profileName.trim() &&
selectedBrowser &&
selectedReleaseType &&
selectedVersion &&
isVersionDownloaded(selectedVersion) &&
!nameError;
useEffect(() => {
if (isOpen) {
void loadExistingProfiles();
void loadStoredProxies();
if (activeTab === "regular") {
return !selectedBrowser || !availableReleaseTypes.stable;
} else {
// For anti-detect, we need camoufox to be available
return !camoufoxReleaseTypes.stable;
}
}, [isOpen, loadExistingProfiles, loadStoredProxies]);
};
useEffect(() => {
if (isOpen && selectedBrowser) {
// Reset selected release type when browser changes
setSelectedReleaseType(null);
void loadReleaseTypes(selectedBrowser);
void loadDownloadedVersions(selectedBrowser);
}
}, [isOpen, selectedBrowser, loadDownloadedVersions, loadReleaseTypes]);
const updateCamoufoxConfig = (key: keyof CamoufoxConfig, value: unknown) => {
setCamoufoxConfig((prev) => ({ ...prev, [key]: value }));
};
// Check if browser version is downloaded and available
const isBrowserVersionAvailable = (browserStr: string) => {
const releaseTypes =
browserStr === "camoufox" ? camoufoxReleaseTypes : availableReleaseTypes;
const latestStableVersion = releaseTypes.stable;
return latestStableVersion && isVersionDownloaded(latestStableVersion);
};
// Get the selected OS for warning
const selectedOS = camoufoxConfig.os?.[0];
const currentOS = getCurrentOS();
const _showOSWarning =
selectedOS && selectedOS !== currentOS && currentOS !== "unknown";
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md max-h-[80vh] my-8 flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>Create New Profile</DialogTitle>
</DialogHeader>
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>Create New Profile</DialogTitle>
</DialogHeader>
<div className="grid overflow-y-scroll flex-1 gap-6 py-4 min-h-0">
{/* Profile Name */}
<div className="grid gap-2">
<Label htmlFor="profile-name">Profile Name</Label>
<Input
id="profile-name"
value={profileName}
onChange={(e) => {
setProfileName(e.target.value);
}}
placeholder="Enter profile name"
className={nameError ? "border-red-500" : ""}
/>
{nameError && <p className="text-sm text-red-600">{nameError}</p>}
</div>
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="flex flex-col flex-1 w-full min-h-0"
>
<TabsList className="grid flex-shrink-0 grid-cols-2 w-full">
<TabsTrigger value="regular">Regular Browsers</TabsTrigger>
<TabsTrigger value="anti-detect">Anti-Detect</TabsTrigger>
</TabsList>
{/* Browser Selection */}
<div className="grid gap-2">
<Label>Browser</Label>
<Select
value={selectedBrowser ?? undefined}
onValueChange={(value) => {
setSelectedBrowser(value as BrowserTypeString);
}}
disabled={isLoadingSupport}
>
<SelectTrigger>
<SelectValue
placeholder={
isLoadingSupport
? "Loading browsers..."
: "Select browser"
}
/>
</SelectTrigger>
<SelectContent>
{(
[
"mullvad-browser",
"firefox",
"firefox-developer",
"chromium",
"brave",
"zen",
"tor-browser",
] as BrowserTypeString[]
).map((browser) => {
const isSupported = isBrowserSupported(browser);
const displayName = getBrowserDisplayName(browser);
<ScrollArea className="flex-1 pr-6 h-[320px]">
<div className="py-4 space-y-6">
{/* Profile Name - Common to both tabs */}
<div className="space-y-2">
<Label htmlFor="profile-name">Profile Name</Label>
<Input
id="profile-name"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
placeholder="Enter profile name"
/>
</div>
if (!isSupported) {
return (
<Tooltip key={browser}>
<TooltipTrigger asChild>
<SelectItem
value={browser}
disabled={true}
className="opacity-50"
>
{displayName} (Not supported)
</SelectItem>
</TooltipTrigger>
<TooltipContent>
<p>
{displayName} is not supported on your current
platform or architecture.
</p>
</TooltipContent>
</Tooltip>
);
}
return (
<SelectItem key={browser} value={browser}>
{displayName}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
{selectedBrowser ? (
<div className="grid gap-2">
<Label>Release Type</Label>
{isLoadingReleaseTypes ? (
<div className="text-sm text-muted-foreground">
Loading release types...
</div>
) : Object.keys(releaseTypes).length === 0 ? (
<Alert>
<AlertDescription>
No releases are available for{" "}
{getBrowserDisplayName(selectedBrowser)}.
</AlertDescription>
</Alert>
) : (
<div className="space-y-4">
{(!releaseTypes.stable || !releaseTypes.nightly) && (
<Alert>
<AlertDescription>
Only {(releaseTypes.stable && "Stable") ?? "Nightly"}{" "}
releases are available for{" "}
{getBrowserDisplayName(selectedBrowser)}.
</AlertDescription>
</Alert>
)}
<ReleaseTypeSelector
selectedReleaseType={selectedReleaseType}
onReleaseTypeSelect={setSelectedReleaseType}
availableReleaseTypes={releaseTypes}
browser={selectedBrowser}
isDownloading={isDownloading}
onDownload={() => {
void handleDownload();
}}
placeholder="Select release type..."
downloadedVersions={downloadedVersions}
<TabsContent value="regular" className="mt-0 space-y-6">
<div className="space-y-4">
<div className="space-y-2">
<Label>Browser</Label>
<Combobox
options={browserOptions
.filter((browser) =>
supportedBrowsers.includes(browser.value),
)
.map((browser) => {
const IconComponent = getBrowserIcon(browser.value);
return {
value: browser.value,
label: browser.label,
icon: IconComponent,
};
})}
value={selectedBrowser || ""}
onValueChange={(value) =>
setSelectedBrowser(value as BrowserTypeString)
}
placeholder="Select a browser..."
searchPlaceholder="Search browsers..."
/>
</div>
)}
</div>
) : null}
{/* Proxy Settings */}
<div className="grid gap-4 pt-4 border-t">
<div className="grid gap-2">
<div className="flex justify-between items-center">
<Label>Proxy Settings</Label>
{!isProxyDisabled && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={handleCreateProxy}
className="flex gap-2 items-center"
>
<FiPlus className="w-4 h-4" />
Create Proxy
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Create a new proxy configuration</p>
</TooltipContent>
</Tooltip>
{selectedBrowser && (
<div className="space-y-3">
{!isBrowserVersionAvailable(selectedBrowser) &&
availableReleaseTypes.stable && (
<div className="flex gap-3 items-center">
<p className="text-sm text-muted-foreground">
Latest stable version (
{availableReleaseTypes.stable}) needs to be
downloaded
</p>
<LoadingButton
onClick={() => handleDownload(selectedBrowser)}
isLoading={isBrowserDownloading(selectedBrowser)}
size="sm"
disabled={isBrowserDownloading(selectedBrowser)}
>
Download
</LoadingButton>
</div>
)}
{isBrowserVersionAvailable(selectedBrowser) && (
<div className="text-sm text-green-600">
Latest stable version (
{availableReleaseTypes.stable}) is available
</div>
)}
</div>
)}
</div>
</TabsContent>
{isProxyDisabled ? (
<Tooltip>
<TooltipTrigger asChild>
<div className="p-3 bg-yellow-50 rounded-md border border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
Tor Browser has its own built-in proxy system and
doesn&apos;t support additional proxy configuration.
</p>
<TabsContent value="anti-detect" className="mt-0 space-y-6">
<div className="p-3 text-center bg-blue-50 rounded-md border border-blue-200 dark:bg-blue-950 dark:border-blue-800">
<p className="text-sm text-blue-800 dark:text-blue-200">
Anti-Detect support is coming soon!
</p>
</div>
</TabsContent>
{IS_ANTI_DETECT_SUPPORTED && (
<TabsContent value="anti-detect" className="mt-0 space-y-6">
{/* Anti-Detect Description */}
<div className="p-3 text-center bg-blue-50 rounded-md border border-blue-200 dark:bg-blue-950 dark:border-blue-800">
<p className="text-sm text-blue-800 dark:text-blue-200">
Powered by Camoufox
</p>
</div>
<div className="space-y-6">
{/* Camoufox Download Status */}
{!isBrowserVersionAvailable("camoufox") &&
camoufoxReleaseTypes.stable && (
<div className="flex gap-3 items-center p-3 bg-amber-50 rounded-md border border-amber-200">
<p className="text-sm text-amber-800">
Camoufox version ({camoufoxReleaseTypes.stable})
needs to be downloaded
</p>
<LoadingButton
onClick={() => handleDownload("camoufox")}
isLoading={isBrowserDownloading("camoufox")}
size="sm"
disabled={isBrowserDownloading("camoufox")}
>
Download
</LoadingButton>
</div>
)}
{isBrowserVersionAvailable("camoufox") && (
<div className="p-3 text-sm text-green-600 bg-green-50 rounded-md border border-green-200">
Camoufox version ({camoufoxReleaseTypes.stable}) is
available
</div>
</TooltipTrigger>
<TooltipContent>
<p>
Tor Browser manages its own proxy routing automatically
</p>
</TooltipContent>
</Tooltip>
) : (
)}
<SharedCamoufoxConfigForm
config={camoufoxConfig}
onConfigChange={updateCamoufoxConfig}
/>
</div>
</TabsContent>
)}
{/* Proxy Selection - Common to both tabs - Compact without card */}
{storedProxies.length > 0 && (
<div className="space-y-3">
<Label>Proxy</Label>
<Select
value={selectedProxyId ?? "none"}
onValueChange={(value) => {
setSelectedProxyId(value === "none" ? null : value);
}}
disabled={isLoadingProxies}
value={selectedProxyId || "none"}
onValueChange={(value) =>
setSelectedProxyId(value === "none" ? undefined : value)
}
>
<SelectTrigger>
<SelectValue
placeholder={
isLoadingProxies
? "Loading proxies..."
: "Select proxy (optional)"
}
/>
<SelectValue placeholder="No proxy" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No Proxy</SelectItem>
<SelectItem value="none">No proxy</SelectItem>
{storedProxies.map((proxy) => (
<SelectItem key={proxy.id} value={proxy.id}>
{proxy.name}
{proxy.name} ({proxy.proxy_settings.proxy_type}://
{proxy.proxy_settings.host}:
{proxy.proxy_settings.port})
</SelectItem>
))}
</SelectContent>
</Select>
)}
{!isProxyDisabled &&
storedProxies.length === 0 &&
!isLoadingProxies && (
<p className="text-sm text-muted-foreground">
No saved proxies available. Use the "Create Proxy" button
above to create proxy configurations.
</p>
)}
</div>
</div>
)}
</div>
</div>
</ScrollArea>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
<DialogFooter className="flex-shrink-0 pt-4 border-t">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<LoadingButton
onClick={handleCreate}
isLoading={isCreating}
onClick={() => void handleCreate()}
disabled={!canCreate}
disabled={isCreateDisabled()}
>
Create Profile
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
<ProxyFormDialog
isOpen={showProxyForm}
onClose={handleProxyFormClose}
onSave={handleProxySaved}
/>
</>
</Tabs>
</DialogContent>
</Dialog>
);
}
+35 -16
View File
@@ -240,26 +240,45 @@ export function UnifiedToast(props: ToastProps) {
)}
{/* App update progress */}
{type === "app-update" &&
progress &&
"percentage" in progress &&
stage === "downloading" && (
<div className="mt-2 space-y-1">
<div className="flex justify-between items-center">
<p className="flex-1 min-w-0 text-xs text-gray-600 dark:text-gray-300">
{progress.percentage.toFixed(1)}%
{progress.speed && `${progress.speed} MB/s`}
{progress.eta && `${progress.eta} remaining`}
</p>
</div>
{type === "app-update" && (
<div className="mt-2 space-y-1">
{/* Download progress with percentage */}
{progress &&
"percentage" in progress &&
stage === "downloading" && (
<>
<div className="flex justify-between items-center">
<p className="flex-1 min-w-0 text-xs text-gray-600 dark:text-gray-300">
{progress.percentage.toFixed(1)}%
{progress.speed && `${progress.speed} MB/s`}
{progress.eta && `${progress.eta} remaining`}
</p>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
style={{ width: `${progress.percentage}%` }}
/>
</div>
</>
)}
{/* Progress indicator for other stages */}
{(stage === "extracting" ||
stage === "installing" ||
stage === "completed") && (
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
style={{ width: `${progress.percentage}%` }}
className={`h-1.5 rounded-full transition-all duration-500 ${
stage === "completed"
? "bg-green-500 w-full"
: "bg-blue-500 w-full animate-pulse"
}`}
/>
</div>
</div>
)}
)}
</div>
)}
{/* Version update progress */}
{type === "version-update" &&
+30 -3
View File
@@ -45,7 +45,12 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useTableSorting } from "@/hooks/use-table-sorting";
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
import {
getBrowserDisplayName,
getBrowserIcon,
getCurrentOS,
} from "@/lib/browser-utils";
import { cn } from "@/lib/utils";
import type { BrowserProfile, StoredProxy } from "@/types";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
@@ -58,6 +63,7 @@ interface ProfilesDataTableProps {
onDeleteProfile: (profile: BrowserProfile) => void | Promise<void>;
onRenameProfile: (oldName: string, newName: string) => Promise<void>;
onChangeVersion: (profile: BrowserProfile) => void;
onConfigureCamoufox?: (profile: BrowserProfile) => void;
runningProfiles: Set<string>;
isUpdating?: (browser: string) => boolean;
onReloadProxyData?: () => void | Promise<void>;
@@ -71,6 +77,7 @@ export function ProfilesDataTable({
onDeleteProfile,
onRenameProfile,
onChangeVersion,
onConfigureCamoufox,
runningProfiles,
isUpdating = () => false,
onReloadProxyData,
@@ -447,7 +454,19 @@ export function ProfilesDataTable({
>
Configure Proxy
</DropdownMenuItem>
{!["chromium", "zen"].includes(profile.browser) && (
{profile.browser === "camoufox" && onConfigureCamoufox && (
<DropdownMenuItem
onClick={() => {
onConfigureCamoufox(profile);
}}
disabled={!isClient || isBrowserUpdating}
>
Configure Camoufox
</DropdownMenuItem>
)}
{!["chromium", "zen", "camoufox"].includes(
profile.browser,
) && (
<DropdownMenuItem
onClick={() => {
onChangeVersion(profile);
@@ -492,6 +511,7 @@ export function ProfilesDataTable({
onKillProfile,
onProxySettings,
onChangeVersion,
onConfigureCamoufox,
getProxyInfo,
hasProxy,
getProxyDisplayName,
@@ -509,9 +529,16 @@ export function ProfilesDataTable({
getCoreRowModel: getCoreRowModel(),
});
const platform = getCurrentOS();
return (
<>
<ScrollArea className="h-[400px] rounded-md border">
<ScrollArea
className={cn(
"rounded-md border",
platform === "macos" ? "h-[380px]" : "h-[320px]",
)}
>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
@@ -0,0 +1,568 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { useEffect, useState } from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { CamoufoxConfig } from "@/types";
const osOptions = [
{ value: "windows", label: "Windows" },
{ value: "macos", label: "macOS" },
{ value: "linux", label: "Linux" },
];
const timezoneOptions = [
{ value: "America/New_York", label: "America/New_York" },
{ value: "America/Los_Angeles", label: "America/Los_Angeles" },
{ value: "America/Chicago", label: "America/Chicago" },
{ value: "America/Denver", label: "America/Denver" },
{ value: "America/Phoenix", label: "America/Phoenix" },
{ value: "America/Toronto", label: "America/Toronto" },
{ value: "America/Vancouver", label: "America/Vancouver" },
{ value: "Europe/London", label: "Europe/London" },
{ value: "Europe/Paris", label: "Europe/Paris" },
{ value: "Europe/Berlin", label: "Europe/Berlin" },
{ value: "Europe/Rome", label: "Europe/Rome" },
{ value: "Europe/Madrid", label: "Europe/Madrid" },
{ value: "Europe/Amsterdam", label: "Europe/Amsterdam" },
{ value: "Europe/Zurich", label: "Europe/Zurich" },
{ value: "Europe/Vienna", label: "Europe/Vienna" },
{ value: "Europe/Warsaw", label: "Europe/Warsaw" },
{ value: "Europe/Prague", label: "Europe/Prague" },
{ value: "Europe/Stockholm", label: "Europe/Stockholm" },
{ value: "Europe/Copenhagen", label: "Europe/Copenhagen" },
{ value: "Europe/Helsinki", label: "Europe/Helsinki" },
{ value: "Europe/Oslo", label: "Europe/Oslo" },
{ value: "Europe/Brussels", label: "Europe/Brussels" },
{ value: "Europe/Dublin", label: "Europe/Dublin" },
{ value: "Europe/Lisbon", label: "Europe/Lisbon" },
{ value: "Europe/Athens", label: "Europe/Athens" },
{ value: "Europe/Budapest", label: "Europe/Budapest" },
{ value: "Europe/Bucharest", label: "Europe/Bucharest" },
{ value: "Europe/Sofia", label: "Europe/Sofia" },
{ value: "Europe/Kiev", label: "Europe/Kiev" },
{ value: "Europe/Moscow", label: "Europe/Moscow" },
{ value: "Asia/Tokyo", label: "Asia/Tokyo" },
{ value: "Asia/Seoul", label: "Asia/Seoul" },
{ value: "Asia/Shanghai", label: "Asia/Shanghai" },
{ value: "Asia/Hong_Kong", label: "Asia/Hong_Kong" },
{ value: "Asia/Singapore", label: "Asia/Singapore" },
{ value: "Asia/Bangkok", label: "Asia/Bangkok" },
{ value: "Asia/Jakarta", label: "Asia/Jakarta" },
{ value: "Asia/Manila", label: "Asia/Manila" },
{ value: "Asia/Kolkata", label: "Asia/Kolkata" },
{ value: "Asia/Dubai", label: "Asia/Dubai" },
{ value: "Asia/Riyadh", label: "Asia/Riyadh" },
{ value: "Asia/Tehran", label: "Asia/Tehran" },
{ value: "Asia/Jerusalem", label: "Asia/Jerusalem" },
{ value: "Asia/Istanbul", label: "Asia/Istanbul" },
{ value: "Australia/Sydney", label: "Australia/Sydney" },
{ value: "Australia/Melbourne", label: "Australia/Melbourne" },
{ value: "Australia/Brisbane", label: "Australia/Brisbane" },
{ value: "Australia/Perth", label: "Australia/Perth" },
{ value: "Australia/Adelaide", label: "Australia/Adelaide" },
{ value: "Pacific/Auckland", label: "Pacific/Auckland" },
{ value: "Pacific/Honolulu", label: "Pacific/Honolulu" },
{ value: "Africa/Cairo", label: "Africa/Cairo" },
{ value: "Africa/Johannesburg", label: "Africa/Johannesburg" },
{ value: "Africa/Lagos", label: "Africa/Lagos" },
{ value: "Africa/Nairobi", label: "Africa/Nairobi" },
{ value: "America/Sao_Paulo", label: "America/Sao_Paulo" },
{ value: "America/Buenos_Aires", label: "America/Buenos_Aires" },
{ value: "America/Lima", label: "America/Lima" },
{ value: "America/Bogota", label: "America/Bogota" },
{ value: "America/Santiago", label: "America/Santiago" },
{ value: "America/Caracas", label: "America/Caracas" },
{ value: "America/Mexico_City", label: "America/Mexico_City" },
];
const localeOptions = [
{ value: "en-US", label: "English (US)" },
{ value: "en-GB", label: "English (UK)" },
{ value: "en-CA", label: "English (Canada)" },
{ value: "en-AU", label: "English (Australia)" },
{ value: "fr-FR", label: "French (France)" },
{ value: "fr-CA", label: "French (Canada)" },
{ value: "de-DE", label: "German (Germany)" },
{ value: "de-AT", label: "German (Austria)" },
{ value: "de-CH", label: "German (Switzerland)" },
{ value: "es-ES", label: "Spanish (Spain)" },
{ value: "es-MX", label: "Spanish (Mexico)" },
{ value: "es-AR", label: "Spanish (Argentina)" },
{ value: "it-IT", label: "Italian (Italy)" },
{ value: "it-CH", label: "Italian (Switzerland)" },
{ value: "pt-BR", label: "Portuguese (Brazil)" },
{ value: "pt-PT", label: "Portuguese (Portugal)" },
{ value: "ru-RU", label: "Russian (Russia)" },
{ value: "zh-CN", label: "Chinese (Simplified)" },
{ value: "zh-TW", label: "Chinese (Traditional)" },
{ value: "ja-JP", label: "Japanese (Japan)" },
{ value: "ko-KR", label: "Korean (Korea)" },
{ value: "ar-SA", label: "Arabic (Saudi Arabia)" },
{ value: "ar-EG", label: "Arabic (Egypt)" },
{ value: "hi-IN", label: "Hindi (India)" },
{ value: "tr-TR", label: "Turkish (Turkey)" },
{ value: "pl-PL", label: "Polish (Poland)" },
{ value: "nl-NL", label: "Dutch (Netherlands)" },
{ value: "nl-BE", label: "Dutch (Belgium)" },
{ value: "sv-SE", label: "Swedish (Sweden)" },
{ value: "da-DK", label: "Danish (Denmark)" },
{ value: "no-NO", label: "Norwegian (Norway)" },
{ value: "fi-FI", label: "Finnish (Finland)" },
{ value: "he-IL", label: "Hebrew (Israel)" },
{ value: "th-TH", label: "Thai (Thailand)" },
{ value: "vi-VN", label: "Vietnamese (Vietnam)" },
{ value: "id-ID", label: "Indonesian (Indonesia)" },
{ value: "ms-MY", label: "Malay (Malaysia)" },
{ value: "uk-UA", label: "Ukrainian (Ukraine)" },
{ value: "cs-CZ", label: "Czech (Czech Republic)" },
{ value: "sk-SK", label: "Slovak (Slovakia)" },
{ value: "hu-HU", label: "Hungarian (Hungary)" },
{ value: "ro-RO", label: "Romanian (Romania)" },
{ value: "bg-BG", label: "Bulgarian (Bulgaria)" },
{ value: "hr-HR", label: "Croatian (Croatia)" },
{ value: "sr-RS", label: "Serbian (Serbia)" },
{ value: "sl-SI", label: "Slovenian (Slovenia)" },
{ value: "lt-LT", label: "Lithuanian (Lithuania)" },
{ value: "lv-LV", label: "Latvian (Latvia)" },
{ value: "et-EE", label: "Estonian (Estonia)" },
{ value: "el-GR", label: "Greek (Greece)" },
{ value: "ca-ES", label: "Catalan (Spain)" },
{ value: "eu-ES", label: "Basque (Spain)" },
{ value: "gl-ES", label: "Galician (Spain)" },
{ value: "is-IS", label: "Icelandic (Iceland)" },
{ value: "mt-MT", label: "Maltese (Malta)" },
];
const getCurrentOS = () => {
if (typeof window !== "undefined") {
const userAgent = window.navigator.userAgent;
if (userAgent.includes("Win")) return "windows";
if (userAgent.includes("Mac")) return "macos";
if (userAgent.includes("Linux")) return "linux";
}
return "unknown";
};
interface SystemLocale {
locale: string;
language: string;
country: string;
}
interface SystemTimezone {
timezone: string;
offset: string;
}
interface SharedCamoufoxConfigFormProps {
config: CamoufoxConfig;
onConfigChange: (key: keyof CamoufoxConfig, value: unknown) => void;
className?: string;
}
export function SharedCamoufoxConfigForm({
config,
onConfigChange,
className = "",
}: SharedCamoufoxConfigFormProps) {
const [systemLocale, setSystemLocale] = useState<SystemLocale | null>(null);
const [systemTimezone, setSystemTimezone] = useState<SystemTimezone | null>(
null,
);
const [isLoadingSystemDefaults, setIsLoadingSystemDefaults] = useState(true);
// Load system defaults on component mount
useEffect(() => {
const loadSystemDefaults = async () => {
try {
const [locale, timezone] = await Promise.all([
invoke<SystemLocale>("get_system_locale"),
invoke<SystemTimezone>("get_system_timezone"),
]);
setSystemLocale(locale);
setSystemTimezone(timezone);
} catch (error) {
console.error("Failed to load system defaults:", error);
// Set fallback defaults
setSystemLocale({
locale: "en-US",
language: "en",
country: "US",
});
setSystemTimezone({
timezone: "America/New_York",
offset: "-05:00",
});
} finally {
setIsLoadingSystemDefaults(false);
}
};
loadSystemDefaults();
}, []);
// Get the selected OS for warning
const selectedOS = config.os?.[0];
const currentOS = getCurrentOS();
const showOSWarning =
selectedOS && selectedOS !== currentOS && currentOS !== "unknown";
return (
<div className={`space-y-6 ${className}`}>
{/* OS Selection */}
<div className="space-y-3">
<Label>Operating System</Label>
<Select
value={config.os?.[0] || getCurrentOS()}
onValueChange={(value) => onConfigChange("os", [value])}
>
<SelectTrigger>
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
{osOptions.map((os) => (
<SelectItem key={os.value} value={os.value}>
{os.label}
</SelectItem>
))}
</SelectContent>
</Select>
{showOSWarning && (
<p className="text-sm text-yellow-600 dark:text-yellow-400">
Selected OS ({selectedOS}) differs from your current OS (
{currentOS}). This may affect fingerprinting effectiveness.
</p>
)}
</div>
{/* Privacy & Blocking */}
<div className="space-y-3">
<Label>Privacy & Blocking</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="block-images"
checked={config.block_images || false}
onCheckedChange={(checked) =>
onConfigChange("block_images", checked)
}
/>
<Label htmlFor="block-images">Block Images</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="block-webrtc"
checked={config.block_webrtc || false}
onCheckedChange={(checked) =>
onConfigChange("block_webrtc", checked)
}
/>
<Label htmlFor="block-webrtc">Block WebRTC</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="block-webgl"
checked={config.block_webgl || false}
onCheckedChange={(checked) =>
onConfigChange("block_webgl", checked)
}
/>
<Label htmlFor="block-webgl">Block WebGL</Label>
</div>
</div>
</div>
{/* Geolocation */}
<div className="space-y-3">
<Label>Geolocation</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="country">Country</Label>
<Input
id="country"
value={config.country || ""}
onChange={(e) =>
onConfigChange("country", e.target.value || undefined)
}
placeholder={
systemLocale
? `e.g., ${systemLocale.country}`
: "e.g., US, GB, DE"
}
/>
</div>
<div className="space-y-2">
<Label>Timezone</Label>
<Select
value={config.timezone || "auto"}
onValueChange={(value) =>
onConfigChange("timezone", value === "auto" ? undefined : value)
}
>
<SelectTrigger>
<SelectValue
placeholder={
isLoadingSystemDefaults ? "Loading..." : "Select timezone"
}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">
{isLoadingSystemDefaults
? "Auto (loading...)"
: `Auto (${systemTimezone?.timezone || "UTC"})`}
</SelectItem>
{timezoneOptions.map((tz) => (
<SelectItem key={tz.value} value={tz.value}>
{tz.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="latitude">Latitude</Label>
<Input
id="latitude"
type="number"
step="any"
value={config.latitude || ""}
onChange={(e) =>
onConfigChange(
"latitude",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
placeholder="e.g., 40.7128"
/>
</div>
<div className="space-y-2">
<Label htmlFor="longitude">Longitude</Label>
<Input
id="longitude"
type="number"
step="any"
value={config.longitude || ""}
onChange={(e) =>
onConfigChange(
"longitude",
e.target.value ? parseFloat(e.target.value) : undefined,
)
}
placeholder="e.g., -74.0060"
/>
</div>
</div>
</div>
{/* Localization */}
<div className="space-y-3">
<Label>Locale</Label>
<Select
value={config.locale?.[0] || ""}
onValueChange={(value) =>
onConfigChange("locale", value ? [value] : undefined)
}
>
<SelectTrigger>
<SelectValue
placeholder={
isLoadingSystemDefaults
? "Loading..."
: `Select locale (system: ${systemLocale?.locale || "unknown"})`
}
/>
</SelectTrigger>
<SelectContent>
{!isLoadingSystemDefaults && systemLocale && (
<SelectItem value={systemLocale.locale}>
{systemLocale.locale} (System Default)
</SelectItem>
)}
{localeOptions.map((locale) => (
<SelectItem key={locale.value} value={locale.value}>
{locale.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Screen Resolution */}
<div className="space-y-3">
<Label>Screen Resolution</Label>
<div className="grid grid-cols-2 gap-4">
<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., 1024"
/>
</div>
<div className="space-y-2">
<Label htmlFor="screen-max-width">Max Width</Label>
<Input
id="screen-max-width"
type="number"
value={config.screen_max_width || ""}
onChange={(e) =>
onConfigChange(
"screen_max_width",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1920"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<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., 768"
/>
</div>
<div className="space-y-2">
<Label htmlFor="screen-max-height">Max Height</Label>
<Input
id="screen-max-height"
type="number"
value={config.screen_max_height || ""}
onChange={(e) =>
onConfigChange(
"screen_max_height",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1080"
/>
</div>
</div>
</div>
{/* Window Size */}
<div className="space-y-3">
<Label>Window Size</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="window-width">Width</Label>
<Input
id="window-width"
type="number"
value={config.window_width || ""}
onChange={(e) =>
onConfigChange(
"window_width",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 1366"
/>
</div>
<div className="space-y-2">
<Label htmlFor="window-height">Height</Label>
<Input
id="window-height"
type="number"
value={config.window_height || ""}
onChange={(e) =>
onConfigChange(
"window_height",
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="e.g., 768"
/>
</div>
</div>
</div>
{/* Advanced Options */}
<div className="space-y-3">
<Label>Advanced Options</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="enable-cache"
checked={config.enable_cache || false}
onCheckedChange={(checked) =>
onConfigChange("enable_cache", checked)
}
/>
<Label htmlFor="enable-cache">Enable Browser Cache</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="main-world-eval"
checked={config.main_world_eval || false}
onCheckedChange={(checked) =>
onConfigChange("main_world_eval", checked)
}
/>
<Label htmlFor="main-world-eval">
Enable Main World Evaluation
</Label>
</div>
</div>
</div>
{/* WebGL Settings */}
<div className="space-y-3">
<Label>WebGL Settings</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="webgl-vendor">WebGL Vendor</Label>
<Input
id="webgl-vendor"
value={config.webgl_vendor || ""}
onChange={(e) =>
onConfigChange("webgl_vendor", e.target.value || undefined)
}
placeholder="e.g., Intel Inc."
/>
</div>
<div className="space-y-2">
<Label htmlFor="webgl-renderer">WebGL Renderer</Label>
<Input
id="webgl-renderer"
value={config.webgl_renderer || ""}
onChange={(e) =>
onConfigChange("webgl_renderer", e.target.value || undefined)
}
placeholder="e.g., Intel HD Graphics"
/>
</div>
</div>
</div>
</div>
);
}
+79
View File
@@ -19,6 +19,85 @@ import {
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
interface ComboboxOption {
value: string;
label: string;
description?: string;
}
interface ComboboxProps {
options: ComboboxOption[];
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
searchPlaceholder?: string;
className?: string;
}
export function Combobox({
options,
value,
onValueChange,
placeholder = "Select option...",
searchPlaceholder = "Search...",
className,
}: ComboboxProps) {
const [open, setOpen] = React.useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn("w-full justify-between", className)}
>
{value
? options.find((option) => option.value === value)?.label
: placeholder}
<LuChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder={searchPlaceholder} />
<CommandList>
<CommandEmpty>No option found.</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {
onValueChange(currentValue === value ? "" : currentValue);
setOpen(false);
}}
>
<LuCheck
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span>{option.label}</span>
{option.description && (
<span className="text-sm text-muted-foreground">
{option.description}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
const frameworks = [
{
value: "next.js",
+55
View File
@@ -0,0 +1,55 @@
"use client";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
+1 -1
View File
@@ -120,7 +120,7 @@ export function useAppUpdateNotifications() {
setTimeout(() => {
setIsUpdating(false);
setUpdateProgress(null);
}, 2000);
}, 5000); // Show completion message for 5 seconds instead of 2
}
},
);
+46 -29
View File
@@ -247,41 +247,58 @@ export function useBrowserDownload() {
// Listen for download progress events
useEffect(() => {
const unlisten = listen<DownloadProgress>("download-progress", (event) => {
const progress = event.payload;
setDownloadProgress(progress);
let unlistenFn: (() => void) | null = null;
const browserName = getBrowserDisplayName(progress.browser);
const setupListener = async () => {
try {
unlistenFn = await listen<DownloadProgress>(
"download-progress",
(event) => {
const progress = event.payload;
setDownloadProgress(progress);
// Show toast with progress
if (progress.stage === "downloading") {
const speedMBps = (
progress.speed_bytes_per_sec /
(1024 * 1024)
).toFixed(1);
const etaText = progress.eta_seconds
? formatTime(progress.eta_seconds)
: "calculating...";
const browserName = getBrowserDisplayName(progress.browser);
showDownloadToast(browserName, progress.version, "downloading", {
percentage: progress.percentage,
speed: speedMBps,
eta: etaText,
});
} else if (progress.stage === "extracting") {
showDownloadToast(browserName, progress.version, "extracting");
} else if (progress.stage === "verifying") {
showDownloadToast(browserName, progress.version, "verifying");
} else if (progress.stage === "completed") {
showDownloadToast(browserName, progress.version, "completed");
setDownloadProgress(null);
// Show toast with progress
if (progress.stage === "downloading") {
const speedMBps = (
progress.speed_bytes_per_sec /
(1024 * 1024)
).toFixed(1);
const etaText = progress.eta_seconds
? formatTime(progress.eta_seconds)
: "calculating...";
showDownloadToast(browserName, progress.version, "downloading", {
percentage: progress.percentage,
speed: speedMBps,
eta: etaText,
});
} else if (progress.stage === "extracting") {
showDownloadToast(browserName, progress.version, "extracting");
} else if (progress.stage === "verifying") {
showDownloadToast(browserName, progress.version, "verifying");
} else if (progress.stage === "completed") {
showDownloadToast(browserName, progress.version, "completed");
setDownloadProgress(null);
}
},
);
} catch (error) {
console.error("Failed to setup download progress listener:", error);
}
});
};
setupListener();
return () => {
void unlisten.then((fn) => {
fn();
});
if (unlistenFn) {
try {
unlistenFn();
} catch (error) {
console.error("Failed to cleanup download progress listener:", error);
}
}
};
}, [formatTime]);
+176 -136
View File
@@ -61,167 +61,207 @@ export function useVersionUpdater() {
// Listen for version update progress events
useEffect(() => {
const unlisten = listen<VersionUpdateProgress>(
"version-update-progress",
(event) => {
const progress = event.payload;
setUpdateProgress(progress);
let unlistenFn: (() => void) | null = null;
if (progress.status === "updating") {
setIsUpdating(true);
const setupListener = async () => {
try {
unlistenFn = await listen<VersionUpdateProgress>(
"version-update-progress",
(event) => {
const progress = event.payload;
setUpdateProgress(progress);
// Show unified progress toast
const currentBrowserName = progress.current_browser
? getBrowserDisplayName(progress.current_browser)
: undefined;
if (progress.status === "updating") {
setIsUpdating(true);
showUnifiedVersionUpdateToast("Checking for browser updates...", {
description: currentBrowserName
? `Fetching ${currentBrowserName} release information...`
: "Initializing version check...",
progress: {
current: progress.completed_browsers,
total: progress.total_browsers,
found: progress.new_versions_found,
current_browser: currentBrowserName,
},
});
} else if (progress.status === "completed") {
setIsUpdating(false);
setUpdateProgress(null);
dismissToast("unified-version-update");
// Show unified progress toast
const currentBrowserName = progress.current_browser
? getBrowserDisplayName(progress.current_browser)
: undefined;
if (progress.new_versions_found > 0) {
showSuccessToast("Browser versions updated successfully", {
duration: 5000,
description:
"Auto-downloads will start shortly for available updates.",
});
} else {
showSuccessToast("No new browser versions found", {
duration: 3000,
description: "All browser versions are up to date",
});
}
showUnifiedVersionUpdateToast("Checking for browser updates...", {
description: currentBrowserName
? `Fetching ${currentBrowserName} release information...`
: "Initializing version check...",
progress: {
current: progress.completed_browsers,
total: progress.total_browsers,
found: progress.new_versions_found,
current_browser: currentBrowserName,
},
});
} else if (progress.status === "completed") {
setIsUpdating(false);
setUpdateProgress(null);
dismissToast("unified-version-update");
// Refresh status
void loadUpdateStatus();
} else if (progress.status === "error") {
setIsUpdating(false);
setUpdateProgress(null);
dismissToast("unified-version-update");
if (progress.new_versions_found > 0) {
showSuccessToast("Browser versions updated successfully", {
duration: 5000,
description:
"Auto-downloads will start shortly for available updates.",
});
} else {
showSuccessToast("No new browser versions found", {
duration: 3000,
description: "All browser versions are up to date",
});
}
showErrorToast("Failed to update browser versions", {
duration: 6000,
description: "Check your internet connection and try again",
});
}
},
);
// Refresh status
void loadUpdateStatus();
} else if (progress.status === "error") {
setIsUpdating(false);
setUpdateProgress(null);
dismissToast("unified-version-update");
showErrorToast("Failed to update browser versions", {
duration: 6000,
description: "Check your internet connection and try again",
});
}
},
);
} catch (error) {
console.error(
"Failed to setup version update progress listener:",
error,
);
}
};
setupListener();
return () => {
void unlisten.then((fn) => {
fn();
});
if (unlistenFn) {
try {
unlistenFn();
} catch (error) {
console.error(
"Failed to cleanup version update progress listener:",
error,
);
}
}
};
}, [loadUpdateStatus]);
// Listen for browser auto-update events
useEffect(() => {
const unlisten = listen<AutoUpdateEvent>(
"browser-auto-update-available",
(event) => {
const handleAutoUpdate = async () => {
const { browser, new_version, notification_id } = event.payload;
console.log("Browser auto-update event received:", event.payload);
let unlistenFn: (() => void) | null = null;
const browserDisplayName = getBrowserDisplayName(browser);
const setupListener = async () => {
try {
unlistenFn = await listen<AutoUpdateEvent>(
"browser-auto-update-available",
(event) => {
const handleAutoUpdate = async () => {
const { browser, new_version, notification_id } = event.payload;
console.log("Browser auto-update event received:", event.payload);
try {
// Show auto-update start notification
showAutoUpdateToast(browserDisplayName, new_version, {
description: `Downloading ${browserDisplayName} ${new_version} automatically. Progress will be shown below.`,
});
const browserDisplayName = getBrowserDisplayName(browser);
// Dismiss the update notification in the backend
await invoke("dismiss_update_notification", {
notificationId: notification_id,
});
try {
// Show auto-update start notification
showAutoUpdateToast(browserDisplayName, new_version, {
description: `Downloading ${browserDisplayName} ${new_version} automatically. Progress will be shown below.`,
});
// Check if browser already exists before downloading
const isDownloaded = await invoke<boolean>("check_browser_exists", {
browserStr: browser,
version: new_version,
});
// Dismiss the update notification in the backend
await invoke("dismiss_update_notification", {
notificationId: notification_id,
});
if (isDownloaded) {
// Browser already exists, skip download and go straight to profile update
console.log(
`${browserDisplayName} ${new_version} already exists, skipping download`,
);
// Check if browser already exists before downloading
const isDownloaded = await invoke<boolean>(
"check_browser_exists",
{
browserStr: browser,
version: new_version,
},
);
showSuccessToast(
`${browserDisplayName} ${new_version} already available`,
{
description: "Updating profile configurations...",
duration: 3000,
},
);
} else {
// Download the browser - this will trigger download progress events automatically
await invoke("download_browser", {
browserStr: browser,
version: new_version,
});
}
if (isDownloaded) {
// Browser already exists, skip download and go straight to profile update
console.log(
`${browserDisplayName} ${new_version} already exists, skipping download`,
);
// Complete the update with auto-update of profile versions
const updatedProfiles = await invoke<string[]>(
"complete_browser_update_with_auto_update",
{
browser,
newVersion: new_version,
},
);
showSuccessToast(
`${browserDisplayName} ${new_version} already available`,
{
description: "Updating profile configurations...",
duration: 3000,
},
);
} else {
// Download the browser - this will trigger download progress events automatically
await invoke("download_browser", {
browserStr: browser,
version: new_version,
});
}
// Show success message based on whether profiles were updated
if (updatedProfiles.length > 0) {
const profileText =
updatedProfiles.length === 1
? `Profile "${updatedProfiles[0]}" has been updated`
: `${updatedProfiles.length} profiles have been updated`;
// Complete the update with auto-update of profile versions
const updatedProfiles = await invoke<string[]>(
"complete_browser_update_with_auto_update",
{
browser,
newVersion: new_version,
},
);
showSuccessToast(`${browserDisplayName} update completed`, {
description: `${profileText} to version ${new_version}. You can now launch your browsers with the latest version.`,
duration: 6000,
});
} else {
showSuccessToast(`${browserDisplayName} update completed`, {
description: `Version ${new_version} is now available. Running profiles will use the new version when restarted.`,
duration: 6000,
});
}
} catch (error) {
console.error("Failed to handle browser auto-update:", error);
showErrorToast(`Failed to auto-update ${browserDisplayName}`, {
description:
error instanceof Error
? error.message
: "Unknown error occurred",
duration: 8000,
});
}
};
// Show success message based on whether profiles were updated
if (updatedProfiles.length > 0) {
const profileText =
updatedProfiles.length === 1
? `Profile "${updatedProfiles[0]}" has been updated`
: `${updatedProfiles.length} profiles have been updated`;
// Call the async handler
void handleAutoUpdate();
},
);
showSuccessToast(`${browserDisplayName} update completed`, {
description: `${profileText} to version ${new_version}. You can now launch your browsers with the latest version.`,
duration: 6000,
});
} else {
showSuccessToast(`${browserDisplayName} update completed`, {
description: `Version ${new_version} is now available. Running profiles will use the new version when restarted.`,
duration: 6000,
});
}
} catch (error) {
console.error("Failed to handle browser auto-update:", error);
showErrorToast(`Failed to auto-update ${browserDisplayName}`, {
description:
error instanceof Error
? error.message
: "Unknown error occurred",
duration: 8000,
});
}
};
// Call the async handler
void handleAutoUpdate();
},
);
} catch (error) {
console.error("Failed to setup browser auto-update listener:", error);
}
};
setupListener();
return () => {
void unlisten.then((fn) => {
fn();
});
if (unlistenFn) {
try {
unlistenFn();
} catch (error) {
console.error(
"Failed to cleanup browser auto-update listener:",
error,
);
}
}
};
}, []);
+14 -1
View File
@@ -3,7 +3,7 @@
* Centralized helpers for browser name mapping, icons, etc.
*/
import { FaChrome, FaFirefox } from "react-icons/fa";
import { FaChrome, FaFirefox, FaShieldAlt } from "react-icons/fa";
import { SiBrave, SiMullvad, SiTorbrowser } from "react-icons/si";
import { ZenBrowser } from "@/components/icons/zen-browser";
@@ -19,6 +19,7 @@ export function getBrowserDisplayName(browserType: string): string {
brave: "Brave",
chromium: "Chromium",
"tor-browser": "Tor Browser",
camoufox: "Anti-Detect",
};
return browserNames[browserType] || browserType;
@@ -42,7 +43,19 @@ export function getBrowserIcon(browserType: string) {
return ZenBrowser;
case "tor-browser":
return SiTorbrowser;
case "camoufox":
return FaShieldAlt;
default:
return null;
}
}
export const getCurrentOS = () => {
if (typeof window !== "undefined") {
const userAgent = window.navigator.userAgent;
if (userAgent.includes("Win")) return "windows";
if (userAgent.includes("Mac")) return "macos";
if (userAgent.includes("Linux")) return "linux";
}
return "unknown";
};
+47
View File
@@ -20,6 +20,7 @@ export interface BrowserProfile {
process_id?: number;
last_launch?: number;
release_type: string; // "stable" or "nightly"
camoufox_config?: CamoufoxConfig; // Camoufox configuration
}
export interface StoredProxy {
@@ -56,3 +57,49 @@ export interface AppUpdateProgress {
eta?: string; // estimated time remaining
message: string;
}
export interface CamoufoxConfig {
os?: string[];
block_images?: boolean;
block_webrtc?: boolean;
block_webgl?: boolean;
disable_coop?: boolean;
geoip?: string | boolean;
country?: string;
timezone?: string;
latitude?: number;
longitude?: number;
humanize?: boolean;
humanize_duration?: number;
headless?: boolean;
locale?: string[];
addons?: string[];
fonts?: string[];
custom_fonts_only?: boolean;
exclude_addons?: string[];
screen_min_width?: number;
screen_max_width?: number;
screen_min_height?: number;
screen_max_height?: number;
window_width?: number;
window_height?: number;
ff_version?: number;
main_world_eval?: boolean;
webgl_vendor?: string;
webgl_renderer?: string;
proxy?: string;
enable_cache?: boolean;
virtual_display?: string;
debug?: boolean;
additional_args?: string[];
env_vars?: Record<string, string>;
firefox_prefs?: Record<string, unknown>;
}
export interface CamoufoxLaunchResult {
id: string;
pid?: number;
executable_path: string;
profile_path: string;
url?: string;
}