fix for dark mode in pop-up notifications, system theme detection
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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 />;
|
|
||||||
};
|
|
||||||
@@ -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";
|
|
||||||
|
|||||||
@@ -1,47 +1,52 @@
|
|||||||
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",
|
||||||
);
|
);
|
||||||
}, [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 {};
|
||||||
};
|
};
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user