feat: e2e encrypted sync

This commit is contained in:
zhom
2026-02-24 05:51:48 +04:00
parent 21d80fde56
commit e6cb4e6082
56 changed files with 5831 additions and 2549 deletions
+53 -36
View File
@@ -7,8 +7,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog";
import { CommercialTrialModal } from "@/components/commercial-trial-modal";
import { CookieCopyDialog } from "@/components/cookie-copy-dialog";
import { CookieExportDialog } from "@/components/cookie-export-dialog";
import { CookieImportDialog } from "@/components/cookie-import-dialog";
import { CookieManagementDialog } from "@/components/cookie-management-dialog";
import { CreateProfileDialog } from "@/components/create-profile-dialog";
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog";
import { GroupAssignmentDialog } from "@/components/group-assignment-dialog";
@@ -28,6 +27,7 @@ import { SettingsDialog } from "@/components/settings-dialog";
import { SyncAllDialog } from "@/components/sync-all-dialog";
import { SyncConfigDialog } from "@/components/sync-config-dialog";
import { WayfernTermsDialog } from "@/components/wayfern-terms-dialog";
import { WindowResizeWarningDialog } from "@/components/window-resize-warning-dialog";
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
import { useCloudAuth } from "@/hooks/use-cloud-auth";
import { useCommercialTrial } from "@/hooks/use-commercial-trial";
@@ -144,12 +144,12 @@ export default function Home() {
const [proxyAssignmentDialogOpen, setProxyAssignmentDialogOpen] =
useState(false);
const [cookieCopyDialogOpen, setCookieCopyDialogOpen] = useState(false);
const [cookieImportDialogOpen, setCookieImportDialogOpen] = useState(false);
const [currentProfileForCookieImport, setCurrentProfileForCookieImport] =
useState<BrowserProfile | null>(null);
const [cookieExportDialogOpen, setCookieExportDialogOpen] = useState(false);
const [currentProfileForCookieExport, setCurrentProfileForCookieExport] =
useState<BrowserProfile | null>(null);
const [cookieManagementDialogOpen, setCookieManagementDialogOpen] =
useState(false);
const [
currentProfileForCookieManagement,
setCurrentProfileForCookieManagement,
] = useState<BrowserProfile | null>(null);
const [selectedProfilesForCookies, setSelectedProfilesForCookies] = useState<
string[]
>([]);
@@ -167,6 +167,10 @@ export default function Home() {
useState<BrowserProfile | null>(null);
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
const [launchOnLoginDialogOpen, setLaunchOnLoginDialogOpen] = useState(false);
const [windowResizeWarningOpen, setWindowResizeWarningOpen] = useState(false);
const windowResizeWarningResolver = useRef<
((proceed: boolean) => void) | null
>(null);
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
const [currentPermissionType, setCurrentPermissionType] =
useState<PermissionType>("microphone");
@@ -528,6 +532,26 @@ export default function Home() {
const launchProfile = useCallback(async (profile: BrowserProfile) => {
console.log("Starting launch for profile:", profile.name);
// Show one-time warning about window resizing for fingerprinted browsers
if (profile.browser === "camoufox" || profile.browser === "wayfern") {
try {
const dismissed = await invoke<boolean>(
"get_window_resize_warning_dismissed",
);
if (!dismissed) {
const proceed = await new Promise<boolean>((resolve) => {
windowResizeWarningResolver.current = resolve;
setWindowResizeWarningOpen(true);
});
if (!proceed) {
return;
}
}
} catch (error) {
console.error("Failed to check window resize warning:", error);
}
}
try {
const result = await invoke<BrowserProfile>("launch_browser_profile", {
profile,
@@ -537,7 +561,6 @@ export default function Home() {
console.error("Failed to launch browser:", err);
const errorMessage = err instanceof Error ? err.message : String(err);
showErrorToast(`Failed to launch browser: ${errorMessage}`);
// Re-throw the error so the table component can handle loading state cleanup
throw err;
}
}, []);
@@ -698,14 +721,9 @@ export default function Home() {
setCookieCopyDialogOpen(true);
}, []);
const handleImportCookies = useCallback((profile: BrowserProfile) => {
setCurrentProfileForCookieImport(profile);
setCookieImportDialogOpen(true);
}, []);
const handleExportCookies = useCallback((profile: BrowserProfile) => {
setCurrentProfileForCookieExport(profile);
setCookieExportDialogOpen(true);
const handleOpenCookieManagement = useCallback((profile: BrowserProfile) => {
setCurrentProfileForCookieManagement(profile);
setCookieManagementDialogOpen(true);
}, []);
const handleGroupAssignmentComplete = useCallback(async () => {
@@ -732,10 +750,10 @@ export default function Home() {
const handleToggleProfileSync = useCallback(
async (profile: BrowserProfile) => {
try {
const enabling = !profile.sync_enabled;
await invoke("set_profile_sync_enabled", {
const enabling = !profile.sync_mode || profile.sync_mode === "Disabled";
await invoke("set_profile_sync_mode", {
profileId: profile.id,
enabled: enabling,
syncMode: enabling ? "Regular" : "Disabled",
});
if (enabling) {
userInitiatedSyncIds.current.add(profile.id);
@@ -1014,8 +1032,7 @@ export default function Home() {
onRenameProfile={handleRenameProfile}
onConfigureCamoufox={handleConfigureCamoufox}
onCopyCookiesToProfile={handleCopyCookiesToProfile}
onImportCookies={handleImportCookies}
onExportCookies={handleExportCookies}
onOpenCookieManagement={handleOpenCookieManagement}
runningProfiles={runningProfiles}
isUpdating={isUpdating}
onDeleteSelectedProfiles={handleDeleteSelectedProfiles}
@@ -1159,22 +1176,13 @@ export default function Home() {
onCopyComplete={() => setSelectedProfilesForCookies([])}
/>
<CookieImportDialog
isOpen={cookieImportDialogOpen}
<CookieManagementDialog
isOpen={cookieManagementDialogOpen}
onClose={() => {
setCookieImportDialogOpen(false);
setCurrentProfileForCookieImport(null);
setCookieManagementDialogOpen(false);
setCurrentProfileForCookieManagement(null);
}}
profile={currentProfileForCookieImport}
/>
<CookieExportDialog
isOpen={cookieExportDialogOpen}
onClose={() => {
setCookieExportDialogOpen(false);
setCurrentProfileForCookieExport(null);
}}
profile={currentProfileForCookieExport}
profile={currentProfileForCookieManagement}
/>
<DeleteConfirmationDialog
@@ -1237,6 +1245,15 @@ export default function Home() {
isOpen={launchOnLoginDialogOpen}
onClose={() => setLaunchOnLoginDialogOpen(false)}
/>
<WindowResizeWarningDialog
isOpen={windowResizeWarningOpen}
onResult={(proceed) => {
setWindowResizeWarningOpen(false);
windowResizeWarningResolver.current?.(proceed);
windowResizeWarningResolver.current = null;
}}
/>
</div>
);
}
+6 -15
View File
@@ -26,9 +26,7 @@ const getCurrentOS = (): CamoufoxOS => {
return "linux";
};
import { LuLock } from "react-icons/lu";
import { LoadingButton } from "./loading-button";
import { ProBadge } from "./ui/pro-badge";
import { RippleButton } from "./ui/ripple";
interface CamoufoxConfigDialogProps {
@@ -157,34 +155,27 @@ export function CamoufoxConfigDialog({
</DialogHeader>
<ScrollArea className="flex-1 h-[300px]">
<div className="py-4 relative">
<div className="py-4">
{profile.browser === "wayfern" ? (
<WayfernConfigForm
config={config as WayfernConfig}
onConfigChange={updateConfig}
forceAdvanced={true}
readOnly={isRunning || !crossOsUnlocked}
readOnly={isRunning}
crossOsUnlocked={crossOsUnlocked}
limitedMode={!crossOsUnlocked}
/>
) : (
<SharedCamoufoxConfigForm
config={config as CamoufoxConfig}
onConfigChange={updateConfig}
forceAdvanced={true}
readOnly={isRunning || !crossOsUnlocked}
readOnly={isRunning}
browserType="camoufox"
crossOsUnlocked={crossOsUnlocked}
limitedMode={!crossOsUnlocked}
/>
)}
{!crossOsUnlocked && (
<div className="absolute inset-0 backdrop-blur-sm bg-background/60 z-10 flex flex-col items-center justify-center gap-2">
<LuLock className="w-6 h-6 text-muted-foreground" />
<p className="text-sm text-muted-foreground font-medium">
Fingerprint editing is a Pro feature
</p>
<ProBadge />
</div>
)}
</div>
</ScrollArea>
@@ -192,7 +183,7 @@ export function CamoufoxConfigDialog({
<RippleButton variant="outline" onClick={handleClose}>
{isRunning ? "Close" : "Cancel"}
</RippleButton>
{!isRunning && crossOsUnlocked && (
{!isRunning && (
<LoadingButton
isLoading={isSaving}
onClick={handleSave}
-129
View File
@@ -1,129 +0,0 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { useCallback, useState } from "react";
import { LuDownload } from "react-icons/lu";
import { toast } from "sonner";
import { LoadingButton } from "@/components/loading-button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { RippleButton } from "@/components/ui/ripple";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { BrowserProfile } from "@/types";
interface CookieExportDialogProps {
isOpen: boolean;
onClose: () => void;
profile: BrowserProfile | null;
}
export function CookieExportDialog({
isOpen,
onClose,
profile,
}: CookieExportDialogProps) {
const [format, setFormat] = useState<"netscape" | "json">("json");
const [isExporting, setIsExporting] = useState(false);
const handleClose = useCallback(() => {
setFormat("json");
setIsExporting(false);
onClose();
}, [onClose]);
const handleExport = useCallback(async () => {
if (!profile) return;
setIsExporting(true);
try {
const content = await invoke<string>("export_profile_cookies", {
profileId: profile.id,
format,
});
const ext = format === "json" ? "json" : "txt";
const defaultName = `${profile.name}_cookies.${ext}`;
const filePath = await save({
defaultPath: defaultName,
filters: [
{
name: format === "json" ? "JSON" : "Text",
extensions: [ext],
},
],
});
if (!filePath) {
setIsExporting(false);
return;
}
await writeTextFile(filePath, content);
toast.success("Cookies exported successfully");
handleClose();
} catch (error) {
toast.error(error instanceof Error ? error.message : String(error));
} finally {
setIsExporting(false);
}
}, [profile, format, handleClose]);
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Export Cookies</DialogTitle>
<DialogDescription>
Export cookies from this profile.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>Format</Label>
<Select
value={format}
onValueChange={(v) => setFormat(v as "netscape" | "json")}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="netscape">Netscape TXT</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<RippleButton variant="outline" onClick={handleClose}>
Cancel
</RippleButton>
<LoadingButton
isLoading={isExporting}
onClick={() => void handleExport()}
>
<LuDownload className="w-4 h-4 mr-2" />
Export
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
-212
View File
@@ -1,212 +0,0 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useState } from "react";
import { LuUpload } from "react-icons/lu";
import { toast } from "sonner";
import { LoadingButton } from "@/components/loading-button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { RippleButton } from "@/components/ui/ripple";
import type { BrowserProfile } from "@/types";
interface CookieImportResult {
cookies_imported: number;
cookies_replaced: number;
errors: string[];
}
interface CookieImportDialogProps {
isOpen: boolean;
onClose: () => void;
profile: BrowserProfile | null;
}
const countCookies = (content: string): number => {
const trimmed = content.trim();
if (trimmed.startsWith("[")) {
try {
const arr = JSON.parse(trimmed);
if (Array.isArray(arr)) return arr.length;
} catch {
// Fall through to Netscape counting
}
}
return content.split("\n").filter((line) => {
const l = line.trim();
return l && !l.startsWith("#");
}).length;
};
export function CookieImportDialog({
isOpen,
onClose,
profile,
}: CookieImportDialogProps) {
const [fileContent, setFileContent] = useState<string | null>(null);
const [fileName, setFileName] = useState<string | null>(null);
const [cookieCount, setCookieCount] = useState(0);
const [isImporting, setIsImporting] = useState(false);
const [result, setResult] = useState<CookieImportResult | null>(null);
const resetState = useCallback(() => {
setFileContent(null);
setFileName(null);
setCookieCount(0);
setIsImporting(false);
setResult(null);
}, []);
const handleClose = useCallback(() => {
resetState();
onClose();
}, [resetState, onClose]);
const handleFileRead = useCallback((file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result as string;
setFileContent(content);
setFileName(file.name);
setCookieCount(countCookies(content));
};
reader.onerror = () => {
toast.error("Failed to read file");
};
reader.readAsText(file);
}, []);
const handleImport = useCallback(async () => {
if (!fileContent || !profile) return;
setIsImporting(true);
try {
const importResult = await invoke<CookieImportResult>(
"import_cookies_from_file",
{
profileId: profile.id,
content: fileContent,
},
);
setResult(importResult);
} catch (error) {
toast.error(error instanceof Error ? error.message : String(error));
} finally {
setIsImporting(false);
}
}, [fileContent, profile]);
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Import Cookies</DialogTitle>
<DialogDescription>
{!fileContent &&
"Import cookies from a Netscape or JSON format file."}
{fileContent &&
!result &&
`${cookieCount} cookies found in ${fileName}`}
{result && "Cookie import completed"}
</DialogDescription>
</DialogHeader>
{!fileContent && (
<div className="space-y-4">
<div
role="button"
tabIndex={0}
className="flex flex-col items-center justify-center border-2 border-dashed rounded-lg p-8 transition-colors cursor-pointer border-muted-foreground/25 hover:border-muted-foreground/50"
onClick={() =>
document.getElementById("cookie-file-input")?.click()
}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
document.getElementById("cookie-file-input")?.click();
}
}}
>
<LuUpload className="w-10 h-10 text-muted-foreground mb-4" />
<p className="text-sm text-muted-foreground text-center">
Click to choose a cookie file
<br />
<span className="text-xs">(.txt, .cookies, or .json)</span>
</p>
<input
id="cookie-file-input"
type="file"
accept=".txt,.cookies,.json"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleFileRead(file);
e.target.value = "";
}}
/>
</div>
</div>
)}
{fileContent && !result && (
<div className="space-y-4">
<div className="flex items-center gap-3 p-4 bg-muted/30 rounded-lg">
<div>
<div className="font-medium">{fileName}</div>
<div className="text-sm text-muted-foreground">
{cookieCount} cookies found
</div>
</div>
</div>
</div>
)}
{result && (
<div className="space-y-4">
<div className="p-4 rounded-lg bg-green-500/10">
<div className="font-medium text-green-600 dark:text-green-400">
Successfully imported {result.cookies_imported} cookies (
{result.cookies_replaced} replaced)
</div>
{result.errors.length > 0 && (
<div className="mt-2 text-sm text-muted-foreground">
{result.errors.length} line(s) skipped
</div>
)}
</div>
</div>
)}
<DialogFooter>
{!fileContent && (
<RippleButton variant="outline" onClick={handleClose}>
Cancel
</RippleButton>
)}
{fileContent && !result && (
<>
<RippleButton variant="outline" onClick={resetState}>
Back
</RippleButton>
<LoadingButton
isLoading={isImporting}
onClick={() => void handleImport()}
disabled={cookieCount === 0}
>
Import
</LoadingButton>
</>
)}
{result && <RippleButton onClick={handleClose}>Done</RippleButton>}
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+649
View File
@@ -0,0 +1,649 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { useCallback, useEffect, useMemo, useState } from "react";
import { LuChevronDown, LuChevronRight, LuUpload } from "react-icons/lu";
import { toast } from "sonner";
import { LoadingButton } from "@/components/loading-button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { RippleButton } from "@/components/ui/ripple";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type {
BrowserProfile,
CookieReadResult,
DomainCookies,
UnifiedCookie,
} from "@/types";
interface CookieImportResult {
cookies_imported: number;
cookies_replaced: number;
errors: string[];
}
interface CookieManagementDialogProps {
isOpen: boolean;
onClose: () => void;
profile: BrowserProfile | null;
initialTab?: "import" | "export";
}
type SelectionState = {
[domain: string]: {
allSelected: boolean;
cookies: Set<string>;
};
};
const countCookies = (content: string): number => {
const trimmed = content.trim();
if (trimmed.startsWith("[")) {
try {
const arr = JSON.parse(trimmed);
if (Array.isArray(arr)) return arr.length;
} catch {
// Fall through to Netscape counting
}
}
return content.split("\n").filter((line) => {
const l = line.trim();
return l && !l.startsWith("#");
}).length;
};
function formatJsonCookies(cookies: UnifiedCookie[]): string {
const arr = cookies.map((c) => {
const sameSite =
c.same_site === 1
? "lax"
: c.same_site === 2
? "strict"
: "no_restriction";
return {
name: c.name,
value: c.value,
domain: c.domain,
path: c.path,
secure: c.is_secure,
httpOnly: c.is_http_only,
sameSite,
expirationDate: c.expires,
session: c.expires === 0,
hostOnly: !c.domain.startsWith("."),
};
});
return JSON.stringify(arr, null, 2);
}
function formatNetscapeCookies(cookies: UnifiedCookie[]): string {
const lines = ["# Netscape HTTP Cookie File"];
for (const c of cookies) {
const flag = c.domain.startsWith(".") ? "TRUE" : "FALSE";
const secure = c.is_secure ? "TRUE" : "FALSE";
lines.push(
`${c.domain}\t${flag}\t${c.path}\t${secure}\t${c.expires}\t${c.name}\t${c.value}`,
);
}
return lines.join("\n");
}
function initSelectionFromCookieData(data: CookieReadResult): SelectionState {
const sel: SelectionState = {};
for (const d of data.domains) {
sel[d.domain] = {
allSelected: true,
cookies: new Set(d.cookies.map((c) => c.name)),
};
}
return sel;
}
export function CookieManagementDialog({
isOpen,
onClose,
profile,
initialTab = "import",
}: CookieManagementDialogProps) {
// Import state
const [fileContent, setFileContent] = useState<string | null>(null);
const [fileName, setFileName] = useState<string | null>(null);
const [cookieCount, setCookieCount] = useState(0);
const [isImporting, setIsImporting] = useState(false);
const [importResult, setImportResult] = useState<CookieImportResult | null>(
null,
);
// Export state
const [format, setFormat] = useState<"netscape" | "json">("json");
const [isExporting, setIsExporting] = useState(false);
const [exportCookieData, setExportCookieData] =
useState<CookieReadResult | null>(null);
const [isLoadingExportCookies, setIsLoadingExportCookies] = useState(false);
const [exportSelection, setExportSelection] = useState<SelectionState>({});
const [expandedDomains, setExpandedDomains] = useState<Set<string>>(
new Set(),
);
const [activeTab, setActiveTab] = useState<string>(initialTab);
const selectedExportCount = useMemo(() => {
let count = 0;
for (const domain of Object.keys(exportSelection)) {
const ds = exportSelection[domain];
if (ds.allSelected) {
const domainData = exportCookieData?.domains.find(
(d) => d.domain === domain,
);
count += domainData?.cookie_count || 0;
} else {
count += ds.cookies.size;
}
}
return count;
}, [exportSelection, exportCookieData]);
const loadExportCookies = useCallback(
async (profileId: string) => {
if (exportCookieData) return;
setIsLoadingExportCookies(true);
try {
const result = await invoke<CookieReadResult>("read_profile_cookies", {
profileId,
});
setExportCookieData(result);
setExportSelection(initSelectionFromCookieData(result));
} catch (err) {
toast.error(
`Failed to load cookies: ${err instanceof Error ? err.message : String(err)}`,
);
} finally {
setIsLoadingExportCookies(false);
}
},
[exportCookieData],
);
useEffect(() => {
if (activeTab === "export" && profile && !exportCookieData) {
void loadExportCookies(profile.id);
}
}, [activeTab, profile, exportCookieData, loadExportCookies]);
const resetImportState = useCallback(() => {
setFileContent(null);
setFileName(null);
setCookieCount(0);
setIsImporting(false);
setImportResult(null);
}, []);
const resetExportState = useCallback(() => {
setFormat("json");
setIsExporting(false);
setExportCookieData(null);
setExportSelection({});
setExpandedDomains(new Set());
}, []);
const handleClose = useCallback(() => {
resetImportState();
resetExportState();
setActiveTab(initialTab);
onClose();
}, [resetImportState, resetExportState, onClose, initialTab]);
const handleTabChange = useCallback(
(tab: string) => {
setActiveTab(tab);
resetImportState();
if (tab !== "export") {
resetExportState();
}
},
[resetImportState, resetExportState],
);
const handleFileRead = useCallback((file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result as string;
setFileContent(content);
setFileName(file.name);
setCookieCount(countCookies(content));
};
reader.onerror = () => {
toast.error("Failed to read file");
};
reader.readAsText(file);
}, []);
const handleImport = useCallback(async () => {
if (!fileContent || !profile) return;
setIsImporting(true);
try {
const result = await invoke<CookieImportResult>(
"import_cookies_from_file",
{
profileId: profile.id,
content: fileContent,
},
);
setImportResult(result);
} catch (error) {
toast.error(error instanceof Error ? error.message : String(error));
} finally {
setIsImporting(false);
}
}, [fileContent, profile]);
const getSelectedCookies = useCallback((): UnifiedCookie[] => {
if (!exportCookieData) return [];
const result: UnifiedCookie[] = [];
for (const domain of exportCookieData.domains) {
const ds = exportSelection[domain.domain];
if (!ds) continue;
if (ds.allSelected) {
result.push(...domain.cookies);
} else {
result.push(...domain.cookies.filter((c) => ds.cookies.has(c.name)));
}
}
return result;
}, [exportCookieData, exportSelection]);
const handleExport = useCallback(async () => {
if (!profile) return;
setIsExporting(true);
try {
const cookies = getSelectedCookies();
const content =
format === "json"
? formatJsonCookies(cookies)
: formatNetscapeCookies(cookies);
const ext = format === "json" ? "json" : "txt";
const defaultName = `${profile.name}_cookies.${ext}`;
const filePath = await save({
defaultPath: defaultName,
filters: [
{
name: format === "json" ? "JSON" : "Text",
extensions: [ext],
},
],
});
if (!filePath) {
setIsExporting(false);
return;
}
await writeTextFile(filePath, content);
toast.success("Cookies exported successfully");
handleClose();
} catch (error) {
toast.error(error instanceof Error ? error.message : String(error));
} finally {
setIsExporting(false);
}
}, [profile, format, getSelectedCookies, handleClose]);
const toggleDomain = useCallback(
(domain: string, cookies: UnifiedCookie[]) => {
setExportSelection((prev) => {
const current = prev[domain];
if (current?.allSelected) {
const next = { ...prev };
delete next[domain];
return next;
}
return {
...prev,
[domain]: {
allSelected: true,
cookies: new Set(cookies.map((c) => c.name)),
},
};
});
},
[],
);
const toggleCookie = useCallback(
(domain: string, cookieName: string, totalCookies: number) => {
setExportSelection((prev) => {
const current = prev[domain] || {
allSelected: false,
cookies: new Set<string>(),
};
const newCookies = new Set(current.cookies);
if (newCookies.has(cookieName)) {
newCookies.delete(cookieName);
} else {
newCookies.add(cookieName);
}
if (newCookies.size === 0) {
const next = { ...prev };
delete next[domain];
return next;
}
return {
...prev,
[domain]: {
allSelected: newCookies.size === totalCookies,
cookies: newCookies,
},
};
});
},
[],
);
const toggleExpand = useCallback((domain: string) => {
setExpandedDomains((prev) => {
const next = new Set(prev);
if (next.has(domain)) {
next.delete(domain);
} else {
next.add(domain);
}
return next;
});
}, []);
const toggleSelectAll = useCallback(() => {
if (!exportCookieData) return;
if (selectedExportCount === exportCookieData.total_count) {
setExportSelection({});
} else {
setExportSelection(initSelectionFromCookieData(exportCookieData));
}
}, [exportCookieData, selectedExportCount]);
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Cookie Management</DialogTitle>
</DialogHeader>
<Tabs
defaultValue={initialTab}
onValueChange={handleTabChange}
className="w-full"
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="import">Import</TabsTrigger>
<TabsTrigger value="export">Export</TabsTrigger>
</TabsList>
<TabsContent value="import" className="space-y-4 mt-4">
{!fileContent && (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Import cookies from a Netscape or JSON format file.
</p>
<div
role="button"
tabIndex={0}
className="flex flex-col items-center justify-center border-2 border-dashed rounded-lg p-8 transition-colors cursor-pointer border-muted-foreground/25 hover:border-muted-foreground/50"
onClick={() =>
document.getElementById("cookie-file-input")?.click()
}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
document.getElementById("cookie-file-input")?.click();
}
}}
>
<LuUpload className="w-10 h-10 text-muted-foreground mb-4" />
<p className="text-sm text-muted-foreground text-center">
Click to choose a cookie file
<br />
<span className="text-xs">(.txt, .cookies, or .json)</span>
</p>
<input
id="cookie-file-input"
type="file"
accept=".txt,.cookies,.json"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleFileRead(file);
e.target.value = "";
}}
/>
</div>
</div>
)}
{fileContent && !importResult && (
<div className="space-y-4">
<div className="flex items-center gap-3 p-4 bg-muted/30 rounded-lg">
<div>
<div className="font-medium">{fileName}</div>
<div className="text-sm text-muted-foreground">
{cookieCount} cookies found
</div>
</div>
</div>
<div className="flex justify-end gap-2">
<RippleButton variant="outline" onClick={resetImportState}>
Back
</RippleButton>
<LoadingButton
isLoading={isImporting}
onClick={() => void handleImport()}
disabled={cookieCount === 0}
>
Import
</LoadingButton>
</div>
</div>
)}
{importResult && (
<div className="space-y-4">
<div className="p-4 rounded-lg bg-green-500/10">
<div className="font-medium text-green-600 dark:text-green-400">
Successfully imported {importResult.cookies_imported}{" "}
cookies ({importResult.cookies_replaced} replaced)
</div>
{importResult.errors.length > 0 && (
<div className="mt-2 text-sm text-muted-foreground">
{importResult.errors.length} line(s) skipped
</div>
)}
</div>
<div className="flex justify-end">
<RippleButton onClick={handleClose}>Done</RippleButton>
</div>
</div>
)}
</TabsContent>
<TabsContent value="export" className="space-y-3 mt-4">
<div className="space-y-2">
<Label>Format</Label>
<Select
value={format}
onValueChange={(v) => setFormat(v as "netscape" | "json")}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="netscape">Netscape TXT</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>
Cookies{" "}
{exportCookieData && (
<span className="text-muted-foreground font-normal">
({selectedExportCount} of {exportCookieData.total_count}{" "}
selected)
</span>
)}
</Label>
{exportCookieData && exportCookieData.total_count > 0 && (
<button
type="button"
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
onClick={toggleSelectAll}
>
{selectedExportCount === exportCookieData.total_count
? "Deselect all"
: "Select all"}
</button>
)}
</div>
{isLoadingExportCookies ? (
<div className="flex items-center justify-center h-24">
<div className="animate-spin h-5 w-5 border-2 border-primary border-t-transparent rounded-full" />
</div>
) : !exportCookieData || exportCookieData.domains.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground border rounded-md">
No cookies found in this profile
</div>
) : (
<ScrollArea className="h-[200px] border rounded-md">
<div className="p-2 space-y-1">
{exportCookieData.domains.map((domain) => (
<ExportDomainRow
key={domain.domain}
domain={domain}
selection={exportSelection}
isExpanded={expandedDomains.has(domain.domain)}
onToggleDomain={toggleDomain}
onToggleCookie={toggleCookie}
onToggleExpand={toggleExpand}
/>
))}
</div>
</ScrollArea>
)}
</div>
<div className="flex justify-end gap-2">
<RippleButton variant="outline" onClick={handleClose}>
Cancel
</RippleButton>
<LoadingButton
isLoading={isExporting}
onClick={() => void handleExport()}
disabled={selectedExportCount === 0}
>
Export
</LoadingButton>
</div>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
);
}
interface ExportDomainRowProps {
domain: DomainCookies;
selection: SelectionState;
isExpanded: boolean;
onToggleDomain: (domain: string, cookies: UnifiedCookie[]) => void;
onToggleCookie: (
domain: string,
cookieName: string,
totalCookies: number,
) => void;
onToggleExpand: (domain: string) => void;
}
function ExportDomainRow({
domain,
selection,
isExpanded,
onToggleDomain,
onToggleCookie,
onToggleExpand,
}: ExportDomainRowProps) {
const domainSelection = selection[domain.domain];
const isAllSelected = domainSelection?.allSelected || false;
const selectedCount = domainSelection?.cookies.size || 0;
const isPartial =
selectedCount > 0 && selectedCount < domain.cookie_count && !isAllSelected;
return (
<div>
<div className="flex items-center gap-2 p-1.5 hover:bg-accent/50 rounded">
<Checkbox
checked={isAllSelected || isPartial}
onCheckedChange={() => onToggleDomain(domain.domain, domain.cookies)}
className={isPartial ? "opacity-70" : ""}
/>
<button
type="button"
className="flex items-center gap-1 flex-1 text-left text-sm bg-transparent border-none cursor-pointer"
onClick={() => onToggleExpand(domain.domain)}
>
{isExpanded ? (
<LuChevronDown className="w-3.5 h-3.5" />
) : (
<LuChevronRight className="w-3.5 h-3.5" />
)}
<span className="font-medium truncate">{domain.domain}</span>
<span className="text-xs text-muted-foreground shrink-0">
({domain.cookie_count})
</span>
</button>
</div>
{isExpanded && (
<div className="ml-7 pl-2 border-l space-y-0.5">
{domain.cookies.map((cookie) => {
const isSelected =
domainSelection?.cookies.has(cookie.name) || false;
return (
<div
key={`${domain.domain}-${cookie.name}`}
className="flex items-center gap-2 p-1 text-sm hover:bg-accent/30 rounded"
>
<Checkbox
checked={isSelected || isAllSelected}
onCheckedChange={() =>
onToggleCookie(
domain.domain,
cookie.name,
domain.cookie_count,
)
}
/>
<span className="truncate">{cookie.name}</span>
</div>
);
})}
</div>
)}
</div>
);
}
+60 -60
View File
@@ -2,8 +2,8 @@
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { GoPlus } from "react-icons/go";
import { LuLock } from "react-icons/lu";
import { LoadingButton } from "@/components/loading-button";
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
@@ -18,7 +18,6 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ProBadge } from "@/components/ui/pro-badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
@@ -31,7 +30,6 @@ import {
} from "@/components/ui/select";
import { Tabs, TabsContent } from "@/components/ui/tabs";
import { WayfernConfigForm } from "@/components/wayfern-config-form";
import { useBrowserDownload } from "@/hooks/use-browser-download";
import { useProxyEvents } from "@/hooks/use-proxy-events";
import { useVpnEvents } from "@/hooks/use-vpn-events";
@@ -117,6 +115,7 @@ export function CreateProfileDialog({
selectedGroupId,
crossOsUnlocked = false,
}: CreateProfileDialogProps) {
const { t } = useTranslation();
const [profileName, setProfileName] = useState("");
const [currentStep, setCurrentStep] = useState<
"browser-selection" | "browser-config"
@@ -180,6 +179,7 @@ export function CreateProfileDialog({
downloadBrowser,
loadDownloadedVersions,
isVersionDownloaded,
downloadedVersions,
} = useBrowserDownload();
const loadSupportedBrowsers = useCallback(async () => {
@@ -338,6 +338,26 @@ export function CreateProfileDialog({
[releaseTypes],
);
const getCreatableVersion = useCallback(
(browserType?: string) => {
const bestVersion = getBestAvailableVersion(browserType);
if (bestVersion && isVersionDownloaded(bestVersion.version)) {
return bestVersion;
}
if (downloadedVersions.length > 0) {
const fallbackVersion = downloadedVersions[0];
const releaseType =
browserType === "firefox-developer" ? "nightly" : "stable";
return {
version: fallbackVersion,
releaseType: releaseType as "stable" | "nightly",
};
}
return null;
},
[getBestAvailableVersion, isVersionDownloaded, downloadedVersions],
);
const handleDownload = async (browserStr: string) => {
const bestVersion = getBestAvailableVersion(browserStr);
@@ -366,7 +386,7 @@ export function CreateProfileDialog({
if (activeTab === "anti-detect") {
// Anti-detect browser - check if Wayfern or Camoufox is selected
if (selectedBrowser === "wayfern") {
const bestWayfernVersion = getBestAvailableVersion("wayfern");
const bestWayfernVersion = getCreatableVersion("wayfern");
if (!bestWayfernVersion) {
console.error("No Wayfern version available");
return;
@@ -389,7 +409,7 @@ export function CreateProfileDialog({
});
} else {
// Default to Camoufox
const bestCamoufoxVersion = getBestAvailableVersion("camoufox");
const bestCamoufoxVersion = getCreatableVersion("camoufox");
if (!bestCamoufoxVersion) {
console.error("No Camoufox version available");
return;
@@ -420,7 +440,7 @@ export function CreateProfileDialog({
}
// Use the best available version (stable preferred, nightly as fallback)
const bestVersion = getBestAvailableVersion(selectedBrowser);
const bestVersion = getCreatableVersion(selectedBrowser);
if (!bestVersion) {
console.error("No version available");
return;
@@ -497,14 +517,14 @@ export function CreateProfileDialog({
if (!profileName.trim()) return true;
if (!selectedBrowser) return true;
if (isBrowserCurrentlyDownloading(selectedBrowser)) return true;
if (!isBrowserVersionAvailable(selectedBrowser)) return true;
if (!getCreatableVersion(selectedBrowser)) return true;
return false;
}, [
profileName,
selectedBrowser,
isBrowserCurrentlyDownloading,
isBrowserVersionAvailable,
getCreatableVersion,
]);
// Filter supported browsers for regular browsers
@@ -666,26 +686,26 @@ export function CreateProfileDialog({
/>
</div>
{/* Ephemeral Toggle */}
<div className="flex items-center gap-2">
<Checkbox
id="ephemeral"
checked={ephemeral}
onCheckedChange={(checked) =>
setEphemeral(checked === true)
}
/>
<div className="flex flex-col">
<Label
htmlFor="ephemeral"
className="cursor-pointer"
>
Ephemeral
{/* Ephemeral Option */}
<div className="space-y-3 p-4 border rounded-lg bg-muted/30">
<div className="flex items-center space-x-2">
<Checkbox
id="ephemeral"
checked={ephemeral}
onCheckedChange={(checked) =>
setEphemeral(checked === true)
}
/>
<Label htmlFor="ephemeral" className="font-medium">
{t("profiles.ephemeral")}
</Label>
<span className="text-xs text-muted-foreground">
Browser data is deleted when closed
<span className="px-1 py-0.5 text-[10px] leading-none rounded bg-muted text-muted-foreground font-medium">
{t("profiles.ephemeralAlpha")}
</span>
</div>
<p className="text-sm text-muted-foreground ml-6">
{t("profiles.ephemeralDescription")}
</p>
</div>
{selectedBrowser === "wayfern" ? (
@@ -778,23 +798,13 @@ export function CreateProfileDialog({
</div>
)}
<div className="relative">
<WayfernConfigForm
config={wayfernConfig}
onConfigChange={updateWayfernConfig}
isCreating
crossOsUnlocked={crossOsUnlocked}
/>
{!crossOsUnlocked && (
<div className="absolute inset-0 backdrop-blur-sm bg-background/60 z-10 flex flex-col items-center justify-center gap-2">
<LuLock className="w-6 h-6 text-muted-foreground" />
<p className="text-sm text-muted-foreground font-medium">
Fingerprint editing is a Pro feature
</p>
<ProBadge />
</div>
)}
</div>
<WayfernConfigForm
config={wayfernConfig}
onConfigChange={updateWayfernConfig}
isCreating
crossOsUnlocked={crossOsUnlocked}
limitedMode={!crossOsUnlocked}
/>
</div>
) : selectedBrowser === "camoufox" ? (
// Camoufox Configuration
@@ -886,24 +896,14 @@ export function CreateProfileDialog({
</div>
)}
<div className="relative">
<SharedCamoufoxConfigForm
config={camoufoxConfig}
onConfigChange={updateCamoufoxConfig}
isCreating
browserType="camoufox"
crossOsUnlocked={crossOsUnlocked}
/>
{!crossOsUnlocked && (
<div className="absolute inset-0 backdrop-blur-sm bg-background/60 z-10 flex flex-col items-center justify-center gap-2">
<LuLock className="w-6 h-6 text-muted-foreground" />
<p className="text-sm text-muted-foreground font-medium">
Fingerprint editing is a Pro feature
</p>
<ProBadge />
</div>
)}
</div>
<SharedCamoufoxConfigForm
config={camoufoxConfig}
onConfigChange={updateCamoufoxConfig}
isCreating
browserType="camoufox"
crossOsUnlocked={crossOsUnlocked}
limitedMode={!crossOsUnlocked}
/>
</div>
) : (
// Regular Browser Configuration (should not happen in anti-detect tab)
+48 -116
View File
@@ -13,6 +13,7 @@ import { invoke } from "@tauri-apps/api/core";
import { emit, listen } from "@tauri-apps/api/event";
import type { Dispatch, SetStateAction } from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { FaApple, FaLinux, FaWindows } from "react-icons/fa";
import { FiWifi } from "react-icons/fi";
import { IoEllipsisHorizontal } from "react-icons/io5";
@@ -68,9 +69,9 @@ import { useTableSorting } from "@/hooks/use-table-sorting";
import { useVpnEvents } from "@/hooks/use-vpn-events";
import {
getBrowserDisplayName,
getBrowserIcon,
getCurrentOS,
getOSDisplayName,
getProfileIcon,
isCrossOsProfile,
} from "@/lib/browser-utils";
import { formatRelativeTime } from "@/lib/flag-utils";
@@ -99,6 +100,7 @@ import { RippleButton } from "./ui/ripple";
// Stable table meta type to pass volatile state/handlers into TanStack Table without
// causing column definitions to be recreated on every render.
type TableMeta = {
t: (key: string, options?: Record<string, unknown>) => string;
selectedProfiles: string[];
selectableCount: number;
showCheckboxes: boolean;
@@ -176,8 +178,7 @@ type TableMeta = {
onConfigureCamoufox?: (profile: BrowserProfile) => void;
onCloneProfile?: (profile: BrowserProfile) => void;
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
onImportCookies?: (profile: BrowserProfile) => void;
onExportCookies?: (profile: BrowserProfile) => void;
onOpenCookieManagement?: (profile: BrowserProfile) => void;
// Traffic snapshots (lightweight real-time data)
trafficSnapshots: Record<string, TrafficSnapshot>;
@@ -213,7 +214,11 @@ function getProfileSyncStatusDot(
| undefined,
errorMessage?: string,
): SyncStatusDot | null {
const status = liveStatus ?? (profile.sync_enabled ? "synced" : "disabled");
const status =
liveStatus ??
(profile.sync_mode && profile.sync_mode !== "Disabled"
? "synced"
: "disabled");
switch (status) {
case "syncing":
@@ -758,8 +763,7 @@ interface ProfilesDataTableProps {
onRenameProfile: (profileId: string, newName: string) => Promise<void>;
onConfigureCamoufox: (profile: BrowserProfile) => void;
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
onImportCookies?: (profile: BrowserProfile) => void;
onExportCookies?: (profile: BrowserProfile) => void;
onOpenCookieManagement?: (profile: BrowserProfile) => void;
runningProfiles: Set<string>;
isUpdating: (browser: string) => boolean;
onDeleteSelectedProfiles: (profileIds: string[]) => Promise<void>;
@@ -786,8 +790,7 @@ export function ProfilesDataTable({
onRenameProfile,
onConfigureCamoufox,
onCopyCookiesToProfile,
onImportCookies,
onExportCookies,
onOpenCookieManagement,
runningProfiles,
isUpdating,
onAssignProfilesToGroup,
@@ -802,6 +805,7 @@ export function ProfilesDataTable({
crossOsUnlocked = false,
syncUnlocked = false,
}: ProfilesDataTableProps) {
const { t } = useTranslation();
const { getTableSorting, updateSorting, isLoaded } = useTableSorting();
const [sorting, setSorting] = React.useState<SortingState>([]);
@@ -1201,9 +1205,8 @@ export function ProfilesDataTable({
browserState.isClient && runningProfiles.has(profile.id);
const isLaunching = launchingProfiles.has(profile.id);
const isStopping = stoppingProfiles.has(profile.id);
const isBrowserUpdating = isUpdating(profile.browser);
if (isRunning || isLaunching || isStopping || isBrowserUpdating) {
if (isRunning || isLaunching || isStopping) {
newSet.delete(profileId);
hasChanges = true;
}
@@ -1218,7 +1221,6 @@ export function ProfilesDataTable({
runningProfiles,
launchingProfiles,
stoppingProfiles,
isUpdating,
browserState.isClient,
onSelectedProfilesChange,
selectedProfiles,
@@ -1364,13 +1366,7 @@ export function ProfilesDataTable({
browserState.isClient && runningProfiles.has(profile.id);
const isLaunching = launchingProfiles.has(profile.id);
const isStopping = stoppingProfiles.has(profile.id);
const isBrowserUpdating = isUpdating(profile.browser);
return (
!isRunning &&
!isLaunching &&
!isStopping &&
!isBrowserUpdating
);
return !isRunning && !isLaunching && !isStopping;
})
.map((profile) => profile.id),
)
@@ -1386,7 +1382,6 @@ export function ProfilesDataTable({
runningProfiles,
launchingProfiles,
stoppingProfiles,
isUpdating,
],
);
@@ -1397,8 +1392,7 @@ export function ProfilesDataTable({
browserState.isClient && runningProfiles.has(profile.id);
const isLaunching = launchingProfiles.has(profile.id);
const isStopping = stoppingProfiles.has(profile.id);
const isBrowserUpdating = isUpdating(profile.browser);
return !isRunning && !isLaunching && !isStopping && !isBrowserUpdating;
return !isRunning && !isLaunching && !isStopping;
});
}, [
profiles,
@@ -1406,12 +1400,12 @@ export function ProfilesDataTable({
runningProfiles,
launchingProfiles,
stoppingProfiles,
isUpdating,
]);
// Build table meta from volatile state so columns can stay stable
const tableMeta = React.useMemo<TableMeta>(
() => ({
t,
selectedProfiles,
selectableCount: selectableProfiles.length,
showCheckboxes,
@@ -1477,8 +1471,7 @@ export function ProfilesDataTable({
onCloneProfile,
onConfigureCamoufox,
onCopyCookiesToProfile,
onImportCookies,
onExportCookies,
onOpenCookieManagement,
// Traffic snapshots (lightweight real-time data)
trafficSnapshots,
@@ -1501,6 +1494,7 @@ export function ProfilesDataTable({
handleCreateCountryProxy,
}),
[
t,
selectedProfiles,
selectableProfiles.length,
showCheckboxes,
@@ -1540,8 +1534,7 @@ export function ProfilesDataTable({
onCloneProfile,
onConfigureCamoufox,
onCopyCookiesToProfile,
onImportCookies,
onExportCookies,
onOpenCookieManagement,
syncStatuses,
onOpenProfileSyncDialog,
onToggleProfileSync,
@@ -1578,7 +1571,7 @@ export function ProfilesDataTable({
const meta = table.options.meta as TableMeta;
const profile = row.original;
const browser = profile.browser;
const IconComponent = getBrowserIcon(browser);
const IconComponent = getProfileIcon(profile);
const isCrossOs = isCrossOsProfile(profile);
const isSelected = meta.isProfileSelected(profile.id);
@@ -1586,9 +1579,7 @@ export function ProfilesDataTable({
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isBrowserUpdating = meta.isUpdating(browser);
const isDisabled =
isRunning || isLaunching || isStopping || isBrowserUpdating;
const isDisabled = isRunning || isLaunching || isStopping;
// Cross-OS profiles: show OS icon when checkboxes aren't visible, show checkbox when they are
if (isCrossOs && !meta.showCheckboxes && !isSelected) {
@@ -1907,13 +1898,8 @@ export function ProfilesDataTable({
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isBrowserUpdating = meta.isUpdating(profile.browser);
const isDisabled =
isRunning ||
isLaunching ||
isStopping ||
isBrowserUpdating ||
isCrossOs;
isRunning || isLaunching || isStopping || isCrossOs;
return (
<button
@@ -1940,14 +1926,7 @@ export function ProfilesDataTable({
}
}}
>
<span className="flex items-center gap-1">
{display}
{profile.ephemeral && (
<span className="px-1 py-0.5 text-[10px] leading-none rounded bg-muted text-muted-foreground font-medium">
Ephemeral
</span>
)}
</span>
{display}
</button>
);
},
@@ -1963,13 +1942,8 @@ export function ProfilesDataTable({
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isBrowserUpdating = meta.isUpdating(profile.browser);
const isDisabled =
isRunning ||
isLaunching ||
isStopping ||
isBrowserUpdating ||
isCrossOs;
isRunning || isLaunching || isStopping || isCrossOs;
return (
<TagsCell
@@ -1996,13 +1970,8 @@ export function ProfilesDataTable({
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isBrowserUpdating = meta.isUpdating(profile.browser);
const isDisabled =
isRunning ||
isLaunching ||
isStopping ||
isBrowserUpdating ||
isCrossOs;
isRunning || isLaunching || isStopping || isCrossOs;
return (
<NoteCell
@@ -2027,13 +1996,8 @@ export function ProfilesDataTable({
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isBrowserUpdating = meta.isUpdating(profile.browser);
const isDisabled =
isRunning ||
isLaunching ||
isStopping ||
isBrowserUpdating ||
isCrossOs;
isRunning || isLaunching || isStopping || isCrossOs;
const hasProxyOverride = Object.hasOwn(
meta.proxyOverrides,
@@ -2331,18 +2295,11 @@ export function ProfilesDataTable({
const isCrossOs = isCrossOsProfile(profile);
const isRunning =
meta.isClient && meta.runningProfiles.has(profile.id);
const isBrowserUpdating =
meta.isClient && meta.isUpdating(profile.browser);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isDisabled =
isRunning ||
isLaunching ||
isStopping ||
isBrowserUpdating ||
isCrossOs;
const isDeleteDisabled =
isRunning || isLaunching || isStopping || isBrowserUpdating;
isRunning || isLaunching || isStopping || isCrossOs;
const isDeleteDisabled = isRunning || isLaunching || isStopping;
return (
<div className="flex justify-end items-center">
@@ -2364,28 +2321,25 @@ export function ProfilesDataTable({
}}
disabled={isCrossOs}
>
View Network
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
if (meta.syncUnlocked) {
meta.onToggleProfileSync?.(profile);
}
}}
disabled={!meta.syncUnlocked || isCrossOs}
>
<span className="flex items-center gap-2">
{profile.sync_enabled ? "Disable Sync" : "Enable Sync"}
{!meta.syncUnlocked && <ProBadge />}
</span>
{meta.t("profiles.actions.viewNetwork")}
</DropdownMenuItem>
{!profile.ephemeral && (
<DropdownMenuItem
onClick={() => {
meta.onOpenProfileSyncDialog?.(profile);
}}
disabled={isCrossOs}
>
{meta.t("profiles.actions.syncSettings")}
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
meta.onAssignProfilesToGroup?.([profile.id]);
}}
disabled={isDisabled}
>
Assign to Group
{meta.t("profiles.actions.assignToGroup")}
</DropdownMenuItem>
{(profile.browser === "camoufox" ||
profile.browser === "wayfern") &&
@@ -2396,10 +2350,7 @@ export function ProfilesDataTable({
}}
disabled={isDisabled}
>
<span className="flex items-center gap-2">
Change Fingerprint
{!meta.crossOsUnlocked && <ProBadge />}
</span>
{meta.t("profiles.actions.changeFingerprint")}
</DropdownMenuItem>
)}
{(profile.browser === "camoufox" ||
@@ -2415,7 +2366,7 @@ export function ProfilesDataTable({
disabled={isDisabled || !meta.crossOsUnlocked}
>
<span className="flex items-center gap-2">
Copy Cookies to Profile
{meta.t("profiles.actions.copyCookiesToProfile")}
{!meta.crossOsUnlocked && <ProBadge />}
</span>
</DropdownMenuItem>
@@ -2423,35 +2374,17 @@ export function ProfilesDataTable({
{(profile.browser === "camoufox" ||
profile.browser === "wayfern") &&
!profile.ephemeral &&
meta.onImportCookies && (
meta.onOpenCookieManagement && (
<DropdownMenuItem
onClick={() => {
if (meta.crossOsUnlocked) {
meta.onImportCookies?.(profile);
meta.onOpenCookieManagement?.(profile);
}
}}
disabled={isDisabled || !meta.crossOsUnlocked}
>
<span className="flex items-center gap-2">
Import Cookies
{!meta.crossOsUnlocked && <ProBadge />}
</span>
</DropdownMenuItem>
)}
{(profile.browser === "camoufox" ||
profile.browser === "wayfern") &&
!profile.ephemeral &&
meta.onExportCookies && (
<DropdownMenuItem
onClick={() => {
if (meta.crossOsUnlocked) {
meta.onExportCookies?.(profile);
}
}}
disabled={isDisabled || !meta.crossOsUnlocked}
>
<span className="flex items-center gap-2">
Export Cookies
{meta.t("cookies.management.menuItem")}
{!meta.crossOsUnlocked && <ProBadge />}
</span>
</DropdownMenuItem>
@@ -2463,7 +2396,7 @@ export function ProfilesDataTable({
}}
disabled={isDisabled}
>
Clone Profile
{meta.t("profiles.actions.clone")}
</DropdownMenuItem>
)}
<DropdownMenuItem
@@ -2472,7 +2405,7 @@ export function ProfilesDataTable({
}}
disabled={isDeleteDisabled}
>
Delete
{meta.t("profiles.actions.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -2499,8 +2432,7 @@ export function ProfilesDataTable({
browserState.isClient && runningProfiles.has(profile.id);
const isLaunching = launchingProfiles.has(profile.id);
const isStopping = stoppingProfiles.has(profile.id);
const isBrowserUpdating = isUpdating(profile.browser);
return !isRunning && !isLaunching && !isStopping && !isBrowserUpdating;
return !isRunning && !isLaunching && !isStopping;
},
getSortedRowModel: getSortedRowModel(),
getCoreRowModel: getCoreRowModel(),
+127 -58
View File
@@ -2,10 +2,10 @@
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { LoadingButton } from "@/components/loading-button";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
@@ -15,8 +15,10 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import type { BrowserProfile, SyncSettings } from "@/types";
import type { BrowserProfile, SyncMode, SyncSettings } from "@/types";
import { isSyncEnabled } from "@/types";
interface ProfileSyncDialogProps {
isOpen: boolean;
@@ -31,12 +33,14 @@ export function ProfileSyncDialog({
profile,
onSyncConfigOpen,
}: ProfileSyncDialogProps) {
const { t } = useTranslation();
const [isSaving, setIsSaving] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
const [syncEnabled, setSyncEnabled] = useState(
profile?.sync_enabled ?? false,
const [syncMode, setSyncMode] = useState<SyncMode>(
profile?.sync_mode ?? "Disabled",
);
const [hasConfig, setHasConfig] = useState(false);
const [hasE2ePassword, setHasE2ePassword] = useState(false);
const [isCheckingConfig, setIsCheckingConfig] = useState(false);
const checkSyncConfig = useCallback(async () => {
@@ -44,6 +48,8 @@ export function ProfileSyncDialog({
try {
const settings = await invoke<SyncSettings>("get_sync_settings");
setHasConfig(Boolean(settings.sync_server_url && settings.sync_token));
const hasPassword = await invoke<boolean>("check_has_e2e_password");
setHasE2ePassword(hasPassword);
} catch {
setHasConfig(false);
} finally {
@@ -54,7 +60,7 @@ export function ProfileSyncDialog({
const handleOpenChange = useCallback(
(open: boolean) => {
if (open && profile) {
setSyncEnabled(profile.sync_enabled ?? false);
setSyncMode(profile.sync_mode ?? "Disabled");
void checkSyncConfig();
}
if (!open) {
@@ -64,39 +70,49 @@ export function ProfileSyncDialog({
[profile, onClose, checkSyncConfig],
);
const handleToggleSync = useCallback(async () => {
if (!profile) return;
const handleModeChange = useCallback(
async (newMode: string) => {
if (!profile) return;
if (!hasConfig) {
showErrorToast("Please configure sync service first");
onSyncConfigOpen();
onClose();
return;
}
if (!hasConfig) {
showErrorToast(t("sync.mode.noPasswordWarning"));
onSyncConfigOpen();
onClose();
return;
}
setIsSaving(true);
try {
await invoke("set_profile_sync_enabled", {
profileId: profile.id,
enabled: !syncEnabled,
});
setSyncEnabled(!syncEnabled);
showSuccessToast(
!syncEnabled ? "Sync enabled - syncing now..." : "Sync disabled",
);
} catch (error) {
console.error("Failed to toggle sync:", error);
showErrorToast("Failed to update sync settings");
} finally {
setIsSaving(false);
}
}, [profile, syncEnabled, hasConfig, onSyncConfigOpen, onClose]);
if (newMode === "Encrypted" && !hasE2ePassword) {
showErrorToast(t("sync.mode.passwordRequired"));
return;
}
setIsSaving(true);
try {
await invoke("set_profile_sync_mode", {
profileId: profile.id,
syncMode: newMode,
});
setSyncMode(newMode as SyncMode);
showSuccessToast(
newMode !== "Disabled"
? t("sync.mode.enabledToast")
: t("sync.mode.disabledToast"),
);
} catch (error) {
console.error("Failed to set sync mode:", error);
showErrorToast(String(error));
} finally {
setIsSaving(false);
}
},
[profile, hasConfig, hasE2ePassword, onSyncConfigOpen, onClose, t],
);
const handleSyncNow = useCallback(async () => {
if (!profile) return;
if (!hasConfig) {
showErrorToast("Please configure sync service first");
showErrorToast(t("sync.mode.noPasswordWarning"));
onSyncConfigOpen();
onClose();
return;
@@ -105,17 +121,17 @@ export function ProfileSyncDialog({
setIsSyncing(true);
try {
await invoke("request_profile_sync", { profileId: profile.id });
showSuccessToast("Sync queued");
showSuccessToast(t("sync.mode.syncQueued"));
} catch (error) {
console.error("Failed to queue sync:", error);
showErrorToast("Failed to queue sync");
showErrorToast(String(error));
} finally {
setIsSyncing(false);
}
}, [profile, hasConfig, onSyncConfigOpen, onClose]);
}, [profile, hasConfig, onSyncConfigOpen, onClose, t]);
const formatLastSync = (timestamp?: number) => {
if (!timestamp) return "Never";
if (!timestamp) return t("common.labels.never", "Never");
const date = new Date(timestamp * 1000);
return date.toLocaleString();
};
@@ -126,9 +142,12 @@ export function ProfileSyncDialog({
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Profile Sync</DialogTitle>
<DialogTitle>{t("sync.mode.title", "Profile Sync")}</DialogTitle>
<DialogDescription>
Manage sync settings for &quot;{profile.name}&quot;
{t("sync.mode.description", {
name: profile.name,
defaultValue: `Manage sync settings for "${profile.name}"`,
})}
</DialogDescription>
</DialogHeader>
@@ -140,7 +159,9 @@ export function ProfileSyncDialog({
<div className="grid gap-4 py-4">
{!hasConfig && (
<div className="p-3 text-sm rounded-md bg-muted">
<p className="mb-2">Sync service not configured.</p>
<p className="mb-2">
{t("sync.mode.notConfigured", "Sync service not configured.")}
</p>
<Button
variant="outline"
size="sm"
@@ -149,39 +170,87 @@ export function ProfileSyncDialog({
onClose();
}}
>
Configure Sync Service
{t("sync.mode.configureService", "Configure Sync Service")}
</Button>
</div>
)}
{hasConfig && (
<>
<div className="flex justify-between items-center">
<div className="space-y-0.5">
<Label htmlFor="sync-enabled">Sync Enabled</Label>
<p className="text-sm text-muted-foreground">
Sync this profile across devices
</p>
<RadioGroup
value={syncMode}
onValueChange={handleModeChange}
disabled={isSaving}
className="grid gap-3"
>
<div className="flex items-start space-x-3">
<RadioGroupItem value="Disabled" id="sync-disabled" />
<Label htmlFor="sync-disabled" className="cursor-pointer">
<span className="font-medium">
{t("sync.mode.disabled", "Disabled")}
</span>
<p className="text-sm text-muted-foreground">
{t(
"sync.mode.disabledDescription",
"No sync for this profile",
)}
</p>
</Label>
</div>
<Checkbox
id="sync-enabled"
checked={syncEnabled}
onCheckedChange={handleToggleSync}
disabled={isSaving}
/>
</div>
<div className="flex items-start space-x-3">
<RadioGroupItem value="Regular" id="sync-regular" />
<Label htmlFor="sync-regular" className="cursor-pointer">
<span className="font-medium">
{t("sync.mode.regular", "Regular Sync")}
</span>
<p className="text-sm text-muted-foreground">
{t(
"sync.mode.regularDescription",
"Fast sync, unencrypted",
)}
</p>
</Label>
</div>
<div className="flex items-start space-x-3">
<RadioGroupItem value="Encrypted" id="sync-encrypted" />
<Label htmlFor="sync-encrypted" className="cursor-pointer">
<span className="font-medium">
{t("sync.mode.encrypted", "E2E Encrypted Sync")}
</span>
<p className="text-sm text-muted-foreground">
{t(
"sync.mode.encryptedDescription",
"Encrypted before upload. Server never sees plaintext data.",
)}
</p>
</Label>
</div>
</RadioGroup>
{syncMode === "Encrypted" && !hasE2ePassword && (
<div className="p-3 text-sm rounded-md bg-destructive/10 text-destructive">
{t(
"sync.mode.noPasswordWarning",
"E2E password not set. Please set a password in Settings.",
)}
</div>
)}
<div className="space-y-2">
<Label>Last Synced</Label>
<Label>{t("sync.mode.lastSynced", "Last Synced")}</Label>
<div className="flex gap-2 items-center">
<Badge variant="outline">
{formatLastSync(profile.last_sync)}
</Badge>
{syncEnabled && (
{isSyncEnabled(profile) && (
<Badge
variant={profile.last_sync ? "default" : "secondary"}
>
{profile.last_sync ? "Synced" : "Pending"}
{profile.last_sync
? t("common.status.synced")
: t("common.status.pending")}
</Badge>
)}
</div>
@@ -193,11 +262,11 @@ export function ProfileSyncDialog({
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Close
{t("common.buttons.close")}
</Button>
{hasConfig && syncEnabled && (
{hasConfig && isSyncEnabled(profile) && (
<LoadingButton onClick={handleSyncNow} isLoading={isSyncing}>
Sync Now
{t("sync.mode.syncNow", "Sync Now")}
</LoadingButton>
)}
</DialogFooter>
+153
View File
@@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next";
import { BsCamera, BsMic } from "react-icons/bs";
import { LoadingButton } from "@/components/loading-button";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
ColorPicker,
ColorPickerAlpha,
@@ -24,6 +25,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
@@ -113,6 +115,11 @@ export function SettingsDialog({
const [requestingPermission, setRequestingPermission] =
useState<PermissionType | null>(null);
const [isMacOS, setIsMacOS] = useState(false);
const [hasE2ePassword, setHasE2ePassword] = useState(false);
const [e2ePassword, setE2ePassword] = useState("");
const [e2ePasswordConfirm, setE2ePasswordConfirm] = useState("");
const [e2eError, setE2eError] = useState("");
const [isSavingE2e, setIsSavingE2e] = useState(false);
const { t } = useTranslation();
const { setTheme } = useTheme();
@@ -202,6 +209,13 @@ export function SettingsDialog({
colors: tokyoNightTheme.colors,
});
}
// Check E2E password status
try {
const hasPassword = await invoke<boolean>("check_has_e2e_password");
setHasE2ePassword(hasPassword);
} catch {
setHasE2ePassword(false);
}
} catch (error) {
console.error("Failed to load settings:", error);
} finally {
@@ -827,6 +841,145 @@ export function SettingsDialog({
</RippleButton>
</div>
{/* Sync Encryption Section */}
<div className="space-y-4">
<Label className="text-base font-medium">
{t("settings.encryption.title", "Sync Encryption")}
</Label>
<p className="text-xs text-muted-foreground">
{t(
"settings.encryption.description",
"Set a password to enable E2E encrypted sync. If you lose this password, encrypted profiles cannot be recovered.",
)}
</p>
{hasE2ePassword ? (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Badge variant="default">
{t("settings.encryption.passwordSet", "Active")}
</Badge>
<span className="text-sm text-muted-foreground">
{t(
"settings.encryption.passwordSetDescription",
"E2E encryption password is set",
)}
</span>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setHasE2ePassword(false);
setE2ePassword("");
setE2ePasswordConfirm("");
setE2eError("");
}}
>
{t("settings.encryption.changePassword", "Change Password")}
</Button>
<Button
variant="destructive"
size="sm"
onClick={async () => {
try {
await invoke("delete_e2e_password");
setHasE2ePassword(false);
showSuccessToast(
t(
"settings.encryption.removed",
"Encryption password removed",
),
);
} catch (error) {
showErrorToast(String(error));
}
}}
>
{t("settings.encryption.removePassword", "Remove Password")}
</Button>
</div>
</div>
) : (
<div className="space-y-3">
<Input
type="password"
placeholder={t(
"settings.encryption.passwordPlaceholder",
"Password (min 8 characters)",
)}
value={e2ePassword}
onChange={(e) => {
setE2ePassword(e.target.value);
setE2eError("");
}}
/>
<Input
type="password"
placeholder={t(
"settings.encryption.confirmPlaceholder",
"Confirm password",
)}
value={e2ePasswordConfirm}
onChange={(e) => {
setE2ePasswordConfirm(e.target.value);
setE2eError("");
}}
/>
{e2eError && (
<p className="text-sm text-destructive">{e2eError}</p>
)}
<LoadingButton
variant="default"
size="sm"
isLoading={isSavingE2e}
onClick={async () => {
if (e2ePassword.length < 8) {
setE2eError(
t(
"settings.encryption.passwordTooShort",
"Password must be at least 8 characters",
),
);
return;
}
if (e2ePassword !== e2ePasswordConfirm) {
setE2eError(
t(
"settings.encryption.passwordMismatch",
"Passwords do not match",
),
);
return;
}
setIsSavingE2e(true);
try {
await invoke("set_e2e_password", {
password: e2ePassword,
});
setHasE2ePassword(true);
setE2ePassword("");
setE2ePasswordConfirm("");
showSuccessToast(
t(
"settings.encryption.passwordSaved",
"Encryption password set",
),
);
} catch (error) {
showErrorToast(String(error));
} finally {
setIsSavingE2e(false);
}
}}
>
{t("settings.encryption.setPassword", "Set Password")}
</LoadingButton>
</div>
)}
</div>
{/* Commercial License Section */}
<div className="space-y-4">
<Label className="text-base font-medium">Commercial License</Label>
File diff suppressed because it is too large Load Diff
+50 -5
View File
@@ -60,7 +60,21 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
const [activeTab, setActiveTab] = useState<string>("cloud");
const isConnected = Boolean(serverUrl && token);
const [connectionStatus, setConnectionStatus] = useState<
"unknown" | "testing" | "connected" | "error"
>("unknown");
const hasConfig = Boolean(serverUrl && token);
const testConnection = useCallback(async (url: string) => {
setConnectionStatus("testing");
try {
const healthUrl = `${url.replace(/\/$/, "")}/health`;
const response = await fetch(healthUrl);
setConnectionStatus(response.ok ? "connected" : "error");
} catch {
setConnectionStatus("error");
}
}, []);
const loadSettings = useCallback(async () => {
setIsLoading(true);
@@ -68,15 +82,19 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
const settings = await invoke<SyncSettings>("get_sync_settings");
setServerUrl(settings.sync_server_url || "");
setToken(settings.sync_token || "");
if (settings.sync_server_url && settings.sync_token) {
void testConnection(settings.sync_server_url);
}
} catch (error) {
console.error("Failed to load sync settings:", error);
} finally {
setIsLoading(false);
}
}, []);
}, [testConnection]);
useEffect(() => {
if (isOpen) {
setConnectionStatus("unknown");
void loadSettings();
setCodeSent(false);
setOtpCode("");
@@ -103,15 +121,19 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
}
setIsTesting(true);
setConnectionStatus("testing");
try {
const healthUrl = `${serverUrl.replace(/\/$/, "")}/health`;
const response = await fetch(healthUrl);
if (response.ok) {
setConnectionStatus("connected");
showSuccessToast("Connection successful!");
} else {
setConnectionStatus("error");
showErrorToast("Server responded with an error");
}
} catch {
setConnectionStatus("error");
showErrorToast("Failed to connect to server");
} finally {
setIsTesting(false);
@@ -125,6 +147,11 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
syncServerUrl: serverUrl || null,
syncToken: token || null,
});
try {
await invoke("restart_sync_service");
} catch (e) {
console.error("Failed to restart sync service:", e);
}
showSuccessToast("Sync settings saved");
onClose();
} catch (error) {
@@ -142,8 +169,14 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
syncServerUrl: null,
syncToken: null,
});
try {
await invoke("restart_sync_service");
} catch (e) {
console.error("Failed to restart sync service:", e);
}
setServerUrl("");
setToken("");
setConnectionStatus("unknown");
showSuccessToast("Sync disconnected");
} catch (error) {
console.error("Failed to disconnect:", error);
@@ -209,7 +242,7 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
}, [logout, t]);
// Determine which tabs are available
const cloudBlocked = !isLoggedIn && isConnected;
const cloudBlocked = !isLoggedIn && hasConfig;
const selfHostedBlocked = isLoggedIn;
return (
@@ -427,17 +460,29 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
</div>
</div>
{isConnected && (
{connectionStatus === "testing" && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="w-4 h-4 rounded-full border-2 border-current animate-spin border-t-transparent" />
{t("sync.status.syncing")}
</div>
)}
{connectionStatus === "connected" && (
<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>
)}
{connectionStatus === "error" && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="w-2 h-2 rounded-full bg-red-500" />
{t("sync.status.disconnected")}
</div>
)}
</div>
)}
<DialogFooter className="flex gap-2">
{isConnected && (
{hasConfig && (
<Button
variant="outline"
onClick={handleDisconnect}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
interface WindowResizeWarningDialogProps {
isOpen: boolean;
onResult: (proceed: boolean) => void;
}
export function WindowResizeWarningDialog({
isOpen,
onResult,
}: WindowResizeWarningDialogProps) {
const { t } = useTranslation();
const [dontShowAgain, setDontShowAgain] = useState(false);
const handleContinue = async () => {
if (dontShowAgain) {
try {
await invoke("dismiss_window_resize_warning");
} catch (error) {
console.error("Failed to dismiss window resize warning:", error);
}
}
onResult(true);
};
const handleCancel = () => {
onResult(false);
};
return (
<Dialog open={isOpen}>
<DialogContent
className="sm:max-w-sm"
onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>{t("warnings.windowResizeTitle")}</DialogTitle>
</DialogHeader>
<p className="text-sm text-muted-foreground">
{t("warnings.windowResizeDescription")}
</p>
<div className="flex items-center space-x-2">
<Checkbox
id="dont-show-again"
checked={dontShowAgain}
onCheckedChange={(checked) => setDontShowAgain(checked === true)}
/>
<Label htmlFor="dont-show-again" className="text-sm">
{t("warnings.dontShowAgain")}
</Label>
</div>
<DialogFooter className="flex-row justify-between sm:justify-between">
<Button variant="ghost" onClick={handleCancel}>
{t("warnings.cancel")}
</Button>
<Button onClick={handleContinue}>{t("warnings.continue")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+7 -35
View File
@@ -12,7 +12,7 @@ import type { BrowserProfile } from "@/types";
export function useBrowserState(
profiles: BrowserProfile[],
runningProfiles: Set<string>,
isUpdating: (browser: string) => boolean,
_isUpdating: (browser: string) => boolean,
launchingProfiles: Set<string>,
stoppingProfiles: Set<string>,
) {
@@ -57,7 +57,6 @@ export function useBrowserState(
const isRunning = runningProfiles.has(profile.id);
const isLaunching = launchingProfiles.has(profile.id);
const isStopping = stoppingProfiles.has(profile.id);
const isBrowserUpdating = isUpdating(profile.browser);
// If the profile is launching or stopping, disable the button
if (isLaunching || isStopping) {
@@ -67,11 +66,6 @@ export function useBrowserState(
// If the profile is already running, it can always be stopped
if (isRunning) return true;
// If THIS specific browser is updating or downloading, block this profile
if (isBrowserUpdating) {
return false;
}
// For single-instance browsers, check if any instance is running
if (isSingleInstanceBrowser(profile.browser)) {
return !isAnyInstanceRunning(profile.browser);
@@ -82,7 +76,6 @@ export function useBrowserState(
[
runningProfiles,
isClient,
isUpdating,
isSingleInstanceBrowser,
isAnyInstanceRunning,
launchingProfiles,
@@ -98,18 +91,17 @@ export function useBrowserState(
(profile: BrowserProfile): boolean => {
if (!isClient) return false;
const isRunning = runningProfiles.has(profile.id);
const isLaunching = launchingProfiles.has(profile.id);
const isStopping = stoppingProfiles.has(profile.id);
const isBrowserUpdating = isUpdating(profile.browser);
// If this specific browser is updating, downloading, launching, or stopping, block it
if (isBrowserUpdating || isLaunching || isStopping) {
// If this specific browser is launching or stopping, block it
if (isLaunching || isStopping) {
return false;
}
// For single-instance browsers
if (isSingleInstanceBrowser(profile.browser)) {
const isRunning = runningProfiles.has(profile.id);
const runningInstancesOfType = profiles.filter(
(p) => p.browser === profile.browser && runningProfiles.has(p.id),
);
@@ -131,7 +123,6 @@ export function useBrowserState(
runningProfiles,
isClient,
isSingleInstanceBrowser,
isUpdating,
launchingProfiles,
stoppingProfiles,
],
@@ -147,22 +138,15 @@ export function useBrowserState(
const isRunning = runningProfiles.has(profile.id);
const isLaunching = launchingProfiles.has(profile.id);
const isStopping = stoppingProfiles.has(profile.id);
const isBrowserUpdating = isUpdating(profile.browser);
// If profile is running, launching, stopping, or browser is updating, block selection
if (isRunning || isLaunching || isStopping || isBrowserUpdating) {
// If profile is running, launching, or stopping, block selection
if (isRunning || isLaunching || isStopping) {
return false;
}
return true;
},
[
isClient,
runningProfiles,
launchingProfiles,
stoppingProfiles,
isUpdating,
],
[isClient, runningProfiles, launchingProfiles, stoppingProfiles],
);
/**
@@ -180,7 +164,6 @@ export function useBrowserState(
const isRunning = runningProfiles.has(profile.id);
const isLaunching = launchingProfiles.has(profile.id);
const isStopping = stoppingProfiles.has(profile.id);
const isBrowserUpdating = isUpdating(profile.browser);
if (isLaunching) {
return "Launching browser...";
@@ -194,10 +177,6 @@ export function useBrowserState(
return "";
}
if (isBrowserUpdating) {
return `${getBrowserDisplayName(profile.browser)} is being updated. Please wait for the update to complete.`;
}
if (
isSingleInstanceBrowser(profile.browser) &&
!canLaunchProfile(profile)
@@ -210,7 +189,6 @@ export function useBrowserState(
[
runningProfiles,
isClient,
isUpdating,
isSingleInstanceBrowser,
canLaunchProfile,
launchingProfiles,
@@ -231,7 +209,6 @@ export function useBrowserState(
const isLaunching = launchingProfiles.has(profile.id);
const isStopping = stoppingProfiles.has(profile.id);
const isBrowserUpdating = isUpdating(profile.browser);
if (isLaunching) {
return "Profile is currently launching. Please wait.";
@@ -241,10 +218,6 @@ export function useBrowserState(
return "Profile is currently stopping. Please wait.";
}
if (isBrowserUpdating) {
return `${getBrowserDisplayName(profile.browser)} is being updated. Please wait for the update to complete.`;
}
if (isSingleInstanceBrowser(profile.browser)) {
const runningInstancesOfType = profiles.filter(
(p) => p.browser === profile.browser && runningProfiles.has(p.id),
@@ -266,7 +239,6 @@ export function useBrowserState(
isClient,
canUseProfileForLinks,
isSingleInstanceBrowser,
isUpdating,
launchingProfiles,
stoppingProfiles,
],
+155 -5
View File
@@ -106,6 +106,22 @@
"description": "Configure Local API and MCP (Model Context Protocol) for integrating with external tools and AI assistants.",
"openSettings": "Open Integrations Settings"
},
"encryption": {
"title": "Sync Encryption",
"description": "Set a password to enable E2E encrypted sync. If you lose this password, encrypted profiles cannot be recovered.",
"passwordSet": "Active",
"passwordSetDescription": "E2E encryption password is set",
"noPassword": "No password set",
"passwordPlaceholder": "Password (min 8 characters)",
"confirmPlaceholder": "Confirm password",
"setPassword": "Set Password",
"changePassword": "Change Password",
"removePassword": "Remove Password",
"removed": "Encryption password removed",
"passwordSaved": "Encryption password set",
"passwordMismatch": "Passwords do not match",
"passwordTooShort": "Password must be at least 8 characters"
},
"commercial": {
"title": "Commercial License",
"trialActive": "Trial: {{days}} days, {{hours}} hours remaining",
@@ -157,11 +173,17 @@
"delete": "Delete",
"copyCookies": "Copy Cookies",
"configure": "Configure",
"clone": "Clone Profile"
"clone": "Clone Profile",
"viewNetwork": "View Network",
"syncSettings": "Sync Settings",
"assignToGroup": "Assign to Group",
"changeFingerprint": "Change Fingerprint",
"copyCookiesToProfile": "Copy Cookies to Profile"
},
"ephemeral": "Ephemeral",
"ephemeralDescription": "Browser data is deleted when closed",
"ephemeralBadge": "Ephemeral"
"ephemeralDescription": "The browser is forced to write profile data into memory instead of disk. Data is deleted when the browser is closed.",
"ephemeralBadge": "Ephemeral",
"ephemeralAlpha": "Alpha"
},
"createProfile": {
"title": "Create New Profile",
@@ -265,6 +287,25 @@
}
},
"sync": {
"mode": {
"title": "Profile Sync",
"description": "Manage sync settings for \"{{name}}\"",
"disabled": "Disabled",
"regular": "Regular Sync",
"encrypted": "E2E Encrypted Sync",
"disabledDescription": "No sync for this profile",
"regularDescription": "Fast sync, unencrypted",
"encryptedDescription": "Encrypted before upload. Server never sees plaintext data.",
"noPasswordWarning": "E2E password not set. Please set a password in Settings.",
"passwordRequired": "E2E password not set. Please set a password in Settings first.",
"enabledToast": "Sync enabled",
"disabledToast": "Sync disabled",
"syncQueued": "Sync queued",
"syncNow": "Sync Now",
"lastSynced": "Last Synced",
"notConfigured": "Sync service not configured.",
"configureService": "Configure Sync Service"
},
"title": "Account",
"config": "Sync Configuration",
"serverUrl": "Server URL",
@@ -486,7 +527,111 @@
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "Spoofing fingerprint for a different operating system is less reliable because it is impossible to perfectly mimic all underlying components. Use with caution."
"crossOsWarning": "Spoofing fingerprint for a different operating system is less reliable because it is impossible to perfectly mimic all underlying components. Use with caution.",
"crossOsLimitations": "Cross-OS fingerprinting has limitations. System-level APIs may still reflect your actual operating system, and some features may have degraded performance.",
"osLabel": "Operating System Fingerprint",
"selectOSPlaceholder": "Select operating system",
"generateRandomOnLaunch": "Generate random fingerprint on every launch",
"generateRandomDescription": "When enabled, a new fingerprint will be generated each time the browser is launched.",
"generateRandomDescriptionAuto": "When enabled, a new fingerprint will be generated each time the browser is launched. The generated fingerprint is saved for reference.",
"autoLocationDescription": "Automatically configure location information based on proxy configuration or your connection if no proxy provided",
"editingDisabledRunning": "Fingerprint editing is disabled because the profile is currently running. Stop the profile to make changes.",
"editingDisabledRandomized": "Fingerprint editing is disabled because random fingerprint generation is enabled. Disable the option above to manually edit the fingerprint configuration.",
"advancedWarning": "Warning: Only edit these parameters if you know what you're doing. Incorrect values may break websites, make them detect you, and lead to hard-to-debug bugs.",
"basicWarning": "Warning: Only edit these parameters if you know what you're doing.",
"automatic": "Automatic",
"manual": "Manual",
"blockingOptions": "Blocking Options",
"blockImages": "Block Images",
"blockWebRTC": "Block WebRTC",
"blockWebGL": "Block WebGL",
"navigatorProperties": "Navigator Properties",
"userAgent": "User Agent",
"userAgentAndPlatform": "User Agent & Platform",
"platform": "Platform",
"platformVersion": "Platform Version",
"appVersion": "App Version",
"osCpu": "OS CPU",
"hardwareConcurrency": "Hardware Concurrency",
"maxTouchPoints": "Max Touch Points",
"doNotTrack": "Do Not Track",
"selectDntPlaceholder": "Select DNT value",
"dntAllowed": "0 (tracking allowed)",
"dntNotAllowed": "1 (tracking not allowed)",
"dntUnspecified": "unspecified",
"language": "Language",
"primaryLanguage": "Primary Language (navigator.language)",
"languages": "Languages (JSON array)",
"languageAndLocale": "Language & Locale",
"screenProperties": "Screen Properties",
"screenWidth": "Screen Width",
"screenHeight": "Screen Height",
"availableWidth": "Available Width",
"availableHeight": "Available Height",
"colorDepth": "Color Depth",
"pixelDepth": "Pixel Depth",
"devicePixelRatio": "Device Pixel Ratio",
"windowProperties": "Window Properties",
"outerWidth": "Outer Width",
"outerHeight": "Outer Height",
"innerWidth": "Inner Width",
"innerHeight": "Inner Height",
"screenX": "Screen X",
"screenY": "Screen Y",
"geolocation": "Geolocation",
"timezoneAndGeolocation": "Timezone & Geolocation",
"timezoneGeolocationDescription": "These values override the browser's timezone and geolocation APIs.",
"latitude": "Latitude",
"longitude": "Longitude",
"timezone": "Timezone",
"timezoneIana": "Timezone (IANA)",
"timezoneOffset": "Offset (minutes from UTC)",
"accuracy": "Accuracy (meters)",
"locale": "Locale",
"region": "Region",
"script": "Script",
"webglProperties": "WebGL Properties",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglParameters": "WebGL Parameters",
"webglParametersJson": "WebGL Parameters (JSON)",
"webgl2Parameters": "WebGL2 Parameters",
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
"canvasFingerprint": "Canvas Fingerprint",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeedDescription": "This seed is used to generate a consistent but unique canvas fingerprint. Each profile should have a different seed.",
"fonts": "Fonts",
"fontsJson": "Fonts (JSON array)",
"battery": "Battery",
"charging": "Charging",
"chargingTime": "Charging Time",
"dischargingTime": "Discharging Time",
"batteryLevel": "Level (0-1)",
"screenResolution": "Screen Resolution",
"maxWidth": "Max Width",
"maxHeight": "Max Height",
"minWidth": "Min Width",
"minHeight": "Min Height",
"hardwareProperties": "Hardware Properties",
"deviceMemory": "Device Memory (GB)",
"audioProperties": "Audio Properties",
"sampleRate": "Sample Rate",
"maxChannelCount": "Max Channel Count",
"vendorInfo": "Vendor Info",
"vendor": "Vendor",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"brand": "Brand",
"brandVersion": "Brand Version",
"proFeature": "This is a Pro feature"
},
"warnings": {
"windowResizeTitle": "Custom Window Dimensions",
"windowResizeDescription": "Changing browser window dimensions may increase the chance of website detection that browser information is spoofed.",
"dontShowAgain": "Don't show this again",
"continue": "Continue",
"cancel": "Cancel"
},
"syncAll": {
"title": "Enable Sync for Existing Items",
@@ -508,6 +653,10 @@
"cannotModify": "Cannot modify sync settings for a cross-OS profile"
},
"cookies": {
"management": {
"title": "Cookie Management",
"menuItem": "Cookie Management"
},
"import": {
"title": "Import Cookies",
"description": "Import cookies from a Netscape or JSON format file.",
@@ -532,6 +681,7 @@
"fingerprintLocked": "Fingerprint editing is a Pro feature",
"cookieCopyLocked": "Cookie copying is a Pro feature",
"cookieImportLocked": "Cookie import is a Pro feature",
"cookieExportLocked": "Cookie export is a Pro feature"
"cookieExportLocked": "Cookie export is a Pro feature",
"cookieManagementLocked": "Cookie management is a Pro feature"
}
}
+155 -5
View File
@@ -106,6 +106,22 @@
"description": "Configura la API Local y MCP (Protocolo de Contexto de Modelo) para integración con herramientas externas y asistentes de IA.",
"openSettings": "Abrir Configuración de Integraciones"
},
"encryption": {
"title": "Cifrado de sincronización",
"description": "Establece una contraseña para habilitar la sincronización cifrada E2E. Si pierdes esta contraseña, los perfiles cifrados no podrán recuperarse.",
"passwordSet": "Activo",
"passwordSetDescription": "La contraseña de cifrado E2E está configurada",
"noPassword": "Sin contraseña configurada",
"passwordPlaceholder": "Contraseña (mín. 8 caracteres)",
"confirmPlaceholder": "Confirmar contraseña",
"setPassword": "Establecer contraseña",
"changePassword": "Cambiar contraseña",
"removePassword": "Eliminar contraseña",
"removed": "Contraseña de cifrado eliminada",
"passwordSaved": "Contraseña de cifrado establecida",
"passwordMismatch": "Las contraseñas no coinciden",
"passwordTooShort": "La contraseña debe tener al menos 8 caracteres"
},
"commercial": {
"title": "Licencia Comercial",
"trialActive": "Prueba: {{days}} días, {{hours}} horas restantes",
@@ -157,11 +173,17 @@
"delete": "Eliminar",
"copyCookies": "Copiar Cookies",
"configure": "Configurar",
"clone": "Clonar perfil"
"clone": "Clonar perfil",
"viewNetwork": "Ver Red",
"syncSettings": "Configuración de Sincronización",
"assignToGroup": "Asignar a Grupo",
"changeFingerprint": "Cambiar Huella Digital",
"copyCookiesToProfile": "Copiar Cookies al Perfil"
},
"ephemeral": "Efímero",
"ephemeralDescription": "Los datos del navegador se eliminan al cerrarlo",
"ephemeralBadge": "Efímero"
"ephemeralDescription": "El navegador es forzado a escribir los datos del perfil en memoria en lugar del disco. Los datos se eliminan al cerrar el navegador.",
"ephemeralBadge": "Efímero",
"ephemeralAlpha": "Alpha"
},
"createProfile": {
"title": "Crear Nuevo Perfil",
@@ -265,6 +287,25 @@
}
},
"sync": {
"mode": {
"title": "Sincronización de perfil",
"description": "Gestionar configuración de sincronización para \"{{name}}\"",
"disabled": "Deshabilitado",
"regular": "Sincronización regular",
"encrypted": "Sincronización cifrada E2E",
"disabledDescription": "Sin sincronización para este perfil",
"regularDescription": "Sincronización rápida, sin cifrar",
"encryptedDescription": "Cifrado antes de subir. El servidor nunca ve los datos en texto plano.",
"noPasswordWarning": "Contraseña E2E no configurada. Por favor establece una contraseña en Ajustes.",
"passwordRequired": "Contraseña E2E no configurada. Por favor establece una contraseña en Ajustes primero.",
"enabledToast": "Sincronización habilitada",
"disabledToast": "Sincronización deshabilitada",
"syncQueued": "Sincronización en cola",
"syncNow": "Sincronizar ahora",
"lastSynced": "Última sincronización",
"notConfigured": "Servicio de sincronización no configurado.",
"configureService": "Configurar servicio de sincronización"
},
"title": "Servicio de Sincronización",
"config": "Configuración de Sincronización",
"serverUrl": "URL del Servidor",
@@ -486,7 +527,111 @@
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "La suplantación de huella digital para un sistema operativo diferente es menos fiable porque es imposible imitar perfectamente todos los componentes subyacentes. Usar con precaución."
"crossOsWarning": "La suplantación de huella digital para un sistema operativo diferente es menos fiable porque es imposible imitar perfectamente todos los componentes subyacentes. Usar con precaución.",
"crossOsLimitations": "La suplantación de huella digital entre sistemas operativos tiene limitaciones. Las APIs a nivel de sistema pueden seguir reflejando su sistema operativo real y algunas funciones pueden tener un rendimiento reducido.",
"osLabel": "Huella digital del sistema operativo",
"selectOSPlaceholder": "Seleccionar sistema operativo",
"generateRandomOnLaunch": "Generar huella digital aleatoria en cada inicio",
"generateRandomDescription": "Cuando está activado, se generará una nueva huella digital cada vez que se inicie el navegador.",
"generateRandomDescriptionAuto": "Cuando está activado, se generará una nueva huella digital cada vez que se inicie el navegador. La huella digital generada se guarda como referencia.",
"autoLocationDescription": "Configurar automáticamente la información de ubicación basándose en la configuración del proxy o en su conexión si no se proporciona un proxy",
"editingDisabledRunning": "La edición de huellas digitales está desactivada porque el perfil se está ejecutando actualmente. Detenga el perfil para realizar cambios.",
"editingDisabledRandomized": "La edición de huellas digitales está desactivada porque la generación aleatoria de huellas digitales está activada. Desactive la opción anterior para editar manualmente la configuración de la huella digital.",
"advancedWarning": "Advertencia: Solo edite estos parámetros si sabe lo que está haciendo. Los valores incorrectos pueden romper sitios web, hacer que lo detecten y provocar errores difíciles de depurar.",
"basicWarning": "Advertencia: Solo edite estos parámetros si sabe lo que está haciendo.",
"automatic": "Automático",
"manual": "Manual",
"blockingOptions": "Opciones de bloqueo",
"blockImages": "Bloquear imágenes",
"blockWebRTC": "Bloquear WebRTC",
"blockWebGL": "Bloquear WebGL",
"navigatorProperties": "Propiedades del navegador",
"userAgent": "User Agent",
"userAgentAndPlatform": "User Agent y plataforma",
"platform": "Plataforma",
"platformVersion": "Versión de plataforma",
"appVersion": "Versión de la aplicación",
"osCpu": "OS CPU",
"hardwareConcurrency": "Concurrencia de hardware",
"maxTouchPoints": "Puntos táctiles máximos",
"doNotTrack": "Do Not Track",
"selectDntPlaceholder": "Seleccionar valor DNT",
"dntAllowed": "0 (rastreo permitido)",
"dntNotAllowed": "1 (rastreo no permitido)",
"dntUnspecified": "no especificado",
"language": "Idioma",
"primaryLanguage": "Idioma principal (navigator.language)",
"languages": "Idiomas (JSON array)",
"languageAndLocale": "Idioma y configuración regional",
"screenProperties": "Propiedades de pantalla",
"screenWidth": "Ancho de pantalla",
"screenHeight": "Alto de pantalla",
"availableWidth": "Ancho disponible",
"availableHeight": "Alto disponible",
"colorDepth": "Profundidad de color",
"pixelDepth": "Profundidad de píxel",
"devicePixelRatio": "Relación de píxeles del dispositivo",
"windowProperties": "Propiedades de ventana",
"outerWidth": "Ancho exterior",
"outerHeight": "Alto exterior",
"innerWidth": "Ancho interior",
"innerHeight": "Alto interior",
"screenX": "Screen X",
"screenY": "Screen Y",
"geolocation": "Geolocalización",
"timezoneAndGeolocation": "Zona horaria y geolocalización",
"timezoneGeolocationDescription": "Estos valores anulan las APIs de zona horaria y geolocalización del navegador.",
"latitude": "Latitud",
"longitude": "Longitud",
"timezone": "Zona horaria",
"timezoneIana": "Zona horaria (IANA)",
"timezoneOffset": "Desfase (minutos desde UTC)",
"accuracy": "Precisión (metros)",
"locale": "Configuración regional",
"region": "Región",
"script": "Script",
"webglProperties": "Propiedades de WebGL",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglParameters": "Parámetros de WebGL",
"webglParametersJson": "Parámetros de WebGL (JSON)",
"webgl2Parameters": "Parámetros de WebGL2",
"webglShaderPrecisionFormats": "Formatos de precisión de WebGL Shader",
"webgl2ShaderPrecisionFormats": "Formatos de precisión de WebGL2 Shader",
"canvasFingerprint": "Canvas Fingerprint",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeedDescription": "Esta semilla se usa para generar una huella digital de Canvas consistente pero única. Cada perfil debe tener una semilla diferente.",
"fonts": "Fuentes",
"fontsJson": "Fuentes (JSON array)",
"battery": "Batería",
"charging": "Cargando",
"chargingTime": "Tiempo de carga",
"dischargingTime": "Tiempo de descarga",
"batteryLevel": "Nivel (0-1)",
"screenResolution": "Resolución de pantalla",
"maxWidth": "Ancho máximo",
"maxHeight": "Alto máximo",
"minWidth": "Ancho mínimo",
"minHeight": "Alto mínimo",
"hardwareProperties": "Propiedades de hardware",
"deviceMemory": "Memoria del dispositivo (GB)",
"audioProperties": "Propiedades de audio",
"sampleRate": "Frecuencia de muestreo",
"maxChannelCount": "Número máximo de canales",
"vendorInfo": "Información del proveedor",
"vendor": "Proveedor",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"brand": "Marca",
"brandVersion": "Versión de marca",
"proFeature": "Esta es una función Pro"
},
"warnings": {
"windowResizeTitle": "Dimensiones de ventana personalizadas",
"windowResizeDescription": "Cambiar las dimensiones de la ventana del navegador puede aumentar la posibilidad de que los sitios web detecten que la información del navegador está falsificada.",
"dontShowAgain": "No mostrar esto de nuevo",
"continue": "Continuar",
"cancel": "Cancelar"
},
"syncAll": {
"title": "Activar sincronización para elementos existentes",
@@ -508,6 +653,10 @@
"cannotModify": "No se pueden modificar los ajustes de sincronización de un perfil de otro sistema operativo"
},
"cookies": {
"management": {
"title": "Gestión de Cookies",
"menuItem": "Gestión de Cookies"
},
"import": {
"title": "Importar Cookies",
"description": "Importar cookies desde un archivo en formato Netscape o JSON.",
@@ -532,6 +681,7 @@
"fingerprintLocked": "La edición de huellas digitales es una función Pro",
"cookieCopyLocked": "La copia de cookies es una función Pro",
"cookieImportLocked": "La importación de cookies es una función Pro",
"cookieExportLocked": "La exportación de cookies es una función Pro"
"cookieExportLocked": "La exportación de cookies es una función Pro",
"cookieManagementLocked": "La gestión de cookies es una función Pro"
}
}
+155 -5
View File
@@ -106,6 +106,22 @@
"description": "Configurez l'API locale et MCP (Model Context Protocol) pour l'intégration avec des outils externes et des assistants IA.",
"openSettings": "Ouvrir les paramètres d'intégration"
},
"encryption": {
"title": "Chiffrement de synchronisation",
"description": "Définissez un mot de passe pour activer la synchronisation chiffrée E2E. Si vous perdez ce mot de passe, les profils chiffrés ne pourront pas être récupérés.",
"passwordSet": "Actif",
"passwordSetDescription": "Le mot de passe de chiffrement E2E est défini",
"noPassword": "Aucun mot de passe défini",
"passwordPlaceholder": "Mot de passe (min. 8 caractères)",
"confirmPlaceholder": "Confirmer le mot de passe",
"setPassword": "Définir le mot de passe",
"changePassword": "Changer le mot de passe",
"removePassword": "Supprimer le mot de passe",
"removed": "Mot de passe de chiffrement supprimé",
"passwordSaved": "Mot de passe de chiffrement défini",
"passwordMismatch": "Les mots de passe ne correspondent pas",
"passwordTooShort": "Le mot de passe doit contenir au moins 8 caractères"
},
"commercial": {
"title": "Licence commerciale",
"trialActive": "Essai: {{days}} jours, {{hours}} heures restantes",
@@ -157,11 +173,17 @@
"delete": "Supprimer",
"copyCookies": "Copier les cookies",
"configure": "Configurer",
"clone": "Cloner le profil"
"clone": "Cloner le profil",
"viewNetwork": "Voir le Réseau",
"syncSettings": "Paramètres de Synchronisation",
"assignToGroup": "Assigner au Groupe",
"changeFingerprint": "Changer l'Empreinte",
"copyCookiesToProfile": "Copier les Cookies vers le Profil"
},
"ephemeral": "Éphémère",
"ephemeralDescription": "Les données du navigateur sont supprimées à la fermeture",
"ephemeralBadge": "Éphémère"
"ephemeralDescription": "Le navigateur est forcé d'écrire les données du profil en mémoire au lieu du disque. Les données sont supprimées à la fermeture du navigateur.",
"ephemeralBadge": "Éphémère",
"ephemeralAlpha": "Alpha"
},
"createProfile": {
"title": "Créer un nouveau profil",
@@ -265,6 +287,25 @@
}
},
"sync": {
"mode": {
"title": "Synchronisation du profil",
"description": "Gérer les paramètres de synchronisation pour \"{{name}}\"",
"disabled": "Désactivé",
"regular": "Synchronisation régulière",
"encrypted": "Synchronisation chiffrée E2E",
"disabledDescription": "Pas de synchronisation pour ce profil",
"regularDescription": "Synchronisation rapide, non chiffrée",
"encryptedDescription": "Chiffré avant l'envoi. Le serveur ne voit jamais les données en clair.",
"noPasswordWarning": "Mot de passe E2E non défini. Veuillez définir un mot de passe dans les Paramètres.",
"passwordRequired": "Mot de passe E2E non défini. Veuillez d'abord définir un mot de passe dans les Paramètres.",
"enabledToast": "Synchronisation activée",
"disabledToast": "Synchronisation désactivée",
"syncQueued": "Synchronisation en file d'attente",
"syncNow": "Synchroniser maintenant",
"lastSynced": "Dernière synchronisation",
"notConfigured": "Service de synchronisation non configuré.",
"configureService": "Configurer le service de synchronisation"
},
"title": "Service de synchronisation",
"config": "Configuration de la synchronisation",
"serverUrl": "URL du serveur",
@@ -486,7 +527,111 @@
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "L'usurpation d'empreinte pour un système d'exploitation différent est moins fiable car il est impossible d'imiter parfaitement tous les composants sous-jacents. À utiliser avec précaution."
"crossOsWarning": "L'usurpation d'empreinte pour un système d'exploitation différent est moins fiable car il est impossible d'imiter parfaitement tous les composants sous-jacents. À utiliser avec précaution.",
"crossOsLimitations": "L'usurpation d'empreinte inter-OS a des limitations. Les APIs au niveau du système peuvent toujours refléter votre système d'exploitation réel et certaines fonctionnalités peuvent avoir des performances réduites.",
"osLabel": "Empreinte du système d'exploitation",
"selectOSPlaceholder": "Sélectionner le système d'exploitation",
"generateRandomOnLaunch": "Générer une empreinte aléatoire à chaque lancement",
"generateRandomDescription": "Lorsque cette option est activée, une nouvelle empreinte sera générée à chaque lancement du navigateur.",
"generateRandomDescriptionAuto": "Lorsque cette option est activée, une nouvelle empreinte sera générée à chaque lancement du navigateur. L'empreinte générée est sauvegardée pour référence.",
"autoLocationDescription": "Configurer automatiquement les informations de localisation en fonction de la configuration du proxy ou de votre connexion si aucun proxy n'est fourni",
"editingDisabledRunning": "La modification de l'empreinte est désactivée car le profil est en cours d'exécution. Arrêtez le profil pour effectuer des modifications.",
"editingDisabledRandomized": "La modification de l'empreinte est désactivée car la génération aléatoire d'empreinte est activée. Désactivez l'option ci-dessus pour modifier manuellement la configuration de l'empreinte.",
"advancedWarning": "Avertissement : Ne modifiez ces paramètres que si vous savez ce que vous faites. Des valeurs incorrectes peuvent casser des sites web, vous faire détecter et provoquer des bugs difficiles à résoudre.",
"basicWarning": "Avertissement : Ne modifiez ces paramètres que si vous savez ce que vous faites.",
"automatic": "Automatique",
"manual": "Manuel",
"blockingOptions": "Options de blocage",
"blockImages": "Bloquer les images",
"blockWebRTC": "Bloquer WebRTC",
"blockWebGL": "Bloquer WebGL",
"navigatorProperties": "Propriétés du navigateur",
"userAgent": "User Agent",
"userAgentAndPlatform": "User Agent et plateforme",
"platform": "Plateforme",
"platformVersion": "Version de la plateforme",
"appVersion": "Version de l'application",
"osCpu": "OS CPU",
"hardwareConcurrency": "Concurrence matérielle",
"maxTouchPoints": "Points tactiles maximum",
"doNotTrack": "Do Not Track",
"selectDntPlaceholder": "Sélectionner la valeur DNT",
"dntAllowed": "0 (suivi autorisé)",
"dntNotAllowed": "1 (suivi non autorisé)",
"dntUnspecified": "non spécifié",
"language": "Langue",
"primaryLanguage": "Langue principale (navigator.language)",
"languages": "Langues (JSON array)",
"languageAndLocale": "Langue et paramètres régionaux",
"screenProperties": "Propriétés de l'écran",
"screenWidth": "Largeur de l'écran",
"screenHeight": "Hauteur de l'écran",
"availableWidth": "Largeur disponible",
"availableHeight": "Hauteur disponible",
"colorDepth": "Profondeur de couleur",
"pixelDepth": "Profondeur de pixel",
"devicePixelRatio": "Ratio de pixels de l'appareil",
"windowProperties": "Propriétés de la fenêtre",
"outerWidth": "Largeur extérieure",
"outerHeight": "Hauteur extérieure",
"innerWidth": "Largeur intérieure",
"innerHeight": "Hauteur intérieure",
"screenX": "Screen X",
"screenY": "Screen Y",
"geolocation": "Géolocalisation",
"timezoneAndGeolocation": "Fuseau horaire et géolocalisation",
"timezoneGeolocationDescription": "Ces valeurs remplacent les APIs de fuseau horaire et de géolocalisation du navigateur.",
"latitude": "Latitude",
"longitude": "Longitude",
"timezone": "Fuseau horaire",
"timezoneIana": "Fuseau horaire (IANA)",
"timezoneOffset": "Décalage (minutes depuis UTC)",
"accuracy": "Précision (mètres)",
"locale": "Paramètres régionaux",
"region": "Région",
"script": "Script",
"webglProperties": "Propriétés WebGL",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglParameters": "Paramètres WebGL",
"webglParametersJson": "Paramètres WebGL (JSON)",
"webgl2Parameters": "Paramètres WebGL2",
"webglShaderPrecisionFormats": "Formats de précision WebGL Shader",
"webgl2ShaderPrecisionFormats": "Formats de précision WebGL2 Shader",
"canvasFingerprint": "Canvas Fingerprint",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeedDescription": "Cette graine est utilisée pour générer une empreinte Canvas cohérente mais unique. Chaque profil doit avoir une graine différente.",
"fonts": "Polices",
"fontsJson": "Polices (JSON array)",
"battery": "Batterie",
"charging": "En charge",
"chargingTime": "Temps de charge",
"dischargingTime": "Temps de décharge",
"batteryLevel": "Niveau (0-1)",
"screenResolution": "Résolution de l'écran",
"maxWidth": "Largeur maximale",
"maxHeight": "Hauteur maximale",
"minWidth": "Largeur minimale",
"minHeight": "Hauteur minimale",
"hardwareProperties": "Propriétés matérielles",
"deviceMemory": "Mémoire de l'appareil (Go)",
"audioProperties": "Propriétés audio",
"sampleRate": "Fréquence d'échantillonnage",
"maxChannelCount": "Nombre maximum de canaux",
"vendorInfo": "Informations du fournisseur",
"vendor": "Fournisseur",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"brand": "Marque",
"brandVersion": "Version de la marque",
"proFeature": "Ceci est une fonctionnalité Pro"
},
"warnings": {
"windowResizeTitle": "Dimensions de fenêtre personnalisées",
"windowResizeDescription": "Modifier les dimensions de la fenêtre du navigateur peut augmenter les chances de détection par les sites web que les informations du navigateur sont falsifiées.",
"dontShowAgain": "Ne plus afficher",
"continue": "Continuer",
"cancel": "Annuler"
},
"syncAll": {
"title": "Activer la synchronisation pour les éléments existants",
@@ -508,6 +653,10 @@
"cannotModify": "Impossible de modifier les paramètres de synchronisation d'un profil d'un autre système d'exploitation"
},
"cookies": {
"management": {
"title": "Gestion des Cookies",
"menuItem": "Gestion des Cookies"
},
"import": {
"title": "Importer des Cookies",
"description": "Importer des cookies depuis un fichier au format Netscape ou JSON.",
@@ -532,6 +681,7 @@
"fingerprintLocked": "La modification d'empreinte est une fonctionnalité Pro",
"cookieCopyLocked": "La copie de cookies est une fonctionnalité Pro",
"cookieImportLocked": "L'importation de cookies est une fonctionnalité Pro",
"cookieExportLocked": "L'exportation de cookies est une fonctionnalité Pro"
"cookieExportLocked": "L'exportation de cookies est une fonctionnalité Pro",
"cookieManagementLocked": "La gestion des cookies est une fonctionnalité Pro"
}
}
+155 -5
View File
@@ -106,6 +106,22 @@
"description": "外部ツールやAIアシスタントと統合するためのローカルAPIとMCP(モデルコンテキストプロトコル)を設定します。",
"openSettings": "統合設定を開く"
},
"encryption": {
"title": "同期暗号化",
"description": "E2E暗号化同期を有効にするためのパスワードを設定してください。このパスワードを紛失すると、暗号化されたプロファイルは復元できません。",
"passwordSet": "有効",
"passwordSetDescription": "E2E暗号化パスワードが設定されています",
"noPassword": "パスワード未設定",
"passwordPlaceholder": "パスワード(8文字以上)",
"confirmPlaceholder": "パスワードを確認",
"setPassword": "パスワードを設定",
"changePassword": "パスワードを変更",
"removePassword": "パスワードを削除",
"removed": "暗号化パスワードが削除されました",
"passwordSaved": "暗号化パスワードが設定されました",
"passwordMismatch": "パスワードが一致しません",
"passwordTooShort": "パスワードは8文字以上である必要があります"
},
"commercial": {
"title": "商用ライセンス",
"trialActive": "トライアル: 残り {{days}} 日 {{hours}} 時間",
@@ -157,11 +173,17 @@
"delete": "削除",
"copyCookies": "Cookieをコピー",
"configure": "設定",
"clone": "プロファイルを複製"
"clone": "プロファイルを複製",
"viewNetwork": "ネットワークを表示",
"syncSettings": "同期設定",
"assignToGroup": "グループに割り当て",
"changeFingerprint": "フィンガープリントを変更",
"copyCookiesToProfile": "Cookieをプロファイルにコピー"
},
"ephemeral": "一時的",
"ephemeralDescription": "ブラウザを閉じるとデータ削除されます",
"ephemeralBadge": "一時的"
"ephemeralDescription": "ブラウザはプロファイルデータをディスクではなくメモリに書き込むよう強制されます。ブラウザを閉じるとデータ削除されます",
"ephemeralBadge": "一時的",
"ephemeralAlpha": "Alpha"
},
"createProfile": {
"title": "新しいプロファイルを作成",
@@ -265,6 +287,25 @@
}
},
"sync": {
"mode": {
"title": "プロファイル同期",
"description": "\"{{name}}\" の同期設定を管理",
"disabled": "無効",
"regular": "通常同期",
"encrypted": "E2E暗号化同期",
"disabledDescription": "このプロファイルの同期なし",
"regularDescription": "高速同期、暗号化なし",
"encryptedDescription": "アップロード前に暗号化。サーバーは平文データを見ることができません。",
"noPasswordWarning": "E2Eパスワードが設定されていません。設定でパスワードを設定してください。",
"passwordRequired": "E2Eパスワードが設定されていません。まず設定でパスワードを設定してください。",
"enabledToast": "同期が有効になりました",
"disabledToast": "同期が無効になりました",
"syncQueued": "同期がキューに追加されました",
"syncNow": "今すぐ同期",
"lastSynced": "最終同期",
"notConfigured": "同期サービスが設定されていません。",
"configureService": "同期サービスを設定"
},
"title": "同期サービス",
"config": "同期設定",
"serverUrl": "サーバーURL",
@@ -486,7 +527,111 @@
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "異なるオペレーティングシステムのフィンガープリント偽装は、すべての基盤コンポーネントを完璧に模倣することが不可能なため、信頼性が低くなります。注意してご使用ください。"
"crossOsWarning": "異なるオペレーティングシステムのフィンガープリント偽装は、すべての基盤コンポーネントを完璧に模倣することが不可能なため、信頼性が低くなります。注意してご使用ください。",
"crossOsLimitations": "クロスOSフィンガープリントには制限があります。システムレベルのAPIは実際のオペレーティングシステムを反映する場合があり、一部の機能のパフォーマンスが低下する可能性があります。",
"osLabel": "オペレーティングシステムのフィンガープリント",
"selectOSPlaceholder": "オペレーティングシステムを選択",
"generateRandomOnLaunch": "起動ごとにランダムなフィンガープリントを生成",
"generateRandomDescription": "有効にすると、ブラウザの起動ごとに新しいフィンガープリントが生成されます。",
"generateRandomDescriptionAuto": "有効にすると、ブラウザの起動ごとに新しいフィンガープリントが生成されます。生成されたフィンガープリントは参照用に保存されます。",
"autoLocationDescription": "プロキシ設定に基づいて位置情報を自動的に設定します。プロキシが提供されていない場合は接続情報を使用します。",
"editingDisabledRunning": "プロファイルが現在実行中のため、フィンガープリントの編集は無効です。変更するにはプロファイルを停止してください。",
"editingDisabledRandomized": "ランダムフィンガープリント生成が有効なため、フィンガープリントの編集は無効です。手動で編集するには上記のオプションを無効にしてください。",
"advancedWarning": "警告: これらのパラメータは、内容を理解している場合にのみ編集してください。不正な値はウェブサイトの動作を妨げたり、検出されたり、デバッグが困難なバグにつながる可能性があります。",
"basicWarning": "警告: これらのパラメータは、内容を理解している場合にのみ編集してください。",
"automatic": "自動",
"manual": "手動",
"blockingOptions": "ブロックオプション",
"blockImages": "画像をブロック",
"blockWebRTC": "WebRTCをブロック",
"blockWebGL": "WebGLをブロック",
"navigatorProperties": "Navigatorプロパティ",
"userAgent": "User Agent",
"userAgentAndPlatform": "User Agent & Platform",
"platform": "Platform",
"platformVersion": "Platform Version",
"appVersion": "App Version",
"osCpu": "OS CPU",
"hardwareConcurrency": "Hardware Concurrency",
"maxTouchPoints": "最大タッチポイント数",
"doNotTrack": "Do Not Track",
"selectDntPlaceholder": "DNT値を選択",
"dntAllowed": "0(トラッキング許可)",
"dntNotAllowed": "1(トラッキング不許可)",
"dntUnspecified": "未指定",
"language": "言語",
"primaryLanguage": "プライマリ言語 (navigator.language)",
"languages": "言語一覧 (JSON配列)",
"languageAndLocale": "言語とロケール",
"screenProperties": "画面プロパティ",
"screenWidth": "画面幅",
"screenHeight": "画面高さ",
"availableWidth": "利用可能な幅",
"availableHeight": "利用可能な高さ",
"colorDepth": "色深度",
"pixelDepth": "ピクセル深度",
"devicePixelRatio": "デバイスピクセル比",
"windowProperties": "ウィンドウプロパティ",
"outerWidth": "外側の幅",
"outerHeight": "外側の高さ",
"innerWidth": "内側の幅",
"innerHeight": "内側の高さ",
"screenX": "Screen X",
"screenY": "Screen Y",
"geolocation": "ジオロケーション",
"timezoneAndGeolocation": "タイムゾーンとジオロケーション",
"timezoneGeolocationDescription": "これらの値はブラウザのタイムゾーンとジオロケーションAPIを上書きします。",
"latitude": "緯度",
"longitude": "経度",
"timezone": "タイムゾーン",
"timezoneIana": "タイムゾーン (IANA)",
"timezoneOffset": "オフセット(UTCからの分数)",
"accuracy": "精度(メートル)",
"locale": "ロケール",
"region": "地域",
"script": "スクリプト",
"webglProperties": "WebGLプロパティ",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglParameters": "WebGLパラメータ",
"webglParametersJson": "WebGLパラメータ (JSON)",
"webgl2Parameters": "WebGL2パラメータ",
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
"canvasFingerprint": "Canvas Fingerprint",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeedDescription": "このシードは一貫性がありながらもユニークなCanvasフィンガープリントを生成するために使用されます。各プロファイルには異なるシードを設定してください。",
"fonts": "フォント",
"fontsJson": "フォント (JSON配列)",
"battery": "バッテリー",
"charging": "充電中",
"chargingTime": "充電時間",
"dischargingTime": "放電時間",
"batteryLevel": "レベル (0-1)",
"screenResolution": "画面解像度",
"maxWidth": "最大幅",
"maxHeight": "最大高さ",
"minWidth": "最小幅",
"minHeight": "最小高さ",
"hardwareProperties": "ハードウェアプロパティ",
"deviceMemory": "デバイスメモリ (GB)",
"audioProperties": "オーディオプロパティ",
"sampleRate": "サンプルレート",
"maxChannelCount": "最大チャンネル数",
"vendorInfo": "ベンダー情報",
"vendor": "ベンダー",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"brand": "ブランド",
"brandVersion": "ブランドバージョン",
"proFeature": "これはPro機能です"
},
"warnings": {
"windowResizeTitle": "カスタムウィンドウサイズ",
"windowResizeDescription": "ブラウザウィンドウのサイズを変更すると、ブラウザ情報が偽装されていることをウェブサイトに検出される可能性が高くなります。",
"dontShowAgain": "今後表示しない",
"continue": "続行",
"cancel": "キャンセル"
},
"syncAll": {
"title": "既存アイテムの同期を有効にする",
@@ -508,6 +653,10 @@
"cannotModify": "他のOSのプロファイルの同期設定は変更できません"
},
"cookies": {
"management": {
"title": "Cookie管理",
"menuItem": "Cookie管理"
},
"import": {
"title": "Cookieのインポート",
"description": "NetscapeまたはJSON形式のファイルからCookieをインポートします。",
@@ -532,6 +681,7 @@
"fingerprintLocked": "フィンガープリント編集はプロ機能です",
"cookieCopyLocked": "Cookieのコピーはプロ機能です",
"cookieImportLocked": "Cookieのインポートはプロ機能です",
"cookieExportLocked": "Cookieのエクスポートはプロ機能です"
"cookieExportLocked": "Cookieのエクスポートはプロ機能です",
"cookieManagementLocked": "Cookie管理はプロ機能です"
}
}
+155 -5
View File
@@ -106,6 +106,22 @@
"description": "Configure a API Local e MCP (Protocolo de Contexto de Modelo) para integração com ferramentas externas e assistentes de IA.",
"openSettings": "Abrir Configurações de Integrações"
},
"encryption": {
"title": "Criptografia de sincronização",
"description": "Defina uma senha para habilitar a sincronização criptografada E2E. Se você perder esta senha, os perfis criptografados não poderão ser recuperados.",
"passwordSet": "Ativo",
"passwordSetDescription": "A senha de criptografia E2E está definida",
"noPassword": "Sem senha definida",
"passwordPlaceholder": "Senha (mín. 8 caracteres)",
"confirmPlaceholder": "Confirmar senha",
"setPassword": "Definir senha",
"changePassword": "Alterar senha",
"removePassword": "Remover senha",
"removed": "Senha de criptografia removida",
"passwordSaved": "Senha de criptografia definida",
"passwordMismatch": "As senhas não coincidem",
"passwordTooShort": "A senha deve ter pelo menos 8 caracteres"
},
"commercial": {
"title": "Licença Comercial",
"trialActive": "Teste: {{days}} dias, {{hours}} horas restantes",
@@ -157,11 +173,17 @@
"delete": "Excluir",
"copyCookies": "Copiar Cookies",
"configure": "Configurar",
"clone": "Clonar perfil"
"clone": "Clonar perfil",
"viewNetwork": "Ver Rede",
"syncSettings": "Configurações de Sincronização",
"assignToGroup": "Atribuir ao Grupo",
"changeFingerprint": "Alterar Impressão Digital",
"copyCookiesToProfile": "Copiar Cookies para o Perfil"
},
"ephemeral": "Efêmero",
"ephemeralDescription": "Os dados do navegador são excluídos ao fechar",
"ephemeralBadge": "Efêmero"
"ephemeralDescription": "O navegador é forçado a gravar os dados do perfil na memória em vez do disco. Os dados são excluídos ao fechar o navegador.",
"ephemeralBadge": "Efêmero",
"ephemeralAlpha": "Alpha"
},
"createProfile": {
"title": "Criar Novo Perfil",
@@ -265,6 +287,25 @@
}
},
"sync": {
"mode": {
"title": "Sincronização de perfil",
"description": "Gerenciar configurações de sincronização para \"{{name}}\"",
"disabled": "Desabilitado",
"regular": "Sincronização regular",
"encrypted": "Sincronização criptografada E2E",
"disabledDescription": "Sem sincronização para este perfil",
"regularDescription": "Sincronização rápida, sem criptografia",
"encryptedDescription": "Criptografado antes do upload. O servidor nunca vê os dados em texto simples.",
"noPasswordWarning": "Senha E2E não definida. Por favor defina uma senha nas Configurações.",
"passwordRequired": "Senha E2E não definida. Por favor defina uma senha nas Configurações primeiro.",
"enabledToast": "Sincronização habilitada",
"disabledToast": "Sincronização desabilitada",
"syncQueued": "Sincronização na fila",
"syncNow": "Sincronizar agora",
"lastSynced": "Última sincronização",
"notConfigured": "Serviço de sincronização não configurado.",
"configureService": "Configurar serviço de sincronização"
},
"title": "Serviço de Sincronização",
"config": "Configuração de Sincronização",
"serverUrl": "URL do Servidor",
@@ -486,7 +527,111 @@
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "A falsificação de impressão digital para um sistema operacional diferente é menos confiável porque é impossível imitar perfeitamente todos os componentes subjacentes. Use com cautela."
"crossOsWarning": "A falsificação de impressão digital para um sistema operacional diferente é menos confiável porque é impossível imitar perfeitamente todos os componentes subjacentes. Use com cautela.",
"crossOsLimitations": "A impressão digital entre sistemas operacionais tem limitações. APIs de nível de sistema ainda podem refletir seu sistema operacional real, e alguns recursos podem ter desempenho reduzido.",
"osLabel": "Impressão Digital do Sistema Operacional",
"selectOSPlaceholder": "Selecionar sistema operacional",
"generateRandomOnLaunch": "Gerar impressão digital aleatória a cada inicialização",
"generateRandomDescription": "Quando ativado, uma nova impressão digital será gerada cada vez que o navegador for iniciado.",
"generateRandomDescriptionAuto": "Quando ativado, uma nova impressão digital será gerada cada vez que o navegador for iniciado. A impressão digital gerada é salva para referência.",
"autoLocationDescription": "Configurar automaticamente as informações de localização com base na configuração do proxy ou na sua conexão se nenhum proxy for fornecido",
"editingDisabledRunning": "A edição de impressão digital está desativada porque o perfil está em execução. Pare o perfil para fazer alterações.",
"editingDisabledRandomized": "A edição de impressão digital está desativada porque a geração aleatória de impressão digital está ativada. Desative a opção acima para editar manualmente a configuração da impressão digital.",
"advancedWarning": "Aviso: Edite estes parâmetros apenas se souber o que está fazendo. Valores incorretos podem quebrar sites, fazer com que detectem você e levar a bugs difíceis de depurar.",
"basicWarning": "Aviso: Edite estes parâmetros apenas se souber o que está fazendo.",
"automatic": "Automático",
"manual": "Manual",
"blockingOptions": "Opções de Bloqueio",
"blockImages": "Bloquear Imagens",
"blockWebRTC": "Bloquear WebRTC",
"blockWebGL": "Bloquear WebGL",
"navigatorProperties": "Propriedades do Navigator",
"userAgent": "User Agent",
"userAgentAndPlatform": "User Agent & Platform",
"platform": "Platform",
"platformVersion": "Platform Version",
"appVersion": "App Version",
"osCpu": "OS CPU",
"hardwareConcurrency": "Hardware Concurrency",
"maxTouchPoints": "Pontos de Toque Máximos",
"doNotTrack": "Do Not Track",
"selectDntPlaceholder": "Selecionar valor DNT",
"dntAllowed": "0 (rastreamento permitido)",
"dntNotAllowed": "1 (rastreamento não permitido)",
"dntUnspecified": "não especificado",
"language": "Idioma",
"primaryLanguage": "Idioma Principal (navigator.language)",
"languages": "Idiomas (JSON array)",
"languageAndLocale": "Idioma e Localidade",
"screenProperties": "Propriedades da Tela",
"screenWidth": "Largura da Tela",
"screenHeight": "Altura da Tela",
"availableWidth": "Largura Disponível",
"availableHeight": "Altura Disponível",
"colorDepth": "Profundidade de Cor",
"pixelDepth": "Profundidade de Pixel",
"devicePixelRatio": "Proporção de Pixels do Dispositivo",
"windowProperties": "Propriedades da Janela",
"outerWidth": "Largura Externa",
"outerHeight": "Altura Externa",
"innerWidth": "Largura Interna",
"innerHeight": "Altura Interna",
"screenX": "Screen X",
"screenY": "Screen Y",
"geolocation": "Geolocalização",
"timezoneAndGeolocation": "Fuso Horário e Geolocalização",
"timezoneGeolocationDescription": "Estes valores substituem as APIs de fuso horário e geolocalização do navegador.",
"latitude": "Latitude",
"longitude": "Longitude",
"timezone": "Fuso Horário",
"timezoneIana": "Fuso Horário (IANA)",
"timezoneOffset": "Deslocamento (minutos a partir do UTC)",
"accuracy": "Precisão (metros)",
"locale": "Localidade",
"region": "Região",
"script": "Script",
"webglProperties": "Propriedades WebGL",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglParameters": "Parâmetros WebGL",
"webglParametersJson": "Parâmetros WebGL (JSON)",
"webgl2Parameters": "Parâmetros WebGL2",
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
"canvasFingerprint": "Canvas Fingerprint",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeedDescription": "Este seed é usado para gerar uma impressão digital Canvas consistente, mas única. Cada perfil deve ter um seed diferente.",
"fonts": "Fontes",
"fontsJson": "Fontes (JSON array)",
"battery": "Bateria",
"charging": "Carregando",
"chargingTime": "Tempo de Carregamento",
"dischargingTime": "Tempo de Descarregamento",
"batteryLevel": "Nível (0-1)",
"screenResolution": "Resolução da Tela",
"maxWidth": "Largura Máxima",
"maxHeight": "Altura Máxima",
"minWidth": "Largura Mínima",
"minHeight": "Altura Mínima",
"hardwareProperties": "Propriedades de Hardware",
"deviceMemory": "Memória do Dispositivo (GB)",
"audioProperties": "Propriedades de Áudio",
"sampleRate": "Taxa de Amostragem",
"maxChannelCount": "Contagem Máxima de Canais",
"vendorInfo": "Informações do Fabricante",
"vendor": "Fabricante",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"brand": "Marca",
"brandVersion": "Versão da Marca",
"proFeature": "Este é um recurso Pro"
},
"warnings": {
"windowResizeTitle": "Dimensões de janela personalizadas",
"windowResizeDescription": "Alterar as dimensões da janela do navegador pode aumentar a chance de detecção pelos sites de que as informações do navegador estão falsificadas.",
"dontShowAgain": "Não mostrar novamente",
"continue": "Continuar",
"cancel": "Cancelar"
},
"syncAll": {
"title": "Ativar sincronização para itens existentes",
@@ -508,6 +653,10 @@
"cannotModify": "Não é possível modificar as configurações de sincronização de um perfil de outro sistema operacional"
},
"cookies": {
"management": {
"title": "Gerenciamento de Cookies",
"menuItem": "Gerenciamento de Cookies"
},
"import": {
"title": "Importar Cookies",
"description": "Importar cookies de um arquivo no formato Netscape ou JSON.",
@@ -532,6 +681,7 @@
"fingerprintLocked": "A edição de impressão digital é um recurso Pro",
"cookieCopyLocked": "A cópia de cookies é um recurso Pro",
"cookieImportLocked": "A importação de cookies é um recurso Pro",
"cookieExportLocked": "A exportação de cookies é um recurso Pro"
"cookieExportLocked": "A exportação de cookies é um recurso Pro",
"cookieManagementLocked": "O gerenciamento de cookies é um recurso Pro"
}
}
+155 -5
View File
@@ -106,6 +106,22 @@
"description": "Настройте локальный API и MCP (Model Context Protocol) для интеграции с внешними инструментами и AI-ассистентами.",
"openSettings": "Открыть настройки интеграций"
},
"encryption": {
"title": "Шифрование синхронизации",
"description": "Установите пароль для включения E2E зашифрованной синхронизации. Если вы потеряете этот пароль, зашифрованные профили не могут быть восстановлены.",
"passwordSet": "Активно",
"passwordSetDescription": "Пароль шифрования E2E установлен",
"noPassword": "Пароль не установлен",
"passwordPlaceholder": "Пароль (мин. 8 символов)",
"confirmPlaceholder": "Подтвердите пароль",
"setPassword": "Установить пароль",
"changePassword": "Изменить пароль",
"removePassword": "Удалить пароль",
"removed": "Пароль шифрования удалён",
"passwordSaved": "Пароль шифрования установлен",
"passwordMismatch": "Пароли не совпадают",
"passwordTooShort": "Пароль должен содержать не менее 8 символов"
},
"commercial": {
"title": "Коммерческая лицензия",
"trialActive": "Пробный период: осталось {{days}} дней, {{hours}} часов",
@@ -157,11 +173,17 @@
"delete": "Удалить",
"copyCookies": "Копировать Cookie",
"configure": "Настроить",
"clone": "Клонировать профиль"
"clone": "Клонировать профиль",
"viewNetwork": "Просмотр сети",
"syncSettings": "Настройки синхронизации",
"assignToGroup": "Назначить группе",
"changeFingerprint": "Изменить отпечаток",
"copyCookiesToProfile": "Копировать Cookie в профиль"
},
"ephemeral": "Временный",
"ephemeralDescription": "Данные браузера удаляются при закрытии",
"ephemeralBadge": "Временный"
"ephemeralDescription": "Браузер принудительно записывает данные профиля в память вместо диска. Данные удаляются при закрытии браузера.",
"ephemeralBadge": "Временный",
"ephemeralAlpha": "Alpha"
},
"createProfile": {
"title": "Создать новый профиль",
@@ -265,6 +287,25 @@
}
},
"sync": {
"mode": {
"title": "Синхронизация профиля",
"description": "Управление настройками синхронизации для \"{{name}}\"",
"disabled": "Отключено",
"regular": "Обычная синхронизация",
"encrypted": "E2E зашифрованная синхронизация",
"disabledDescription": "Без синхронизации для этого профиля",
"regularDescription": "Быстрая синхронизация, без шифрования",
"encryptedDescription": "Шифрование перед загрузкой. Сервер никогда не видит данные в открытом виде.",
"noPasswordWarning": "Пароль E2E не установлен. Пожалуйста, установите пароль в Настройках.",
"passwordRequired": "Пароль E2E не установлен. Сначала установите пароль в Настройках.",
"enabledToast": "Синхронизация включена",
"disabledToast": "Синхронизация отключена",
"syncQueued": "Синхронизация в очереди",
"syncNow": "Синхронизировать сейчас",
"lastSynced": "Последняя синхронизация",
"notConfigured": "Сервис синхронизации не настроен.",
"configureService": "Настроить сервис синхронизации"
},
"title": "Служба синхронизации",
"config": "Настройка синхронизации",
"serverUrl": "URL сервера",
@@ -486,7 +527,111 @@
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "Подмена отпечатка для другой операционной системы менее надёжна, так как невозможно идеально имитировать все базовые компоненты. Используйте с осторожностью."
"crossOsWarning": "Подмена отпечатка для другой операционной системы менее надёжна, так как невозможно идеально имитировать все базовые компоненты. Используйте с осторожностью.",
"crossOsLimitations": "Подмена отпечатка другой ОС имеет ограничения. Системные API могут по-прежнему отражать вашу реальную операционную систему, а некоторые функции могут работать с пониженной производительностью.",
"osLabel": "Отпечаток операционной системы",
"selectOSPlaceholder": "Выберите операционную систему",
"generateRandomOnLaunch": "Генерировать случайный отпечаток при каждом запуске",
"generateRandomDescription": "При включении новый отпечаток будет генерироваться при каждом запуске браузера.",
"generateRandomDescriptionAuto": "При включении новый отпечаток будет генерироваться при каждом запуске браузера. Сгенерированный отпечаток сохраняется для справки.",
"autoLocationDescription": "Автоматически настраивать информацию о местоположении на основе конфигурации прокси или вашего соединения, если прокси не указан",
"editingDisabledRunning": "Редактирование отпечатка отключено, так как профиль в данный момент запущен. Остановите профиль для внесения изменений.",
"editingDisabledRandomized": "Редактирование отпечатка отключено, так как включена генерация случайного отпечатка. Отключите опцию выше для ручного редактирования конфигурации отпечатка.",
"advancedWarning": "Внимание: редактируйте эти параметры только если вы знаете, что делаете. Неправильные значения могут нарушить работу сайтов, привести к вашему обнаружению и вызвать трудноотлаживаемые ошибки.",
"basicWarning": "Внимание: редактируйте эти параметры только если вы знаете, что делаете.",
"automatic": "Автоматически",
"manual": "Вручную",
"blockingOptions": "Параметры блокировки",
"blockImages": "Блокировать изображения",
"blockWebRTC": "Блокировать WebRTC",
"blockWebGL": "Блокировать WebGL",
"navigatorProperties": "Свойства Navigator",
"userAgent": "User Agent",
"userAgentAndPlatform": "User Agent и платформа",
"platform": "Платформа",
"platformVersion": "Версия платформы",
"appVersion": "Версия приложения",
"osCpu": "OS CPU",
"hardwareConcurrency": "Количество потоков процессора",
"maxTouchPoints": "Максимальное количество точек касания",
"doNotTrack": "Do Not Track",
"selectDntPlaceholder": "Выберите значение DNT",
"dntAllowed": "0 (отслеживание разрешено)",
"dntNotAllowed": "1 (отслеживание не разрешено)",
"dntUnspecified": "не указано",
"language": "Язык",
"primaryLanguage": "Основной язык (navigator.language)",
"languages": "Языки (JSON-массив)",
"languageAndLocale": "Язык и локаль",
"screenProperties": "Свойства экрана",
"screenWidth": "Ширина экрана",
"screenHeight": "Высота экрана",
"availableWidth": "Доступная ширина",
"availableHeight": "Доступная высота",
"colorDepth": "Глубина цвета",
"pixelDepth": "Глубина пикселей",
"devicePixelRatio": "Соотношение пикселей устройства",
"windowProperties": "Свойства окна",
"outerWidth": "Внешняя ширина",
"outerHeight": "Внешняя высота",
"innerWidth": "Внутренняя ширина",
"innerHeight": "Внутренняя высота",
"screenX": "Screen X",
"screenY": "Screen Y",
"geolocation": "Геолокация",
"timezoneAndGeolocation": "Часовой пояс и геолокация",
"timezoneGeolocationDescription": "Эти значения переопределяют API часового пояса и геолокации браузера.",
"latitude": "Широта",
"longitude": "Долгота",
"timezone": "Часовой пояс",
"timezoneIana": "Часовой пояс (IANA)",
"timezoneOffset": "Смещение (минуты от UTC)",
"accuracy": "Точность (метры)",
"locale": "Локаль",
"region": "Регион",
"script": "Скрипт",
"webglProperties": "Свойства WebGL",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglParameters": "Параметры WebGL",
"webglParametersJson": "Параметры WebGL (JSON)",
"webgl2Parameters": "Параметры WebGL2",
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
"canvasFingerprint": "Отпечаток Canvas",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeedDescription": "Это зерно используется для генерации постоянного, но уникального отпечатка Canvas. У каждого профиля должно быть своё зерно.",
"fonts": "Шрифты",
"fontsJson": "Шрифты (JSON-массив)",
"battery": "Батарея",
"charging": "Зарядка",
"chargingTime": "Время зарядки",
"dischargingTime": "Время разрядки",
"batteryLevel": "Уровень (0-1)",
"screenResolution": "Разрешение экрана",
"maxWidth": "Макс. ширина",
"maxHeight": "Макс. высота",
"minWidth": "Мин. ширина",
"minHeight": "Мин. высота",
"hardwareProperties": "Свойства оборудования",
"deviceMemory": "Память устройства (ГБ)",
"audioProperties": "Свойства аудио",
"sampleRate": "Частота дискретизации",
"maxChannelCount": "Максимальное количество каналов",
"vendorInfo": "Информация о производителе",
"vendor": "Производитель",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"brand": "Бренд",
"brandVersion": "Версия бренда",
"proFeature": "Это функция Pro"
},
"warnings": {
"windowResizeTitle": "Пользовательские размеры окна",
"windowResizeDescription": "Изменение размеров окна браузера может повысить вероятность обнаружения сайтами того, что информация браузера подменена.",
"dontShowAgain": "Больше не показывать",
"continue": "Продолжить",
"cancel": "Отмена"
},
"syncAll": {
"title": "Включить синхронизацию для существующих элементов",
@@ -508,6 +653,10 @@
"cannotModify": "Невозможно изменить настройки синхронизации профиля другой ОС"
},
"cookies": {
"management": {
"title": "Управление Cookies",
"menuItem": "Управление Cookies"
},
"import": {
"title": "Импорт Cookies",
"description": "Импорт cookies из файла в формате Netscape или JSON.",
@@ -532,6 +681,7 @@
"fingerprintLocked": "Редактирование отпечатка — функция Pro",
"cookieCopyLocked": "Копирование cookies — функция Pro",
"cookieImportLocked": "Импорт cookies — функция Pro",
"cookieExportLocked": "Экспорт cookies — функция Pro"
"cookieExportLocked": "Экспорт cookies — функция Pro",
"cookieManagementLocked": "Управление cookies — функция Pro"
}
}
+155 -5
View File
@@ -106,6 +106,22 @@
"description": "配置本地 API 和 MCP(模型上下文协议)以与外部工具和 AI 助手集成。",
"openSettings": "打开集成设置"
},
"encryption": {
"title": "同步加密",
"description": "设置密码以启用E2E加密同步。如果您丢失此密码,加密的配置文件将无法恢复。",
"passwordSet": "已启用",
"passwordSetDescription": "E2E加密密码已设置",
"noPassword": "未设置密码",
"passwordPlaceholder": "密码(至少8个字符)",
"confirmPlaceholder": "确认密码",
"setPassword": "设置密码",
"changePassword": "更改密码",
"removePassword": "删除密码",
"removed": "加密密码已删除",
"passwordSaved": "加密密码已设置",
"passwordMismatch": "密码不匹配",
"passwordTooShort": "密码必须至少8个字符"
},
"commercial": {
"title": "商业许可",
"trialActive": "试用期:剩余 {{days}} 天 {{hours}} 小时",
@@ -157,11 +173,17 @@
"delete": "删除",
"copyCookies": "复制 Cookies",
"configure": "配置",
"clone": "克隆配置文件"
"clone": "克隆配置文件",
"viewNetwork": "查看网络",
"syncSettings": "同步设置",
"assignToGroup": "分配到组",
"changeFingerprint": "更改指纹",
"copyCookiesToProfile": "复制 Cookies 到配置文件"
},
"ephemeral": "临时",
"ephemeralDescription": "关闭浏览器时数据将被删除",
"ephemeralBadge": "临时"
"ephemeralDescription": "浏览器被强制将配置数据写入内存而非磁盘。关闭浏览器时数据将被删除",
"ephemeralBadge": "临时",
"ephemeralAlpha": "Alpha"
},
"createProfile": {
"title": "创建新配置文件",
@@ -265,6 +287,25 @@
}
},
"sync": {
"mode": {
"title": "配置文件同步",
"description": "管理 \"{{name}}\" 的同步设置",
"disabled": "已禁用",
"regular": "常规同步",
"encrypted": "E2E加密同步",
"disabledDescription": "此配置文件不同步",
"regularDescription": "快速同步,无加密",
"encryptedDescription": "上传前加密。服务器永远不会看到明文数据。",
"noPasswordWarning": "未设置E2E密码。请在设置中设置密码。",
"passwordRequired": "未设置E2E密码。请先在设置中设置密码。",
"enabledToast": "同步已启用",
"disabledToast": "同步已禁用",
"syncQueued": "同步已排队",
"syncNow": "立即同步",
"lastSynced": "上次同步",
"notConfigured": "同步服务未配置。",
"configureService": "配置同步服务"
},
"title": "同步服务",
"config": "同步配置",
"serverUrl": "服务器 URL",
@@ -486,7 +527,111 @@
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "伪装不同操作系统的指纹不太可靠,因为不可能完美模拟所有底层组件。请谨慎使用。"
"crossOsWarning": "伪装不同操作系统的指纹不太可靠,因为不可能完美模拟所有底层组件。请谨慎使用。",
"crossOsLimitations": "跨操作系统指纹伪装存在局限性。系统级 API 可能仍会反映您的实际操作系统,某些功能的性能可能会降低。",
"osLabel": "操作系统指纹",
"selectOSPlaceholder": "选择操作系统",
"generateRandomOnLaunch": "每次启动时生成随机指纹",
"generateRandomDescription": "启用后,每次启动浏览器时将生成新的指纹。",
"generateRandomDescriptionAuto": "启用后,每次启动浏览器时将生成新的指纹。生成的指纹会保存以供参考。",
"autoLocationDescription": "根据代理配置或您的连接(未提供代理时)自动配置位置信息",
"editingDisabledRunning": "指纹编辑已禁用,因为配置文件正在运行中。停止配置文件后才能进行更改。",
"editingDisabledRandomized": "指纹编辑已禁用,因为已启用随机指纹生成。禁用上方选项后才能手动编辑指纹配置。",
"advancedWarning": "警告:请仅在了解自己操作的情况下编辑这些参数。错误的值可能导致网站无法正常工作、被检测到,并引发难以调试的问题。",
"basicWarning": "警告:请仅在了解自己操作的情况下编辑这些参数。",
"automatic": "自动",
"manual": "手动",
"blockingOptions": "阻止选项",
"blockImages": "阻止图片",
"blockWebRTC": "阻止 WebRTC",
"blockWebGL": "阻止 WebGL",
"navigatorProperties": "Navigator 属性",
"userAgent": "User Agent",
"userAgentAndPlatform": "User Agent 和平台",
"platform": "平台",
"platformVersion": "平台版本",
"appVersion": "应用版本",
"osCpu": "OS CPU",
"hardwareConcurrency": "硬件并发数",
"maxTouchPoints": "最大触摸点数",
"doNotTrack": "Do Not Track",
"selectDntPlaceholder": "选择 DNT 值",
"dntAllowed": "0(允许跟踪)",
"dntNotAllowed": "1(不允许跟踪)",
"dntUnspecified": "未指定",
"language": "语言",
"primaryLanguage": "主要语言 (navigator.language)",
"languages": "语言列表 (JSON 数组)",
"languageAndLocale": "语言和区域设置",
"screenProperties": "屏幕属性",
"screenWidth": "屏幕宽度",
"screenHeight": "屏幕高度",
"availableWidth": "可用宽度",
"availableHeight": "可用高度",
"colorDepth": "颜色深度",
"pixelDepth": "像素深度",
"devicePixelRatio": "设备像素比",
"windowProperties": "窗口属性",
"outerWidth": "外部宽度",
"outerHeight": "外部高度",
"innerWidth": "内部宽度",
"innerHeight": "内部高度",
"screenX": "Screen X",
"screenY": "Screen Y",
"geolocation": "地理位置",
"timezoneAndGeolocation": "时区和地理位置",
"timezoneGeolocationDescription": "这些值会覆盖浏览器的时区和地理位置 API。",
"latitude": "纬度",
"longitude": "经度",
"timezone": "时区",
"timezoneIana": "时区 (IANA)",
"timezoneOffset": "偏移量(距 UTC 分钟数)",
"accuracy": "精度(米)",
"locale": "区域设置",
"region": "地区",
"script": "脚本",
"webglProperties": "WebGL 属性",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglParameters": "WebGL 参数",
"webglParametersJson": "WebGL 参数 (JSON)",
"webgl2Parameters": "WebGL2 参数",
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
"canvasFingerprint": "Canvas 指纹",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeedDescription": "此种子用于生成一致但唯一的 Canvas 指纹。每个配置文件应使用不同的种子。",
"fonts": "字体",
"fontsJson": "字体 (JSON 数组)",
"battery": "电池",
"charging": "充电中",
"chargingTime": "充电时间",
"dischargingTime": "放电时间",
"batteryLevel": "电量 (0-1)",
"screenResolution": "屏幕分辨率",
"maxWidth": "最大宽度",
"maxHeight": "最大高度",
"minWidth": "最小宽度",
"minHeight": "最小高度",
"hardwareProperties": "硬件属性",
"deviceMemory": "设备内存 (GB)",
"audioProperties": "音频属性",
"sampleRate": "采样率",
"maxChannelCount": "最大通道数",
"vendorInfo": "供应商信息",
"vendor": "供应商",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"brand": "品牌",
"brandVersion": "品牌版本",
"proFeature": "这是 Pro 功能"
},
"warnings": {
"windowResizeTitle": "自定义窗口尺寸",
"windowResizeDescription": "更改浏览器窗口尺寸可能会增加网站检测到浏览器信息被伪装的概率。",
"dontShowAgain": "不再显示",
"continue": "继续",
"cancel": "取消"
},
"syncAll": {
"title": "为现有项目启用同步",
@@ -508,6 +653,10 @@
"cannotModify": "无法修改跨操作系统配置文件的同步设置"
},
"cookies": {
"management": {
"title": "Cookie 管理",
"menuItem": "Cookie 管理"
},
"import": {
"title": "导入 Cookies",
"description": "从 Netscape 或 JSON 格式文件导入 Cookies。",
@@ -532,6 +681,7 @@
"fingerprintLocked": "指纹编辑是 Pro 功能",
"cookieCopyLocked": "Cookie 复制是 Pro 功能",
"cookieImportLocked": "Cookie 导入是 Pro 功能",
"cookieExportLocked": "Cookie 导出是 Pro 功能"
"cookieExportLocked": "Cookie 导出是 Pro 功能",
"cookieManagementLocked": "Cookie 管理是 Pro 功能"
}
}
+14 -1
View File
@@ -3,7 +3,12 @@
* Centralized helpers for browser name mapping, icons, etc.
*/
import { FaChrome, FaExclamationTriangle, FaFirefox } from "react-icons/fa";
import {
FaChrome,
FaExclamationTriangle,
FaFire,
FaFirefox,
} from "react-icons/fa";
/**
* Map internal browser names to display names
@@ -39,6 +44,14 @@ export function getBrowserIcon(browserType: string) {
}
}
export function getProfileIcon(profile: {
browser: string;
ephemeral?: boolean;
}) {
if (profile.ephemeral) return FaFire;
return getBrowserIcon(profile.browser);
}
export const getCurrentOS = () => {
if (typeof window !== "undefined") {
const userAgent = window.navigator.userAgent;
+8 -1
View File
@@ -26,12 +26,15 @@ export interface BrowserProfile {
group_id?: string; // Reference to profile group
tags?: string[];
note?: string; // User note
sync_enabled?: boolean; // Whether sync is enabled for this profile
sync_mode?: SyncMode;
encryption_salt?: string;
last_sync?: number; // Timestamp of last successful sync (epoch seconds)
host_os?: string; // OS where profile was created ("macos", "windows", "linux")
ephemeral?: boolean;
}
export type SyncMode = "Disabled" | "Regular" | "Encrypted";
export type SyncStatus = "Disabled" | "Syncing" | "Synced" | "Error";
export interface SyncSettings {
@@ -70,6 +73,10 @@ export interface ProxyCheckResult {
is_valid: boolean;
}
export function isSyncEnabled(profile: BrowserProfile): boolean {
return profile.sync_mode != null && profile.sync_mode !== "Disabled";
}
export const CLOUD_PROXY_ID = "cloud-included-proxy";
export interface StoredProxy {