mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-18 08:57:24 +02:00
refactor: fully deprecate camoufox
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user