diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 5f88edd..a424945 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -1,5 +1,6 @@ use crate::browser::{create_browser, BrowserType, ProxySettings}; use crate::camoufox_manager::{CamoufoxConfig, CamoufoxManager}; +use crate::cloud_auth::CLOUD_AUTH; use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; use crate::events; use crate::platform_browser; @@ -37,6 +38,17 @@ impl BrowserRunner { crate::app_dirs::binaries_dir() } + /// Refresh cloud proxy credentials if the profile uses a cloud or cloud-derived proxy, + /// then resolve the proxy settings. + async fn resolve_proxy_with_refresh(&self, proxy_id: Option<&String>) -> Option { + let proxy_id = proxy_id?; + if PROXY_MANAGER.is_cloud_or_derived(proxy_id) { + log::info!("Refreshing cloud proxy credentials before launch for proxy {proxy_id}"); + CLOUD_AUTH.sync_cloud_proxy().await; + } + PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) + } + /// Get the executable path for a browser profile /// This is a common helper to eliminate code duplication across the codebase pub fn get_browser_executable_path( @@ -92,10 +104,10 @@ impl BrowserRunner { }); // Always start a local proxy for Camoufox (for traffic monitoring and geoip support) - let mut upstream_proxy = profile - .proxy_id - .as_ref() - .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); + // Refresh cloud proxy credentials if needed before resolving + let mut upstream_proxy = self + .resolve_proxy_with_refresh(profile.proxy_id.as_ref()) + .await; // If profile has a VPN instead of proxy, start VPN worker and use it as upstream if upstream_proxy.is_none() { @@ -332,10 +344,10 @@ impl BrowserRunner { }); // Always start a local proxy for Wayfern (for traffic monitoring and geoip support) - let mut upstream_proxy = profile - .proxy_id - .as_ref() - .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); + // Refresh cloud proxy credentials if needed before resolving + let mut upstream_proxy = self + .resolve_proxy_with_refresh(profile.proxy_id.as_ref()) + .await; // If profile has a VPN instead of proxy, start VPN worker and use it as upstream if upstream_proxy.is_none() { @@ -567,11 +579,10 @@ impl BrowserRunner { // Continue anyway, the error might not be critical } - // Get stored proxy settings for later use (removed as we handle this in proxy startup) - let _stored_proxy_settings = profile - .proxy_id - .as_ref() - .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); + // Refresh cloud proxy credentials if needed before resolving + let _stored_proxy_settings = self + .resolve_proxy_with_refresh(profile.proxy_id.as_ref()) + .await; // Use provided local proxy for Chromium-based browsers launch arguments let proxy_for_launch_args: Option<&ProxySettings> = local_proxy_settings; diff --git a/src-tauri/src/cloud_auth.rs b/src-tauri/src/cloud_auth.rs index 878acb7..9e0f1f6 100644 --- a/src-tauri/src/cloud_auth.rs +++ b/src-tauri/src/cloud_auth.rs @@ -37,6 +37,8 @@ pub struct CloudUser { pub proxy_bandwidth_limit_mb: i64, #[serde(rename = "proxyBandwidthUsedMb")] pub proxy_bandwidth_used_mb: i64, + #[serde(rename = "proxyBandwidthExtraMb", default)] + pub proxy_bandwidth_extra_mb: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index 83279d9..7fc05c9 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -1,6 +1,7 @@ use crate::api_client::is_browser_version_nightly; use crate::browser::{create_browser, BrowserType, ProxySettings}; use crate::camoufox_manager::CamoufoxConfig; +use crate::cloud_auth::CLOUD_AUTH; use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; use crate::events; use crate::profile::types::{get_host_os, BrowserProfile, SyncMode}; @@ -53,6 +54,15 @@ impl ProfileManager { if proxy_id.is_some() && vpn_id.is_some() { return Err("Cannot set both proxy_id and vpn_id".into()); } + + // Sync cloud proxy credentials if the profile uses a cloud or cloud-derived proxy + if let Some(ref pid) = proxy_id { + if PROXY_MANAGER.is_cloud_or_derived(pid) || pid == crate::proxy_manager::CLOUD_PROXY_ID { + log::info!("Syncing cloud proxy credentials before profile creation"); + CLOUD_AUTH.sync_cloud_proxy().await; + } + } + log::info!("Attempting to create profile: {name}"); // Check if a profile with this name already exists (case insensitive) diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index b035469..4b6ab73 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -769,6 +769,14 @@ impl ProxyManager { Ok(()) } + // Check if a proxy is cloud-managed or cloud-derived (needs fresh credentials) + pub fn is_cloud_or_derived(&self, proxy_id: &str) -> bool { + let stored_proxies = self.stored_proxies.lock().unwrap(); + stored_proxies + .get(proxy_id) + .is_some_and(|p| p.is_cloud_managed || p.is_cloud_derived) + } + // Get proxy settings for a stored proxy ID pub fn get_proxy_settings_by_id(&self, proxy_id: &str) -> Option { let stored_proxies = self.stored_proxies.lock().unwrap(); diff --git a/src-tauri/src/version_updater.rs b/src-tauri/src/version_updater.rs index d672ed1..a42ad48 100644 --- a/src-tauri/src/version_updater.rs +++ b/src-tauri/src/version_updater.rs @@ -362,20 +362,15 @@ impl VersionUpdater { eprintln!("Failed to emit completion progress: {e}"); } - // After all version updates are complete, trigger auto-update check - if total_new_versions > 0 { - println!( - "Found {total_new_versions} new versions across all browsers. Checking for auto-updates..." - ); - - // Trigger auto-update check which will automatically download browsers - self - .auto_updater - .check_for_updates_with_progress(app_handle) - .await; - } else { - println!("No new versions found, skipping auto-update check"); - } + // Always check for auto-updates — profiles may still be on older versions + // even if no new versions were found in the cache this cycle + println!( + "Checking for browser auto-updates (found {total_new_versions} new versions in cache)..." + ); + self + .auto_updater + .check_for_updates_with_progress(app_handle) + .await; Ok(results) } diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index 6d8f90a..423b27e 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -16,16 +16,17 @@ 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"; import { LuCheck, LuChevronDown, LuChevronUp, LuCookie, + LuInfo, LuTrash2, LuUsers, } from "react-icons/lu"; import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"; +import { ProfileInfoDialog } from "@/components/profile-info-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -37,18 +38,11 @@ import { CommandItem, CommandList, } from "@/components/ui/command"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { ProBadge } from "@/components/ui/pro-badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Table, @@ -868,6 +862,8 @@ export function ProfilesDataTable({ const [profileToDelete, setProfileToDelete] = React.useState(null); const [isDeleting, setIsDeleting] = React.useState(false); + const [profileForInfoDialog, setProfileForInfoDialog] = + React.useState(null); const [launchingProfiles, setLaunchingProfiles] = React.useState>( new Set(), ); @@ -2292,123 +2288,18 @@ export function ProfilesDataTable({ cell: ({ row, table }) => { const meta = table.options.meta as TableMeta; const profile = row.original; - const isCrossOs = isCrossOsProfile(profile); - const isRunning = - meta.isClient && meta.runningProfiles.has(profile.id); - const isLaunching = meta.launchingProfiles.has(profile.id); - const isStopping = meta.stoppingProfiles.has(profile.id); - const isDisabled = - isRunning || isLaunching || isStopping || isCrossOs; - const isDeleteDisabled = isRunning || isLaunching || isStopping; return (
- - - - - - { - meta.onOpenTrafficDialog?.(profile.id); - }} - disabled={isCrossOs} - > - {meta.t("profiles.actions.viewNetwork")} - - {!profile.ephemeral && ( - { - meta.onOpenProfileSyncDialog?.(profile); - }} - disabled={isCrossOs} - > - {meta.t("profiles.actions.syncSettings")} - - )} - { - meta.onAssignProfilesToGroup?.([profile.id]); - }} - disabled={isDisabled} - > - {meta.t("profiles.actions.assignToGroup")} - - {(profile.browser === "camoufox" || - profile.browser === "wayfern") && - meta.onConfigureCamoufox && ( - { - meta.onConfigureCamoufox?.(profile); - }} - disabled={isDisabled} - > - {meta.t("profiles.actions.changeFingerprint")} - - )} - {(profile.browser === "camoufox" || - profile.browser === "wayfern") && - !profile.ephemeral && - meta.onCopyCookiesToProfile && ( - { - if (meta.crossOsUnlocked) { - meta.onCopyCookiesToProfile?.(profile); - } - }} - disabled={isDisabled || !meta.crossOsUnlocked} - > - - {meta.t("profiles.actions.copyCookiesToProfile")} - {!meta.crossOsUnlocked && } - - - )} - {(profile.browser === "camoufox" || - profile.browser === "wayfern") && - !profile.ephemeral && - meta.onOpenCookieManagement && ( - { - if (meta.crossOsUnlocked) { - meta.onOpenCookieManagement?.(profile); - } - }} - disabled={isDisabled || !meta.crossOsUnlocked} - > - - {meta.t("cookies.management.menuItem")} - {!meta.crossOsUnlocked && } - - - )} - {!profile.ephemeral && ( - { - meta.onCloneProfile?.(profile); - }} - disabled={isDisabled} - > - {meta.t("profiles.actions.clone")} - - )} - { - setProfileToDelete(profile); - }} - disabled={isDeleteDisabled} - > - {meta.t("profiles.actions.delete")} - - - +
); }, @@ -2512,6 +2403,45 @@ export function ProfilesDataTable({ confirmButtonText="Delete Profile" isLoading={isDeleting} /> + {profileForInfoDialog && + (() => { + const infoProfile = profileForInfoDialog; + const infoIsRunning = + browserState.isClient && runningProfiles.has(infoProfile.id); + const infoIsLaunching = launchingProfiles.has(infoProfile.id); + const infoIsStopping = stoppingProfiles.has(infoProfile.id); + const infoIsCrossOs = isCrossOsProfile(infoProfile); + const infoIsDisabled = + infoIsRunning || infoIsLaunching || infoIsStopping || infoIsCrossOs; + return ( + setProfileForInfoDialog(null)} + profile={infoProfile} + storedProxies={storedProxies} + vpnConfigs={vpnConfigs} + onOpenTrafficDialog={(profileId) => { + const profile = profiles.find((p) => p.id === profileId); + setTrafficDialogProfile({ id: profileId, name: profile?.name }); + }} + onOpenProfileSyncDialog={onOpenProfileSyncDialog} + onAssignProfilesToGroup={onAssignProfilesToGroup} + onConfigureCamoufox={onConfigureCamoufox} + onCopyCookiesToProfile={onCopyCookiesToProfile} + onOpenCookieManagement={onOpenCookieManagement} + onCloneProfile={onCloneProfile} + onDeleteProfile={(profile) => { + setProfileForInfoDialog(null); + setProfileToDelete(profile); + }} + crossOsUnlocked={crossOsUnlocked} + isRunning={infoIsRunning} + isDisabled={infoIsDisabled} + isCrossOs={infoIsCrossOs} + syncStatuses={syncStatuses} + /> + ); + })()} {onBulkGroupAssignment && ( diff --git a/src/components/profile-info-dialog.tsx b/src/components/profile-info-dialog.tsx new file mode 100644 index 0000000..0810034 --- /dev/null +++ b/src/components/profile-info-dialog.tsx @@ -0,0 +1,402 @@ +"use client"; + +import { invoke } from "@tauri-apps/api/core"; +import * as React from "react"; +import { useTranslation } from "react-i18next"; +import { FaApple, FaLinux, FaWindows } from "react-icons/fa"; +import { + LuChevronRight, + LuClipboard, + LuClipboardCheck, + LuCookie, + LuCopy, + LuFingerprint, + LuGlobe, + LuGroup, + LuRefreshCw, + LuSettings, + LuTrash2, +} from "react-icons/lu"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { ProBadge } from "@/components/ui/pro-badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + getBrowserDisplayName, + getOSDisplayName, + getProfileIcon, + isCrossOsProfile, +} from "@/lib/browser-utils"; +import { formatRelativeTime } from "@/lib/flag-utils"; +import { cn } from "@/lib/utils"; +import type { + BrowserProfile, + ProfileGroup, + StoredProxy, + VpnConfig, +} from "@/types"; + +interface ProfileInfoDialogProps { + isOpen: boolean; + onClose: () => void; + profile: BrowserProfile | null; + storedProxies: StoredProxy[]; + vpnConfigs: VpnConfig[]; + onOpenTrafficDialog?: (profileId: string) => void; + onOpenProfileSyncDialog?: (profile: BrowserProfile) => void; + onAssignProfilesToGroup?: (profileIds: string[]) => void; + onConfigureCamoufox?: (profile: BrowserProfile) => void; + onCopyCookiesToProfile?: (profile: BrowserProfile) => void; + onOpenCookieManagement?: (profile: BrowserProfile) => void; + onCloneProfile?: (profile: BrowserProfile) => void; + onDeleteProfile?: (profile: BrowserProfile) => void; + crossOsUnlocked?: boolean; + isRunning?: boolean; + isDisabled?: boolean; + isCrossOs?: boolean; + syncStatuses: Record; +} + +function OSIcon({ os }: { os: string }) { + switch (os) { + case "macos": + return ; + case "windows": + return ; + case "linux": + return ; + default: + return null; + } +} + +export function ProfileInfoDialog({ + isOpen, + onClose, + profile, + storedProxies, + vpnConfigs, + onOpenTrafficDialog, + onOpenProfileSyncDialog, + onAssignProfilesToGroup, + onConfigureCamoufox, + onCopyCookiesToProfile, + onOpenCookieManagement, + onCloneProfile, + onDeleteProfile, + crossOsUnlocked = false, + isRunning = false, + isDisabled = false, + isCrossOs = false, + syncStatuses, +}: ProfileInfoDialogProps) { + const { t } = useTranslation(); + const [copied, setCopied] = React.useState(false); + const [groupName, setGroupName] = React.useState(null); + + React.useEffect(() => { + if (!isOpen || !profile?.group_id) { + setGroupName(null); + return; + } + (async () => { + try { + const groups = await invoke("get_groups"); + const group = groups.find((g) => g.id === profile.group_id); + setGroupName(group?.name ?? null); + } catch { + setGroupName(null); + } + })(); + }, [isOpen, profile?.group_id]); + + React.useEffect(() => { + if (!isOpen) { + setCopied(false); + } + }, [isOpen]); + + if (!profile) return null; + + const ProfileIcon = getProfileIcon(profile); + const isCamoufoxOrWayfern = + profile.browser === "camoufox" || profile.browser === "wayfern"; + const isDeleteDisabled = isRunning; + + const proxyName = profile.proxy_id + ? storedProxies.find((p) => p.id === profile.proxy_id)?.name + : null; + const vpnName = profile.vpn_id + ? vpnConfigs.find((v) => v.id === profile.vpn_id)?.name + : null; + const networkLabel = vpnName + ? `VPN: ${vpnName}` + : proxyName + ? `Proxy: ${proxyName}` + : t("profileInfo.values.none"); + + const syncStatus = syncStatuses[profile.id]; + const syncMode = profile.sync_mode ?? "Disabled"; + const syncLabel = syncStatus + ? `${syncMode} (${syncStatus.status})` + : syncMode; + + const handleCopyId = async () => { + try { + await navigator.clipboard.writeText(profile.id); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // ignore + } + }; + + const handleAction = (action: () => void) => { + onClose(); + action(); + }; + + const infoFields: { label: string; value: React.ReactNode }[] = [ + { + label: t("profileInfo.fields.profileId"), + value: ( + + + {profile.id} + + + + ), + }, + { + label: t("profileInfo.fields.browser"), + value: `${getBrowserDisplayName(profile.browser)} ${profile.version}`, + }, + { + label: t("profileInfo.fields.releaseType"), + value: + profile.release_type.charAt(0).toUpperCase() + + profile.release_type.slice(1), + }, + { + label: t("profileInfo.fields.proxyVpn"), + value: networkLabel, + }, + { + label: t("profileInfo.fields.group"), + value: groupName ?? t("profileInfo.values.none"), + }, + { + label: t("profileInfo.fields.tags"), + value: + profile.tags && profile.tags.length > 0 ? ( + + {profile.tags.map((tag) => ( + + {tag} + + ))} + + ) : ( + t("profileInfo.values.none") + ), + }, + { + label: t("profileInfo.fields.note"), + value: profile.note || t("profileInfo.values.none"), + }, + { + label: t("profileInfo.fields.syncStatus"), + value: syncLabel, + }, + { + label: t("profileInfo.fields.lastLaunched"), + value: profile.last_launch + ? formatRelativeTime(profile.last_launch) + : t("profileInfo.values.never"), + }, + ]; + + if (profile.host_os && isCrossOsProfile(profile)) { + infoFields.push({ + label: t("profileInfo.fields.hostOs"), + value: ( + + + {getOSDisplayName(profile.host_os)} + + ), + }); + } + + if (profile.ephemeral) { + infoFields.push({ + label: t("profileInfo.fields.ephemeral"), + value: ( + + {t("profileInfo.values.yes")} + + ), + }); + } + + type ActionItem = { + icon: React.ReactNode; + label: string; + onClick: () => void; + disabled?: boolean; + destructive?: boolean; + proBadge?: boolean; + hidden?: boolean; + }; + + const actions: ActionItem[] = [ + { + icon: , + label: t("profiles.actions.viewNetwork"), + onClick: () => handleAction(() => onOpenTrafficDialog?.(profile.id)), + disabled: isCrossOs, + }, + { + icon: , + label: t("profiles.actions.syncSettings"), + onClick: () => handleAction(() => onOpenProfileSyncDialog?.(profile)), + disabled: isCrossOs, + hidden: profile.ephemeral === true, + }, + { + icon: , + label: t("profiles.actions.assignToGroup"), + onClick: () => + handleAction(() => onAssignProfilesToGroup?.([profile.id])), + disabled: isDisabled, + }, + { + icon: , + label: t("profiles.actions.changeFingerprint"), + onClick: () => handleAction(() => onConfigureCamoufox?.(profile)), + disabled: isDisabled, + hidden: !isCamoufoxOrWayfern || !onConfigureCamoufox, + }, + { + icon: , + label: t("profiles.actions.copyCookiesToProfile"), + onClick: () => handleAction(() => onCopyCookiesToProfile?.(profile)), + disabled: isDisabled || !crossOsUnlocked, + proBadge: !crossOsUnlocked, + hidden: + !isCamoufoxOrWayfern || + profile.ephemeral === true || + !onCopyCookiesToProfile, + }, + { + icon: , + label: t("profileInfo.actions.manageCookies"), + onClick: () => handleAction(() => onOpenCookieManagement?.(profile)), + disabled: isDisabled || !crossOsUnlocked, + proBadge: !crossOsUnlocked, + hidden: + !isCamoufoxOrWayfern || + profile.ephemeral === true || + !onOpenCookieManagement, + }, + { + icon: , + label: t("profiles.actions.clone"), + onClick: () => handleAction(() => onCloneProfile?.(profile)), + disabled: isDisabled, + hidden: profile.ephemeral === true, + }, + { + icon: , + label: t("profiles.actions.delete"), + onClick: () => handleAction(() => onDeleteProfile?.(profile)), + disabled: isDeleteDisabled, + destructive: true, + }, + ]; + + const visibleActions = actions.filter((a) => !a.hidden); + + return ( + !open && onClose()}> + + + + + {profile.name} + + + + + + {t("profileInfo.tabs.info")} + + + {t("profileInfo.tabs.settings")} + + + +
+ {infoFields.map((field) => ( + + + {field.label} + + {field.value} + + ))} +
+
+ +
+ {visibleActions.map((action) => ( + + ))} +
+
+
+ + + +
+
+ ); +} diff --git a/src/components/sync-config-dialog.tsx b/src/components/sync-config-dialog.tsx index dfe0da7..f3fdbb1 100644 --- a/src/components/sync-config-dialog.tsx +++ b/src/components/sync-config-dialog.tsx @@ -292,11 +292,23 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
Proxy Bandwidth - {user.proxyBandwidthUsedMb} / {user.proxyBandwidthLimitMb}{" "} + {user.proxyBandwidthUsedMb} /{" "} + {user.proxyBandwidthLimitMb + + (user.proxyBandwidthExtraMb || 0)}{" "} MB
)} + {(user.proxyBandwidthExtraMb || 0) > 0 && ( +
+ Extra Bandwidth + + {user.proxyBandwidthExtraMb >= 1000 + ? `${(user.proxyBandwidthExtraMb / 1000).toFixed(1)} GB` + : `${user.proxyBandwidthExtraMb} MB`} + +
+ )}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index bcfa79c..69ca99d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -678,6 +678,35 @@ "error": "Failed to export cookies" } }, + "profileInfo": { + "title": "Profile Details", + "tabs": { + "info": "Info", + "settings": "Settings" + }, + "fields": { + "profileId": "Profile ID", + "browser": "Browser", + "releaseType": "Release Type", + "proxyVpn": "Proxy / VPN", + "group": "Group", + "tags": "Tags", + "note": "Note", + "syncStatus": "Sync Status", + "lastLaunched": "Last Launched", + "hostOs": "Host OS", + "ephemeral": "Ephemeral" + }, + "values": { + "none": "None", + "never": "Never", + "copied": "Copied!", + "yes": "Yes" + }, + "actions": { + "manageCookies": "Manage Cookies" + } + }, "pro": { "badge": "PRO", "fingerprintLocked": "Fingerprint editing is a Pro feature", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 71f8558..213683e 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -678,6 +678,35 @@ "error": "Error al exportar cookies" } }, + "profileInfo": { + "title": "Detalles del Perfil", + "tabs": { + "info": "Info", + "settings": "Configuración" + }, + "fields": { + "profileId": "ID del Perfil", + "browser": "Navegador", + "releaseType": "Tipo de Versión", + "proxyVpn": "Proxy / VPN", + "group": "Grupo", + "tags": "Etiquetas", + "note": "Nota", + "syncStatus": "Estado de Sincronización", + "lastLaunched": "Último Lanzamiento", + "hostOs": "SO Host", + "ephemeral": "Efímero" + }, + "values": { + "none": "Ninguno", + "never": "Nunca", + "copied": "¡Copiado!", + "yes": "Sí" + }, + "actions": { + "manageCookies": "Administrar Cookies" + } + }, "pro": { "badge": "PRO", "fingerprintLocked": "La edición de huellas digitales es una función Pro", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index f8943db..134c73c 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -678,6 +678,35 @@ "error": "Échec de l'exportation des cookies" } }, + "profileInfo": { + "title": "Détails du Profil", + "tabs": { + "info": "Info", + "settings": "Paramètres" + }, + "fields": { + "profileId": "ID du Profil", + "browser": "Navigateur", + "releaseType": "Type de Version", + "proxyVpn": "Proxy / VPN", + "group": "Groupe", + "tags": "Tags", + "note": "Note", + "syncStatus": "État de Synchronisation", + "lastLaunched": "Dernier Lancement", + "hostOs": "OS Hôte", + "ephemeral": "Éphémère" + }, + "values": { + "none": "Aucun", + "never": "Jamais", + "copied": "Copié !", + "yes": "Oui" + }, + "actions": { + "manageCookies": "Gérer les Cookies" + } + }, "pro": { "badge": "PRO", "fingerprintLocked": "La modification d'empreinte est une fonctionnalité Pro", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 8ccfad1..e70bfa9 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -678,6 +678,35 @@ "error": "Cookieのエクスポートに失敗しました" } }, + "profileInfo": { + "title": "プロフィール詳細", + "tabs": { + "info": "情報", + "settings": "設定" + }, + "fields": { + "profileId": "プロフィールID", + "browser": "ブラウザ", + "releaseType": "リリースタイプ", + "proxyVpn": "プロキシ / VPN", + "group": "グループ", + "tags": "タグ", + "note": "メモ", + "syncStatus": "同期ステータス", + "lastLaunched": "最終起動", + "hostOs": "ホストOS", + "ephemeral": "エフェメラル" + }, + "values": { + "none": "なし", + "never": "なし", + "copied": "コピーしました!", + "yes": "はい" + }, + "actions": { + "manageCookies": "Cookieを管理" + } + }, "pro": { "badge": "PRO", "fingerprintLocked": "フィンガープリント編集はプロ機能です", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 4ad863c..955b122 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -678,6 +678,35 @@ "error": "Falha ao exportar cookies" } }, + "profileInfo": { + "title": "Detalhes do Perfil", + "tabs": { + "info": "Info", + "settings": "Configurações" + }, + "fields": { + "profileId": "ID do Perfil", + "browser": "Navegador", + "releaseType": "Tipo de Versão", + "proxyVpn": "Proxy / VPN", + "group": "Grupo", + "tags": "Tags", + "note": "Nota", + "syncStatus": "Status de Sincronização", + "lastLaunched": "Último Lançamento", + "hostOs": "SO Host", + "ephemeral": "Efêmero" + }, + "values": { + "none": "Nenhum", + "never": "Nunca", + "copied": "Copiado!", + "yes": "Sim" + }, + "actions": { + "manageCookies": "Gerenciar Cookies" + } + }, "pro": { "badge": "PRO", "fingerprintLocked": "A edição de impressão digital é um recurso Pro", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 6924efe..5fe83db 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -678,6 +678,35 @@ "error": "Ошибка экспорта cookies" } }, + "profileInfo": { + "title": "Детали профиля", + "tabs": { + "info": "Информация", + "settings": "Настройки" + }, + "fields": { + "profileId": "ID профиля", + "browser": "Браузер", + "releaseType": "Тип релиза", + "proxyVpn": "Прокси / VPN", + "group": "Группа", + "tags": "Теги", + "note": "Заметка", + "syncStatus": "Статус синхронизации", + "lastLaunched": "Последний запуск", + "hostOs": "ОС хоста", + "ephemeral": "Эфемерный" + }, + "values": { + "none": "Нет", + "never": "Никогда", + "copied": "Скопировано!", + "yes": "Да" + }, + "actions": { + "manageCookies": "Управление Cookie" + } + }, "pro": { "badge": "PRO", "fingerprintLocked": "Редактирование отпечатка — функция Pro", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 32fa7c1..8652db3 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -678,6 +678,35 @@ "error": "导出 Cookies 失败" } }, + "profileInfo": { + "title": "配置文件详情", + "tabs": { + "info": "信息", + "settings": "设置" + }, + "fields": { + "profileId": "配置文件 ID", + "browser": "浏览器", + "releaseType": "发布类型", + "proxyVpn": "代理 / VPN", + "group": "分组", + "tags": "标签", + "note": "备注", + "syncStatus": "同步状态", + "lastLaunched": "上次启动", + "hostOs": "主机操作系统", + "ephemeral": "临时" + }, + "values": { + "none": "无", + "never": "从未", + "copied": "已复制!", + "yes": "是" + }, + "actions": { + "manageCookies": "管理 Cookie" + } + }, "pro": { "badge": "PRO", "fingerprintLocked": "指纹编辑是 Pro 功能", diff --git a/src/types.ts b/src/types.ts index e281255..f4ff0b7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,6 +52,7 @@ export interface CloudUser { cloudProfilesUsed: number; proxyBandwidthLimitMb: number; proxyBandwidthUsedMb: number; + proxyBandwidthExtraMb: number; } export interface CloudAuthState {