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 { AppDataProvider } from "./providers/app-data-provider";
import { ThemeProvider } from "@/components/layout/theme-provider";
import Layout from "./pages/_layout"; import Layout from "./pages/_layout";
function App() { function App() {
return ( return (
<ThemeProvider>
<AppDataProvider> <AppDataProvider>
<Layout /> <Layout />
</AppDataProvider> </AppDataProvider>
</ThemeProvider>
); );
} }
export default App; 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 { BaseErrorBoundary } from "./base-error-boundary";
export { Switch } from "./base-switch"; export { Switch } from "./base-switch";
export { BaseLoadingOverlay } from "./base-loading-overlay"; export { BaseLoadingOverlay } from "./base-loading-overlay";
export { NoticeManager } from "./NoticeManager";

View File

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

View File

@@ -1,7 +1,9 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@variant dark .dark &; @theme {
--tailwind-darkMode: 'class';
}
@theme inline { @theme inline {
--radius-sm: calc(var(--radius) - 4px); --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 { initGlobalLogService } from "@/services/global-log-service";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { showNotice } from "@/services/noticeService"; 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 { SidebarProvider, useSidebar } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/layout/sidebar"; import { AppSidebar } from "@/components/layout/sidebar";
import { useZoomControls } from "@/hooks/useZoomControls"; import { useZoomControls } from "@/hooks/useZoomControls";
@@ -472,9 +472,9 @@ const Layout = () => {
return ( return (
<SWRConfig value={{ errorRetryCount: 3 }}> <SWRConfig value={{ errorRetryCount: 3 }}>
<NoticeManager />
<SidebarProvider defaultOpen={false}> <SidebarProvider defaultOpen={false}>
<AppLayout /> <AppLayout />
<Toaster />
</SidebarProvider> </SidebarProvider>
</SWRConfig> </SWRConfig>
); );

View File

@@ -1,81 +1,25 @@
import { ReactNode } from "react"; import { toast } from "sonner";
export interface NoticeItem { type NoticeType = 'success' | 'error' | 'info' | 'warning';
id: number;
type: "success" | "error" | "info";
message: ReactNode;
duration: number;
timerId?: ReturnType<typeof setTimeout>;
}
type Listener = (notices: NoticeItem[]) => void; export const showNotice = (type: NoticeType, message: string, duration?: number) => {
const options = duration ? { duration } : {};
let nextId = 0; switch (type) {
let notices: NoticeItem[] = []; case 'success':
const listeners: Set<Listener> = new Set(); toast.success(message, options);
break;
function notifyListeners() { case 'error':
listeners.forEach((listener) => listener([...notices])); // Pass a copy toast.error(message, options);
} break;
case 'info':
// Shows a notification. toast.info(message, options);
break;
export function showNotice( case 'warning':
type: "success" | "error" | "info", toast.warning(message, options);
message: ReactNode, break;
duration?: number, default:
): number { toast(message, options);
const id = nextId++; break;
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);
} }
};
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();
}