fix for dark mode in pop-up notifications, system theme detection

This commit is contained in:
coolcoala
2025-08-03 11:06:19 +03:00
parent 1f7561298c
commit 9abc30b60c
7 changed files with 58 additions and 141 deletions

View File

@@ -1,14 +1,11 @@
import { AppDataProvider } from "./providers/app-data-provider";
import { ThemeProvider } from "@/components/layout/theme-provider";
import Layout from "./pages/_layout";
function App() {
return (
<ThemeProvider>
<AppDataProvider>
<Layout />
</AppDataProvider>
</ThemeProvider>
<AppDataProvider>
<Layout />
</AppDataProvider>
);
}
export default App;

View File

@@ -1,30 +0,0 @@
"use client";
import { Toaster, toast } from "sonner";
import { useEffect, useSyncExternalStore } from "react";
import {
getSnapshotNotices,
hideNotice,
subscribeNotices,
} from "@/services/noticeService";
export const NoticeManager = () => {
const currentNotices = useSyncExternalStore(
subscribeNotices,
getSnapshotNotices,
);
useEffect(() => {
for (const notice of currentNotices) {
const toastId = toast(notice.message, {
id: notice.id,
duration: notice.duration,
onDismiss: (t) => {
hideNotice(t.id as number);
},
});
}
}, [currentNotices]);
return <Toaster />;
};

View File

@@ -5,4 +5,3 @@ export { BaseLoading } from "./base-loading";
export { BaseErrorBoundary } from "./base-error-boundary";
export { Switch } from "./base-switch";
export { BaseLoadingOverlay } from "./base-loading-overlay";
export { NoticeManager } from "./NoticeManager";

View File

@@ -1,47 +1,52 @@
import { useEffect, useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import { useSetThemeMode, useThemeMode } from "@/services/states";
import { useVerge } from "@/hooks/use-verge";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { getCurrentWebviewWindow, WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { Theme } from "@tauri-apps/api/window";
export const useCustomTheme = () => {
const appWindow = useMemo(() => getCurrentWebviewWindow(), []);
const appWindow: WebviewWindow = useMemo(() => getCurrentWebviewWindow(), []);
const { verge } = useVerge();
const { theme_mode } = verge ?? {};
const mode = useThemeMode();
const setMode = useSetThemeMode();
const [systemTheme, setSystemTheme] = useState(
() => window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
);
useEffect(() => {
setMode(
theme_mode === "light" || theme_mode === "dark" ? theme_mode : "system",
theme_mode === "light" || theme_mode === "dark" ? theme_mode : "system",
);
}, [theme_mode, setMode]);
useEffect(() => {
const root = document.documentElement;
if (mode !== 'system') return;
const activeTheme =
mode === "system"
? window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: mode;
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = (e: MediaQueryListEvent) => {
setSystemTheme(e.matches ? "dark" : "light");
};
root.classList.remove("light", "dark");
root.classList.add(activeTheme);
appWindow.setTheme(activeTheme as Theme).catch(console.error);
}, [mode, appWindow]);
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [mode]);
useEffect(() => {
if (theme_mode !== "system") return;
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
setMode(payload);
});
return () => {
unlistenPromise.then((f) => f());
};
}, [theme_mode, appWindow, setMode]);
const root = document.documentElement;
const activeTheme = mode === "system" ? systemTheme : mode;
root.classList.remove("light", "dark");
root.classList.add(activeTheme);
if (theme_mode === "system") {
appWindow.setTheme(null).catch(console.error);
} else {
appWindow.setTheme(activeTheme as Theme).catch(console.error);
}
}, [mode, systemTheme, appWindow, theme_mode]);
return {};
};
};

View File

@@ -1,7 +1,9 @@
@import "tailwindcss";
@import "tw-animate-css";
@variant dark .dark &;
@theme {
--tailwind-darkMode: 'class';
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);

View File

@@ -21,7 +21,7 @@ import { useClashInfo } from "@/hooks/use-clash";
import { initGlobalLogService } from "@/services/global-log-service";
import { invoke } from "@tauri-apps/api/core";
import { showNotice } from "@/services/noticeService";
import { NoticeManager } from "@/components/base/NoticeManager";
import { Toaster } from "@/components/ui/sonner";
import { SidebarProvider, useSidebar } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/layout/sidebar";
import { useZoomControls } from "@/hooks/useZoomControls";
@@ -472,9 +472,9 @@ const Layout = () => {
return (
<SWRConfig value={{ errorRetryCount: 3 }}>
<NoticeManager />
<SidebarProvider defaultOpen={false}>
<AppLayout />
<Toaster />
</SidebarProvider>
</SWRConfig>
);

View File

@@ -1,81 +1,25 @@
import { ReactNode } from "react";
import { toast } from "sonner";
export interface NoticeItem {
id: number;
type: "success" | "error" | "info";
message: ReactNode;
duration: number;
timerId?: ReturnType<typeof setTimeout>;
}
type NoticeType = 'success' | 'error' | 'info' | 'warning';
type Listener = (notices: NoticeItem[]) => void;
export const showNotice = (type: NoticeType, message: string, duration?: number) => {
const options = duration ? { duration } : {};
let nextId = 0;
let notices: NoticeItem[] = [];
const listeners: Set<Listener> = new Set();
function notifyListeners() {
listeners.forEach((listener) => listener([...notices])); // Pass a copy
}
// Shows a notification.
export function showNotice(
type: "success" | "error" | "info",
message: ReactNode,
duration?: number,
): number {
const id = nextId++;
const effectiveDuration =
duration ?? (type === "error" ? 8000 : type === "info" ? 5000 : 3000); // Longer defaults
const newNotice: NoticeItem = {
id,
type,
message,
duration: effectiveDuration,
};
// Auto-hide timer (only if duration is not null/0)
if (effectiveDuration > 0) {
newNotice.timerId = setTimeout(() => {
hideNotice(id);
}, effectiveDuration);
switch (type) {
case 'success':
toast.success(message, options);
break;
case 'error':
toast.error(message, options);
break;
case 'info':
toast.info(message, options);
break;
case 'warning':
toast.warning(message, options);
break;
default:
toast(message, options);
break;
}
notices = [...notices, newNotice];
notifyListeners();
return id;
}
// Hides a specific notification by its ID.
export function hideNotice(id: number) {
const notice = notices.find((n) => n.id === id);
if (notice?.timerId) {
clearTimeout(notice.timerId); // Clear timeout if manually closed
}
notices = notices.filter((n) => n.id !== id);
notifyListeners();
}
// Subscribes a listener function to notice state changes.
export function subscribeNotices(listener: () => void) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
export function getSnapshotNotices() {
return notices;
}
// Function to clear all notices at once
export function clearAllNotices() {
notices.forEach((n) => {
if (n.timerId) clearTimeout(n.timerId);
});
notices = [];
notifyListeners();
}
};