feat: finalize camoufox integration

This commit is contained in:
zhom
2025-08-03 14:38:44 +04:00
parent 54fd9b7282
commit b088ae675b
10 changed files with 385 additions and 351 deletions
+1
View File
@@ -63,6 +63,7 @@
"kdeglobals",
"keras",
"KHTML",
"Kolkata",
"kreadconfig",
"launchservices",
"letterboxing",
+36 -10
View File
@@ -176,29 +176,48 @@ export async function stopCamoufoxProcess(id: string): Promise<boolean> {
}
try {
const killByPattern = spawn("pkill", ["-f", `camoufox-worker.*${id}`], {
stdio: "ignore",
});
console.log(`Stopping Camoufox process ${id} (PID: ${config.processId})`);
// Method 2: If we have a process ID, kill by PID
// Method 1: If we have a process ID, kill by PID with proper signal sequence
if (config.processId) {
try {
// First try SIGTERM for graceful shutdown
process.kill(config.processId, "SIGTERM");
console.log(`Sent SIGTERM to Camoufox process ${config.processId}`);
// Give it a moment to terminate gracefully
await new Promise((resolve) => setTimeout(resolve, 2000));
// Give it more time to terminate gracefully (increased from 2s to 5s)
await new Promise((resolve) => setTimeout(resolve, 5000));
// Force kill if still running
// Check if process is still running
try {
process.kill(config.processId, 0); // Signal 0 checks if process exists
// Process still exists, force kill
console.log(
`Camoufox process ${config.processId} still running, sending SIGKILL`,
);
process.kill(config.processId, "SIGKILL");
} catch {
// Process already terminated
console.log(
`Camoufox process ${config.processId} terminated gracefully`,
);
}
} catch (error) {
// Process not found or already terminated
} catch {
console.log(
`Camoufox process ${config.processId} not found or already terminated`,
);
}
}
// Method 2: Pattern-based kill as fallback
const killByPattern = spawn(
"pkill",
["-TERM", "-f", `camoufox-worker.*${id}`],
{
stdio: "ignore",
},
);
// Wait for pattern-based kill command to complete
await new Promise<void>((resolve) => {
killByPattern.on("exit", () => resolve());
@@ -206,10 +225,17 @@ export async function stopCamoufoxProcess(id: string): Promise<boolean> {
setTimeout(() => resolve(), 3000);
});
// Final cleanup with SIGKILL if needed
setTimeout(() => {
spawn("pkill", ["-KILL", "-f", `camoufox-worker.*${id}`], {
stdio: "ignore",
});
}, 1000);
// Delete the configuration
deleteCamoufoxConfig(id);
return true;
} catch (error) {
} catch {
// Delete the configuration even if stopping failed
deleteCamoufoxConfig(id);
return false;
+146 -32
View File
@@ -1,5 +1,5 @@
import { Camoufox } from "camoufox-js";
import type { Page } from "playwright-core";
import { launchServer } from "camoufox-js";
import { type Browser, type BrowserServer, firefox } from "playwright-core";
import { getCamoufoxConfig, saveCamoufoxConfig } from "./camoufox-storage.js";
/**
@@ -20,34 +20,56 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
process.exit(1);
}
// Return success immediately - before any async operations
const processId = process.pid;
config.processId = process.pid;
saveCamoufoxConfig(config);
console.log(
JSON.stringify({
success: true,
id: id,
processId,
processId: process.pid,
profilePath: config.profilePath,
message: "Camoufox worker started successfully",
}),
);
// Update config with process details
config.processId = processId;
saveCamoufoxConfig(config);
// Handle process termination gracefully
const gracefulShutdown = async () => {
process.exit(0);
};
process.on("SIGTERM", () => void gracefulShutdown());
process.on("SIGINT", () => void gracefulShutdown());
// Launch browser in background - this can take time and may fail
setImmediate(async () => {
let page: Page | null = null;
let browser: Browser | null = null;
let server: BrowserServer | null = null;
let windowCheckInterval: NodeJS.Timeout | null = null;
// Graceful shutdown handler with access to browser and server
const gracefulShutdown = async () => {
try {
// Clear any intervals first
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
// Close browser context and server if they exist
if (browser?.isConnected()) {
await browser.close();
}
if (server) {
server.process().kill();
await server.close();
}
} catch {
// Ignore cleanup errors during shutdown
}
process.exit(0);
};
// Handle various quit signals for proper macOS Command+Q support
process.on("SIGTERM", () => void gracefulShutdown());
process.on("SIGINT", () => void gracefulShutdown());
process.on("SIGHUP", () => void gracefulShutdown());
process.on("SIGQUIT", () => void gracefulShutdown());
// Handle uncaught exceptions and unhandled rejections
process.on("uncaughtException", () => void gracefulShutdown());
process.on("unhandledRejection", () => void gracefulShutdown());
try {
// Prepare options for Camoufox
@@ -58,7 +80,7 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
camoufoxOptions.user_data_dir = config.profilePath;
}
// Remove custom properties before passing to Camoufox
// Theming
camoufoxOptions.disableTheming = true;
camoufoxOptions.showcursor = false;
@@ -72,24 +94,108 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
camoufoxOptions.headless = false;
}
const browser = await Camoufox(camoufoxOptions);
// Launch the server with proper options
server = await launchServer({
ws_path: `/ws_${config.id}`,
os: camoufoxOptions.os,
block_images: camoufoxOptions.block_images,
block_webrtc: camoufoxOptions.block_webrtc,
block_webgl: camoufoxOptions.block_webgl,
disable_coop: camoufoxOptions.disable_coop,
geoip: camoufoxOptions.geoip,
humanize: camoufoxOptions.humanize,
locale: camoufoxOptions.locale,
addons: camoufoxOptions.addons,
fonts: camoufoxOptions.fonts,
custom_fonts_only: camoufoxOptions.custom_fonts_only,
exclude_addons: camoufoxOptions.exclude_addons,
screen: camoufoxOptions.screen,
window: camoufoxOptions.window,
fingerprint: camoufoxOptions.fingerprint,
ff_version: camoufoxOptions.ff_version,
headless: camoufoxOptions.headless,
main_world_eval: camoufoxOptions.main_world_eval,
executable_path: camoufoxOptions.executable_path,
firefox_user_prefs: camoufoxOptions.firefox_user_prefs,
proxy: camoufoxOptions.proxy,
enable_cache: camoufoxOptions.enable_cache,
args: camoufoxOptions.args,
env: camoufoxOptions.env,
debug: camoufoxOptions.debug,
virtual_display: camoufoxOptions.virtual_display,
webgl_config: camoufoxOptions.webgl_config,
config: {
disableTheming: true,
showcursor: false,
timezone: camoufoxOptions.timezone,
},
});
// Connect to the server
browser = await firefox.connect(server.wsEndpoint());
const context = await browser.newContext();
// Handle browser disconnection for proper cleanup
browser.on("disconnected", () => void gracefulShutdown());
saveCamoufoxConfig(config);
// Handle URL opening if provided
if (config.url && context) {
try {
if (!page) {
page = await context.newPage();
// Monitor for window closure to handle Command+Q properly
const startWindowMonitoring = () => {
windowCheckInterval = setInterval(async () => {
try {
if (browser?.isConnected()) {
const contexts = browser.contexts();
let totalPages = 0;
for (const ctx of contexts) {
const pages = ctx.pages();
totalPages += pages.length;
}
// If no pages are open, terminate the server
if (totalPages === 0) {
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
await gracefulShutdown();
}
}
} catch {
// If we can't check windows, assume browser is closing
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
await gracefulShutdown();
}
}, 1000); // Check every second
};
// Handle URL opening if provided
if (config.url) {
try {
const page = await context.newPage();
await page.goto(config.url, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
} catch {
// Start monitoring after page is created
startWindowMonitoring();
} catch (urlError) {
console.error({
message: "Failed to open URL",
error: urlError,
});
// URL opening failure doesn't affect startup success
// Still start monitoring
startWindowMonitoring();
}
} else {
await context.newPage();
// Start monitoring after page is created
startWindowMonitoring();
}
// Monitor browser connection
@@ -97,16 +203,24 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
try {
if (!browser || !browser.isConnected()) {
clearInterval(keepAlive);
process.exit(0);
await gracefulShutdown();
}
} catch {
} catch (error) {
console.error({
message: "Error in keepAlive check",
error,
});
clearInterval(keepAlive);
process.exit(0);
await gracefulShutdown();
}
}, 2000);
} catch {
// Browser launch failed, but worker is still "successful"
// Process will stay alive due to the main setInterval above
} catch (error) {
console.error({
message: "Failed to launch Camoufox",
error,
});
// Browser launch failed, attempt cleanup
await gracefulShutdown();
}
});
+4 -11
View File
@@ -233,8 +233,7 @@ program
// Firefox preferences
.option("--firefox-prefs <prefs>", "Firefox user preferences (JSON string)")
.option("--disable-theming", "disable Firefox theming")
.option("--no-showcursor", "disable cursor display")
// Note: theming and cursor options are hardcoded and not user-configurable
.description("manage Camoufox browser instances")
.action(
@@ -262,11 +261,12 @@ program
// Security options
if (options.disableCoop) camoufoxOptions.disable_coop = true;
// Geolocation
// Geolocation - always enable geoip for proper spoofing
if (options.geoip) {
camoufoxOptions.geoip =
options.geoip === "auto" ? true : (options.geoip as string);
}
if (options.latitude && options.longitude) {
camoufoxOptions.geolocation = {
latitude: options.latitude as number,
@@ -279,9 +279,8 @@ program
if (options.timezone)
camoufoxOptions.timezone = options.timezone as string;
// UI and behavior
if (options.humanize)
camoufoxOptions.humanize = options.humanize as boolean | number;
camoufoxOptions.humanize = options.humanize as boolean;
if (options.headless) camoufoxOptions.headless = true;
// Localization
@@ -388,11 +387,6 @@ program
}
}
// Theming and cursor - these are custom properties for camoufox-js
if (options.disableTheming) camoufoxOptions.disableTheming = true;
if (options.showcursor === false) camoufoxOptions.showcursor = false;
// Use the launcher to start Camoufox properly
const config = await startCamoufoxProcess(
camoufoxOptions,
typeof options.profilePath === "string"
@@ -401,7 +395,6 @@ program
typeof options.url === "string" ? options.url : undefined,
);
// Output the configuration as JSON for the Rust side to parse
console.log(
JSON.stringify({
id: config.id,
+13 -20
View File
@@ -185,29 +185,17 @@ impl BrowserRunner {
// Set proxy in camoufox config
camoufox_config.proxy = Some(proxy_url);
// Ensure geoip is always enabled for proper geolocation spoofing
if camoufox_config.geoip.is_none() {
camoufox_config.geoip = Some(serde_json::Value::Bool(true));
}
println!(
"Configured local proxy for Camoufox: {:?}",
camoufox_config.proxy
"Configured local proxy for Camoufox: {:?}, geoip: {:?}",
camoufox_config.proxy, camoufox_config.geoip
);
// Use the existing config or create a test config if none exists
let final_config = if camoufox_config.timezone.is_some()
|| camoufox_config.screen_min_width.is_some()
|| camoufox_config.window_width.is_some()
{
camoufox_config.clone()
} else {
// No meaningful config provided, use test config to ensure anti-fingerprinting works
println!("No Camoufox configuration provided, using test configuration");
let mut test_config = crate::camoufox::CamoufoxNodecarLauncher::create_test_config();
// Preserve any proxy settings from the original config
test_config.proxy = camoufox_config.proxy.clone();
test_config.headless = camoufox_config.headless;
test_config.debug = Some(true); // Enable debug for troubleshooting
test_config
};
// Use the nodecar camoufox launcher
println!(
"Launching Camoufox via nodecar for profile: {}",
@@ -215,7 +203,12 @@ impl BrowserRunner {
);
let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance();
let camoufox_result = camoufox_launcher
.launch_camoufox_profile_nodecar(app_handle.clone(), profile.clone(), final_config, url)
.launch_camoufox_profile_nodecar(
app_handle.clone(),
profile.clone(),
camoufox_config,
url,
)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
format!("Failed to launch camoufox via nodecar: {e}").into()
+7 -33
View File
@@ -56,7 +56,7 @@ impl Default for CamoufoxConfig {
block_webrtc: None,
block_webgl: None,
disable_coop: None,
geoip: None,
geoip: Some(serde_json::Value::Bool(true)),
country: None,
timezone: None,
latitude: None,
@@ -80,7 +80,7 @@ impl Default for CamoufoxConfig {
webgl_vendor: None,
webgl_renderer: None,
proxy: None,
enable_cache: Some(true), // Cache enabled by default
enable_cache: Some(true),
virtual_display: None,
debug: None,
additional_args: None,
@@ -133,44 +133,20 @@ impl CamoufoxNodecarLauncher {
&CAMOUFOX_NODECAR_LAUNCHER
}
/// Create a test configuration to verify anti-fingerprinting is working
/// Create a test configuration
#[allow(dead_code)]
pub fn create_test_config() -> CamoufoxConfig {
CamoufoxConfig {
// Core anti-fingerprinting settings
timezone: Some("Europe/London".to_string()),
screen_min_width: Some(1440),
screen_min_height: Some(900),
window_width: Some(1200),
window_height: Some(800),
// Locale settings
locale: Some(vec!["en-GB".to_string(), "en-US".to_string()]),
// WebGL spoofing
webgl_vendor: Some("Intel Inc.".to_string()),
webgl_renderer: Some("Intel Iris Pro OpenGL Engine".to_string()),
// Geolocation spoofing (London coordinates)
latitude: Some(51.5074),
longitude: Some(-0.1278),
// Font settings
fonts: Some(vec![
"Arial".to_string(),
"Times New Roman".to_string(),
"Helvetica".to_string(),
"Georgia".to_string(),
]),
custom_fonts_only: Some(true),
// Humanization
humanize: Some(true),
humanize_duration: Some(2.0),
// Blocking features
block_images: Some(false), // Don't block images for testing
block_webrtc: Some(true),
block_webgl: Some(false), // Don't block WebGL so we can test spoofing
// Other settings
debug: Some(true),
@@ -646,22 +622,19 @@ mod tests {
let test_config = CamoufoxNodecarLauncher::create_test_config();
// Verify test config has expected values
assert_eq!(test_config.timezone, Some("Europe/London".to_string()));
assert_eq!(test_config.screen_min_width, Some(1440));
assert_eq!(test_config.screen_min_height, Some(900));
assert_eq!(test_config.window_width, Some(1200));
assert_eq!(test_config.window_height, Some(800));
assert_eq!(test_config.webgl_vendor, Some("Intel Inc.".to_string()));
assert_eq!(
test_config.webgl_renderer,
Some("Intel Iris Pro OpenGL Engine".to_string())
);
assert_eq!(test_config.latitude, Some(51.5074));
assert_eq!(test_config.longitude, Some(-0.1278));
assert_eq!(test_config.humanize, Some(true));
assert_eq!(test_config.debug, Some(true));
assert_eq!(test_config.enable_cache, Some(true));
assert_eq!(test_config.headless, Some(false));
// Verify that geoip is enabled by default (from Default implementation)
assert_eq!(test_config.geoip, Some(serde_json::Value::Bool(true)));
}
#[test]
@@ -670,6 +643,7 @@ mod tests {
// Verify defaults
assert_eq!(default_config.enable_cache, Some(true));
assert_eq!(default_config.geoip, Some(serde_json::Value::Bool(true)));
assert_eq!(default_config.timezone, None);
assert_eq!(default_config.debug, None);
assert_eq!(default_config.headless, None);
-9
View File
@@ -591,15 +591,6 @@ async fn test_nodecar_command_validation() -> Result<(), Box<dyn std::error::Err
assert!(!output.status.success(), "Invalid command should fail");
// Test proxy without required arguments
let incomplete_args = ["proxy", "start"];
let output = TestUtils::execute_nodecar_command(&nodecar_path, &incomplete_args, 10).await?;
assert!(
!output.status.success(),
"Incomplete proxy command should fail"
);
tracker.cleanup_all().await;
Ok(())
}
@@ -30,6 +30,7 @@ export function CamoufoxConfigDialog({
const [config, setConfig] = useState<CamoufoxConfig>({
enable_cache: true,
os: [getCurrentOS()],
geoip: true,
});
const [isSaving, setIsSaving] = useState(false);
@@ -40,6 +41,7 @@ export function CamoufoxConfigDialog({
profile.camoufox_config || {
enable_cache: true,
os: [getCurrentOS()],
geoip: true,
},
);
}
@@ -70,6 +72,7 @@ export function CamoufoxConfigDialog({
profile.camoufox_config || {
enable_cache: true,
os: [getCurrentOS()],
geoip: true,
},
);
}
-7
View File
@@ -453,13 +453,6 @@ export function CreateProfileDialog({
</TabsContent>
<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") &&
+175 -229
View File
@@ -14,11 +14,11 @@ import {
} from "@/components/ui/select";
import type { CamoufoxConfig } from "@/types";
const osOptions = [
{ value: "windows", label: "Windows" },
{ value: "macos", label: "macOS" },
{ value: "linux", label: "Linux" },
];
// const osOptions = [
// { value: "windows", label: "Windows" },
// { value: "macos", label: "macOS" },
// { value: "linux", label: "Linux" },
// ];
const timezoneOptions = [
{ value: "America/New_York", label: "America/New_York" },
@@ -143,15 +143,15 @@ const localeOptions = [
{ 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";
};
// 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;
@@ -211,16 +211,36 @@ export function SharedCamoufoxConfigForm({
loadSystemDefaults();
}, []);
// Determine if automatic location configuration is enabled
// Default to true if geoip is not explicitly set to false
const isAutoLocationEnabled = config.geoip !== false;
// Handle automatic location configuration toggle
const handleAutoLocationToggle = (enabled: boolean) => {
if (enabled) {
// Enable automatic configuration - set geoip to true and clear manual fields
onConfigChange("geoip", true);
onConfigChange("country", undefined);
onConfigChange("timezone", undefined);
onConfigChange("latitude", undefined);
onConfigChange("longitude", undefined);
onConfigChange("locale", undefined);
} else {
// Disable automatic configuration - set geoip to false
onConfigChange("geoip", false);
}
};
// Get the selected OS for warning
const selectedOS = config.os?.[0];
const currentOS = getCurrentOS();
const showOSWarning =
selectedOS && selectedOS !== currentOS && currentOS !== "unknown";
// 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">
{/*<div className="space-y-3">
<Label>Operating System</Label>
<Select
value={config.os?.[0] || getCurrentOS()}
@@ -243,162 +263,154 @@ export function SharedCamoufoxConfigForm({
{currentOS}). This may affect fingerprinting effectiveness.
</p>
)}
</div>
</div>*/}
{/* Privacy & Blocking */}
{/* Automatic Location Configuration */}
<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 className="flex items-center space-x-2">
<Checkbox
id="auto-location"
checked={isAutoLocationEnabled}
onCheckedChange={handleAutoLocationToggle}
/>
<Label htmlFor="auto-location">
Automatically configure location information based on proxy
configuration or your connection if no proxy provided
</Label>
</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"
}
/>
{!isAutoLocationEnabled && (
<div className="space-y-3">
<Label>Geolocation</Label>
<div className="mb-4 p-3 bg-amber-50 rounded-md border border-amber-200">
<p className="text-sm text-amber-800">
Warning: Configuring variables yourself may not always work due
to underlying technology. It's recommended to use automatic
location configuration.
</p>
</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}
<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>
))}
</SelectContent>
</Select>
{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>
<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>
{!isAutoLocationEnabled && (
<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">
@@ -469,72 +481,6 @@ export function SharedCamoufoxConfigForm({
</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>