refactor: fully deprecate camoufox

This commit is contained in:
zhom
2026-07-08 01:19:52 +04:00
parent 23dab4c8e4
commit 23859333c6
88 changed files with 702 additions and 37495 deletions
-213
View File
@@ -1,213 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { WayfernConfigForm } from "@/components/wayfern-config-form";
import type {
BrowserProfile,
CamoufoxConfig,
CamoufoxOS,
WayfernConfig,
} from "@/types";
const getCurrentOS = (): CamoufoxOS => {
if (typeof navigator === "undefined") return "linux";
const platform = navigator.platform.toLowerCase();
if (platform.includes("win")) return "windows";
if (platform.includes("mac")) return "macos";
return "linux";
};
import { LoadingButton } from "./loading-button";
import { RippleButton } from "./ui/ripple";
interface CamoufoxConfigDialogProps {
isOpen: boolean;
onClose: () => void;
profile: BrowserProfile | null;
onSave: (profile: BrowserProfile, config: CamoufoxConfig) => Promise<void>;
onSaveWayfern?: (
profile: BrowserProfile,
config: CamoufoxConfig,
) => Promise<void>;
isRunning?: boolean;
crossOsUnlocked?: boolean;
}
export function CamoufoxConfigDialog({
isOpen,
onClose,
profile,
onSave,
onSaveWayfern,
isRunning = false,
crossOsUnlocked = false,
}: CamoufoxConfigDialogProps) {
const { t } = useTranslation();
// Use union type to support both Camoufox and Wayfern configs
const [config, setConfig] = useState<CamoufoxConfig | WayfernConfig>(() => ({
geoip: true,
os: getCurrentOS(),
}));
const [isSaving, setIsSaving] = useState(false);
const isAntiDetectBrowser =
profile?.browser === "camoufox" || profile?.browser === "wayfern";
// Initialize config when profile changes
useEffect(() => {
if (profile && isAntiDetectBrowser) {
const profileConfig =
profile.browser === "wayfern"
? profile.wayfern_config
: profile.camoufox_config;
setConfig(
profileConfig || {
geoip: true,
os: getCurrentOS(),
},
);
}
}, [profile, isAntiDetectBrowser]);
const updateConfig = (
key: keyof CamoufoxConfig | keyof WayfernConfig,
value: unknown,
) => {
setConfig((prev) => ({ ...prev, [key]: value }));
};
const handleSave = async () => {
if (!profile) return;
// Validate fingerprint JSON if it exists
if (config.fingerprint) {
try {
JSON.parse(config.fingerprint);
} catch (_error) {
const { toast } = await import("sonner");
toast.error(t("camoufoxDialog.invalidFingerprint"), {
description: t("camoufoxDialog.invalidFingerprintDescription"),
});
return;
}
}
setIsSaving(true);
try {
if (profile.browser === "wayfern" && onSaveWayfern) {
await onSaveWayfern(profile, config as CamoufoxConfig);
} else {
await onSave(profile, config as CamoufoxConfig);
}
onClose();
} catch (error) {
console.error("Failed to save config:", error);
const { toast } = await import("sonner");
toast.error(t("camoufoxDialog.saveFailed"), {
description:
error instanceof Error
? error.message
: t("camoufoxDialog.unknownError"),
});
} finally {
setIsSaving(false);
}
};
const handleClose = () => {
// Reset config to original when closing without saving
if (profile && isAntiDetectBrowser) {
const profileConfig =
profile.browser === "wayfern"
? profile.wayfern_config
: profile.camoufox_config;
setConfig(
profileConfig || {
geoip: true,
os: getCurrentOS(),
},
);
}
onClose();
};
if (!profile || !isAntiDetectBrowser) {
return null;
}
const browserName = profile.browser === "wayfern" ? "Wayfern" : "Camoufox";
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="flex h-[min(85vh,52rem)] max-w-3xl flex-col">
<DialogHeader className="shrink-0">
<DialogTitle>
{isRunning
? t("camoufoxDialog.titleView", {
name: profile.name,
browser: browserName,
})
: t("camoufoxDialog.titleConfigure", {
name: profile.name,
browser: browserName,
})}
</DialogTitle>
</DialogHeader>
<ScrollArea className="min-h-0 flex-1">
<div className="py-4">
{profile.browser === "wayfern" ? (
<WayfernConfigForm
config={config as WayfernConfig}
onConfigChange={updateConfig}
forceAdvanced={true}
readOnly={isRunning}
crossOsUnlocked={crossOsUnlocked}
limitedMode={!crossOsUnlocked}
profileVersion={profile.version}
profileBrowser="wayfern"
/>
) : (
<SharedCamoufoxConfigForm
config={config as CamoufoxConfig}
onConfigChange={updateConfig}
forceAdvanced={true}
readOnly={isRunning}
browserType="camoufox"
crossOsUnlocked={crossOsUnlocked}
limitedMode={!crossOsUnlocked}
profileVersion={profile.version}
profileBrowser="camoufox"
/>
)}
</div>
</ScrollArea>
<DialogFooter className="shrink-0 border-t pt-4">
<RippleButton variant="outline" onClick={handleClose}>
{isRunning ? t("common.buttons.close") : t("common.buttons.cancel")}
</RippleButton>
{!isRunning && (
<LoadingButton
isLoading={isSaving}
onClick={handleSave}
disabled={isSaving}
>
{t("common.buttons.save")}
</LoadingButton>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -1,78 +0,0 @@
"use client";
import { openUrl } from "@tauri-apps/plugin-opener";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuTriangleAlert } from "react-icons/lu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import type { BrowserProfile } from "@/types";
import { RippleButton } from "./ui/ripple";
interface CamoufoxDeprecationDialogProps {
profiles: BrowserProfile[];
}
/**
* Warns users who still have Camoufox profiles that Camoufox support is ending.
* Shown once per app session (this component mounts for the app lifetime), only
* when at least one Camoufox profile exists. Not a toast — a blocking dialog so
* the deprecation can't be missed.
*/
export function CamoufoxDeprecationDialog({
profiles,
}: CamoufoxDeprecationDialogProps) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [shown, setShown] = useState(false);
useEffect(() => {
if (shown) return;
const hasCamoufox = profiles.some((p) => p.browser === "camoufox");
if (hasCamoufox) {
setIsOpen(true);
setShown(true);
}
}, [profiles, shown]);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<LuTriangleAlert className="size-5 text-warning" />
{t("camoufoxDeprecation.title")}
</DialogTitle>
<DialogDescription>
{t("camoufoxDeprecation.description")}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<RippleButton
variant="outline"
onClick={() => {
void openUrl(
"https://github.com/zhom/donutbrowser/discussions/426",
);
}}
>
{t("common.buttons.learnMore")}
</RippleButton>
<RippleButton
onClick={() => {
setIsOpen(false);
}}
>
{t("camoufoxDeprecation.acknowledge")}
</RippleButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+2 -4
View File
@@ -84,9 +84,7 @@ export function CookieCopyDialog({
// dead-end state (source picked = target list empty = copy button disabled).
const eligibleSourceProfiles = useMemo(() => {
return profiles.filter(
(p) =>
!selectedProfiles.includes(p.id) &&
(p.browser === "wayfern" || p.browser === "camoufox"),
(p) => !selectedProfiles.includes(p.id) && p.browser === "wayfern",
);
}, [profiles, selectedProfiles]);
@@ -95,7 +93,7 @@ export function CookieCopyDialog({
(p) =>
selectedProfiles.includes(p.id) &&
p.id !== sourceProfileId &&
(p.browser === "wayfern" || p.browser === "camoufox"),
p.browser === "wayfern",
);
}, [profiles, selectedProfiles, sourceProfileId]);
+10 -21
View File
@@ -66,7 +66,7 @@ const getCurrentOS = (): WayfernOS => {
import { RippleButton } from "./ui/ripple";
type BrowserTypeString = "camoufox" | "wayfern";
type BrowserTypeString = "wayfern";
interface CreateProfileDialogProps {
isOpen: boolean;
@@ -113,8 +113,8 @@ export function CreateProfileDialog({
const proxyListboxIdAntiDetect = useId();
const proxyListboxIdRegular = useId();
const [profileName, setProfileName] = useState("");
// Camoufox is deprecated: only Wayfern profiles can be created, so the dialog
// opens straight into the Wayfern config step (no browser-selection screen).
// Only Wayfern profiles can be created, so the dialog opens straight into
// the Wayfern config step (no browser-selection screen).
const [currentStep, setCurrentStep] = useState<
"browser-selection" | "browser-config"
>("browser-config");
@@ -139,8 +139,7 @@ export function CreateProfileDialog({
setCurrentStep("browser-config");
};
// Reset the form fields without leaving the Wayfern config step — Camoufox is
// deprecated, so there is no browser-selection screen to go back to.
// Reset the form fields without leaving the Wayfern config step.
const resetForm = () => {
setSelectedBrowser("wayfern");
setProfileName("");
@@ -286,8 +285,7 @@ export function CreateProfileDialog({
useEffect(() => {
if (isOpen) {
void loadSupportedBrowsers();
// Load downloaded Wayfern versions up front so the availability gate is
// accurate. Camoufox is deprecated and no longer creatable.
// Load downloaded Wayfern versions up front so the availability gate is accurate.
void loadDownloadedVersions("wayfern");
// Load release types when a browser is selected
if (selectedBrowser) {
@@ -395,7 +393,7 @@ export function CreateProfileDialog({
: undefined;
try {
if (activeTab === "anti-detect") {
// Camoufox is deprecated — only Wayfern anti-detect profiles are created.
// Only Wayfern anti-detect profiles are created.
const bestWayfernVersion = getCreatableVersion("wayfern");
if (!bestWayfernVersion) {
console.error("No Wayfern version available");
@@ -430,7 +428,7 @@ export function CreateProfileDialog({
return;
}
// Use the best available version (stable preferred, nightly as fallback)
// Use the latest available Wayfern version
const bestVersion = getCreatableVersion(selectedBrowser);
if (!bestVersion) {
console.error("No version available");
@@ -465,8 +463,7 @@ export function CreateProfileDialog({
// Cancel any ongoing loading
loadingBrowserRef.current = null;
// Reset all states. Stay on the Wayfern config step — Camoufox is
// deprecated, so the browser-selection screen is gone.
// Reset all states. Stay on the Wayfern config step.
setProfileName("");
setCurrentStep("browser-config");
setActiveTab("anti-detect");
@@ -535,10 +532,7 @@ export function CreateProfileDialog({
{currentStep === "browser-selection"
? t("createProfile.title")
: t("createProfile.configureTitle", {
browser:
selectedBrowser === "wayfern"
? t("createProfile.chromiumLabel")
: t("createProfile.firefoxLabel"),
browser: t("createProfile.chromiumLabel"),
})}
</DialogTitle>
</DialogHeader>
@@ -591,9 +585,6 @@ export function CreateProfileDialog({
</div>
</Button>
{/* Camoufox is deprecated — no longer offered for new
profiles. Only Wayfern can be created. */}
{!getCreatableVersion("wayfern") && (
<p className="pt-2 text-center text-sm text-muted-foreground">
{t("createProfile.browsersDownloading")}
@@ -616,7 +607,6 @@ export function CreateProfileDialog({
<div className="space-y-3">
{regularBrowsers.map((browser) => {
if (browser.value === "camoufox") return null; // Skip camoufox as it's handled in anti-detect tab
const IconComponent = getBrowserIcon(browser.value);
return (
<Button
@@ -904,8 +894,7 @@ export function CreateProfileDialog({
/>
</div>
) : (
// Regular Browser Configuration (should not happen in
// the anti-detect tab; Camoufox creation is removed).
// Regular Browser Configuration (should not happen in the anti-detect tab).
<div className="space-y-4">
{selectedBrowser && (
<div className="space-y-3">
+12 -27
View File
@@ -13,7 +13,7 @@ import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { FaChrome, FaFirefox } from "react-icons/fa";
import { FaChrome } from "react-icons/fa";
import { GoPlus } from "react-icons/go";
import {
LuChevronDown,
@@ -652,34 +652,19 @@ export function ExtensionManagementDialog({
const renderCompatIcons = useCallback(
(compat: string[]) => {
const hasChromium = compat.includes("chromium");
const hasFirefox = compat.includes("firefox");
if (!hasChromium && !hasFirefox) return null;
if (!hasChromium) return null;
return (
<div className="flex shrink-0 items-center gap-1">
{hasChromium && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex">
<FaChrome className="size-3.5 text-muted-foreground" />
</span>
</TooltipTrigger>
<TooltipContent>
{t("extensions.compatibility.chromium")}
</TooltipContent>
</Tooltip>
)}
{hasFirefox && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex">
<FaFirefox className="size-3.5 text-muted-foreground" />
</span>
</TooltipTrigger>
<TooltipContent>
{t("extensions.compatibility.firefox")}
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex">
<FaChrome className="size-3.5 text-muted-foreground" />
</span>
</TooltipTrigger>
<TooltipContent>
{t("extensions.compatibility.chromium")}
</TooltipContent>
</Tooltip>
</div>
);
},
+2 -9
View File
@@ -39,9 +39,7 @@ import { cn } from "@/lib/utils";
import type { DetectedProfile, WayfernConfig } from "@/types";
import { RippleButton } from "./ui/ripple";
const getMappedBrowser = (browser: string): "camoufox" | "wayfern" => {
if (["firefox", "firefox-developer", "zen"].includes(browser))
return "camoufox";
const getMappedBrowser = (_browser: string): "wayfern" => {
return "wayfern";
};
@@ -90,8 +88,7 @@ export function ImportProfileDialog({
useBrowserSupport();
const { storedProxies } = useProxyEvents();
// Firefox-based browsers map to the deprecated Camoufox and can no longer be
// imported (the backend rejects them); only offer Chromium-family sources.
// Only Chromium-family browsers can be imported as Wayfern profiles.
const importableBrowsers = supportedBrowsers.filter(
(browser) => getMappedBrowser(browser) === "wayfern",
);
@@ -189,8 +186,6 @@ export function ImportProfileDialog({
browserType,
newProfileName,
proxyId: selectedProxyId ?? null,
// Camoufox import is deprecated/blocked; only Wayfern configs are sent.
camoufoxConfig: null,
wayfernConfig: mappedBrowser === "wayfern" ? wayfernConfig : null,
});
@@ -587,8 +582,6 @@ export function ImportProfileDialog({
</Select>
</div>
{/* Only Wayfern profiles are importable now (Camoufox/Firefox
import is deprecated and blocked). */}
<WayfernConfigForm
config={wayfernConfig}
onConfigChange={(key, value) => {
+8 -15
View File
@@ -209,7 +209,7 @@ interface TableMeta {
// Overflow actions
onAssignProfilesToGroup?: (profileIds: string[]) => void;
onConfigureCamoufox?: (profile: BrowserProfile) => void;
onConfigureWayfern?: (profile: BrowserProfile) => void;
onCloneProfile?: (profile: BrowserProfile) => void;
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
onOpenCookieManagement?: (profile: BrowserProfile) => void;
@@ -1127,7 +1127,7 @@ interface ProfilesDataTableProps {
onCloneProfile: (profile: BrowserProfile) => void | Promise<void>;
onDeleteProfile: (profile: BrowserProfile) => void | Promise<void>;
onRenameProfile: (profileId: string, newName: string) => Promise<void>;
onConfigureCamoufox: (profile: BrowserProfile) => void;
onConfigureWayfern: (profile: BrowserProfile) => void;
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
onOpenCookieManagement?: (profile: BrowserProfile) => void;
runningProfiles: Set<string>;
@@ -1177,7 +1177,7 @@ export function ProfilesDataTable({
onCloneProfile,
onDeleteProfile,
onRenameProfile,
onConfigureCamoufox,
onConfigureWayfern,
onCopyCookiesToProfile,
onOpenCookieManagement,
runningProfiles,
@@ -1952,7 +1952,7 @@ export function ProfilesDataTable({
void onCloneProfile(profile);
}
: undefined,
onConfigureCamoufox,
onConfigureWayfern,
onCopyCookiesToProfile,
onOpenCookieManagement,
@@ -2030,7 +2030,7 @@ export function ProfilesDataTable({
onLaunchProfile,
onAssignProfilesToGroup,
onCloneProfile,
onConfigureCamoufox,
onConfigureWayfern,
onCopyCookiesToProfile,
onOpenCookieManagement,
syncStatuses,
@@ -2086,10 +2086,7 @@ export function ProfilesDataTable({
// Cross-OS profiles: show OS icon when checkboxes aren't visible, show checkbox when they are
if (isCrossOs && !meta.showCheckboxes && !isSelected) {
const resolvedOs =
profile.host_os ||
profile.camoufox_config?.os ||
profile.wayfern_config?.os;
const resolvedOs = profile.host_os || profile.wayfern_config?.os;
const osName = resolvedOs
? getOSDisplayName(resolvedOs)
: "another OS";
@@ -2128,10 +2125,7 @@ export function ProfilesDataTable({
// Cross-OS profiles with checkboxes visible: show checkbox (selectable for bulk delete)
if (isCrossOs && (meta.showCheckboxes || isSelected)) {
const resolvedOs =
profile.host_os ||
profile.camoufox_config?.os ||
profile.wayfern_config?.os;
const resolvedOs = profile.host_os || profile.wayfern_config?.os;
const osName = resolvedOs
? getOSDisplayName(resolvedOs)
: "another OS";
@@ -3183,7 +3177,6 @@ export function ProfilesDataTable({
? t("crossOs.viewOnly", {
os: getOSDisplayName(
row.original.host_os ||
row.original.camoufox_config?.os ||
row.original.wayfern_config?.os ||
"",
),
@@ -3273,7 +3266,7 @@ export function ProfilesDataTable({
}}
onOpenProfileSyncDialog={onOpenProfileSyncDialog}
onAssignProfilesToGroup={onAssignProfilesToGroup}
onConfigureCamoufox={onConfigureCamoufox}
onConfigureWayfern={onConfigureWayfern}
onCopyCookiesToProfile={onCopyCookiesToProfile}
onOpenCookieManagement={onOpenCookieManagement}
onAssignExtensionGroup={onAssignExtensionGroup}
+28 -77
View File
@@ -33,7 +33,7 @@ import {
LuUsers,
LuX,
} from "react-icons/lu";
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
import { SharedFingerprintConfigForm } from "@/components/shared-fingerprint-config-form";
import { Button } from "@/components/ui/button";
import {
ColorPicker,
@@ -71,7 +71,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { WayfernConfigForm } from "@/components/wayfern-config-form";
import { translateBackendError } from "@/lib/backend-errors";
import { getProfileIcon } from "@/lib/browser-utils";
import { formatRelativeTime } from "@/lib/flag-utils";
@@ -79,7 +78,6 @@ import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import { cn } from "@/lib/utils";
import type {
BrowserProfile,
CamoufoxConfig,
ProfileGroup,
StoredProxy,
VpnConfig,
@@ -95,7 +93,7 @@ interface ProfileInfoDialogProps {
onOpenTrafficDialog?: (profileId: string) => void;
onOpenProfileSyncDialog?: (profile: BrowserProfile) => void;
onAssignProfilesToGroup?: (profileIds: string[]) => void;
onConfigureCamoufox?: (profile: BrowserProfile) => void;
onConfigureWayfern?: (profile: BrowserProfile) => void;
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
onOpenCookieManagement?: (profile: BrowserProfile) => void;
onAssignExtensionGroup?: (profileIds: string[]) => void;
@@ -271,7 +269,7 @@ export function ProfileInfoDialog({
onOpenTrafficDialog,
onOpenProfileSyncDialog,
onAssignProfilesToGroup,
onConfigureCamoufox,
onConfigureWayfern,
onCopyCookiesToProfile,
onOpenCookieManagement,
onAssignExtensionGroup,
@@ -340,8 +338,7 @@ export function ProfileInfoDialog({
if (!profile) return null;
const ProfileIcon = getProfileIcon(profile);
const isCamoufoxOrWayfern =
profile.browser === "camoufox" || profile.browser === "wayfern";
const isWayfern = profile.browser === "wayfern";
const isDeleteDisabled = isRunning;
const proxyName = profile.proxy_id
@@ -435,13 +432,13 @@ export function ProfileInfoDialog({
icon: <LuFingerprint className="size-4" />,
label: t("profiles.actions.changeFingerprint"),
onClick: () => {
handleAction(() => onConfigureCamoufox?.(profile));
handleAction(() => onConfigureWayfern?.(profile));
},
// Viewing and editing fingerprints both require an active paid plan.
disabled: isDisabled || !crossOsUnlocked,
proBadge: !crossOsUnlocked,
runningBadge: isRunning,
hidden: !isCamoufoxOrWayfern || !onConfigureCamoufox,
hidden: !isWayfern || !onConfigureWayfern,
},
{
icon: <LuUsers className="size-4" />,
@@ -463,9 +460,7 @@ export function ProfileInfoDialog({
disabled: isDisabled,
runningBadge: isRunning,
hidden:
!isCamoufoxOrWayfern ||
profile.ephemeral === true ||
!onCopyCookiesToProfile,
!isWayfern || profile.ephemeral === true || !onCopyCookiesToProfile,
},
{
id: "cookiesManage",
@@ -477,9 +472,7 @@ export function ProfileInfoDialog({
disabled: isDisabled,
runningBadge: isRunning,
hidden:
!isCamoufoxOrWayfern ||
profile.ephemeral === true ||
!onOpenCookieManagement,
!isWayfern || profile.ephemeral === true || !onOpenCookieManagement,
},
{
icon: <LuSettings className="size-4" />,
@@ -1772,9 +1765,8 @@ function CookiesSectionInline({
// Inline password set / change / remove form. Replaces three separate
// nested modal dialogs with one in-page form that branches on the current
// `password_protected` state of the profile.
// Inline fingerprint editor. Reuses SharedCamoufoxConfigForm (Camoufox/Firefox
// engine) and WayfernConfigForm (Chromium engine) so the same field set as
// the standalone dialog is available without opening a nested modal.
// Inline fingerprint editor. Reuses SharedFingerprintConfigForm so the same
// field set as the standalone dialog is available without opening a nested modal.
function FingerprintSectionInline({
profile,
isDisabled,
@@ -1788,9 +1780,6 @@ function FingerprintSectionInline({
onSaved: () => void;
t: (key: string, options?: Record<string, unknown>) => string;
}) {
const [camoufoxConfig, setCamoufoxConfig] = React.useState<CamoufoxConfig>(
() => profile.camoufox_config ?? {},
);
const [wayfernConfig, setWayfernConfig] = React.useState<WayfernConfig>(
() => profile.wayfern_config ?? {},
);
@@ -1798,19 +1787,15 @@ function FingerprintSectionInline({
const [error, setError] = React.useState<string | null>(null);
const [success, setSuccess] = React.useState<string | null>(null);
// When the underlying profile changes (e.g. a different profile is opened
// in the dialog) reset the local form state to match.
React.useEffect(() => {
setCamoufoxConfig(profile.camoufox_config ?? {});
setWayfernConfig(profile.wayfern_config ?? {});
setError(null);
setSuccess(null);
}, [profile.camoufox_config, profile.wayfern_config]);
}, [profile.wayfern_config]);
const isCamoufox = profile.browser === "camoufox";
const isWayfern = profile.browser === "wayfern";
if (!isCamoufox && !isWayfern) {
if (!isWayfern) {
return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2 text-sm font-semibold">
@@ -1824,9 +1809,6 @@ function FingerprintSectionInline({
);
}
// Viewing and editing fingerprints both require an active paid plan
// (`crossOsUnlocked` is that paid flag here). Render a locked state instead of
// the editor so free users can neither see nor change the fingerprint.
if (!crossOsUnlocked) {
return (
<div className="flex flex-col items-center gap-3 rounded-lg border p-6 text-center">
@@ -1841,10 +1823,6 @@ function FingerprintSectionInline({
);
}
const onCamoufoxChange = (key: keyof CamoufoxConfig, value: unknown) => {
setCamoufoxConfig((prev) => ({ ...prev, [key]: value }));
setSuccess(null);
};
const onWayfernChange = (key: keyof WayfernConfig, value: unknown) => {
setWayfernConfig((prev) => ({ ...prev, [key]: value }));
setSuccess(null);
@@ -1855,19 +1833,11 @@ function FingerprintSectionInline({
setError(null);
setSuccess(null);
try {
if (isCamoufox) {
await invoke("update_camoufox_config", {
profileId: profile.id,
config: camoufoxConfig,
});
} else {
await invoke("update_wayfern_config", {
profileId: profile.id,
config: wayfernConfig,
});
}
await invoke("update_wayfern_config", {
profileId: profile.id,
config: wayfernConfig,
});
setSuccess(t("common.buttons.saved"));
// Close the dialog once the fingerprint is saved.
onSaved();
} catch (e) {
setError(translateBackendError(t as never, e));
@@ -1876,12 +1846,8 @@ function FingerprintSectionInline({
}
};
const initial = isCamoufox
? JSON.stringify(profile.camoufox_config ?? {})
: JSON.stringify(profile.wayfern_config ?? {});
const current = isCamoufox
? JSON.stringify(camoufoxConfig)
: JSON.stringify(wayfernConfig);
const initial = JSON.stringify(profile.wayfern_config ?? {});
const current = JSON.stringify(wayfernConfig);
const dirty = current !== initial;
return (
@@ -1894,30 +1860,16 @@ function FingerprintSectionInline({
{t("profileInfo.sectionDesc.fingerprint")}
</p>
{isCamoufox && (
<SharedCamoufoxConfigForm
config={camoufoxConfig}
onConfigChange={onCamoufoxChange}
forceAdvanced={true}
readOnly={isDisabled}
browserType="camoufox"
crossOsUnlocked={crossOsUnlocked}
limitedMode={false}
profileVersion={profile.version}
profileBrowser={profile.browser}
/>
)}
{isWayfern && (
<WayfernConfigForm
config={wayfernConfig}
onConfigChange={onWayfernChange}
forceAdvanced={true}
readOnly={isDisabled}
crossOsUnlocked={crossOsUnlocked}
profileVersion={profile.version}
profileBrowser={profile.browser}
/>
)}
<SharedFingerprintConfigForm
config={wayfernConfig}
onConfigChange={onWayfernChange}
forceAdvanced={true}
readOnly={isDisabled}
crossOsUnlocked={crossOsUnlocked}
limitedMode={false}
profileVersion={profile.version}
profileBrowser={profile.browser}
/>
{error && <p className="text-xs text-destructive">{error}</p>}
{success && !error && <p className="text-xs text-success">{success}</p>}
@@ -1939,7 +1891,6 @@ function FingerprintSectionInline({
variant="ghost"
className="h-7 text-xs"
onClick={() => {
setCamoufoxConfig(profile.camoufox_config ?? {});
setWayfernConfig(profile.wayfern_config ?? {});
setError(null);
setSuccess(null);
-200
View File
@@ -1,200 +0,0 @@
"use client";
import { useId, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuCheck, LuChevronsUpDown, LuDownload } from "react-icons/lu";
import { LoadingButton } from "@/components/loading-button";
import { Badge } from "@/components/ui/badge";
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import type { BrowserReleaseTypes } from "@/types";
import { RippleButton } from "./ui/ripple";
interface ReleaseTypeSelectorProps {
selectedReleaseType: "stable" | "nightly" | null;
onReleaseTypeSelect: (releaseType: "stable" | "nightly" | null) => void;
availableReleaseTypes: BrowserReleaseTypes;
isDownloading: boolean;
onDownload: () => void;
placeholder?: string;
showDownloadButton?: boolean;
downloadedVersions?: string[];
}
export function ReleaseTypeSelector({
selectedReleaseType,
onReleaseTypeSelect,
availableReleaseTypes,
isDownloading,
onDownload,
placeholder,
showDownloadButton = true,
downloadedVersions = [],
}: ReleaseTypeSelectorProps) {
const { t } = useTranslation();
const [popoverOpen, setPopoverOpen] = useState(false);
const listboxId = useId();
const effectivePlaceholder =
placeholder ?? t("releaseTypeSelector.placeholder");
const releaseOptions = [
...(availableReleaseTypes.stable
? [{ type: "stable" as const, version: availableReleaseTypes.stable }]
: []),
...(availableReleaseTypes.nightly
? [{ type: "nightly" as const, version: availableReleaseTypes.nightly }]
: []),
];
// Only show dropdown if there are multiple release types available
const showDropdown = releaseOptions.length > 1;
// If only one release type is available, auto-select it
if (!showDropdown && releaseOptions.length === 1 && !selectedReleaseType) {
setTimeout(() => {
onReleaseTypeSelect(releaseOptions[0].type);
}, 0);
}
const selectedDisplayText = selectedReleaseType
? selectedReleaseType === "stable"
? t("releaseTypeSelector.stable")
: t("releaseTypeSelector.nightly")
: effectivePlaceholder;
const selectedVersion =
selectedReleaseType === "stable"
? availableReleaseTypes.stable
: selectedReleaseType === "nightly"
? availableReleaseTypes.nightly
: null;
const isVersionDownloaded =
selectedVersion && downloadedVersions.includes(selectedVersion);
return (
<div className="space-y-4">
{showDropdown ? (
<Popover open={popoverOpen} onOpenChange={setPopoverOpen} modal={true}>
<PopoverTrigger asChild>
<RippleButton
variant="outline"
role="combobox"
aria-expanded={popoverOpen}
aria-controls={listboxId}
className="w-full justify-between"
>
{selectedDisplayText}
<LuChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
</RippleButton>
</PopoverTrigger>
<PopoverContent id={listboxId} className="p-0">
<Command>
<CommandEmpty>
{t("releaseTypeSelector.noReleaseTypes")}
</CommandEmpty>
<CommandList>
<CommandGroup>
{releaseOptions.map((option) => {
const isDownloaded = downloadedVersions.includes(
option.version,
);
return (
<CommandItem
key={option.type}
value={option.type}
onSelect={(currentValue) => {
const selectedType = currentValue as
| "stable"
| "nightly";
onReleaseTypeSelect(
selectedType === selectedReleaseType
? null
: selectedType,
);
setPopoverOpen(false);
}}
>
<LuCheck
className={cn(
"mr-2 size-4",
selectedReleaseType === option.type
? "opacity-100"
: "opacity-0",
)}
/>
<div className="flex items-center gap-2">
<span className="capitalize">{option.type}</span>
{option.type === "nightly" && (
<Badge variant="secondary" className="text-xs">
{t("releaseTypeSelector.nightly")}
</Badge>
)}
<Badge variant="outline" className="text-xs">
{option.version}
</Badge>
{isDownloaded && (
<Badge variant="default" className="text-xs">
{t("releaseTypeSelector.downloaded")}
</Badge>
)}
</div>
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
// Show a simple display when only one release type is available
releaseOptions.length === 1 && (
<div className="flex items-center justify-center gap-2 rounded-md border bg-muted/50 p-3">
<span className="text-sm font-medium capitalize">
{releaseOptions[0].type}
</span>
<Badge variant="outline" className="text-xs">
{releaseOptions[0].version}
</Badge>
{downloadedVersions.includes(releaseOptions[0].version) && (
<Badge variant="default" className="text-xs">
{t("releaseTypeSelector.downloaded")}
</Badge>
)}
</div>
)
)}
{showDownloadButton &&
selectedReleaseType &&
selectedVersion &&
!isVersionDownloaded && (
<LoadingButton
isLoading={isDownloading}
onClick={() => {
onDownload();
}}
variant="outline"
className="w-full"
>
<LuDownload className="mr-2 size-4" />
{isDownloading
? t("releaseTypeSelector.downloading")
: t("releaseTypeSelector.downloadBrowser")}
</LoadingButton>
)}
</div>
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
export { WayfernConfigForm as SharedFingerprintConfigForm } from "@/components/wayfern-config-form";
+163
View File
@@ -0,0 +1,163 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { SharedFingerprintConfigForm } from "@/components/shared-fingerprint-config-form";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import type { BrowserProfile, WayfernConfig, WayfernOS } from "@/types";
import { LoadingButton } from "./loading-button";
import { RippleButton } from "./ui/ripple";
const getCurrentOS = (): WayfernOS => {
if (typeof navigator === "undefined") return "linux";
const platform = navigator.platform.toLowerCase();
if (platform.includes("win")) return "windows";
if (platform.includes("mac")) return "macos";
return "linux";
};
interface WayfernConfigDialogProps {
isOpen: boolean;
onClose: () => void;
profile: BrowserProfile | null;
onSave: (profile: BrowserProfile, config: WayfernConfig) => Promise<void>;
isRunning?: boolean;
crossOsUnlocked?: boolean;
}
export function WayfernConfigDialog({
isOpen,
onClose,
profile,
onSave,
isRunning = false,
crossOsUnlocked = false,
}: WayfernConfigDialogProps) {
const { t } = useTranslation();
const [config, setConfig] = useState<WayfernConfig>(() => ({
geoip: true,
os: getCurrentOS(),
}));
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
if (profile?.browser === "wayfern") {
setConfig(
profile.wayfern_config || {
geoip: true,
os: getCurrentOS(),
},
);
}
}, [profile]);
const updateConfig = (key: keyof WayfernConfig, value: unknown) => {
setConfig((prev) => ({ ...prev, [key]: value }));
};
const handleSave = async () => {
if (!profile) return;
if (config.fingerprint) {
try {
JSON.parse(config.fingerprint);
} catch (_error) {
const { toast } = await import("sonner");
toast.error(t("wayfernConfigDialog.invalidFingerprint"), {
description: t("wayfernConfigDialog.invalidFingerprintDescription"),
});
return;
}
}
setIsSaving(true);
try {
await onSave(profile, config);
onClose();
} catch (error) {
console.error("Failed to save config:", error);
const { toast } = await import("sonner");
toast.error(t("wayfernConfigDialog.saveFailed"), {
description:
error instanceof Error
? error.message
: t("wayfernConfigDialog.unknownError"),
});
} finally {
setIsSaving(false);
}
};
const handleClose = () => {
if (profile?.browser === "wayfern") {
setConfig(
profile.wayfern_config || {
geoip: true,
os: getCurrentOS(),
},
);
}
onClose();
};
if (profile?.browser !== "wayfern") {
return null;
}
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="flex h-[min(85vh,52rem)] max-w-3xl flex-col">
<DialogHeader className="shrink-0">
<DialogTitle>
{isRunning
? t("wayfernConfigDialog.titleView", {
name: profile.name,
browser: "Wayfern",
})
: t("wayfernConfigDialog.titleConfigure", {
name: profile.name,
browser: "Wayfern",
})}
</DialogTitle>
</DialogHeader>
<ScrollArea className="min-h-0 flex-1">
<div className="py-4">
<SharedFingerprintConfigForm
config={config}
onConfigChange={updateConfig}
forceAdvanced={true}
readOnly={isRunning}
crossOsUnlocked={crossOsUnlocked}
limitedMode={!crossOsUnlocked}
profileVersion={profile.version}
profileBrowser="wayfern"
/>
</div>
</ScrollArea>
<DialogFooter className="shrink-0 border-t pt-4">
<RippleButton variant="outline" onClick={handleClose}>
{isRunning ? t("common.buttons.close") : t("common.buttons.cancel")}
</RippleButton>
{!isRunning && (
<LoadingButton
isLoading={isSaving}
onClick={handleSave}
disabled={isSaving}
>
{t("common.buttons.save")}
</LoadingButton>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -17,13 +17,11 @@ import { Label } from "@/components/ui/label";
interface WindowResizeWarningDialogProps {
isOpen: boolean;
onResult: (proceed: boolean) => void;
browserType?: string;
}
export function WindowResizeWarningDialog({
isOpen,
onResult,
browserType,
}: WindowResizeWarningDialogProps) {
const { t } = useTranslation();
const [dontShowAgain, setDontShowAgain] = useState(false);
@@ -49,15 +47,8 @@ export function WindowResizeWarningDialog({
onResult(false);
};
const isCamoufox = browserType === "camoufox";
const title = isCamoufox
? t("warnings.windowResizeCamoufoxTitle")
: t("warnings.windowResizeTitle");
const description = isCamoufox
? t("warnings.windowResizeCamoufoxDescription")
: t("warnings.windowResizeDescription");
const title = t("warnings.windowResizeTitle");
const description = t("warnings.windowResizeDescription");
return (
<Dialog open={isOpen}>