-
-
+
+ {/* Default Browser Section - hidden in portable mode */}
+ {!systemInfo?.portable && (
+
+
+
+ Default Browser
-
+
+ {isDefaultBrowser ? "Active" : "Inactive"}
+
-
Custom Colors
-
- {THEME_VARIABLES.map(({ key, label }) => {
- const colorValue =
- customThemeState.colors[key] ?? "#000000";
- return (
+
{
+ handleSetDefaultBrowser().catch((err: unknown) => {
+ console.error(err);
+ });
+ }}
+ disabled={isDefaultBrowser}
+ variant={isDefaultBrowser ? "outline" : "default"}
+ className="w-full"
+ >
+ {isDefaultBrowser
+ ? "Already Default Browser"
+ : "Set as Default Browser"}
+
+
+
+ When set as default, Donut Browser will handle web links and
+ allow you to choose which profile to use.
+
+
+ )}
+
+ {/* Permissions Section - Only show on macOS */}
+ {isMacOS && (
+
+
+ System Permissions
+
+
+ {isLoadingPermissions ? (
+
+ Loading permissions...
+
+ ) : (
+
+ {permissions.map((permission) => (
-
-
-
-
-
- {
- const next = Color({ r, g, b }).alpha(a);
- const nextStr = next.hexa();
- const newColors = {
- ...customThemeState.colors,
- [key]: nextStr,
- };
-
- // Check if colors match any preset theme
- const matchingTheme =
- getThemeByColors(newColors);
-
- setCustomThemeState({
- selectedThemeId: matchingTheme?.id ?? null,
- colors: newColors,
+
+ {getPermissionIcon(permission.permission_type)}
+
+
+ {getPermissionDisplayName(
+ permission.permission_type,
+ )}
+
+
+ {permission.description}
+
+
+
+
+ {getStatusBadge(permission.isGranted)}
+ {!permission.isGranted && (
+
{
+ handleRequestPermission(
+ permission.permission_type,
+ ).catch((err: unknown) => {
+ console.error(err);
});
}}
>
-
-
-
-
-
-
-
-
-
-
- {label}
+ Grant
+
+ )}
- );
- })}
-
+ ))}
+
+ )}
+
+
+ These permissions allow browsers launched from Donut Browser
+ to access system resources. Each website will still ask for
+ your permission individually.
+
)}
-
- {/* Language Section */}
-
-
Language
-
-
-
- Interface Language
-
-
-
-
-
- Choose your preferred language for the application interface.
-
-
-
- {/* Default Browser Section - hidden in portable mode */}
- {!systemInfo?.portable && (
+ {/* Integrations Section */}
-
- Default Browser
-
- {isDefaultBrowser ? "Active" : "Inactive"}
-
-
-
-
{
- handleSetDefaultBrowser().catch((err: unknown) => {
- console.error(err);
- });
- }}
- disabled={isDefaultBrowser}
- variant={isDefaultBrowser ? "outline" : "default"}
- className="w-full"
- >
- {isDefaultBrowser
- ? "Already Default Browser"
- : "Set as Default Browser"}
-
-
+
Integrations
- When set as default, Donut Browser will handle web links and
- allow you to choose which profile to use.
+ Configure Local API and MCP (Model Context Protocol) for
+ integrating with external tools and AI assistants.
+
+ Open Integrations Settings
+
- )}
- {/* Permissions Section - Only show on macOS */}
- {isMacOS && (
+ {/* DNS Blocklist Section */}
- System Permissions
+ {t("dnsBlocklist.title")}
+
+ {t("dnsBlocklist.settingsDescription")}
+
+
setDnsBlocklistDialogOpen(true)}
+ >
+ {t("dnsBlocklist.manageLists")}
+
+
- {isLoadingPermissions ? (
-
- Loading permissions...
+ {/* Sync Encryption Section */}
+
+
+ {t("settings.encryption.title", "Sync Encryption")}
+
+
+ {t(
+ "settings.encryption.description",
+ "Set a password to enable E2E encrypted sync. If you lose this password, encrypted profiles cannot be recovered.",
+ )}
+
+
+ {!canUseEncryption ? (
+
+ {t(
+ "settings.encryption.requiresProOrOwner",
+ "Profile encryption is available for Pro users and team owners.",
+ )}
+
+ ) : hasE2ePassword ? (
+
+
+
+ {t("settings.encryption.passwordSet", "Active")}
+
+
+ {t(
+ "settings.encryption.passwordSetDescription",
+ "E2E encryption password is set",
+ )}
+
+
+
+
+
+
) : (
- {permissions.map((permission) => (
-
-
- {getPermissionIcon(permission.permission_type)}
-
-
- {getPermissionDisplayName(
- permission.permission_type,
- )}
-
-
- {permission.description}
-
-
-
-
- {getStatusBadge(permission.isGranted)}
- {!permission.isGranted && (
- {
- handleRequestPermission(
- permission.permission_type,
- ).catch((err: unknown) => {
- console.error(err);
- });
- }}
- >
- Grant
-
- )}
-
-
- ))}
-
- )}
-
-
- These permissions allow browsers launched from Donut Browser to
- access system resources. Each website will still ask for your
- permission individually.
-
-
- )}
-
- {/* Integrations Section */}
-
-
Integrations
-
- Configure Local API and MCP (Model Context Protocol) for
- integrating with external tools and AI assistants.
-
-
- Open Integrations Settings
-
-
-
- {/* Sync Encryption Section */}
-
-
- {t("settings.encryption.title", "Sync Encryption")}
-
-
- {t(
- "settings.encryption.description",
- "Set a password to enable E2E encrypted sync. If you lose this password, encrypted profiles cannot be recovered.",
- )}
-
-
- {!canUseEncryption ? (
-
- {t(
- "settings.encryption.requiresProOrOwner",
- "Profile encryption is available for Pro users and team owners.",
- )}
-
- ) : hasE2ePassword ? (
-
-
-
- {t("settings.encryption.passwordSet", "Active")}
-
-
- {t(
- "settings.encryption.passwordSetDescription",
- "E2E encryption password is set",
+
-
-
-
-
+
{
+ setE2ePasswordConfirm(e.target.value);
+ setE2eError("");
+ }}
+ />
+ {e2eError && (
+
{e2eError}
+ )}
+
{
+ 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("delete_e2e_password");
- setHasE2ePassword(false);
+ await invoke("set_e2e_password", {
+ password: e2ePassword,
+ });
+ setHasE2ePassword(true);
+ setE2ePassword("");
+ setE2ePasswordConfirm("");
showSuccessToast(
t(
- "settings.encryption.removed",
- "Encryption password removed",
+ "settings.encryption.passwordSaved",
+ "Encryption password set",
),
);
} catch (error) {
showErrorToast(String(error));
+ } finally {
+ setIsSavingE2e(false);
}
}}
>
- {t("settings.encryption.removePassword", "Remove Password")}
-
-
-
- ) : (
-
-
{
- setE2ePassword(e.target.value);
- setE2eError("");
- }}
- />
-
{
- setE2ePasswordConfirm(e.target.value);
- setE2eError("");
- }}
- />
- {e2eError && (
-
{e2eError}
- )}
-
{
- 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")}
-
-
- )}
-
-
- {/* Commercial License Section */}
-
-
Commercial License
-
-
- {trialStatus?.type === "Active" ? (
-
-
- Trial: {trialStatus.days_remaining} days,{" "}
- {trialStatus.hours_remaining} hours remaining
-
-
- Commercial use is free during the trial period
-
-
- ) : (
-
-
- Trial expired
-
-
- Personal use remains free. Commercial use requires a
- license.
-
+ {t("settings.encryption.setPassword", "Set Password")}
+
)}
-
- {/* Advanced Section */}
-
-
Advanced
+ {/* Commercial License Section */}
+
+
+ Commercial License
+
- {!isLinux && (
-
-
{
- updateSetting("disable_auto_updates", checked as boolean);
- }}
- />
-
-
- {t("settings.disableAutoUpdates")}
-
-
- {t("settings.disableAutoUpdatesDescription")}
-
+
+ {trialStatus?.type === "Active" ? (
+
+
+ Trial: {trialStatus.days_remaining} days,{" "}
+ {trialStatus.hours_remaining} hours remaining
+
+
+ Commercial use is free during the trial period
+
+
+ ) : (
+
+
+ Trial expired
+
+
+ Personal use remains free. Commercial use requires a
+ license.
+
+
+ )}
+
+
+
+ {/* Advanced Section */}
+
+
Advanced
+
+ {!isLinux && (
+
+
{
+ updateSetting("disable_auto_updates", checked as boolean);
+ }}
+ />
+
+
+ {t("settings.disableAutoUpdates")}
+
+
+ {t("settings.disableAutoUpdatesDescription")}
+
+
+ )}
+
+
{
+ handleClearCache().catch((err: unknown) => {
+ console.error(err);
+ });
+ }}
+ variant="outline"
+ className="w-full"
+ >
+ Clear All Version Cache
+
+
+
+ Clear all cached browser version data and refresh all browser
+ versions from their sources. This will force a fresh download of
+ version information for all browsers.
+
+
+
+ {/* System Info */}
+ {systemInfo && (
+
+
+ {`Donut Browser ${systemInfo.app_version}\n${systemInfo.os} ${systemInfo.arch}${systemInfo.portable ? " (portable)" : ""}`}
+
)}
+
+
+
+ Cancel
+
{
- handleClearCache().catch((err: unknown) => {
+ handleSave().catch((err: unknown) => {
console.error(err);
});
}}
- variant="outline"
- className="w-full"
+ disabled={isLoading || !hasChanges}
>
- Clear All Version Cache
+ Save Settings
-
-
- Clear all cached browser version data and refresh all browser
- versions from their sources. This will force a fresh download of
- version information for all browsers.
-
-
-
- {/* System Info */}
- {systemInfo && (
-
-
- {`Donut Browser ${systemInfo.app_version}\n${systemInfo.os} ${systemInfo.arch}${systemInfo.portable ? " (portable)" : ""}`}
-
-
- )}
-
-
-
-
- Cancel
-
- {
- handleSave().catch((err: unknown) => {
- console.error(err);
- });
- }}
- disabled={isLoading || !hasChanges}
- >
- Save Settings
-
-
-
-
+
+
+
+
setDnsBlocklistDialogOpen(false)}
+ />
+ >
);
}
diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx
index 83cc1b8..e1b7543 100644
--- a/src/components/theme-provider.tsx
+++ b/src/components/theme-provider.tsx
@@ -1,7 +1,13 @@
"use client";
-import { ThemeProvider } from "next-themes";
-import { useEffect, useState } from "react";
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
import { applyThemeColors, clearThemeColors } from "@/lib/themes";
interface AppSettings {
@@ -10,43 +16,62 @@ interface AppSettings {
custom_theme?: Record;
}
+interface ThemeContextValue {
+ theme: string;
+ setTheme: (theme: string) => void;
+}
+
+const ThemeContext = createContext({
+ theme: "system",
+ setTheme: () => {},
+});
+
+export function useTheme() {
+ return useContext(ThemeContext);
+}
+
interface CustomThemeProviderProps {
children: React.ReactNode;
}
+function resolveSystemTheme(): "light" | "dark" {
+ if (typeof window === "undefined") return "dark";
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+}
+
+function applyClassToHtml(theme: string) {
+ const resolved = theme === "system" ? resolveSystemTheme() : theme;
+ const root = document.documentElement;
+ root.classList.remove("light", "dark");
+ root.classList.add(resolved);
+}
+
export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
const [isLoading, setIsLoading] = useState(true);
- const [defaultTheme, setDefaultTheme] = useState("system");
- const [_mounted, setMounted] = useState(false);
+ const [theme, setThemeState] = useState("system");
- useEffect(() => {
- setMounted(true);
+ const setTheme = useCallback((newTheme: string) => {
+ setThemeState(newTheme);
+ if (newTheme === "custom") {
+ applyClassToHtml("dark");
+ } else {
+ applyClassToHtml(newTheme);
+ }
}, []);
+ // Load initial theme from Tauri settings
useEffect(() => {
const loadTheme = async () => {
try {
- // Lazy import to avoid pulling Tauri API on SSR
const { invoke } = await import("@tauri-apps/api/core");
const settings = await invoke("get_app_settings");
const themeValue = settings?.theme ?? "system";
- console.log("[theme-provider] Loaded settings:", {
- theme: themeValue,
- hasCustomTheme: !!settings?.custom_theme,
- customThemeKeys: settings?.custom_theme
- ? Object.keys(settings.custom_theme).length
- : 0,
- });
-
- if (
- themeValue === "light" ||
- themeValue === "dark" ||
- themeValue === "system"
- ) {
- setDefaultTheme(themeValue);
- } else if (themeValue === "custom") {
- setDefaultTheme("dark");
+ if (themeValue === "custom") {
+ setThemeState("custom");
+ applyClassToHtml("dark");
if (
settings.custom_theme &&
Object.keys(settings.custom_theme).length > 0
@@ -57,16 +82,22 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
console.warn("Failed to apply custom theme variables:", error);
}
}
+ } else if (
+ themeValue === "light" ||
+ themeValue === "dark" ||
+ themeValue === "system"
+ ) {
+ setThemeState(themeValue);
+ applyClassToHtml(themeValue);
} else {
- setDefaultTheme("system");
+ applyClassToHtml("system");
}
} catch (error) {
- // Failed to load settings; fall back to system (handled by next-themes)
console.warn(
"Failed to load theme settings; defaulting to system:",
error,
);
- setDefaultTheme("system");
+ applyClassToHtml("system");
} finally {
setIsLoading(false);
}
@@ -75,44 +106,44 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
void loadTheme();
}, []);
- // Additional effect to ensure custom theme is applied after mount
+ // Re-apply custom theme after mount
useEffect(() => {
- if (!isLoading && _mounted) {
+ if (!isLoading && theme === "custom") {
const reapplyCustomTheme = async () => {
try {
const { invoke } = await import("@tauri-apps/api/core");
const settings = await invoke("get_app_settings");
-
if (settings?.theme === "custom" && settings.custom_theme) {
applyThemeColors(settings.custom_theme);
- } else {
- clearThemeColors();
}
} catch (error) {
console.warn("Failed to reapply custom theme:", error);
}
};
-
- // Apply after a short delay to ensure CSS has loaded
setTimeout(() => {
void reapplyCustomTheme();
}, 100);
+ } else if (!isLoading) {
+ clearThemeColors();
}
- }, [isLoading, _mounted]);
+ }, [isLoading, theme]);
+
+ // Listen for system theme changes when in "system" mode
+ useEffect(() => {
+ if (theme !== "system") return;
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
+ const handler = () => applyClassToHtml("system");
+ mq.addEventListener("change", handler);
+ return () => mq.removeEventListener("change", handler);
+ }, [theme]);
+
+ const value = useMemo(() => ({ theme, setTheme }), [theme, setTheme]);
if (isLoading) {
- // Keep UI simple during initial settings load to avoid flicker
return null;
}
return (
-
- {children}
-
+ {children}
);
}
diff --git a/src/components/traffic-details-dialog.tsx b/src/components/traffic-details-dialog.tsx
index 6a1dcb1..6a61d84 100644
--- a/src/components/traffic-details-dialog.tsx
+++ b/src/components/traffic-details-dialog.tsx
@@ -295,7 +295,12 @@ export function TrafficDetailsDialog({
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
index ce54483..7003e7d 100644
--- a/src/components/ui/sonner.tsx
+++ b/src/components/ui/sonner.tsx
@@ -1,7 +1,7 @@
"use client";
-import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
+import { useTheme } from "@/components/theme-provider";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 246a58e..67b3eb2 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -858,5 +858,22 @@
"cookieImportLocked": "Cookie import is a Pro feature",
"cookieExportLocked": "Cookie export is a Pro feature",
"cookieManagementLocked": "Cookie management is a Pro feature"
+ },
+ "dnsBlocklist": {
+ "title": "DNS Blocklist",
+ "none": "None",
+ "light": "Light",
+ "normal": "Normal",
+ "pro": "Pro",
+ "proPlus": "Pro++",
+ "ultimate": "Ultimate",
+ "settingsDescription": "DNS blocklists block ads, trackers, and malware domains at the proxy level. Lists are automatically refreshed every 12 hours.",
+ "manageLists": "Manage DNS Blocklists",
+ "refreshAll": "Refresh All Lists",
+ "refreshFailed": "Failed to refresh DNS blocklists",
+ "domains": "domains",
+ "fresh": "Fresh",
+ "stale": "Stale",
+ "notCached": "Not cached"
}
}
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json
index 3cd58f8..d6230f9 100644
--- a/src/i18n/locales/es.json
+++ b/src/i18n/locales/es.json
@@ -858,5 +858,22 @@
"cookieImportLocked": "La importació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"
+ },
+ "dnsBlocklist": {
+ "title": "Lista de bloqueo DNS",
+ "none": "Ninguno",
+ "light": "Light",
+ "normal": "Normal",
+ "pro": "Pro",
+ "proPlus": "Pro++",
+ "ultimate": "Ultimate",
+ "settingsDescription": "Las listas de bloqueo DNS bloquean anuncios, rastreadores y dominios de malware a nivel de proxy. Las listas se actualizan automáticamente cada 12 horas.",
+ "manageLists": "Gestionar listas de bloqueo DNS",
+ "refreshAll": "Actualizar todas las listas",
+ "refreshFailed": "Error al actualizar las listas de bloqueo DNS",
+ "domains": "dominios",
+ "fresh": "Actualizado",
+ "stale": "Desactualizado",
+ "notCached": "Sin caché"
}
}
diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json
index 34f58c2..e74fa4e 100644
--- a/src/i18n/locales/fr.json
+++ b/src/i18n/locales/fr.json
@@ -858,5 +858,22 @@
"cookieImportLocked": "L'importation de cookies est une fonctionnalité Pro",
"cookieExportLocked": "L'exportation de cookies est une fonctionnalité Pro",
"cookieManagementLocked": "La gestion des cookies est une fonctionnalité Pro"
+ },
+ "dnsBlocklist": {
+ "title": "Liste de blocage DNS",
+ "none": "Aucun",
+ "light": "Light",
+ "normal": "Normal",
+ "pro": "Pro",
+ "proPlus": "Pro++",
+ "ultimate": "Ultimate",
+ "settingsDescription": "Les listes de blocage DNS bloquent les publicités, les traqueurs et les domaines malveillants au niveau du proxy. Les listes sont automatiquement rafraîchies toutes les 12 heures.",
+ "manageLists": "Gérer les listes de blocage DNS",
+ "refreshAll": "Rafraîchir toutes les listes",
+ "refreshFailed": "Échec du rafraîchissement des listes de blocage DNS",
+ "domains": "domaines",
+ "fresh": "À jour",
+ "stale": "Obsolète",
+ "notCached": "Non mis en cache"
}
}
diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json
index ceacea3..7081525 100644
--- a/src/i18n/locales/ja.json
+++ b/src/i18n/locales/ja.json
@@ -858,5 +858,22 @@
"cookieImportLocked": "Cookieのインポートはプロ機能です",
"cookieExportLocked": "Cookieのエクスポートはプロ機能です",
"cookieManagementLocked": "Cookie管理はプロ機能です"
+ },
+ "dnsBlocklist": {
+ "title": "DNSブロックリスト",
+ "none": "なし",
+ "light": "Light",
+ "normal": "Normal",
+ "pro": "Pro",
+ "proPlus": "Pro++",
+ "ultimate": "Ultimate",
+ "settingsDescription": "DNSブロックリストは、プロキシレベルで広告、トラッカー、マルウェアドメインをブロックします。リストは12時間ごとに自動的に更新されます。",
+ "manageLists": "DNSブロックリストを管理",
+ "refreshAll": "すべてのリストを更新",
+ "refreshFailed": "DNSブロックリストの更新に失敗しました",
+ "domains": "ドメイン",
+ "fresh": "最新",
+ "stale": "期限切れ",
+ "notCached": "キャッシュなし"
}
}
diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json
index 5ee1630..de674a6 100644
--- a/src/i18n/locales/pt.json
+++ b/src/i18n/locales/pt.json
@@ -858,5 +858,22 @@
"cookieImportLocked": "A importação de cookies é um recurso Pro",
"cookieExportLocked": "A exportação de cookies é um recurso Pro",
"cookieManagementLocked": "O gerenciamento de cookies é um recurso Pro"
+ },
+ "dnsBlocklist": {
+ "title": "Lista de bloqueio DNS",
+ "none": "Nenhum",
+ "light": "Light",
+ "normal": "Normal",
+ "pro": "Pro",
+ "proPlus": "Pro++",
+ "ultimate": "Ultimate",
+ "settingsDescription": "As listas de bloqueio DNS bloqueiam anúncios, rastreadores e domínios de malware no nível do proxy. As listas são atualizadas automaticamente a cada 12 horas.",
+ "manageLists": "Gerenciar listas de bloqueio DNS",
+ "refreshAll": "Atualizar todas as listas",
+ "refreshFailed": "Falha ao atualizar as listas de bloqueio DNS",
+ "domains": "domínios",
+ "fresh": "Atualizado",
+ "stale": "Desatualizado",
+ "notCached": "Sem cache"
}
}
diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json
index ca704e7..84bfe4e 100644
--- a/src/i18n/locales/ru.json
+++ b/src/i18n/locales/ru.json
@@ -858,5 +858,22 @@
"cookieImportLocked": "Импорт cookies — функция Pro",
"cookieExportLocked": "Экспорт cookies — функция Pro",
"cookieManagementLocked": "Управление cookies — функция Pro"
+ },
+ "dnsBlocklist": {
+ "title": "Список блокировки DNS",
+ "none": "Нет",
+ "light": "Light",
+ "normal": "Normal",
+ "pro": "Pro",
+ "proPlus": "Pro++",
+ "ultimate": "Ultimate",
+ "settingsDescription": "Списки блокировки DNS блокируют рекламу, трекеры и вредоносные домены на уровне прокси. Списки автоматически обновляются каждые 12 часов.",
+ "manageLists": "Управление списками блокировки DNS",
+ "refreshAll": "Обновить все списки",
+ "refreshFailed": "Не удалось обновить списки блокировки DNS",
+ "domains": "доменов",
+ "fresh": "Актуальный",
+ "stale": "Устаревший",
+ "notCached": "Не кэшировано"
}
}
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index ca6ae2e..312d4ef 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -858,5 +858,22 @@
"cookieImportLocked": "Cookie 导入是 Pro 功能",
"cookieExportLocked": "Cookie 导出是 Pro 功能",
"cookieManagementLocked": "Cookie 管理是 Pro 功能"
+ },
+ "dnsBlocklist": {
+ "title": "DNS 拦截列表",
+ "none": "无",
+ "light": "Light",
+ "normal": "Normal",
+ "pro": "Pro",
+ "proPlus": "Pro++",
+ "ultimate": "Ultimate",
+ "settingsDescription": "DNS 拦截列表在代理级别拦截广告、跟踪器和恶意软件域名。列表每 12 小时自动刷新一次。",
+ "manageLists": "管理 DNS 拦截列表",
+ "refreshAll": "刷新所有列表",
+ "refreshFailed": "刷新 DNS 拦截列表失败",
+ "domains": "个域名",
+ "fresh": "最新",
+ "stale": "过期",
+ "notCached": "未缓存"
}
}
diff --git a/src/types.ts b/src/types.ts
index 258c9af..2cc1d43 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -35,6 +35,7 @@ export interface BrowserProfile {
proxy_bypass_rules?: string[];
created_by_id?: string;
created_by_email?: string;
+ dns_blocklist?: string;
}
export interface Extension {