diff --git a/src-tauri/src/api_server.rs b/src-tauri/src/api_server.rs
index 20ab6d5..a2ddb5b 100644
--- a/src-tauri/src/api_server.rs
+++ b/src-tauri/src/api_server.rs
@@ -78,6 +78,7 @@ pub struct UpdateProfileRequest {
pub camoufox_config: Option,
pub group_id: Option,
pub tags: Option>,
+ pub extension_group_id: Option,
}
#[derive(Clone)]
@@ -305,6 +306,10 @@ impl ApiServer {
.routes(routes!(get_tags))
.routes(routes!(get_proxies, create_proxy))
.routes(routes!(get_proxy, update_proxy, delete_proxy))
+ .routes(routes!(get_extensions))
+ .routes(routes!(delete_extension_api))
+ .routes(routes!(get_extension_groups))
+ .routes(routes!(delete_extension_group_api))
.routes(routes!(download_browser_api))
.routes(routes!(get_browser_versions))
.routes(routes!(check_browser_downloaded))
@@ -737,6 +742,20 @@ async fn update_profile(
}
}
+ if let Some(extension_group_id) = request.extension_group_id {
+ let ext_group = if extension_group_id.is_empty() {
+ None
+ } else {
+ Some(extension_group_id)
+ };
+ if profile_manager
+ .update_profile_extension_group(&id, ext_group)
+ .is_err()
+ {
+ return Err(StatusCode::BAD_REQUEST);
+ }
+ }
+
// Return updated profile
get_profile(Path(id), State(state)).await
}
@@ -1142,6 +1161,94 @@ async fn delete_proxy(
}
}
+// Extension API endpoints
+
+#[utoipa::path(
+ get,
+ path = "/v1/extensions",
+ responses(
+ (status = 200, description = "List of extensions"),
+ (status = 401, description = "Unauthorized"),
+ ),
+ security(("bearer_auth" = [])),
+ tag = "extensions"
+)]
+async fn get_extensions(
+ State(_state): State,
+) -> Result>, StatusCode> {
+ let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap();
+ mgr
+ .list_extensions()
+ .map(Json)
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
+}
+
+#[utoipa::path(
+ get,
+ path = "/v1/extension-groups",
+ responses(
+ (status = 200, description = "List of extension groups"),
+ (status = 401, description = "Unauthorized"),
+ ),
+ security(("bearer_auth" = [])),
+ tag = "extensions"
+)]
+async fn get_extension_groups(
+ State(_state): State,
+) -> Result>, StatusCode> {
+ let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap();
+ mgr
+ .list_groups()
+ .map(Json)
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
+}
+
+#[utoipa::path(
+ delete,
+ path = "/v1/extensions/{id}",
+ params(("id" = String, Path, description = "Extension ID")),
+ responses(
+ (status = 204, description = "Extension deleted"),
+ (status = 401, description = "Unauthorized"),
+ (status = 404, description = "Extension not found"),
+ ),
+ security(("bearer_auth" = [])),
+ tag = "extensions"
+)]
+async fn delete_extension_api(
+ Path(id): Path,
+ State(state): State,
+) -> Result {
+ let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap();
+ mgr
+ .delete_extension(&state.app_handle, &id)
+ .map(|_| StatusCode::NO_CONTENT)
+ .map_err(|_| StatusCode::NOT_FOUND)
+}
+
+#[utoipa::path(
+ delete,
+ path = "/v1/extension-groups/{id}",
+ params(("id" = String, Path, description = "Extension Group ID")),
+ responses(
+ (status = 204, description = "Extension group deleted"),
+ (status = 401, description = "Unauthorized"),
+ (status = 404, description = "Extension group not found"),
+ ),
+ security(("bearer_auth" = [])),
+ tag = "extensions"
+)]
+async fn delete_extension_group_api(
+ Path(id): Path,
+ State(state): State,
+) -> Result {
+ let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap();
+ mgr
+ .delete_group(&state.app_handle, &id)
+ .map(|_| StatusCode::NO_CONTENT)
+ .map_err(|_| StatusCode::NOT_FOUND)
+}
+
// API Handler - Run Profile with Remote Debugging
#[utoipa::path(
post,
diff --git a/src-tauri/src/app_dirs.rs b/src-tauri/src/app_dirs.rs
index 3cdb70d..2d41295 100644
--- a/src-tauri/src/app_dirs.rs
+++ b/src-tauri/src/app_dirs.rs
@@ -70,6 +70,10 @@ pub fn vpn_dir() -> PathBuf {
data_dir().join("vpn")
}
+pub fn extensions_dir() -> PathBuf {
+ data_dir().join("extensions")
+}
+
#[cfg(test)]
thread_local! {
static TEST_DATA_DIR: std::cell::RefCell
- }
+ content={{crossOsTooltip}
}
sideOffset={4}
horizontalOffset={8}
>
@@ -2305,7 +2299,7 @@ export function ProfilesDataTable({
},
},
],
- [],
+ [t],
);
const table = useReactTable({
@@ -2362,25 +2356,34 @@ export function ProfilesDataTable({
{table.getRowModel().rows?.length ? (
- table.getRowModel().rows.map((row) => (
-
- {row.getVisibleCells().map((cell) => (
-
- {flexRender(
- cell.column.columnDef.cell,
- cell.getContext(),
- )}
-
- ))}
-
- ))
+ table.getRowModel().rows.map((row) => {
+ const rowIsCrossOs = isCrossOsProfile(row.original);
+ const crossOsTitle = rowIsCrossOs
+ ? t("crossOs.viewOnly", {
+ os: getOSDisplayName(row.original.host_os ?? ""),
+ })
+ : undefined;
+ return (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext(),
+ )}
+
+ ))}
+
+ );
+ })
) : (
(null);
+ const [extensionGroupName, setExtensionGroupName] = React.useState<
+ string | null
+ >(null);
+ const [bypassRules, setBypassRules] = React.useState([]);
+ const [newRule, setNewRule] = React.useState("");
React.useEffect(() => {
if (!isOpen || !profile?.group_id) {
@@ -117,11 +125,33 @@ export function ProfileInfoDialog({
})();
}, [isOpen, profile?.group_id]);
+ React.useEffect(() => {
+ if (!isOpen || !profile?.extension_group_id) {
+ setExtensionGroupName(null);
+ return;
+ }
+ (async () => {
+ try {
+ const group = await invoke<{ name: string } | null>(
+ "get_extension_group_for_profile",
+ { profileId: profile.id },
+ );
+ setExtensionGroupName(group?.name ?? null);
+ } catch {
+ setExtensionGroupName(null);
+ }
+ })();
+ }, [isOpen, profile?.extension_group_id, profile?.id]);
+
React.useEffect(() => {
if (!isOpen) {
setCopied(false);
+ setNewRule("");
}
- }, [isOpen]);
+ if (isOpen && profile) {
+ setBypassRules(profile.proxy_bypass_rules ?? []);
+ }
+ }, [isOpen, profile]);
if (!profile) return null;
@@ -163,6 +193,31 @@ export function ProfileInfoDialog({
action();
};
+ const updateBypassRules = async (rules: string[]) => {
+ if (!profile) return;
+ try {
+ await invoke("update_profile_proxy_bypass_rules", {
+ profileId: profile.id,
+ rules,
+ });
+ setBypassRules(rules);
+ } catch {
+ // ignore
+ }
+ };
+
+ const handleAddRule = () => {
+ const trimmed = newRule.trim();
+ if (!trimmed || bypassRules.includes(trimmed)) return;
+ const updated = [...bypassRules, trimmed];
+ setNewRule("");
+ void updateBypassRules(updated);
+ };
+
+ const handleRemoveRule = (rule: string) => {
+ void updateBypassRules(bypassRules.filter((r) => r !== rule));
+ };
+
const infoFields: { label: string; value: React.ReactNode }[] = [
{
label: t("profileInfo.fields.profileId"),
@@ -203,6 +258,10 @@ export function ProfileInfoDialog({
label: t("profileInfo.fields.group"),
value: groupName ?? t("profileInfo.values.none"),
},
+ {
+ label: t("profileInfo.fields.extensionGroup"),
+ value: extensionGroupName ?? t("profileInfo.values.none"),
+ },
{
label: t("profileInfo.fields.tags"),
value:
@@ -349,6 +408,9 @@ export function ProfileInfoDialog({
{t("profileInfo.tabs.info")}
+
+ {t("profileInfo.tabs.network")}
+
{t("profileInfo.tabs.settings")}
@@ -365,6 +427,63 @@ export function ProfileInfoDialog({
))}
+
+
+
+
+ {t("profileInfo.network.bypassRules")}
+
+
+ {t("profileInfo.network.bypassRulesDescription")}
+
+
+
+ setNewRule(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") handleAddRule();
+ }}
+ placeholder={t("profileInfo.network.rulePlaceholder")}
+ className="flex-1 text-sm"
+ />
+
+
+ {bypassRules.length === 0 ? (
+
+ {t("profileInfo.network.noRules")}
+
+ ) : (
+
+ {bypassRules.map((rule) => (
+
+ {rule}
+
+
+ ))}
+
+ )}
+
+ {t("profileInfo.network.ruleTypes")}
+
+
+
{visibleActions.map((action) => (
diff --git a/src/hooks/use-extension-events.ts b/src/hooks/use-extension-events.ts
new file mode 100644
index 0000000..16bef26
--- /dev/null
+++ b/src/hooks/use-extension-events.ts
@@ -0,0 +1,73 @@
+import { invoke } from "@tauri-apps/api/core";
+import { listen } from "@tauri-apps/api/event";
+import { useCallback, useEffect, useState } from "react";
+import type { Extension, ExtensionGroup } from "@/types";
+
+export function useExtensionEvents() {
+ const [extensions, setExtensions] = useState([]);
+ const [extensionGroups, setExtensionGroups] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const loadExtensions = useCallback(async () => {
+ try {
+ const exts = await invoke("list_extensions");
+ setExtensions(exts);
+ setError(null);
+ } catch (err: unknown) {
+ console.error("Failed to load extensions:", err);
+ setExtensions([]);
+ }
+ }, []);
+
+ const loadExtensionGroups = useCallback(async () => {
+ try {
+ const groups = await invoke("list_extension_groups");
+ setExtensionGroups(groups);
+ setError(null);
+ } catch (err: unknown) {
+ console.error("Failed to load extension groups:", err);
+ setExtensionGroups([]);
+ }
+ }, []);
+
+ const loadAll = useCallback(async () => {
+ await Promise.all([loadExtensions(), loadExtensionGroups()]);
+ }, [loadExtensions, loadExtensionGroups]);
+
+ useEffect(() => {
+ let unlisten: (() => void) | undefined;
+
+ const setup = async () => {
+ try {
+ await loadAll();
+ unlisten = await listen("extensions-changed", () => {
+ void loadAll();
+ });
+ } catch (err) {
+ console.error("Failed to setup extension event listeners:", err);
+ setError(
+ `Failed to setup extension event listeners: ${JSON.stringify(err)}`,
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ void setup();
+
+ return () => {
+ if (unlisten) unlisten();
+ };
+ }, [loadAll]);
+
+ return {
+ extensions,
+ extensionGroups,
+ isLoading,
+ error,
+ loadExtensions,
+ loadExtensionGroups,
+ loadAll,
+ };
+}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 69ca99d..845e213 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -146,7 +146,8 @@
"groups": "Groups",
"syncService": "Account",
"integrations": "Integrations",
- "importProfile": "Import Profile"
+ "importProfile": "Import Profile",
+ "extensions": "Extensions"
}
},
"profiles": {
@@ -682,6 +683,7 @@
"title": "Profile Details",
"tabs": {
"info": "Info",
+ "network": "Network",
"settings": "Settings"
},
"fields": {
@@ -695,7 +697,8 @@
"syncStatus": "Sync Status",
"lastLaunched": "Last Launched",
"hostOs": "Host OS",
- "ephemeral": "Ephemeral"
+ "ephemeral": "Ephemeral",
+ "extensionGroup": "Extension Group"
},
"values": {
"none": "None",
@@ -703,10 +706,55 @@
"copied": "Copied!",
"yes": "Yes"
},
+ "network": {
+ "bypassRules": "Proxy Bypass Rules",
+ "bypassRulesDescription": "Requests matching these rules will connect directly, bypassing the proxy.",
+ "addRule": "Add Rule",
+ "rulePlaceholder": "e.g. example.com, 192.168.1.*, .*\\.local",
+ "noRules": "No bypass rules configured.",
+ "ruleTypes": "Supports hostnames, IP addresses, and regex patterns."
+ },
"actions": {
"manageCookies": "Manage Cookies"
}
},
+ "extensions": {
+ "title": "Extensions",
+ "description": "Manage browser extensions and extension groups for your profiles.",
+ "upload": "Upload",
+ "delete": "Delete",
+ "extensionsTab": "Extensions",
+ "groupsTab": "Groups",
+ "managedNotice": "Extensions managed here will replace any manually installed extensions in profiles when launched.",
+ "proRequired": "Extension management is a Pro feature",
+ "empty": "No extensions uploaded yet.",
+ "noGroups": "No extension groups created yet.",
+ "createGroup": "Create Group",
+ "addToGroup": "Add extension...",
+ "removeFromGroup": "Remove from group",
+ "deleteGroup": "Delete group",
+ "extensionGroup": "Extension Group",
+ "compatibility": {
+ "label": "Compatibility",
+ "chromium": "Chromium",
+ "firefox": "Firefox",
+ "both": "Chromium & Firefox"
+ },
+ "selectedFile": "Selected file",
+ "namePlaceholder": "Extension name",
+ "groupNamePlaceholder": "Group name",
+ "uploadSuccess": "Extension uploaded successfully",
+ "deleteSuccess": "Extension deleted successfully",
+ "groupCreateSuccess": "Extension group created successfully",
+ "groupUpdateSuccess": "Extension group updated successfully",
+ "groupDeleteSuccess": "Extension group deleted successfully",
+ "deleteConfirmTitle": "Delete Extension",
+ "deleteConfirmDescription": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
+ "deleteGroupConfirmTitle": "Delete Extension Group",
+ "deleteGroupConfirmDescription": "Are you sure you want to delete the group \"{{name}}\"? This action cannot be undone.",
+ "invalidFileType": "Invalid file type. Please upload a .crx, .xpi, or .zip file.",
+ "readError": "Failed to read the extension file."
+ },
"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 213683e..ad8f07e 100644
--- a/src/i18n/locales/es.json
+++ b/src/i18n/locales/es.json
@@ -146,7 +146,8 @@
"groups": "Grupos",
"syncService": "Cuenta",
"integrations": "Integraciones",
- "importProfile": "Importar Perfil"
+ "importProfile": "Importar Perfil",
+ "extensions": "Extensiones"
}
},
"profiles": {
@@ -682,6 +683,7 @@
"title": "Detalles del Perfil",
"tabs": {
"info": "Info",
+ "network": "Red",
"settings": "Configuración"
},
"fields": {
@@ -695,7 +697,8 @@
"syncStatus": "Estado de Sincronización",
"lastLaunched": "Último Lanzamiento",
"hostOs": "SO Host",
- "ephemeral": "Efímero"
+ "ephemeral": "Efímero",
+ "extensionGroup": "Grupo de Extensiones"
},
"values": {
"none": "Ninguno",
@@ -703,10 +706,55 @@
"copied": "¡Copiado!",
"yes": "Sí"
},
+ "network": {
+ "bypassRules": "Reglas de Omisión de Proxy",
+ "bypassRulesDescription": "Las solicitudes que coincidan con estas reglas se conectarán directamente, omitiendo el proxy.",
+ "addRule": "Agregar Regla",
+ "rulePlaceholder": "ej. example.com, 192.168.1.*, .*\\.local",
+ "noRules": "No hay reglas de omisión configuradas.",
+ "ruleTypes": "Soporta nombres de host, direcciones IP y patrones regex."
+ },
"actions": {
"manageCookies": "Administrar Cookies"
}
},
+ "extensions": {
+ "title": "Extensiones",
+ "description": "Administra extensiones de navegador y grupos de extensiones para tus perfiles.",
+ "upload": "Subir",
+ "delete": "Eliminar",
+ "extensionsTab": "Extensiones",
+ "groupsTab": "Grupos",
+ "managedNotice": "Las extensiones administradas aquí reemplazarán cualquier extensión instalada manualmente en los perfiles al iniciarlos.",
+ "proRequired": "La gestión de extensiones es una función Pro",
+ "empty": "No se han subido extensiones aún.",
+ "noGroups": "No se han creado grupos de extensiones aún.",
+ "createGroup": "Crear Grupo",
+ "addToGroup": "Agregar extensión...",
+ "removeFromGroup": "Eliminar del grupo",
+ "deleteGroup": "Eliminar grupo",
+ "extensionGroup": "Grupo de Extensiones",
+ "compatibility": {
+ "label": "Compatibilidad",
+ "chromium": "Chromium",
+ "firefox": "Firefox",
+ "both": "Chromium y Firefox"
+ },
+ "selectedFile": "Archivo seleccionado",
+ "namePlaceholder": "Nombre de la extensión",
+ "groupNamePlaceholder": "Nombre del grupo",
+ "uploadSuccess": "Extensión subida exitosamente",
+ "deleteSuccess": "Extensión eliminada exitosamente",
+ "groupCreateSuccess": "Grupo de extensiones creado exitosamente",
+ "groupUpdateSuccess": "Grupo de extensiones actualizado exitosamente",
+ "groupDeleteSuccess": "Grupo de extensiones eliminado exitosamente",
+ "deleteConfirmTitle": "Eliminar Extensión",
+ "deleteConfirmDescription": "¿Estás seguro de que deseas eliminar \"{{name}}\"? Esta acción no se puede deshacer.",
+ "deleteGroupConfirmTitle": "Eliminar Grupo de Extensiones",
+ "deleteGroupConfirmDescription": "¿Estás seguro de que deseas eliminar el grupo \"{{name}}\"? Esta acción no se puede deshacer.",
+ "invalidFileType": "Tipo de archivo no válido. Suba un archivo .crx, .xpi o .zip.",
+ "readError": "No se pudo leer el archivo de extensión."
+ },
"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 134c73c..5eb5c99 100644
--- a/src/i18n/locales/fr.json
+++ b/src/i18n/locales/fr.json
@@ -146,7 +146,8 @@
"groups": "Groupes",
"syncService": "Compte",
"integrations": "Intégrations",
- "importProfile": "Importer un profil"
+ "importProfile": "Importer un profil",
+ "extensions": "Extensions"
}
},
"profiles": {
@@ -682,6 +683,7 @@
"title": "Détails du Profil",
"tabs": {
"info": "Info",
+ "network": "Réseau",
"settings": "Paramètres"
},
"fields": {
@@ -695,7 +697,8 @@
"syncStatus": "État de Synchronisation",
"lastLaunched": "Dernier Lancement",
"hostOs": "OS Hôte",
- "ephemeral": "Éphémère"
+ "ephemeral": "Éphémère",
+ "extensionGroup": "Groupe d'Extensions"
},
"values": {
"none": "Aucun",
@@ -703,10 +706,55 @@
"copied": "Copié !",
"yes": "Oui"
},
+ "network": {
+ "bypassRules": "Règles de Contournement du Proxy",
+ "bypassRulesDescription": "Les requêtes correspondant à ces règles se connecteront directement, contournant le proxy.",
+ "addRule": "Ajouter une Règle",
+ "rulePlaceholder": "ex. example.com, 192.168.1.*, .*\\.local",
+ "noRules": "Aucune règle de contournement configurée.",
+ "ruleTypes": "Prend en charge les noms d'hôte, les adresses IP et les expressions régulières."
+ },
"actions": {
"manageCookies": "Gérer les Cookies"
}
},
+ "extensions": {
+ "title": "Extensions",
+ "description": "Gérez les extensions de navigateur et les groupes d'extensions pour vos profils.",
+ "upload": "Télécharger",
+ "delete": "Supprimer",
+ "extensionsTab": "Extensions",
+ "groupsTab": "Groupes",
+ "managedNotice": "Les extensions gérées ici remplaceront toutes les extensions installées manuellement dans les profils lors du lancement.",
+ "proRequired": "La gestion des extensions est une fonctionnalité Pro",
+ "empty": "Aucune extension téléchargée pour l'instant.",
+ "noGroups": "Aucun groupe d'extensions créé pour l'instant.",
+ "createGroup": "Créer un Groupe",
+ "addToGroup": "Ajouter une extension...",
+ "removeFromGroup": "Retirer du groupe",
+ "deleteGroup": "Supprimer le groupe",
+ "extensionGroup": "Groupe d'Extensions",
+ "compatibility": {
+ "label": "Compatibilité",
+ "chromium": "Chromium",
+ "firefox": "Firefox",
+ "both": "Chromium et Firefox"
+ },
+ "selectedFile": "Fichier sélectionné",
+ "namePlaceholder": "Nom de l'extension",
+ "groupNamePlaceholder": "Nom du groupe",
+ "uploadSuccess": "Extension téléchargée avec succès",
+ "deleteSuccess": "Extension supprimée avec succès",
+ "groupCreateSuccess": "Groupe d'extensions créé avec succès",
+ "groupUpdateSuccess": "Groupe d'extensions mis à jour avec succès",
+ "groupDeleteSuccess": "Groupe d'extensions supprimé avec succès",
+ "deleteConfirmTitle": "Supprimer l'Extension",
+ "deleteConfirmDescription": "Êtes-vous sûr de vouloir supprimer \"{{name}}\" ? Cette action est irréversible.",
+ "deleteGroupConfirmTitle": "Supprimer le Groupe d'Extensions",
+ "deleteGroupConfirmDescription": "Êtes-vous sûr de vouloir supprimer le groupe \"{{name}}\" ? Cette action est irréversible.",
+ "invalidFileType": "Type de fichier non valide. Veuillez télécharger un fichier .crx, .xpi ou .zip.",
+ "readError": "Impossible de lire le fichier d'extension."
+ },
"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 e70bfa9..ef3646e 100644
--- a/src/i18n/locales/ja.json
+++ b/src/i18n/locales/ja.json
@@ -146,7 +146,8 @@
"groups": "グループ",
"syncService": "アカウント",
"integrations": "統合",
- "importProfile": "プロファイルをインポート"
+ "importProfile": "プロファイルをインポート",
+ "extensions": "拡張機能"
}
},
"profiles": {
@@ -682,6 +683,7 @@
"title": "プロフィール詳細",
"tabs": {
"info": "情報",
+ "network": "ネットワーク",
"settings": "設定"
},
"fields": {
@@ -695,7 +697,8 @@
"syncStatus": "同期ステータス",
"lastLaunched": "最終起動",
"hostOs": "ホストOS",
- "ephemeral": "エフェメラル"
+ "ephemeral": "エフェメラル",
+ "extensionGroup": "拡張機能グループ"
},
"values": {
"none": "なし",
@@ -703,10 +706,55 @@
"copied": "コピーしました!",
"yes": "はい"
},
+ "network": {
+ "bypassRules": "プロキシバイパスルール",
+ "bypassRulesDescription": "これらのルールに一致するリクエストは、プロキシをバイパスして直接接続します。",
+ "addRule": "ルールを追加",
+ "rulePlaceholder": "例: example.com, 192.168.1.*, .*\\.local",
+ "noRules": "バイパスルールは設定されていません。",
+ "ruleTypes": "ホスト名、IPアドレス、正規表現パターンをサポートしています。"
+ },
"actions": {
"manageCookies": "Cookieを管理"
}
},
+ "extensions": {
+ "title": "拡張機能",
+ "description": "プロファイル用のブラウザ拡張機能と拡張機能グループを管理します。",
+ "upload": "アップロード",
+ "delete": "削除",
+ "extensionsTab": "拡張機能",
+ "groupsTab": "グループ",
+ "managedNotice": "ここで管理される拡張機能は、起動時にプロファイルに手動でインストールされた拡張機能を置き換えます。",
+ "proRequired": "拡張機能管理はプロ機能です",
+ "empty": "まだ拡張機能がアップロードされていません。",
+ "noGroups": "まだ拡張機能グループが作成されていません。",
+ "createGroup": "グループを作成",
+ "addToGroup": "拡張機能を追加...",
+ "removeFromGroup": "グループから削除",
+ "deleteGroup": "グループを削除",
+ "extensionGroup": "拡張機能グループ",
+ "compatibility": {
+ "label": "互換性",
+ "chromium": "Chromium",
+ "firefox": "Firefox",
+ "both": "Chromium & Firefox"
+ },
+ "selectedFile": "選択されたファイル",
+ "namePlaceholder": "拡張機能名",
+ "groupNamePlaceholder": "グループ名",
+ "uploadSuccess": "拡張機能が正常にアップロードされました",
+ "deleteSuccess": "拡張機能が正常に削除されました",
+ "groupCreateSuccess": "拡張機能グループが正常に作成されました",
+ "groupUpdateSuccess": "拡張機能グループが正常に更新されました",
+ "groupDeleteSuccess": "拡張機能グループが正常に削除されました",
+ "deleteConfirmTitle": "拡張機能を削除",
+ "deleteConfirmDescription": "「{{name}}」を削除してもよろしいですか?この操作は元に戻せません。",
+ "deleteGroupConfirmTitle": "拡張機能グループを削除",
+ "deleteGroupConfirmDescription": "グループ「{{name}}」を削除してもよろしいですか?この操作は元に戻せません。",
+ "invalidFileType": "無効なファイルタイプです。.crx、.xpi、または .zip ファイルをアップロードしてください。",
+ "readError": "拡張機能ファイルの読み取りに失敗しました。"
+ },
"pro": {
"badge": "PRO",
"fingerprintLocked": "フィンガープリント編集はプロ機能です",
diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json
index 955b122..7393c3a 100644
--- a/src/i18n/locales/pt.json
+++ b/src/i18n/locales/pt.json
@@ -146,7 +146,8 @@
"groups": "Grupos",
"syncService": "Conta",
"integrations": "Integrações",
- "importProfile": "Importar Perfil"
+ "importProfile": "Importar Perfil",
+ "extensions": "Extensões"
}
},
"profiles": {
@@ -682,6 +683,7 @@
"title": "Detalhes do Perfil",
"tabs": {
"info": "Info",
+ "network": "Rede",
"settings": "Configurações"
},
"fields": {
@@ -695,7 +697,8 @@
"syncStatus": "Status de Sincronização",
"lastLaunched": "Último Lançamento",
"hostOs": "SO Host",
- "ephemeral": "Efêmero"
+ "ephemeral": "Efêmero",
+ "extensionGroup": "Grupo de Extensões"
},
"values": {
"none": "Nenhum",
@@ -703,10 +706,55 @@
"copied": "Copiado!",
"yes": "Sim"
},
+ "network": {
+ "bypassRules": "Regras de Bypass de Proxy",
+ "bypassRulesDescription": "Solicitações que correspondam a estas regras se conectarão diretamente, ignorando o proxy.",
+ "addRule": "Adicionar Regra",
+ "rulePlaceholder": "ex. example.com, 192.168.1.*, .*\\.local",
+ "noRules": "Nenhuma regra de bypass configurada.",
+ "ruleTypes": "Suporta nomes de host, endereços IP e padrões regex."
+ },
"actions": {
"manageCookies": "Gerenciar Cookies"
}
},
+ "extensions": {
+ "title": "Extensões",
+ "description": "Gerencie extensões de navegador e grupos de extensões para seus perfis.",
+ "upload": "Enviar",
+ "delete": "Excluir",
+ "extensionsTab": "Extensões",
+ "groupsTab": "Grupos",
+ "managedNotice": "As extensões gerenciadas aqui substituirão quaisquer extensões instaladas manualmente nos perfis ao serem iniciados.",
+ "proRequired": "O gerenciamento de extensões é um recurso Pro",
+ "empty": "Nenhuma extensão enviada ainda.",
+ "noGroups": "Nenhum grupo de extensões criado ainda.",
+ "createGroup": "Criar Grupo",
+ "addToGroup": "Adicionar extensão...",
+ "removeFromGroup": "Remover do grupo",
+ "deleteGroup": "Excluir grupo",
+ "extensionGroup": "Grupo de Extensões",
+ "compatibility": {
+ "label": "Compatibilidade",
+ "chromium": "Chromium",
+ "firefox": "Firefox",
+ "both": "Chromium e Firefox"
+ },
+ "selectedFile": "Arquivo selecionado",
+ "namePlaceholder": "Nome da extensão",
+ "groupNamePlaceholder": "Nome do grupo",
+ "uploadSuccess": "Extensão enviada com sucesso",
+ "deleteSuccess": "Extensão excluída com sucesso",
+ "groupCreateSuccess": "Grupo de extensões criado com sucesso",
+ "groupUpdateSuccess": "Grupo de extensões atualizado com sucesso",
+ "groupDeleteSuccess": "Grupo de extensões excluído com sucesso",
+ "deleteConfirmTitle": "Excluir Extensão",
+ "deleteConfirmDescription": "Tem certeza de que deseja excluir \"{{name}}\"? Esta ação não pode ser desfeita.",
+ "deleteGroupConfirmTitle": "Excluir Grupo de Extensões",
+ "deleteGroupConfirmDescription": "Tem certeza de que deseja excluir o grupo \"{{name}}\"? Esta ação não pode ser desfeita.",
+ "invalidFileType": "Tipo de arquivo inválido. Envie um arquivo .crx, .xpi ou .zip.",
+ "readError": "Falha ao ler o arquivo de extensão."
+ },
"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 5fe83db..f6cef7f 100644
--- a/src/i18n/locales/ru.json
+++ b/src/i18n/locales/ru.json
@@ -146,7 +146,8 @@
"groups": "Группы",
"syncService": "Аккаунт",
"integrations": "Интеграции",
- "importProfile": "Импорт профиля"
+ "importProfile": "Импорт профиля",
+ "extensions": "Расширения"
}
},
"profiles": {
@@ -682,6 +683,7 @@
"title": "Детали профиля",
"tabs": {
"info": "Информация",
+ "network": "Сеть",
"settings": "Настройки"
},
"fields": {
@@ -695,7 +697,8 @@
"syncStatus": "Статус синхронизации",
"lastLaunched": "Последний запуск",
"hostOs": "ОС хоста",
- "ephemeral": "Эфемерный"
+ "ephemeral": "Эфемерный",
+ "extensionGroup": "Группа расширений"
},
"values": {
"none": "Нет",
@@ -703,10 +706,55 @@
"copied": "Скопировано!",
"yes": "Да"
},
+ "network": {
+ "bypassRules": "Правила обхода прокси",
+ "bypassRulesDescription": "Запросы, соответствующие этим правилам, будут подключаться напрямую, минуя прокси.",
+ "addRule": "Добавить правило",
+ "rulePlaceholder": "напр. example.com, 192.168.1.*, .*\\.local",
+ "noRules": "Правила обхода не настроены.",
+ "ruleTypes": "Поддерживает имена хостов, IP-адреса и шаблоны регулярных выражений."
+ },
"actions": {
"manageCookies": "Управление Cookie"
}
},
+ "extensions": {
+ "title": "Расширения",
+ "description": "Управляйте расширениями браузера и группами расширений для ваших профилей.",
+ "upload": "Загрузить",
+ "delete": "Удалить",
+ "extensionsTab": "Расширения",
+ "groupsTab": "Группы",
+ "managedNotice": "Расширения, управляемые здесь, заменят все вручную установленные расширения в профилях при запуске.",
+ "proRequired": "Управление расширениями — функция Pro",
+ "empty": "Расширения ещё не загружены.",
+ "noGroups": "Группы расширений ещё не созданы.",
+ "createGroup": "Создать группу",
+ "addToGroup": "Добавить расширение...",
+ "removeFromGroup": "Удалить из группы",
+ "deleteGroup": "Удалить группу",
+ "extensionGroup": "Группа расширений",
+ "compatibility": {
+ "label": "Совместимость",
+ "chromium": "Chromium",
+ "firefox": "Firefox",
+ "both": "Chromium и Firefox"
+ },
+ "selectedFile": "Выбранный файл",
+ "namePlaceholder": "Название расширения",
+ "groupNamePlaceholder": "Название группы",
+ "uploadSuccess": "Расширение успешно загружено",
+ "deleteSuccess": "Расширение успешно удалено",
+ "groupCreateSuccess": "Группа расширений успешно создана",
+ "groupUpdateSuccess": "Группа расширений успешно обновлена",
+ "groupDeleteSuccess": "Группа расширений успешно удалена",
+ "deleteConfirmTitle": "Удалить расширение",
+ "deleteConfirmDescription": "Вы уверены, что хотите удалить «{{name}}»? Это действие нельзя отменить.",
+ "deleteGroupConfirmTitle": "Удалить группу расширений",
+ "deleteGroupConfirmDescription": "Вы уверены, что хотите удалить группу «{{name}}»? Это действие нельзя отменить.",
+ "invalidFileType": "Недопустимый тип файла. Загрузите файл .crx, .xpi или .zip.",
+ "readError": "Не удалось прочитать файл расширения."
+ },
"pro": {
"badge": "PRO",
"fingerprintLocked": "Редактирование отпечатка — функция Pro",
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index 8652db3..8a55786 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -146,7 +146,8 @@
"groups": "分组",
"syncService": "账户",
"integrations": "集成",
- "importProfile": "导入配置文件"
+ "importProfile": "导入配置文件",
+ "extensions": "扩展程序"
}
},
"profiles": {
@@ -682,6 +683,7 @@
"title": "配置文件详情",
"tabs": {
"info": "信息",
+ "network": "网络",
"settings": "设置"
},
"fields": {
@@ -695,7 +697,8 @@
"syncStatus": "同步状态",
"lastLaunched": "上次启动",
"hostOs": "主机操作系统",
- "ephemeral": "临时"
+ "ephemeral": "临时",
+ "extensionGroup": "扩展程序组"
},
"values": {
"none": "无",
@@ -703,10 +706,55 @@
"copied": "已复制!",
"yes": "是"
},
+ "network": {
+ "bypassRules": "代理绕过规则",
+ "bypassRulesDescription": "匹配这些规则的请求将直接连接,绕过代理。",
+ "addRule": "添加规则",
+ "rulePlaceholder": "例如 example.com, 192.168.1.*, .*\\.local",
+ "noRules": "未配置绕过规则。",
+ "ruleTypes": "支持主机名、IP地址和正则表达式模式。"
+ },
"actions": {
"manageCookies": "管理 Cookie"
}
},
+ "extensions": {
+ "title": "扩展程序",
+ "description": "管理配置文件的浏览器扩展程序和扩展程序组。",
+ "upload": "上传",
+ "delete": "删除",
+ "extensionsTab": "扩展程序",
+ "groupsTab": "分组",
+ "managedNotice": "此处管理的扩展程序将在启动时替换配置文件中手动安装的所有扩展程序。",
+ "proRequired": "扩展程序管理是 Pro 功能",
+ "empty": "尚未上传任何扩展程序。",
+ "noGroups": "尚未创建任何扩展程序组。",
+ "createGroup": "创建分组",
+ "addToGroup": "添加扩展程序...",
+ "removeFromGroup": "从分组中移除",
+ "deleteGroup": "删除分组",
+ "extensionGroup": "扩展程序组",
+ "compatibility": {
+ "label": "兼容性",
+ "chromium": "Chromium",
+ "firefox": "Firefox",
+ "both": "Chromium 和 Firefox"
+ },
+ "selectedFile": "已选文件",
+ "namePlaceholder": "扩展程序名称",
+ "groupNamePlaceholder": "分组名称",
+ "uploadSuccess": "扩展程序上传成功",
+ "deleteSuccess": "扩展程序删除成功",
+ "groupCreateSuccess": "扩展程序组创建成功",
+ "groupUpdateSuccess": "扩展程序组更新成功",
+ "groupDeleteSuccess": "扩展程序组删除成功",
+ "deleteConfirmTitle": "删除扩展程序",
+ "deleteConfirmDescription": "确定要删除「{{name}}」吗?此操作无法撤消。",
+ "deleteGroupConfirmTitle": "删除扩展程序组",
+ "deleteGroupConfirmDescription": "确定要删除分组「{{name}}」吗?此操作无法撤消。",
+ "invalidFileType": "无效的文件类型。请上传 .crx、.xpi 或 .zip 文件。",
+ "readError": "读取扩展程序文件失败。"
+ },
"pro": {
"badge": "PRO",
"fingerprintLocked": "指纹编辑是 Pro 功能",
diff --git a/src/types.ts b/src/types.ts
index f4ff0b7..7e4267c 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -31,6 +31,30 @@ export interface BrowserProfile {
last_sync?: number; // Timestamp of last successful sync (epoch seconds)
host_os?: string; // OS where profile was created ("macos", "windows", "linux")
ephemeral?: boolean;
+ extension_group_id?: string;
+ proxy_bypass_rules?: string[];
+}
+
+export interface Extension {
+ id: string;
+ name: string;
+ file_name: string;
+ file_type: string;
+ browser_compatibility: string[];
+ created_at: number;
+ updated_at: number;
+ sync_enabled?: boolean;
+ last_sync?: number;
+}
+
+export interface ExtensionGroup {
+ id: string;
+ name: string;
+ extension_ids: string[];
+ created_at: number;
+ updated_at: number;
+ sync_enabled?: boolean;
+ last_sync?: number;
}
export type SyncMode = "Disabled" | "Regular" | "Encrypted";