mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-06 23:13:58 +02:00
feat: windows support
This commit is contained in:
+17
-2
@@ -26,6 +26,7 @@ import { SettingsDialog } from "@/components/settings-dialog";
|
||||
import { SyncConfigDialog } from "@/components/sync-config-dialog";
|
||||
import { WayfernTermsDialog } from "@/components/wayfern-terms-dialog";
|
||||
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
||||
import { useCloudAuth } from "@/hooks/use-cloud-auth";
|
||||
import { useCommercialTrial } from "@/hooks/use-commercial-trial";
|
||||
import { useGroupEvents } from "@/hooks/use-group-events";
|
||||
import type { PermissionType } from "@/hooks/use-permissions";
|
||||
@@ -88,6 +89,11 @@ export default function Home() {
|
||||
checkTrialStatus,
|
||||
} = useCommercialTrial();
|
||||
|
||||
// Cloud auth for cross-OS unlock
|
||||
const { user: cloudUser } = useCloudAuth();
|
||||
const crossOsUnlocked =
|
||||
cloudUser?.plan !== "free" && cloudUser?.subscriptionStatus === "active";
|
||||
|
||||
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
|
||||
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
|
||||
const [integrationsDialogOpen, setIntegrationsDialogOpen] = useState(false);
|
||||
@@ -719,6 +725,13 @@ export default function Home() {
|
||||
void checkMissingBinaries();
|
||||
}
|
||||
|
||||
// Proactively download Wayfern and Camoufox if not already available
|
||||
if (!profilesLoading) {
|
||||
void invoke("ensure_active_browsers_downloaded").catch((err: unknown) => {
|
||||
console.error("Failed to auto-download browsers:", err);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(updateInterval);
|
||||
if (cleanup) {
|
||||
@@ -766,7 +779,7 @@ export default function Home() {
|
||||
}
|
||||
}, [profiles]);
|
||||
|
||||
// Show warning for non-wayfern/camoufox profiles (support ending March 1, 2026)
|
||||
// Show warning for non-wayfern/camoufox profiles (support ending March 15, 2026)
|
||||
useEffect(() => {
|
||||
if (profiles.length === 0) return;
|
||||
|
||||
@@ -783,7 +796,7 @@ export default function Home() {
|
||||
id: "browser-support-ending-warning",
|
||||
type: "error",
|
||||
title: "Browser support ending soon",
|
||||
description: `Support for the following profiles will be removed on March 1, 2026: ${unsupportedNames}. Please migrate to Wayfern or Camoufox profiles.`,
|
||||
description: `Support for the following profiles will be removed on March 15, 2026: ${unsupportedNames}. Please migrate to Wayfern or Camoufox profiles.`,
|
||||
duration: 15000,
|
||||
action: {
|
||||
label: "Learn more",
|
||||
@@ -917,6 +930,7 @@ export default function Home() {
|
||||
}}
|
||||
onCreateProfile={handleCreateProfile}
|
||||
selectedGroupId={selectedGroupId}
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
/>
|
||||
|
||||
<SettingsDialog
|
||||
@@ -988,6 +1002,7 @@ export default function Home() {
|
||||
? runningProfiles.has(currentProfileForCamoufoxConfig.id)
|
||||
: false
|
||||
}
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
/>
|
||||
|
||||
<GroupManagementDialog
|
||||
|
||||
@@ -39,6 +39,7 @@ interface CamoufoxConfigDialogProps {
|
||||
config: CamoufoxConfig,
|
||||
) => Promise<void>;
|
||||
isRunning?: boolean;
|
||||
crossOsUnlocked?: boolean;
|
||||
}
|
||||
|
||||
export function CamoufoxConfigDialog({
|
||||
@@ -48,6 +49,7 @@ export function CamoufoxConfigDialog({
|
||||
onSave,
|
||||
onSaveWayfern,
|
||||
isRunning = false,
|
||||
crossOsUnlocked = false,
|
||||
}: CamoufoxConfigDialogProps) {
|
||||
// Use union type to support both Camoufox and Wayfern configs
|
||||
const [config, setConfig] = useState<CamoufoxConfig | WayfernConfig>(() => ({
|
||||
@@ -160,6 +162,7 @@ export function CamoufoxConfigDialog({
|
||||
onConfigChange={updateConfig}
|
||||
forceAdvanced={true}
|
||||
readOnly={isRunning}
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
/>
|
||||
) : (
|
||||
<SharedCamoufoxConfigForm
|
||||
@@ -168,6 +171,7 @@ export function CamoufoxConfigDialog({
|
||||
forceAdvanced={true}
|
||||
readOnly={isRunning}
|
||||
browserType="camoufox"
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -71,6 +71,7 @@ interface CreateProfileDialogProps {
|
||||
groupId?: string;
|
||||
}) => Promise<void>;
|
||||
selectedGroupId?: string;
|
||||
crossOsUnlocked?: boolean;
|
||||
}
|
||||
|
||||
interface BrowserOption {
|
||||
@@ -106,6 +107,7 @@ export function CreateProfileDialog({
|
||||
onClose,
|
||||
onCreateProfile,
|
||||
selectedGroupId,
|
||||
crossOsUnlocked = false,
|
||||
}: CreateProfileDialogProps) {
|
||||
const [profileName, setProfileName] = useState("");
|
||||
const [currentStep, setCurrentStep] = useState<
|
||||
@@ -677,6 +679,16 @@ export function CreateProfileDialog({
|
||||
</RippleButton>
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingReleaseTypes &&
|
||||
!releaseTypesError &&
|
||||
!getBestAvailableVersion("wayfern") && (
|
||||
<div className="flex gap-3 items-center p-3 rounded-md border border-yellow-500/50 bg-yellow-500/10">
|
||||
<p className="text-sm text-yellow-500">
|
||||
Wayfern is not available on your platform
|
||||
yet.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingReleaseTypes &&
|
||||
!releaseTypesError &&
|
||||
!isBrowserCurrentlyDownloading("wayfern") &&
|
||||
@@ -732,6 +744,7 @@ export function CreateProfileDialog({
|
||||
config={wayfernConfig}
|
||||
onConfigChange={updateWayfernConfig}
|
||||
isCreating
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
/>
|
||||
</div>
|
||||
) : selectedBrowser === "camoufox" ? (
|
||||
@@ -763,6 +776,16 @@ export function CreateProfileDialog({
|
||||
</RippleButton>
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingReleaseTypes &&
|
||||
!releaseTypesError &&
|
||||
!getBestAvailableVersion("camoufox") && (
|
||||
<div className="flex gap-3 items-center p-3 rounded-md border border-yellow-500/50 bg-yellow-500/10">
|
||||
<p className="text-sm text-yellow-500">
|
||||
Camoufox is not available on your platform
|
||||
yet.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingReleaseTypes &&
|
||||
!releaseTypesError &&
|
||||
!isBrowserCurrentlyDownloading("camoufox") &&
|
||||
@@ -819,6 +842,7 @@ export function CreateProfileDialog({
|
||||
onConfigChange={updateCamoufoxConfig}
|
||||
isCreating
|
||||
browserType="camoufox"
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LuLock } from "react-icons/lu";
|
||||
import MultipleSelector, { type Option } from "@/components/multiple-selector";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -29,6 +31,7 @@ interface SharedCamoufoxConfigFormProps {
|
||||
forceAdvanced?: boolean; // Force advanced mode (for editing)
|
||||
readOnly?: boolean; // Flag to indicate if the form should be read-only
|
||||
browserType?: "camoufox" | "wayfern"; // Browser type to customize form options
|
||||
crossOsUnlocked?: boolean; // Allow selecting non-current OS (paid feature)
|
||||
}
|
||||
|
||||
// Determine if fingerprint editing should be disabled
|
||||
@@ -118,7 +121,9 @@ export function SharedCamoufoxConfigForm({
|
||||
forceAdvanced = false,
|
||||
readOnly = false,
|
||||
browserType = "camoufox",
|
||||
crossOsUnlocked = false,
|
||||
}: SharedCamoufoxConfigFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
forceAdvanced ? "manual" : "automatic",
|
||||
);
|
||||
@@ -128,7 +133,6 @@ export function SharedCamoufoxConfigForm({
|
||||
|
||||
// Get selected OS (defaults to current OS)
|
||||
const selectedOS = config.os || currentOS;
|
||||
const isOSDifferent = selectedOS !== currentOS;
|
||||
|
||||
// Set screen resolution to user's screen size when creating a new profile
|
||||
useEffect(() => {
|
||||
@@ -227,18 +231,25 @@ export function SharedCamoufoxConfigForm({
|
||||
<SelectValue placeholder="Select operating system" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="windows">{osLabels.windows}</SelectItem>
|
||||
<SelectItem value="macos">{osLabels.macos}</SelectItem>
|
||||
<SelectItem value="linux">{osLabels.linux}</SelectItem>
|
||||
{(["windows", "macos", "linux"] as CamoufoxOS[]).map((os) => {
|
||||
const isDisabled = os !== currentOS && !crossOsUnlocked;
|
||||
return (
|
||||
<SelectItem key={os} value={os} disabled={isDisabled}>
|
||||
<span className="flex items-center gap-2">
|
||||
{osLabels[os]}
|
||||
{isDisabled && (
|
||||
<LuLock className="w-3 h-3 text-muted-foreground" />
|
||||
)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{isOSDifferent && (
|
||||
<Alert className="border-yellow-500/50 bg-yellow-500/10">
|
||||
<AlertDescription className="text-yellow-600 dark:text-yellow-400">
|
||||
⚠️ Warning: Selecting an OS different from your current system (
|
||||
{osLabels[currentOS]}) increases the risk of detection. Websites
|
||||
can detect mismatches between your fingerprint and actual system
|
||||
behavior.
|
||||
{selectedOS !== currentOS && crossOsUnlocked && (
|
||||
<Alert className="mt-2">
|
||||
<AlertDescription>
|
||||
{t("fingerprint.crossOsWarning")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -994,19 +1005,27 @@ export function SharedCamoufoxConfigForm({
|
||||
<SelectValue placeholder="Select operating system" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="windows">{osLabels.windows}</SelectItem>
|
||||
<SelectItem value="macos">{osLabels.macos}</SelectItem>
|
||||
<SelectItem value="linux">{osLabels.linux}</SelectItem>
|
||||
{(["windows", "macos", "linux"] as CamoufoxOS[]).map((os) => {
|
||||
const isDisabled = os !== currentOS && !crossOsUnlocked;
|
||||
return (
|
||||
<SelectItem key={os} value={os} disabled={isDisabled}>
|
||||
<span className="flex items-center gap-2">
|
||||
{osLabels[os]}
|
||||
{isDisabled && (
|
||||
<LuLock className="w-3 h-3 text-muted-foreground" />
|
||||
)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{isOSDifferent && (
|
||||
<Alert className="border-yellow-500/50 bg-yellow-500/10">
|
||||
<AlertDescription className="text-yellow-600 dark:text-yellow-400">
|
||||
⚠️ Warning: Selecting an OS different from your current
|
||||
system ({osLabels[currentOS]}) increases the risk of
|
||||
detection. Websites with advanced protections can detect
|
||||
mismatches between your fingerprint and actual system
|
||||
behavior.
|
||||
{selectedOS !== currentOS && crossOsUnlocked && (
|
||||
<Alert className="mt-2">
|
||||
<AlertDescription>
|
||||
Cross-OS fingerprinting has limitations. System-level APIs
|
||||
may still reflect your actual operating system, and some
|
||||
features may have degraded performance.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LuEye, LuEyeOff } from "react-icons/lu";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -15,11 +16,13 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useCloudAuth } from "@/hooks/use-cloud-auth";
|
||||
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||
import type { SyncSettings } from "@/types";
|
||||
|
||||
@@ -29,6 +32,9 @@ interface SyncConfigDialogProps {
|
||||
}
|
||||
|
||||
export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Self-hosted state
|
||||
const [serverUrl, setServerUrl] = useState("");
|
||||
const [token, setToken] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -36,6 +42,24 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [showToken, setShowToken] = useState(false);
|
||||
|
||||
// Cloud auth state
|
||||
const {
|
||||
user,
|
||||
isLoggedIn,
|
||||
isLoading: isCloudLoading,
|
||||
requestOtp,
|
||||
verifyOtp,
|
||||
logout,
|
||||
} = useCloudAuth();
|
||||
const [email, setEmail] = useState("");
|
||||
const [otpCode, setOtpCode] = useState("");
|
||||
const [codeSent, setCodeSent] = useState(false);
|
||||
const [isSendingCode, setIsSendingCode] = useState(false);
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
|
||||
// Default to self-hosted tab if self-hosted is configured and not cloud-logged-in
|
||||
const [activeTab, setActiveTab] = useState<string>("cloud");
|
||||
|
||||
const loadSettings = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -52,9 +76,19 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
void loadSettings();
|
||||
setCodeSent(false);
|
||||
setOtpCode("");
|
||||
setEmail("");
|
||||
}
|
||||
}, [isOpen, loadSettings]);
|
||||
|
||||
// If self-hosted is configured and not cloud-logged-in, default to self-hosted tab
|
||||
useEffect(() => {
|
||||
if (!isCloudLoading && !isLoggedIn && serverUrl && token) {
|
||||
setActiveTab("self-hosted");
|
||||
}
|
||||
}, [isCloudLoading, isLoggedIn, serverUrl, token]);
|
||||
|
||||
const handleTestConnection = useCallback(async () => {
|
||||
if (!serverUrl) {
|
||||
showErrorToast("Please enter a server URL");
|
||||
@@ -112,102 +146,283 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSendCode = useCallback(async () => {
|
||||
if (!email) return;
|
||||
setIsSendingCode(true);
|
||||
try {
|
||||
await requestOtp(email);
|
||||
setCodeSent(true);
|
||||
showSuccessToast(t("sync.cloud.codeSent"));
|
||||
} catch (error) {
|
||||
console.error("Failed to send OTP:", error);
|
||||
showErrorToast(String(error));
|
||||
} finally {
|
||||
setIsSendingCode(false);
|
||||
}
|
||||
}, [email, requestOtp, t]);
|
||||
|
||||
const handleVerifyOtp = useCallback(async () => {
|
||||
if (!email || !otpCode) return;
|
||||
setIsVerifying(true);
|
||||
try {
|
||||
await verifyOtp(email, otpCode);
|
||||
showSuccessToast(t("sync.cloud.loginSuccess"));
|
||||
// Restart sync service with cloud credentials
|
||||
try {
|
||||
await invoke("restart_sync_service");
|
||||
} catch (e) {
|
||||
console.error("Failed to restart sync service:", e);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("OTP verification failed:", error);
|
||||
showErrorToast(String(error));
|
||||
} finally {
|
||||
setIsVerifying(false);
|
||||
}
|
||||
}, [email, otpCode, verifyOtp, t]);
|
||||
|
||||
const handleCloudLogout = useCallback(async () => {
|
||||
try {
|
||||
await logout();
|
||||
showSuccessToast(t("sync.cloud.logoutSuccess"));
|
||||
// Restart sync service (will fall back to self-hosted or stop)
|
||||
try {
|
||||
await invoke("restart_sync_service");
|
||||
} catch (e) {
|
||||
console.error("Failed to restart sync service:", e);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to logout:", error);
|
||||
showErrorToast(String(error));
|
||||
}
|
||||
}, [logout, t]);
|
||||
|
||||
const isConnected = Boolean(serverUrl && token);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Sync Service</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure connection to a sync server to synchronize your profiles
|
||||
across devices.
|
||||
</DialogDescription>
|
||||
<DialogTitle>{t("sync.title")}</DialogTitle>
|
||||
<DialogDescription>{t("sync.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sync-server-url">Server URL</Label>
|
||||
<Input
|
||||
id="sync-server-url"
|
||||
placeholder="https://sync.example.com"
|
||||
value={serverUrl}
|
||||
onChange={(e) => setServerUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="cloud" className="flex-1">
|
||||
{t("sync.cloud.tabLabel")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="self-hosted" className="flex-1">
|
||||
{t("sync.cloud.selfHostedTabLabel")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sync-token">Access Token</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="sync-token"
|
||||
type={showToken ? "text" : "password"}
|
||||
placeholder="Enter your sync token"
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
className="pr-10"
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToken(!showToken)}
|
||||
className="absolute right-3 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
|
||||
aria-label={showToken ? "Hide token" : "Show token"}
|
||||
>
|
||||
{showToken ? (
|
||||
<LuEyeOff className="w-4 h-4 text-muted-foreground hover:text-foreground" />
|
||||
) : (
|
||||
<LuEye className="w-4 h-4 text-muted-foreground hover:text-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{showToken ? "Hide token" : "Show token"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<TabsContent value="cloud">
|
||||
{isCloudLoading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
) : isLoggedIn && user ? (
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="flex gap-2 items-center text-sm">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
{t("sync.cloud.connected")}
|
||||
</div>
|
||||
|
||||
{isConnected && (
|
||||
<div className="flex gap-2 items-center text-sm text-muted-foreground">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
Connected
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
{t("sync.cloud.email")}
|
||||
</span>
|
||||
<span>{user.email}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
{t("sync.cloud.plan")}
|
||||
</span>
|
||||
<span className="capitalize">
|
||||
{user.plan}
|
||||
{user.planPeriod ? ` (${user.planPeriod})` : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
{t("sync.cloud.profiles")}
|
||||
</span>
|
||||
<span>
|
||||
{t("sync.cloud.profileUsage", {
|
||||
used: user.cloudProfilesUsed,
|
||||
limit: user.profileLimit,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" className="flex-1" asChild>
|
||||
<a
|
||||
href="https://donutbrowser.com/account"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("sync.cloud.manageAccount")}
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={handleCloudLogout}
|
||||
>
|
||||
{t("sync.cloud.logout")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cloud-email">{t("sync.cloud.email")}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="cloud-email"
|
||||
type="email"
|
||||
placeholder={t("sync.cloud.emailPlaceholder")}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !codeSent) {
|
||||
void handleSendCode();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<LoadingButton
|
||||
onClick={handleSendCode}
|
||||
isLoading={isSendingCode}
|
||||
disabled={!email || codeSent}
|
||||
variant="outline"
|
||||
>
|
||||
{t("sync.cloud.sendCode")}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{codeSent && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cloud-otp">
|
||||
{t("sync.cloud.verificationCode")}
|
||||
</Label>
|
||||
<Input
|
||||
id="cloud-otp"
|
||||
placeholder={t("sync.cloud.codePlaceholder")}
|
||||
value={otpCode}
|
||||
onChange={(e) => setOtpCode(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
void handleVerifyOtp();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<LoadingButton
|
||||
onClick={handleVerifyOtp}
|
||||
isLoading={isVerifying}
|
||||
disabled={!otpCode}
|
||||
className="w-full"
|
||||
>
|
||||
{isVerifying
|
||||
? t("sync.cloud.loggingIn")
|
||||
: t("sync.cloud.verifyAndLogin")}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<DialogFooter className="flex gap-2">
|
||||
{isConnected && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleDisconnect}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
disabled={isTesting || !serverUrl}
|
||||
>
|
||||
{isTesting ? "Testing..." : "Test Connection"}
|
||||
</Button>
|
||||
<LoadingButton
|
||||
onClick={handleSave}
|
||||
isLoading={isSaving}
|
||||
disabled={!serverUrl || !token}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
<TabsContent value="self-hosted">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sync-server-url">{t("sync.serverUrl")}</Label>
|
||||
<Input
|
||||
id="sync-server-url"
|
||||
placeholder={t("sync.serverUrlPlaceholder")}
|
||||
value={serverUrl}
|
||||
onChange={(e) => setServerUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sync-token">{t("sync.token")}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="sync-token"
|
||||
type={showToken ? "text" : "password"}
|
||||
placeholder={t("sync.tokenPlaceholder")}
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
className="pr-10"
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToken(!showToken)}
|
||||
className="absolute right-3 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
|
||||
aria-label={showToken ? "Hide token" : "Show token"}
|
||||
>
|
||||
{showToken ? (
|
||||
<LuEyeOff className="w-4 h-4 text-muted-foreground hover:text-foreground" />
|
||||
) : (
|
||||
<LuEye className="w-4 h-4 text-muted-foreground hover:text-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{showToken ? "Hide token" : "Show token"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isConnected && (
|
||||
<div className="flex gap-2 items-center text-sm text-muted-foreground">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
{t("sync.status.connected")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="flex gap-2">
|
||||
{isConnected && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleDisconnect}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
disabled={isTesting || !serverUrl}
|
||||
>
|
||||
{isTesting ? "Testing..." : "Test Connection"}
|
||||
</Button>
|
||||
<LoadingButton
|
||||
onClick={handleSave}
|
||||
isLoading={isSaving}
|
||||
disabled={!serverUrl || !token}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LuLock } from "react-icons/lu";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -28,6 +29,7 @@ interface WayfernConfigFormProps {
|
||||
isCreating?: boolean;
|
||||
forceAdvanced?: boolean;
|
||||
readOnly?: boolean;
|
||||
crossOsUnlocked?: boolean;
|
||||
}
|
||||
|
||||
const isFingerprintEditingDisabled = (config: WayfernConfig): boolean => {
|
||||
@@ -57,7 +59,9 @@ export function WayfernConfigForm({
|
||||
isCreating = false,
|
||||
forceAdvanced = false,
|
||||
readOnly = false,
|
||||
crossOsUnlocked = false,
|
||||
}: WayfernConfigFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
forceAdvanced ? "manual" : "automatic",
|
||||
);
|
||||
@@ -157,7 +161,7 @@ export function WayfernConfigForm({
|
||||
{(
|
||||
["windows", "macos", "linux", "android", "ios"] as WayfernOS[]
|
||||
).map((os) => {
|
||||
const isDisabled = os !== currentOS;
|
||||
const isDisabled = os !== currentOS && !crossOsUnlocked;
|
||||
return (
|
||||
<SelectItem key={os} value={os} disabled={isDisabled}>
|
||||
<span className="flex items-center gap-2">
|
||||
@@ -171,6 +175,13 @@ export function WayfernConfigForm({
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedOS !== currentOS && crossOsUnlocked && (
|
||||
<Alert className="mt-2">
|
||||
<AlertDescription>
|
||||
{t("fingerprint.crossOsWarning")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Randomize Fingerprint Option */}
|
||||
@@ -943,7 +954,7 @@ export function WayfernConfigForm({
|
||||
"ios",
|
||||
] as WayfernOS[]
|
||||
).map((os) => {
|
||||
const isDisabled = os !== currentOS;
|
||||
const isDisabled = os !== currentOS && !crossOsUnlocked;
|
||||
return (
|
||||
<SelectItem key={os} value={os} disabled={isDisabled}>
|
||||
<span className="flex items-center gap-2">
|
||||
@@ -957,6 +968,15 @@ export function WayfernConfigForm({
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedOS !== currentOS && crossOsUnlocked && (
|
||||
<Alert className="mt-2">
|
||||
<AlertDescription>
|
||||
Cross-OS fingerprinting has limitations. System-level APIs
|
||||
may still reflect your actual operating system, and some
|
||||
features may have degraded performance.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Randomize Fingerprint Option */}
|
||||
|
||||
@@ -3,17 +3,20 @@
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Platform = "macos" | "windows" | "linux";
|
||||
|
||||
function detectPlatform(): Platform {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
if (userAgent.includes("mac")) return "macos";
|
||||
if (userAgent.includes("win")) return "windows";
|
||||
return "linux";
|
||||
}
|
||||
|
||||
export function WindowDragArea() {
|
||||
const [isMacOS, setIsMacOS] = useState(false);
|
||||
const [platform, setPlatform] = useState<Platform | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if we're on macOS using user agent detection
|
||||
const checkPlatform = () => {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
setIsMacOS(userAgent.includes("mac"));
|
||||
};
|
||||
|
||||
checkPlatform();
|
||||
setPlatform(detectPlatform());
|
||||
}, []);
|
||||
|
||||
const handlePointerDown = (e: React.PointerEvent) => {
|
||||
@@ -33,21 +36,97 @@ export function WindowDragArea() {
|
||||
void startDrag();
|
||||
};
|
||||
|
||||
// Only render on macOS and when no dialogs are open
|
||||
if (!isMacOS) {
|
||||
// Linux: system decorations handle everything
|
||||
if (!platform || platform === "linux") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// macOS: transparent drag area overlay
|
||||
if (platform === "macos") {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="fixed top-0 right-0 left-0 h-10 bg-transparent border-0 z-[999999] select-none"
|
||||
data-window-drag-area="true"
|
||||
onPointerDown={handlePointerDown}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Windows: custom title bar with drag area + minimize/close buttons
|
||||
const handleMinimize = async () => {
|
||||
try {
|
||||
await getCurrentWindow().minimize();
|
||||
} catch (error) {
|
||||
console.error("Failed to minimize window:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = async () => {
|
||||
try {
|
||||
await getCurrentWindow().close();
|
||||
} catch (error) {
|
||||
console.error("Failed to close window:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="fixed top-0 right-0 left-0 h-10 bg-transparent border-0 z-[999999] select-none"
|
||||
<div
|
||||
className="fixed top-0 right-0 left-0 h-10 z-[999999] flex items-center select-none"
|
||||
data-window-drag-area="true"
|
||||
onPointerDown={handlePointerDown}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{/* Draggable area */}
|
||||
<button
|
||||
type="button"
|
||||
className="flex-1 h-full bg-transparent border-0 cursor-default"
|
||||
onPointerDown={handlePointerDown}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
{/* Window control buttons */}
|
||||
<div className="flex items-center h-full">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleMinimize}
|
||||
className="flex items-center justify-center w-12 h-full hover:bg-muted/50 transition-colors text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg
|
||||
width="10"
|
||||
height="1"
|
||||
viewBox="0 0 10 1"
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
aria-label="Minimize"
|
||||
>
|
||||
<rect width="10" height="1" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="flex items-center justify-center w-12 h-full hover:bg-destructive/90 transition-colors text-muted-foreground hover:text-destructive-foreground"
|
||||
>
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.2"
|
||||
role="img"
|
||||
aria-label="Close"
|
||||
>
|
||||
<line x1="1" y1="1" x2="9" y2="9" />
|
||||
<line x1="9" y1="1" x2="1" y2="9" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { CloudAuthState, CloudUser } from "@/types";
|
||||
|
||||
interface UseCloudAuthReturn {
|
||||
user: CloudUser | null;
|
||||
isLoggedIn: boolean;
|
||||
isLoading: boolean;
|
||||
requestOtp: (email: string) => Promise<string>;
|
||||
verifyOtp: (email: string, code: string) => Promise<CloudAuthState>;
|
||||
logout: () => Promise<void>;
|
||||
refreshProfile: () => Promise<CloudUser>;
|
||||
}
|
||||
|
||||
export function useCloudAuth(): UseCloudAuthReturn {
|
||||
const [authState, setAuthState] = useState<CloudAuthState | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const loadUser = useCallback(async () => {
|
||||
try {
|
||||
const state = await invoke<CloudAuthState | null>("cloud_get_user");
|
||||
setAuthState(state);
|
||||
} catch (error) {
|
||||
console.error("Failed to load cloud auth state:", error);
|
||||
setAuthState(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadUser();
|
||||
|
||||
const unlistenPromise = listen("cloud-auth-expired", () => {
|
||||
setAuthState(null);
|
||||
});
|
||||
|
||||
return () => {
|
||||
void unlistenPromise.then((unlisten) => {
|
||||
unlisten();
|
||||
});
|
||||
};
|
||||
}, [loadUser]);
|
||||
|
||||
const requestOtp = useCallback(async (email: string): Promise<string> => {
|
||||
return invoke<string>("cloud_request_otp", { email });
|
||||
}, []);
|
||||
|
||||
const verifyOtp = useCallback(
|
||||
async (email: string, code: string): Promise<CloudAuthState> => {
|
||||
const state = await invoke<CloudAuthState>("cloud_verify_otp", {
|
||||
email,
|
||||
code,
|
||||
});
|
||||
setAuthState(state);
|
||||
return state;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
await invoke("cloud_logout");
|
||||
setAuthState(null);
|
||||
}, []);
|
||||
|
||||
const refreshProfile = useCallback(async (): Promise<CloudUser> => {
|
||||
const user = await invoke<CloudUser>("cloud_refresh_profile");
|
||||
setAuthState((prev) =>
|
||||
prev
|
||||
? { ...prev, user }
|
||||
: { user, logged_in_at: new Date().toISOString() },
|
||||
);
|
||||
return user;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
user: authState?.user ?? null,
|
||||
isLoggedIn: authState !== null,
|
||||
isLoading,
|
||||
requestOtp,
|
||||
verifyOtp,
|
||||
logout,
|
||||
refreshProfile,
|
||||
};
|
||||
}
|
||||
@@ -274,7 +274,28 @@
|
||||
"syncing": "Syncing...",
|
||||
"error": "Sync Error"
|
||||
},
|
||||
"description": "Connect to a sync server to synchronize your profiles, proxies, and groups across devices."
|
||||
"description": "Connect to a sync server to synchronize your profiles, proxies, and groups across devices.",
|
||||
"cloud": {
|
||||
"tabLabel": "Cloud",
|
||||
"selfHostedTabLabel": "Self-Hosted",
|
||||
"email": "Email",
|
||||
"emailPlaceholder": "you@example.com",
|
||||
"sendCode": "Send Code",
|
||||
"codeSent": "Code sent! Check your email.",
|
||||
"verificationCode": "Verification Code",
|
||||
"codePlaceholder": "Enter 6-digit code",
|
||||
"verifyAndLogin": "Verify & Log In",
|
||||
"loggingIn": "Logging in...",
|
||||
"connected": "Connected",
|
||||
"plan": "Plan",
|
||||
"profiles": "Profiles",
|
||||
"profileUsage": "{{used}} / {{limit}}",
|
||||
"manageAccount": "Manage Account",
|
||||
"logout": "Log Out",
|
||||
"logoutConfirm": "Are you sure you want to log out? Cloud sync will stop.",
|
||||
"loginSuccess": "Successfully logged in!",
|
||||
"logoutSuccess": "Successfully logged out."
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrations",
|
||||
@@ -457,5 +478,8 @@
|
||||
"zen": "Zen Browser",
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
"crossOsWarning": "Spoofing a different operating system is harder — system-level APIs are more difficult to mask, making it easier for websites to detect inconsistencies. No anti-detect browser can perfectly spoof every detail across operating systems."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,28 @@
|
||||
"syncing": "Sincronizando...",
|
||||
"error": "Error de Sincronización"
|
||||
},
|
||||
"description": "Conéctate a un servidor de sincronización para sincronizar tus perfiles, proxies y grupos entre dispositivos."
|
||||
"description": "Conéctate a un servidor de sincronización para sincronizar tus perfiles, proxies y grupos entre dispositivos.",
|
||||
"cloud": {
|
||||
"tabLabel": "Nube",
|
||||
"selfHostedTabLabel": "Autoalojado",
|
||||
"email": "Correo electrónico",
|
||||
"emailPlaceholder": "tu@ejemplo.com",
|
||||
"sendCode": "Enviar Código",
|
||||
"codeSent": "¡Código enviado! Revisa tu correo electrónico.",
|
||||
"verificationCode": "Código de Verificación",
|
||||
"codePlaceholder": "Ingresa el código de 6 dígitos",
|
||||
"verifyAndLogin": "Verificar e Iniciar Sesión",
|
||||
"loggingIn": "Iniciando sesión...",
|
||||
"connected": "Conectado",
|
||||
"plan": "Plan",
|
||||
"profiles": "Perfiles",
|
||||
"profileUsage": "{{used}} / {{limit}}",
|
||||
"manageAccount": "Administrar Cuenta",
|
||||
"logout": "Cerrar Sesión",
|
||||
"logoutConfirm": "¿Estás seguro de que deseas cerrar sesión? La sincronización en la nube se detendrá.",
|
||||
"loginSuccess": "¡Sesión iniciada exitosamente!",
|
||||
"logoutSuccess": "Sesión cerrada exitosamente."
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integraciones",
|
||||
@@ -457,5 +478,8 @@
|
||||
"zen": "Zen Browser",
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
"crossOsWarning": "Suplantar un sistema operativo diferente es más difícil: las API a nivel de sistema son más difíciles de enmascarar, lo que facilita que los sitios web detecten inconsistencias. Ningún navegador antidetección puede suplantar perfectamente cada detalle entre sistemas operativos."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,28 @@
|
||||
"syncing": "Synchronisation...",
|
||||
"error": "Erreur de synchronisation"
|
||||
},
|
||||
"description": "Connectez-vous à un serveur de synchronisation pour synchroniser vos profils, proxies et groupes entre appareils."
|
||||
"description": "Connectez-vous à un serveur de synchronisation pour synchroniser vos profils, proxies et groupes entre appareils.",
|
||||
"cloud": {
|
||||
"tabLabel": "Cloud",
|
||||
"selfHostedTabLabel": "Auto-hébergé",
|
||||
"email": "E-mail",
|
||||
"emailPlaceholder": "vous@exemple.com",
|
||||
"sendCode": "Envoyer le Code",
|
||||
"codeSent": "Code envoyé ! Vérifiez votre e-mail.",
|
||||
"verificationCode": "Code de Vérification",
|
||||
"codePlaceholder": "Entrez le code à 6 chiffres",
|
||||
"verifyAndLogin": "Vérifier et Se Connecter",
|
||||
"loggingIn": "Connexion en cours...",
|
||||
"connected": "Connecté",
|
||||
"plan": "Forfait",
|
||||
"profiles": "Profils",
|
||||
"profileUsage": "{{used}} / {{limit}}",
|
||||
"manageAccount": "Gérer le Compte",
|
||||
"logout": "Se Déconnecter",
|
||||
"logoutConfirm": "Êtes-vous sûr de vouloir vous déconnecter ? La synchronisation cloud sera arrêtée.",
|
||||
"loginSuccess": "Connexion réussie !",
|
||||
"logoutSuccess": "Déconnexion réussie."
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Intégrations",
|
||||
@@ -457,5 +478,8 @@
|
||||
"zen": "Zen Browser",
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
"crossOsWarning": "Usurper un système d'exploitation différent est plus difficile : les API au niveau du système sont plus difficiles à masquer, ce qui permet aux sites web de détecter plus facilement les incohérences. Aucun navigateur anti-détection ne peut parfaitement usurper chaque détail d'un système d'exploitation à l'autre."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,28 @@
|
||||
"syncing": "同期中...",
|
||||
"error": "同期エラー"
|
||||
},
|
||||
"description": "同期サーバーに接続して、デバイス間でプロファイル、プロキシ、グループを同期します。"
|
||||
"description": "同期サーバーに接続して、デバイス間でプロファイル、プロキシ、グループを同期します。",
|
||||
"cloud": {
|
||||
"tabLabel": "クラウド",
|
||||
"selfHostedTabLabel": "セルフホスト",
|
||||
"email": "メールアドレス",
|
||||
"emailPlaceholder": "you@example.com",
|
||||
"sendCode": "コードを送信",
|
||||
"codeSent": "コードを送信しました!メールを確認してください。",
|
||||
"verificationCode": "認証コード",
|
||||
"codePlaceholder": "6桁のコードを入力",
|
||||
"verifyAndLogin": "認証してログイン",
|
||||
"loggingIn": "ログイン中...",
|
||||
"connected": "接続済み",
|
||||
"plan": "プラン",
|
||||
"profiles": "プロファイル",
|
||||
"profileUsage": "{{used}} / {{limit}}",
|
||||
"manageAccount": "アカウント管理",
|
||||
"logout": "ログアウト",
|
||||
"logoutConfirm": "ログアウトしてもよろしいですか?クラウド同期が停止します。",
|
||||
"loginSuccess": "ログインに成功しました!",
|
||||
"logoutSuccess": "ログアウトしました。"
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
"title": "統合",
|
||||
@@ -457,5 +478,8 @@
|
||||
"zen": "Zen Browser",
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
"crossOsWarning": "異なるオペレーティングシステムの偽装はより困難です。システムレベルのAPIはマスクしにくく、ウェブサイトが矛盾を検出しやすくなります。どのアンチディテクトブラウザも、異なるOS間のすべての詳細を完璧に偽装することはできません。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,28 @@
|
||||
"syncing": "Sincronizando...",
|
||||
"error": "Erro de Sincronização"
|
||||
},
|
||||
"description": "Conecte-se a um servidor de sincronização para sincronizar seus perfis, proxies e grupos entre dispositivos."
|
||||
"description": "Conecte-se a um servidor de sincronização para sincronizar seus perfis, proxies e grupos entre dispositivos.",
|
||||
"cloud": {
|
||||
"tabLabel": "Nuvem",
|
||||
"selfHostedTabLabel": "Auto-hospedado",
|
||||
"email": "E-mail",
|
||||
"emailPlaceholder": "voce@exemplo.com",
|
||||
"sendCode": "Enviar Código",
|
||||
"codeSent": "Código enviado! Verifique seu e-mail.",
|
||||
"verificationCode": "Código de Verificação",
|
||||
"codePlaceholder": "Digite o código de 6 dígitos",
|
||||
"verifyAndLogin": "Verificar e Entrar",
|
||||
"loggingIn": "Entrando...",
|
||||
"connected": "Conectado",
|
||||
"plan": "Plano",
|
||||
"profiles": "Perfis",
|
||||
"profileUsage": "{{used}} / {{limit}}",
|
||||
"manageAccount": "Gerenciar Conta",
|
||||
"logout": "Sair",
|
||||
"logoutConfirm": "Tem certeza de que deseja sair? A sincronização na nuvem será interrompida.",
|
||||
"loginSuccess": "Login realizado com sucesso!",
|
||||
"logoutSuccess": "Logout realizado com sucesso."
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrações",
|
||||
@@ -457,5 +478,8 @@
|
||||
"zen": "Zen Browser",
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
"crossOsWarning": "Falsificar um sistema operacional diferente é mais difícil: as APIs de nível de sistema são mais difíceis de mascarar, facilitando a detecção de inconsistências pelos sites. Nenhum navegador antidetecção consegue falsificar perfeitamente todos os detalhes entre sistemas operacionais."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,28 @@
|
||||
"syncing": "Синхронизация...",
|
||||
"error": "Ошибка синхронизации"
|
||||
},
|
||||
"description": "Подключитесь к серверу синхронизации для синхронизации профилей, прокси и групп между устройствами."
|
||||
"description": "Подключитесь к серверу синхронизации для синхронизации профилей, прокси и групп между устройствами.",
|
||||
"cloud": {
|
||||
"tabLabel": "Облако",
|
||||
"selfHostedTabLabel": "Свой сервер",
|
||||
"email": "Электронная почта",
|
||||
"emailPlaceholder": "вы@пример.com",
|
||||
"sendCode": "Отправить код",
|
||||
"codeSent": "Код отправлен! Проверьте вашу почту.",
|
||||
"verificationCode": "Код подтверждения",
|
||||
"codePlaceholder": "Введите 6-значный код",
|
||||
"verifyAndLogin": "Подтвердить и Войти",
|
||||
"loggingIn": "Вход...",
|
||||
"connected": "Подключено",
|
||||
"plan": "Тариф",
|
||||
"profiles": "Профили",
|
||||
"profileUsage": "{{used}} / {{limit}}",
|
||||
"manageAccount": "Управление аккаунтом",
|
||||
"logout": "Выйти",
|
||||
"logoutConfirm": "Вы уверены, что хотите выйти? Облачная синхронизация будет остановлена.",
|
||||
"loginSuccess": "Вход выполнен успешно!",
|
||||
"logoutSuccess": "Выход выполнен успешно."
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Интеграции",
|
||||
@@ -457,5 +478,8 @@
|
||||
"zen": "Zen Browser",
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
"crossOsWarning": "Подмена другой операционной системы сложнее — системные API труднее замаскировать, что упрощает обнаружение несоответствий веб-сайтами. Ни один антидетект-браузер не может идеально подменить все детали при смене операционной системы."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,28 @@
|
||||
"syncing": "同步中...",
|
||||
"error": "同步错误"
|
||||
},
|
||||
"description": "连接到同步服务器以在设备之间同步您的配置文件、代理和分组。"
|
||||
"description": "连接到同步服务器以在设备之间同步您的配置文件、代理和分组。",
|
||||
"cloud": {
|
||||
"tabLabel": "云端",
|
||||
"selfHostedTabLabel": "自托管",
|
||||
"email": "电子邮件",
|
||||
"emailPlaceholder": "you@example.com",
|
||||
"sendCode": "发送验证码",
|
||||
"codeSent": "验证码已发送!请检查您的邮箱。",
|
||||
"verificationCode": "验证码",
|
||||
"codePlaceholder": "输入6位验证码",
|
||||
"verifyAndLogin": "验证并登录",
|
||||
"loggingIn": "登录中...",
|
||||
"connected": "已连接",
|
||||
"plan": "套餐",
|
||||
"profiles": "配置文件",
|
||||
"profileUsage": "{{used}} / {{limit}}",
|
||||
"manageAccount": "管理账户",
|
||||
"logout": "退出登录",
|
||||
"logoutConfirm": "您确定要退出登录吗?云同步将会停止。",
|
||||
"loginSuccess": "登录成功!",
|
||||
"logoutSuccess": "已成功退出登录。"
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
"title": "集成",
|
||||
@@ -457,5 +478,8 @@
|
||||
"zen": "Zen Browser",
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
"crossOsWarning": "伪装不同的操作系统更加困难——系统级API更难以掩盖,使网站更容易检测到不一致之处。没有任何反检测浏览器能够完美伪装跨操作系统的所有细节。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,23 @@ export interface SyncSettings {
|
||||
sync_token?: string;
|
||||
}
|
||||
|
||||
export interface CloudUser {
|
||||
id: string;
|
||||
email: string;
|
||||
plan: string;
|
||||
planPeriod: string;
|
||||
subscriptionStatus: string;
|
||||
profileLimit: number;
|
||||
cloudProfilesUsed: number;
|
||||
proxyBandwidthLimitMb: number;
|
||||
proxyBandwidthUsedMb: number;
|
||||
}
|
||||
|
||||
export interface CloudAuthState {
|
||||
user: CloudUser;
|
||||
logged_in_at: string;
|
||||
}
|
||||
|
||||
export interface ProfileSyncStatusEvent {
|
||||
profile_id: string;
|
||||
status: "disabled" | "syncing" | "synced" | "error" | "pending";
|
||||
|
||||
Reference in New Issue
Block a user