From c61b3d318822e8e096cb7a5d640706716623449a Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:50:23 +0400 Subject: [PATCH] feat: netscape cookie import --- src-tauri/src/cookie_manager.rs | 110 ++++++++++ src-tauri/src/lib.rs | 22 ++ src-tauri/src/profile/manager.rs | 16 ++ src/app/page.tsx | 74 ++++--- src/components/camoufox-config-dialog.tsx | 19 +- src/components/cookie-import-dialog.tsx | 202 ++++++++++++++++++ src/components/create-profile-dialog.tsx | 50 +++-- src/components/profile-data-table.tsx | 85 ++++++-- .../shared-camoufox-config-form.tsx | 10 +- src/components/sync-config-dialog.tsx | 11 +- src/components/ui/pro-badge.tsx | 14 ++ src/components/wayfern-config-form.tsx | 10 +- src/i18n/locales/en.json | 17 ++ src/i18n/locales/es.json | 17 ++ src/i18n/locales/fr.json | 17 ++ src/i18n/locales/ja.json | 17 ++ src/i18n/locales/pt.json | 17 ++ src/i18n/locales/ru.json | 17 ++ src/i18n/locales/zh.json | 17 ++ 19 files changed, 657 insertions(+), 85 deletions(-) create mode 100644 src/components/cookie-import-dialog.tsx create mode 100644 src/components/ui/pro-badge.tsx diff --git a/src-tauri/src/cookie_manager.rs b/src-tauri/src/cookie_manager.rs index dd19b51..5225348 100644 --- a/src-tauri/src/cookie_manager.rs +++ b/src-tauri/src/cookie_manager.rs @@ -62,6 +62,14 @@ pub struct CookieCopyResult { pub errors: Vec, } +/// Result of a cookie import operation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CookieImportResult { + pub cookies_imported: usize, + pub cookies_replaced: usize, + pub errors: Vec, +} + pub struct CookieManager; impl CookieManager { @@ -493,4 +501,106 @@ impl CookieManager { Ok(results) } + + /// Parse Netscape format cookies from text content + fn parse_netscape_cookies(content: &str) -> (Vec, Vec) { + let mut cookies = Vec::new(); + let mut errors = Vec::new(); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + + for (i, line) in content.lines().enumerate() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + let fields: Vec<&str> = line.split('\t').collect(); + if fields.len() < 7 { + errors.push(format!( + "Line {}: expected 7 tab-separated fields, got {}", + i + 1, + fields.len() + )); + continue; + } + + let domain = fields[0].to_string(); + let path = fields[2].to_string(); + let is_secure = fields[3].eq_ignore_ascii_case("TRUE"); + let expires = fields[4].parse::().unwrap_or(0); + let name = fields[5].to_string(); + let value = fields[6].to_string(); + + cookies.push(UnifiedCookie { + name, + value, + domain, + path, + expires, + is_secure, + is_http_only: false, + same_site: 0, + creation_time: now, + last_accessed: now, + }); + } + + (cookies, errors) + } + + /// Public API: Import cookies from Netscape format content + pub async fn import_netscape_cookies( + app_handle: &AppHandle, + profile_id: &str, + content: &str, + ) -> Result { + let profile_manager = ProfileManager::instance(); + let profiles_dir = profile_manager.get_profiles_dir(); + let profiles = profile_manager + .list_profiles() + .map_err(|e| format!("Failed to list profiles: {e}"))?; + + let profile = profiles + .iter() + .find(|p| p.id.to_string() == profile_id) + .ok_or_else(|| format!("Profile not found: {profile_id}"))?; + + let is_running = profile_manager + .check_browser_status(app_handle.clone(), profile) + .await + .unwrap_or(false); + + if is_running { + return Err(format!( + "Cannot import cookies while browser is running for profile: {}", + profile.name + )); + } + + let (cookies, parse_errors) = Self::parse_netscape_cookies(content); + + if cookies.is_empty() { + return Err("No valid cookies found in the file".to_string()); + } + + let db_path = Self::get_cookie_db_path(profile, &profiles_dir)?; + + let write_result = match profile.browser.as_str() { + "camoufox" => Self::write_firefox_cookies(&db_path, &cookies), + "wayfern" => Self::write_chrome_cookies(&db_path, &cookies), + _ => return Err(format!("Unsupported browser type: {}", profile.browser)), + }; + + match write_result { + Ok((imported, replaced)) => Ok(CookieImportResult { + cookies_imported: imported, + cookies_replaced: replaced, + errors: parse_errors, + }), + Err(e) => Err(format!("Failed to write cookies: {e}")), + } + } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7143f4e..3bf5b2c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -283,9 +283,30 @@ async fn copy_profile_cookies( app_handle: tauri::AppHandle, request: cookie_manager::CookieCopyRequest, ) -> Result, String> { + if !crate::cloud_auth::CLOUD_AUTH + .has_active_paid_subscription() + .await + { + return Err("Cookie copying requires an active Pro subscription".to_string()); + } cookie_manager::CookieManager::copy_cookies(&app_handle, request).await } +#[tauri::command] +async fn import_cookies_from_file( + app_handle: tauri::AppHandle, + profile_id: String, + content: String, +) -> Result { + if !crate::cloud_auth::CLOUD_AUTH + .has_active_paid_subscription() + .await + { + return Err("Cookie import requires an active Pro subscription".to_string()); + } + cookie_manager::CookieManager::import_netscape_cookies(&app_handle, &profile_id, &content).await +} + #[tauri::command] fn check_wayfern_terms_accepted() -> bool { wayfern_terms::WayfernTermsManager::instance().is_terms_accepted() @@ -1313,6 +1334,7 @@ pub fn run() { enable_sync_for_all_entities, read_profile_cookies, copy_profile_cookies, + import_cookies_from_file, check_wayfern_terms_accepted, check_wayfern_downloaded, accept_wayfern_terms, diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index c0e4dee..7104f80 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -2008,6 +2008,14 @@ pub async fn update_camoufox_config( profile_id: String, config: CamoufoxConfig, ) -> Result<(), String> { + if config.fingerprint.is_some() + && !crate::cloud_auth::CLOUD_AUTH + .has_active_paid_subscription() + .await + { + return Err("Fingerprint editing requires an active Pro subscription".to_string()); + } + if !crate::cloud_auth::CLOUD_AUTH .is_fingerprint_os_allowed(config.os.as_deref()) .await @@ -2028,6 +2036,14 @@ pub async fn update_wayfern_config( profile_id: String, config: WayfernConfig, ) -> Result<(), String> { + if config.fingerprint.is_some() + && !crate::cloud_auth::CLOUD_AUTH + .has_active_paid_subscription() + .await + { + return Err("Fingerprint editing requires an active Pro subscription".to_string()); + } + if !crate::cloud_auth::CLOUD_AUTH .is_fingerprint_os_allowed(config.os.as_deref()) .await diff --git a/src/app/page.tsx b/src/app/page.tsx index 03755ce..38b2d1f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,6 +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 { CookieImportDialog } from "@/components/cookie-import-dialog"; import { CreateProfileDialog } from "@/components/create-profile-dialog"; import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"; import { GroupAssignmentDialog } from "@/components/group-assignment-dialog"; @@ -142,6 +143,9 @@ export default function Home() { const [proxyAssignmentDialogOpen, setProxyAssignmentDialogOpen] = useState(false); const [cookieCopyDialogOpen, setCookieCopyDialogOpen] = useState(false); + const [cookieImportDialogOpen, setCookieImportDialogOpen] = useState(false); + const [currentProfileForCookieImport, setCurrentProfileForCookieImport] = + useState(null); const [selectedProfilesForCookies, setSelectedProfilesForCookies] = useState< string[] >([]); @@ -688,6 +692,11 @@ export default function Home() { setCookieCopyDialogOpen(true); }, []); + const handleImportCookies = useCallback((profile: BrowserProfile) => { + setCurrentProfileForCookieImport(profile); + setCookieImportDialogOpen(true); + }, []); + const handleGroupAssignmentComplete = useCallback(async () => { // No need to manually reload - useProfileEvents will handle the update setGroupAssignmentDialogOpen(false); @@ -737,34 +746,37 @@ export default function Home() { let unlisten: (() => void) | undefined; (async () => { try { - unlisten = await listen<{ profile_id: string; status: string }>( - "profile-sync-status", - (event) => { - const { profile_id, status } = event.payload; - if (!userInitiatedSyncIds.current.has(profile_id)) return; + unlisten = await listen<{ + profile_id: string; + status: string; + error?: string; + }>("profile-sync-status", (event) => { + const { profile_id, status, error } = event.payload; + if (!userInitiatedSyncIds.current.has(profile_id)) return; - const toastId = `sync-${profile_id}`; - const profile = profiles.find((p) => p.id === profile_id); - const name = profile?.name ?? "Unknown"; + const toastId = `sync-${profile_id}`; + const profile = profiles.find((p) => p.id === profile_id); + const name = profile?.name ?? "Unknown"; - if (status === "syncing") { - showToast({ - type: "loading", - title: `Syncing profile '${name}'...`, - id: toastId, - duration: 30000, - }); - } else if (status === "synced") { - dismissToast(toastId); - showSuccessToast(`Profile '${name}' synced successfully`); - userInitiatedSyncIds.current.delete(profile_id); - } else if (status === "error") { - dismissToast(toastId); - showErrorToast(`Failed to sync profile '${name}'`); - userInitiatedSyncIds.current.delete(profile_id); - } - }, - ); + if (status === "syncing") { + showToast({ + type: "loading", + title: `Syncing profile '${name}'...`, + id: toastId, + duration: 30000, + }); + } else if (status === "synced") { + dismissToast(toastId); + showSuccessToast(`Profile '${name}' synced successfully`); + userInitiatedSyncIds.current.delete(profile_id); + } else if (status === "error") { + dismissToast(toastId); + showErrorToast( + `Failed to sync profile '${name}'${error ? `: ${error}` : ""}`, + ); + userInitiatedSyncIds.current.delete(profile_id); + } + }); } catch (error) { console.error("Failed to listen for sync status events:", error); } @@ -991,6 +1003,7 @@ export default function Home() { onRenameProfile={handleRenameProfile} onConfigureCamoufox={handleConfigureCamoufox} onCopyCookiesToProfile={handleCopyCookiesToProfile} + onImportCookies={handleImportCookies} runningProfiles={runningProfiles} isUpdating={isUpdating} onDeleteSelectedProfiles={handleDeleteSelectedProfiles} @@ -1134,6 +1147,15 @@ export default function Home() { onCopyComplete={() => setSelectedProfilesForCookies([])} /> + { + setCookieImportDialogOpen(false); + setCurrentProfileForCookieImport(null); + }} + profile={currentProfileForCookieImport} + /> + setShowBulkDeleteConfirmation(false)} diff --git a/src/components/camoufox-config-dialog.tsx b/src/components/camoufox-config-dialog.tsx index 83cd918..f8a2b76 100644 --- a/src/components/camoufox-config-dialog.tsx +++ b/src/components/camoufox-config-dialog.tsx @@ -26,7 +26,9 @@ 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 { @@ -155,13 +157,13 @@ export function CamoufoxConfigDialog({ -
+
{profile.browser === "wayfern" ? ( ) : ( @@ -169,11 +171,20 @@ export function CamoufoxConfigDialog({ config={config as CamoufoxConfig} onConfigChange={updateConfig} forceAdvanced={true} - readOnly={isRunning} + readOnly={isRunning || !crossOsUnlocked} browserType="camoufox" crossOsUnlocked={crossOsUnlocked} /> )} + {!crossOsUnlocked && ( +
+ +

+ Fingerprint editing is a Pro feature +

+ +
+ )}
@@ -181,7 +192,7 @@ export function CamoufoxConfigDialog({ {isRunning ? "Close" : "Cancel"} - {!isRunning && ( + {!isRunning && crossOsUnlocked && ( void; + profile: BrowserProfile | null; +} + +const countCookies = (content: string): number => { + return content.split("\n").filter((line) => { + const trimmed = line.trim(); + return trimmed && !trimmed.startsWith("#"); + }).length; +}; + +export function CookieImportDialog({ + isOpen, + onClose, + profile, +}: CookieImportDialogProps) { + const [fileContent, setFileContent] = useState(null); + const [fileName, setFileName] = useState(null); + const [cookieCount, setCookieCount] = useState(0); + const [isImporting, setIsImporting] = useState(false); + const [result, setResult] = useState(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( + "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 ( + + + + Import Cookies + + {!fileContent && "Import cookies from a Netscape format file."} + {fileContent && + !result && + `${cookieCount} cookies found in ${fileName}`} + {result && "Cookie import completed"} + + + + {!fileContent && ( +
+
+ document.getElementById("cookie-file-input")?.click() + } + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + document.getElementById("cookie-file-input")?.click(); + } + }} + > + +

+ Click to choose a Netscape cookie file +
+ (.txt or .cookies) +

+ { + const file = e.target.files?.[0]; + if (file) handleFileRead(file); + e.target.value = ""; + }} + /> +
+
+ )} + + {fileContent && !result && ( +
+
+
+
{fileName}
+
+ {cookieCount} cookies found +
+
+
+
+ )} + + {result && ( +
+
+
+ Successfully imported {result.cookies_imported} cookies ( + {result.cookies_replaced} replaced) +
+ {result.errors.length > 0 && ( +
+ {result.errors.length} line(s) skipped +
+ )} +
+
+ )} + + + {!fileContent && ( + + Cancel + + )} + + {fileContent && !result && ( + <> + + Back + + void handleImport()} + disabled={cookieCount === 0} + > + Import + + + )} + + {result && Done} + +
+
+ ); +} diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index c74a877..9a5f942 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -3,6 +3,7 @@ import { invoke } from "@tauri-apps/api/core"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 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"; @@ -16,6 +17,7 @@ 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, @@ -748,12 +750,23 @@ export function CreateProfileDialog({
)} - +
+ + {!crossOsUnlocked && ( +
+ +

+ Fingerprint editing is a Pro feature +

+ +
+ )} +
) : selectedBrowser === "camoufox" ? ( // Camoufox Configuration @@ -845,13 +858,24 @@ export function CreateProfileDialog({ )} - +
+ + {!crossOsUnlocked && ( +
+ +

+ Fingerprint editing is a Pro feature +

+ +
+ )} +
) : ( // Regular Browser Configuration (should not happen in anti-detect tab) diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index a1ca3d9..2f47ff3 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -21,7 +21,6 @@ import { LuChevronDown, LuChevronUp, LuCookie, - LuLock, LuTrash2, LuUsers, } from "react-icons/lu"; @@ -48,6 +47,7 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { ProBadge } from "@/components/ui/pro-badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Table, @@ -176,13 +176,14 @@ type TableMeta = { onConfigureCamoufox?: (profile: BrowserProfile) => void; onCloneProfile?: (profile: BrowserProfile) => void; onCopyCookiesToProfile?: (profile: BrowserProfile) => void; + onImportCookies?: (profile: BrowserProfile) => void; // Traffic snapshots (lightweight real-time data) trafficSnapshots: Record; onOpenTrafficDialog?: (profileId: string) => void; // Sync - syncStatuses: Record; + syncStatuses: Record; onOpenProfileSyncDialog?: (profile: BrowserProfile) => void; onToggleProfileSync?: (profile: BrowserProfile) => void; crossOsUnlocked?: boolean; @@ -209,6 +210,7 @@ function getProfileSyncStatusDot( | "error" | "disabled" | undefined, + errorMessage?: string, ): SyncStatusDot | null { const status = liveStatus ?? (profile.sync_enabled ? "synced" : "disabled"); @@ -230,7 +232,11 @@ function getProfileSyncStatusDot( animate: false, }; case "error": - return { color: "bg-red-500", tooltip: "Sync error", animate: false }; + return { + color: "bg-red-500", + tooltip: errorMessage ? `Sync error: ${errorMessage}` : "Sync error", + animate: false, + }; case "disabled": if (profile.last_sync) { return { @@ -751,6 +757,7 @@ interface ProfilesDataTableProps { onRenameProfile: (profileId: string, newName: string) => Promise; onConfigureCamoufox: (profile: BrowserProfile) => void; onCopyCookiesToProfile?: (profile: BrowserProfile) => void; + onImportCookies?: (profile: BrowserProfile) => void; runningProfiles: Set; isUpdating: (browser: string) => boolean; onDeleteSelectedProfiles: (profileIds: string[]) => Promise; @@ -777,6 +784,7 @@ export function ProfilesDataTable({ onRenameProfile, onConfigureCamoufox, onCopyCookiesToProfile, + onImportCookies, runningProfiles, isUpdating, onAssignProfilesToGroup, @@ -900,7 +908,7 @@ export function ProfilesDataTable({ name?: string; } | null>(null); const [syncStatuses, setSyncStatuses] = React.useState< - Record + Record >({}); // Country proxy creation state (for inline proxy creation in dropdown) @@ -1041,13 +1049,17 @@ export function ProfilesDataTable({ let unlisten: (() => void) | undefined; (async () => { try { - unlisten = await listen<{ profile_id: string; status: string }>( - "profile-sync-status", - (event) => { - const { profile_id, status } = event.payload; - setSyncStatuses((prev) => ({ ...prev, [profile_id]: status })); - }, - ); + unlisten = await listen<{ + profile_id: string; + status: string; + error?: string; + }>("profile-sync-status", (event) => { + const { profile_id, status, error } = event.payload; + setSyncStatuses((prev) => ({ + ...prev, + [profile_id]: { status, error }, + })); + }); } catch (error) { console.error("Failed to listen for sync status events:", error); } @@ -1462,6 +1474,7 @@ export function ProfilesDataTable({ onCloneProfile, onConfigureCamoufox, onCopyCookiesToProfile, + onImportCookies, // Traffic snapshots (lightweight real-time data) trafficSnapshots, @@ -1523,6 +1536,7 @@ export function ProfilesDataTable({ onCloneProfile, onConfigureCamoufox, onCopyCookiesToProfile, + onImportCookies, syncStatuses, onOpenProfileSyncDialog, onToggleProfileSync, @@ -2267,7 +2281,8 @@ export function ProfilesDataTable({ cell: ({ row, table }) => { const profile = row.original; const meta = table.options.meta as TableMeta; - const liveStatus = meta.syncStatuses[profile.id] as + const syncEntry = meta.syncStatuses[profile.id]; + const liveStatus = syncEntry?.status as | "syncing" | "waiting" | "synced" @@ -2275,7 +2290,11 @@ export function ProfilesDataTable({ | "disabled" | undefined; - const dot = getProfileSyncStatusDot(profile, liveStatus); + const dot = getProfileSyncStatusDot( + profile, + liveStatus, + syncEntry?.error, + ); if (!dot) return null; return ( @@ -2345,9 +2364,7 @@ export function ProfilesDataTable({ > {profile.sync_enabled ? "Disable Sync" : "Enable Sync"} - {!meta.syncUnlocked && ( - - )} + {!meta.syncUnlocked && } - Change Fingerprint + + Change Fingerprint + {!meta.crossOsUnlocked && } + )} {(profile.browser === "camoufox" || @@ -2375,11 +2395,33 @@ export function ProfilesDataTable({ meta.onCopyCookiesToProfile && ( { - meta.onCopyCookiesToProfile?.(profile); + if (meta.crossOsUnlocked) { + meta.onCopyCookiesToProfile?.(profile); + } }} - disabled={isDisabled} + disabled={isDisabled || !meta.crossOsUnlocked} > - Copy Cookies to Profile + + Copy Cookies to Profile + {!meta.crossOsUnlocked && } + + + )} + {(profile.browser === "camoufox" || + profile.browser === "wayfern") && + meta.onImportCookies && ( + { + if (meta.crossOsUnlocked) { + meta.onImportCookies?.(profile); + } + }} + disabled={isDisabled || !meta.crossOsUnlocked} + > + + Import Cookies + {!meta.crossOsUnlocked && } + )} diff --git a/src/components/shared-camoufox-config-form.tsx b/src/components/shared-camoufox-config-form.tsx index 6be5577..3ce1036 100644 --- a/src/components/shared-camoufox-config-form.tsx +++ b/src/components/shared-camoufox-config-form.tsx @@ -2,12 +2,12 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { LuLock } from "react-icons/lu"; import MultipleSelector, { type Option } from "@/components/multiple-selector"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { ProBadge } from "@/components/ui/pro-badge"; import { Select, SelectContent, @@ -237,9 +237,7 @@ export function SharedCamoufoxConfigForm({ {osLabels[os]} - {isDisabled && ( - - )} + {isDisabled && } ); @@ -1011,9 +1009,7 @@ export function SharedCamoufoxConfigForm({ {osLabels[os]} - {isDisabled && ( - - )} + {isDisabled && } ); diff --git a/src/components/sync-config-dialog.tsx b/src/components/sync-config-dialog.tsx index 0171bbd..67dc331 100644 --- a/src/components/sync-config-dialog.tsx +++ b/src/components/sync-config-dialog.tsx @@ -3,7 +3,7 @@ import { invoke } from "@tauri-apps/api/core"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { LuEye, LuEyeOff, LuLock } from "react-icons/lu"; +import { LuEye, LuEyeOff } from "react-icons/lu"; import { LoadingButton } from "@/components/loading-button"; import { Button } from "@/components/ui/button"; import { @@ -16,6 +16,7 @@ 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, @@ -294,9 +295,7 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) { > {t("sync.cloud.tabLabel")} - {cloudBlocked && ( - - )} + {cloudBlocked && } {t("sync.cloud.selfHostedTabLabel")} - {selfHostedBlocked && ( - - )} + {selfHostedBlocked && } diff --git a/src/components/ui/pro-badge.tsx b/src/components/ui/pro-badge.tsx new file mode 100644 index 0000000..4c03381 --- /dev/null +++ b/src/components/ui/pro-badge.tsx @@ -0,0 +1,14 @@ +import { cn } from "@/lib/utils"; + +export function ProBadge({ className }: { className?: string }) { + return ( + + PRO + + ); +} diff --git a/src/components/wayfern-config-form.tsx b/src/components/wayfern-config-form.tsx index 6a5a14f..1b1bc26 100644 --- a/src/components/wayfern-config-form.tsx +++ b/src/components/wayfern-config-form.tsx @@ -2,11 +2,11 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { LuLock } from "react-icons/lu"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { ProBadge } from "@/components/ui/pro-badge"; import { Select, SelectContent, @@ -166,9 +166,7 @@ export function WayfernConfigForm({ {osLabels[os]} - {isDisabled && ( - - )} + {isDisabled && } ); @@ -959,9 +957,7 @@ export function WayfernConfigForm({ {osLabels[os]} - {isDisabled && ( - - )} + {isDisabled && } ); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 9159bd3..c51e16b 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -503,5 +503,22 @@ "viewOnly": "This profile was created on {{os}} and is not supported on this system", "cannotLaunch": "This profile was created on {{os}} and is not supported on this system", "cannotModify": "Cannot modify sync settings for a cross-OS profile" + }, + "cookies": { + "import": { + "title": "Import Cookies", + "description": "Import cookies from a Netscape format file.", + "selectFile": "Choose File", + "preview": "{{count}} cookies found", + "success": "Successfully imported {{imported}} cookies ({{replaced}} replaced)", + "error": "Failed to import cookies", + "proFeature": "Cookie import is a Pro feature" + } + }, + "pro": { + "badge": "PRO", + "fingerprintLocked": "Fingerprint editing is a Pro feature", + "cookieCopyLocked": "Cookie copying is a Pro feature", + "cookieImportLocked": "Cookie import is a Pro feature" } } diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index aa7da48..f1959c0 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -503,5 +503,22 @@ "viewOnly": "Este perfil fue creado en {{os}} y no es compatible con este sistema", "cannotLaunch": "Este perfil fue creado en {{os}} y no es compatible con este sistema", "cannotModify": "No se pueden modificar los ajustes de sincronización de un perfil de otro sistema operativo" + }, + "cookies": { + "import": { + "title": "Importar Cookies", + "description": "Importar cookies desde un archivo en formato Netscape.", + "selectFile": "Elegir Archivo", + "preview": "{{count}} cookies encontradas", + "success": "Se importaron {{imported}} cookies exitosamente ({{replaced}} reemplazadas)", + "error": "Error al importar cookies", + "proFeature": "La importación de cookies es una función Pro" + } + }, + "pro": { + "badge": "PRO", + "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" } } diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 798edea..0096c4f 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -503,5 +503,22 @@ "viewOnly": "Ce profil a été créé sur {{os}} et n'est pas pris en charge sur ce système", "cannotLaunch": "Ce profil a été créé sur {{os}} et n'est pas pris en charge sur ce système", "cannotModify": "Impossible de modifier les paramètres de synchronisation d'un profil d'un autre système d'exploitation" + }, + "cookies": { + "import": { + "title": "Importer des Cookies", + "description": "Importer des cookies depuis un fichier au format Netscape.", + "selectFile": "Choisir un Fichier", + "preview": "{{count}} cookies trouvés", + "success": "{{imported}} cookies importés avec succès ({{replaced}} remplacés)", + "error": "Échec de l'importation des cookies", + "proFeature": "L'importation de cookies est une fonctionnalité Pro" + } + }, + "pro": { + "badge": "PRO", + "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" } } diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 7e2fe65..c5795e4 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -503,5 +503,22 @@ "viewOnly": "このプロファイルは{{os}}で作成されたもので、このシステムではサポートされていません", "cannotLaunch": "このプロファイルは{{os}}で作成されたもので、このシステムではサポートされていません", "cannotModify": "他のOSのプロファイルの同期設定は変更できません" + }, + "cookies": { + "import": { + "title": "Cookieのインポート", + "description": "Netscape形式のファイルからCookieをインポートします。", + "selectFile": "ファイルを選択", + "preview": "{{count}}件のCookieが見つかりました", + "success": "{{imported}}件のCookieをインポートしました({{replaced}}件を置換)", + "error": "Cookieのインポートに失敗しました", + "proFeature": "Cookieのインポートはプロ機能です" + } + }, + "pro": { + "badge": "PRO", + "fingerprintLocked": "フィンガープリント編集はプロ機能です", + "cookieCopyLocked": "Cookieのコピーはプロ機能です", + "cookieImportLocked": "Cookieのインポートはプロ機能です" } } diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 41d4d85..2b903f4 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -503,5 +503,22 @@ "viewOnly": "Este perfil foi criado em {{os}} e não é compatível com este sistema", "cannotLaunch": "Este perfil foi criado em {{os}} e não é compatível com este sistema", "cannotModify": "Não é possível modificar as configurações de sincronização de um perfil de outro sistema operacional" + }, + "cookies": { + "import": { + "title": "Importar Cookies", + "description": "Importar cookies de um arquivo no formato Netscape.", + "selectFile": "Escolher Arquivo", + "preview": "{{count}} cookies encontrados", + "success": "{{imported}} cookies importados com sucesso ({{replaced}} substituídos)", + "error": "Falha ao importar cookies", + "proFeature": "A importação de cookies é um recurso Pro" + } + }, + "pro": { + "badge": "PRO", + "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" } } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 3ca6e0f..a8b81ec 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -503,5 +503,22 @@ "viewOnly": "Этот профиль был создан на {{os}} и не поддерживается в этой системе", "cannotLaunch": "Этот профиль был создан на {{os}} и не поддерживается в этой системе", "cannotModify": "Невозможно изменить настройки синхронизации профиля другой ОС" + }, + "cookies": { + "import": { + "title": "Импорт Cookies", + "description": "Импорт cookies из файла в формате Netscape.", + "selectFile": "Выбрать файл", + "preview": "Найдено {{count}} cookies", + "success": "Успешно импортировано {{imported}} cookies ({{replaced}} заменено)", + "error": "Ошибка импорта cookies", + "proFeature": "Импорт cookies — функция Pro" + } + }, + "pro": { + "badge": "PRO", + "fingerprintLocked": "Редактирование отпечатка — функция Pro", + "cookieCopyLocked": "Копирование cookies — функция Pro", + "cookieImportLocked": "Импорт cookies — функция Pro" } } diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 2a6af7d..ab3dd81 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -503,5 +503,22 @@ "viewOnly": "此配置文件在 {{os}} 上创建,不受此系统支持", "cannotLaunch": "此配置文件在 {{os}} 上创建,不受此系统支持", "cannotModify": "无法修改跨操作系统配置文件的同步设置" + }, + "cookies": { + "import": { + "title": "导入 Cookies", + "description": "从 Netscape 格式文件导入 Cookies。", + "selectFile": "选择文件", + "preview": "找到 {{count}} 个 Cookies", + "success": "成功导入 {{imported}} 个 Cookies(替换了 {{replaced}} 个)", + "error": "导入 Cookies 失败", + "proFeature": "导入 Cookies 是 Pro 功能" + } + }, + "pro": { + "badge": "PRO", + "fingerprintLocked": "指纹编辑是 Pro 功能", + "cookieCopyLocked": "Cookie 复制是 Pro 功能", + "cookieImportLocked": "Cookie 导入是 Pro 功能" } }