mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-10 05:00:43 +02:00
feat: dns block lists
This commit is contained in:
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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")} ·{" "}
|
||||
{formatSize(status.file_size_bytes)} ·{" "}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+531
-496
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
Reference in New Issue
Block a user