feat: dns block lists

This commit is contained in:
zhom
2026-03-31 14:21:31 +04:00
parent cb8093fbde
commit 35723de96a
39 changed files with 1880 additions and 579 deletions
+2 -19
View File
@@ -1,14 +1,7 @@
"use client";
import { Geist, Geist_Mono } from "next/font/google";
import "@/styles/globals.css";
import "flag-icons/css/flag-icons.min.css";
import { useEffect } from "react";
import { I18nProvider } from "@/components/i18n-provider";
import { CustomThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { WindowDragArea } from "@/components/window-drag-area";
import { setupLogging } from "@/lib/logger";
import { ClientProviders } from "@/components/client-providers";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -25,22 +18,12 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
useEffect(() => {
void setupLogging();
}, []);
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased overflow-hidden bg-background`}
>
<I18nProvider>
<CustomThemeProvider>
<WindowDragArea />
<TooltipProvider>{children}</TooltipProvider>
<Toaster />
</CustomThemeProvider>
</I18nProvider>
<ClientProviders>{children}</ClientProviders>
</body>
</html>
);
+2
View File
@@ -515,6 +515,7 @@ export default function Home() {
groupId?: string;
extensionGroupId?: string;
ephemeral?: boolean;
dnsBlocklist?: string;
}) => {
try {
const profile = await invoke<BrowserProfile>(
@@ -532,6 +533,7 @@ export default function Home() {
profileData.groupId ??
(selectedGroupId !== "default" ? selectedGroupId : undefined),
ephemeral: profileData.ephemeral,
dnsBlocklist: profileData.dnsBlocklist,
},
);
+6 -1
View File
@@ -68,7 +68,12 @@ export function BandwidthMiniChart({
)}
>
<div className="flex-1 h-3 pointer-events-none">
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer
width="100%"
height="100%"
minWidth={1}
minHeight={1}
>
<AreaChart
data={chartData}
margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
+25
View File
@@ -0,0 +1,25 @@
"use client";
import { useEffect } from "react";
import { I18nProvider } from "@/components/i18n-provider";
import { CustomThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { WindowDragArea } from "@/components/window-drag-area";
import { setupLogging } from "@/lib/logger";
export function ClientProviders({ children }: { children: React.ReactNode }) {
useEffect(() => {
void setupLogging();
}, []);
return (
<I18nProvider>
<CustomThemeProvider>
<WindowDragArea />
<TooltipProvider>{children}</TooltipProvider>
<Toaster />
</CustomThemeProvider>
</I18nProvider>
);
}
+42
View File
@@ -84,6 +84,7 @@ interface CreateProfileDialogProps {
groupId?: string;
extensionGroupId?: string;
ephemeral?: boolean;
dnsBlocklist?: string;
}) => Promise<void>;
selectedGroupId?: string;
crossOsUnlocked?: boolean;
@@ -124,6 +125,7 @@ export function CreateProfileDialog({
useState<BrowserTypeString | null>(null);
const [selectedProxyId, setSelectedProxyId] = useState<string>();
const [proxyPopoverOpen, setProxyPopoverOpen] = useState(false);
const [dnsBlocklist, setDnsBlocklist] = useState<string>("");
// Camoufox anti-detect states
const [camoufoxConfig, setCamoufoxConfig] = useState<CamoufoxConfig>(() => ({
@@ -395,6 +397,7 @@ export function CreateProfileDialog({
selectedGroupId !== "default" ? selectedGroupId : undefined,
extensionGroupId: selectedExtensionGroupId,
ephemeral,
dnsBlocklist: dnsBlocklist || undefined,
});
} else {
// Default to Camoufox
@@ -420,6 +423,7 @@ export function CreateProfileDialog({
selectedGroupId !== "default" ? selectedGroupId : undefined,
extensionGroupId: selectedExtensionGroupId,
ephemeral,
dnsBlocklist: dnsBlocklist || undefined,
});
}
} else {
@@ -443,6 +447,7 @@ export function CreateProfileDialog({
releaseType: bestVersion.releaseType,
proxyId: selectedProxyId,
groupId: selectedGroupId !== "default" ? selectedGroupId : undefined,
dnsBlocklist: dnsBlocklist || undefined,
});
}
@@ -1162,6 +1167,43 @@ export function CreateProfileDialog({
)}
</div>
{/* DNS Blocklist */}
<div className="space-y-2">
<Label>{t("dnsBlocklist.title")}</Label>
<Select
value={dnsBlocklist || "none"}
onValueChange={(val) => {
setDnsBlocklist(val === "none" ? "" : val);
}}
>
<SelectTrigger>
<SelectValue
placeholder={t("dnsBlocklist.none")}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="none">
{t("dnsBlocklist.none")}
</SelectItem>
<SelectItem value="light">
{t("dnsBlocklist.light")}
</SelectItem>
<SelectItem value="normal">
{t("dnsBlocklist.normal")}
</SelectItem>
<SelectItem value="pro">
{t("dnsBlocklist.pro")}
</SelectItem>
<SelectItem value="pro_plus">
{t("dnsBlocklist.proPlus")}
</SelectItem>
<SelectItem value="ultimate">
{t("dnsBlocklist.ultimate")}
</SelectItem>
</SelectContent>
</Select>
</div>
{/* Extension Group */}
{extensionGroups.length > 0 && (
<div className="space-y-2">
+147
View File
@@ -0,0 +1,147 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuRefreshCw } from "react-icons/lu";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
interface BlocklistCacheStatus {
level: string;
display_name: string;
entry_count: number;
file_size_bytes: number;
last_updated: number | null;
is_fresh: boolean;
is_cached: boolean;
}
interface DnsBlocklistDialogProps {
isOpen: boolean;
onClose: () => void;
}
export function DnsBlocklistDialog({
isOpen,
onClose,
}: DnsBlocklistDialogProps) {
const { t } = useTranslation();
const [statuses, setStatuses] = useState<BlocklistCacheStatus[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
const loadStatuses = useCallback(async () => {
try {
const result = await invoke<BlocklistCacheStatus[]>(
"get_dns_blocklist_cache_status",
);
setStatuses(result);
} catch (e) {
console.error("Failed to load blocklist status:", e);
}
}, []);
useEffect(() => {
if (isOpen) {
void loadStatuses();
}
}, [isOpen, loadStatuses]);
const handleRefreshAll = async () => {
setIsRefreshing(true);
try {
await invoke("refresh_dns_blocklists");
await loadStatuses();
} catch (e) {
console.error("Failed to refresh blocklists:", e);
} finally {
setIsRefreshing(false);
}
};
const formatSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
const formatDate = (timestamp: number | null) => {
if (!timestamp) return t("dnsBlocklist.notCached");
return new Date(timestamp * 1000).toLocaleString();
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t("dnsBlocklist.title")}</DialogTitle>
</DialogHeader>
<p className="text-sm text-muted-foreground">
{t("dnsBlocklist.settingsDescription")}
</p>
<div className="space-y-3">
{statuses.map((status) => (
<div
key={status.level}
className="flex items-center justify-between rounded-md border border-border p-3"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">
{status.display_name}
</span>
{status.is_cached ? (
status.is_fresh ? (
<Badge variant="default" className="text-[10px] px-1.5">
{t("dnsBlocklist.fresh")}
</Badge>
) : (
<Badge variant="secondary" className="text-[10px] px-1.5">
{t("dnsBlocklist.stale")}
</Badge>
)
) : (
<Badge
variant="outline"
className="text-[10px] px-1.5 text-muted-foreground"
>
{t("dnsBlocklist.notCached")}
</Badge>
)}
</div>
{status.is_cached && (
<div className="text-xs text-muted-foreground">
{status.entry_count.toLocaleString()}{" "}
{t("dnsBlocklist.domains")} &middot;{" "}
{formatSize(status.file_size_bytes)} &middot;{" "}
{formatDate(status.last_updated)}
</div>
)}
</div>
</div>
))}
</div>
<Button
onClick={handleRefreshAll}
disabled={isRefreshing}
variant="outline"
className="w-full"
>
<LuRefreshCw
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
/>
{t("dnsBlocklist.refreshAll")}
</Button>
</DialogContent>
</Dialog>
);
}
+14
View File
@@ -31,6 +31,7 @@ import {
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog";
import {
ProfileBypassRulesDialog,
ProfileDnsBlocklistDialog,
ProfileInfoDialog,
} from "@/components/profile-info-dialog";
import { Badge } from "@/components/ui/badge";
@@ -934,6 +935,8 @@ export function ProfilesDataTable({
React.useState<BrowserProfile | null>(null);
const [bypassRulesProfile, setBypassRulesProfile] =
React.useState<BrowserProfile | null>(null);
const [dnsBlocklistProfile, setDnsBlocklistProfile] =
React.useState<BrowserProfile | null>(null);
const [launchingProfiles, setLaunchingProfiles] = React.useState<Set<string>>(
new Set(),
);
@@ -2674,6 +2677,9 @@ export function ProfilesDataTable({
onOpenBypassRules={(profile) => {
setBypassRulesProfile(profile);
}}
onOpenDnsBlocklist={(profile) => {
setDnsBlocklistProfile(profile);
}}
onCloneProfile={onCloneProfile}
onLaunchWithSync={onLaunchWithSync}
onDeleteProfile={(profile) => {
@@ -2756,6 +2762,14 @@ export function ProfilesDataTable({
profileId={bypassRulesProfile?.id ?? null}
initialRules={bypassRulesProfile?.proxy_bypass_rules ?? []}
/>
<ProfileDnsBlocklistDialog
isOpen={dnsBlocklistProfile !== null}
onClose={() => {
setDnsBlocklistProfile(null);
}}
profileId={dnsBlocklistProfile?.id ?? null}
currentLevel={dnsBlocklistProfile?.dns_blocklist ?? null}
/>
</>
);
}
+107 -3
View File
@@ -17,6 +17,7 @@ import {
LuPuzzle,
LuRefreshCw,
LuSettings,
LuShield,
LuShieldCheck,
LuTrash2,
LuUsers,
@@ -64,6 +65,7 @@ interface ProfileInfoDialogProps {
onOpenCookieManagement?: (profile: BrowserProfile) => void;
onAssignExtensionGroup?: (profileIds: string[]) => void;
onOpenBypassRules?: (profile: BrowserProfile) => void;
onOpenDnsBlocklist?: (profile: BrowserProfile) => void;
onCloneProfile?: (profile: BrowserProfile) => void;
onDeleteProfile?: (profile: BrowserProfile) => void;
onLaunchWithSync?: (profile: BrowserProfile) => void;
@@ -110,6 +112,7 @@ export function ProfileInfoDialog({
onOpenCookieManagement,
onAssignExtensionGroup,
onOpenBypassRules,
onOpenDnsBlocklist,
onCloneProfile,
onDeleteProfile,
onLaunchWithSync,
@@ -315,9 +318,8 @@ export function ProfileInfoDialog({
onClick: () => {
handleAction(() => onAssignExtensionGroup?.([profile.id]));
},
disabled: isDisabled || !crossOsUnlocked,
proBadge: !crossOsUnlocked,
runningBadge: isRunning && crossOsUnlocked,
disabled: isDisabled,
runningBadge: isRunning,
hidden: profile.ephemeral === true,
},
{
@@ -327,6 +329,13 @@ export function ProfileInfoDialog({
handleAction(() => onOpenBypassRules?.(profile));
},
},
{
icon: <LuShield className="w-4 h-4" />,
label: t("dnsBlocklist.title"),
onClick: () => {
handleAction(() => onOpenDnsBlocklist?.(profile));
},
},
{
icon: <LuTrash2 className="w-4 h-4" />,
label: t("profiles.actions.delete"),
@@ -455,6 +464,16 @@ export function ProfileInfoDialog({
: t("profileInfo.values.never")
}
/>
<InfoCard
label={t("dnsBlocklist.title")}
value={
profile.dns_blocklist
? t(
`dnsBlocklist.${profile.dns_blocklist === "pro_plus" ? "proPlus" : profile.dns_blocklist}`,
)
: t("dnsBlocklist.none")
}
/>
</div>
{/* Sync */}
@@ -563,6 +582,91 @@ export function ProfileInfoDialog({
);
}
interface ProfileDnsBlocklistDialogProps {
isOpen: boolean;
onClose: () => void;
profileId: string | null;
currentLevel: string | null;
}
export function ProfileDnsBlocklistDialog({
isOpen,
onClose,
profileId,
currentLevel,
}: ProfileDnsBlocklistDialogProps) {
const { t } = useTranslation();
const [level, setLevel] = React.useState(currentLevel ?? "");
const [isSaving, setIsSaving] = React.useState(false);
React.useEffect(() => {
if (isOpen) {
setLevel(currentLevel ?? "");
}
}, [isOpen, currentLevel]);
const handleSave = async () => {
if (!profileId) return;
setIsSaving(true);
try {
await invoke("update_profile_dns_blocklist", {
profileId,
dnsBlocklist: level || null,
});
onClose();
} catch (err) {
console.error("Failed to update DNS blocklist:", err);
} finally {
setIsSaving(false);
}
};
const options = [
{ value: "", label: t("dnsBlocklist.none") },
{ value: "light", label: t("dnsBlocklist.light") },
{ value: "normal", label: t("dnsBlocklist.normal") },
{ value: "pro", label: t("dnsBlocklist.pro") },
{ value: "pro_plus", label: t("dnsBlocklist.proPlus") },
{ value: "ultimate", label: t("dnsBlocklist.ultimate") },
];
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-xs">
<DialogHeader>
<DialogTitle>{t("dnsBlocklist.title")}</DialogTitle>
</DialogHeader>
<p className="text-xs text-muted-foreground">
{t("dnsBlocklist.settingsDescription")}
</p>
<div className="space-y-1">
{options.map((option) => (
<button
key={option.value}
type="button"
onClick={() => setLevel(option.value)}
className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors ${
level === option.value
? "bg-primary/10 text-primary border border-primary/30"
: "hover:bg-accent border border-transparent"
}`}
>
{option.label}
</button>
))}
</div>
<Button
onClick={() => void handleSave()}
disabled={isSaving || level === (currentLevel ?? "")}
className="w-full"
>
{t("common.save", "Save")}
</Button>
</DialogContent>
</Dialog>
);
}
interface ProfileBypassRulesDialogProps {
isOpen: boolean;
onClose: () => void;
File diff suppressed because it is too large Load Diff
+74 -43
View File
@@ -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<string, string>;
}
interface ThemeContextValue {
theme: string;
setTheme: (theme: string) => void;
}
const ThemeContext = createContext<ThemeContextValue>({
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<string>("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<AppSettings>("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<AppSettings>("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 (
<ThemeProvider
attribute="class"
defaultTheme={defaultTheme}
enableSystem={true}
disableTransitionOnChange={false}
>
{children}
</ThemeProvider>
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
+6 -1
View File
@@ -295,7 +295,12 @@ export function TrafficDetailsDialog({
</div>
<div className="h-[200px] w-full">
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer
width="100%"
height="100%"
minWidth={1}
minHeight={1}
>
<AreaChart
data={chartData}
margin={{ top: 10, right: 10, bottom: 0, left: 0 }}
+1 -1
View File
@@ -67,7 +67,7 @@ const ChartContainer = React.forwardRef<
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
<RechartsPrimitive.ResponsiveContainer minWidth={1} minHeight={1}>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
+1 -1
View File
@@ -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();
+17
View File
@@ -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"
}
}
+17
View File
@@ -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é"
}
}
+17
View File
@@ -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"
}
}
+17
View File
@@ -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": "キャッシュなし"
}
}
+17
View File
@@ -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"
}
}
+17
View File
@@ -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": "Не кэшировано"
}
}
+17
View File
@@ -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": "未缓存"
}
}
+1
View File
@@ -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 {