mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-12 17:57:50 +02:00
feat: windows support
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user