From 8296675574cde7a022500bc0289b8331ed9e7281 Mon Sep 17 00:00:00 2001 From: wonfen Date: Sun, 4 May 2025 22:17:08 +0800 Subject: [PATCH] refactor: notification system --- src/components/base/NoticeManager.tsx | 71 ++++++++ src/components/base/base-notice.tsx | 161 ------------------ src/components/base/index.ts | 2 +- src/components/home/home-profile-card.tsx | 10 +- src/components/home/proxy-tun-card.tsx | 4 +- src/components/home/system-info-card.tsx | 18 +- src/components/profile/editor-viewer.tsx | 8 +- .../profile/groups-editor-viewer.tsx | 10 +- src/components/profile/profile-item.tsx | 6 +- src/components/profile/profile-more.tsx | 4 +- src/components/profile/profile-viewer.tsx | 9 +- .../profile/proxies-editor-viewer.tsx | 7 +- .../profile/rules-editor-viewer.tsx | 10 +- src/components/proxy/provider-button.tsx | 12 +- src/components/rule/provider-button.tsx | 12 +- .../setting/mods/backup-config-viewer.tsx | 18 +- .../setting/mods/backup-table-viewer.tsx | 4 +- .../setting/mods/clash-core-viewer.tsx | 15 +- .../setting/mods/clash-port-viewer.tsx | 13 +- .../setting/mods/controller-viewer.tsx | 7 +- src/components/setting/mods/dns-viewer.tsx | 9 +- src/components/setting/mods/hotkey-viewer.tsx | 5 +- src/components/setting/mods/layout-viewer.tsx | 5 +- .../setting/mods/lite-mode-viewer.tsx | 5 +- src/components/setting/mods/misc-viewer.tsx | 5 +- .../setting/mods/network-interface-viewer.tsx | 5 +- .../setting/mods/sysproxy-viewer.tsx | 13 +- src/components/setting/mods/theme-viewer.tsx | 5 +- src/components/setting/mods/tun-viewer.tsx | 9 +- src/components/setting/mods/update-viewer.tsx | 9 +- src/components/setting/mods/web-ui-viewer.tsx | 5 +- src/components/setting/setting-clash.tsx | 14 +- src/components/setting/setting-system.tsx | 13 +- .../setting/setting-verge-advanced.tsx | 9 +- .../setting/setting-verge-basic.tsx | 5 +- .../shared/ProxyControlSwitches.tsx | 18 +- src/components/test/test-item.tsx | 5 +- src/components/test/test-viewer.tsx | 5 +- src/pages/_layout.tsx | 64 +++---- src/pages/profiles.tsx | 25 ++- src/pages/settings.tsx | 5 +- src/services/cmds.ts | 20 ++- src/services/noticeService.ts | 80 +++++++++ 43 files changed, 384 insertions(+), 355 deletions(-) create mode 100644 src/components/base/NoticeManager.tsx delete mode 100644 src/components/base/base-notice.tsx create mode 100644 src/services/noticeService.ts diff --git a/src/components/base/NoticeManager.tsx b/src/components/base/NoticeManager.tsx new file mode 100644 index 00000000..0fb261f4 --- /dev/null +++ b/src/components/base/NoticeManager.tsx @@ -0,0 +1,71 @@ +import React, { useState, useEffect } from 'react'; +import { Snackbar, Alert, IconButton, Box } from '@mui/material'; +import { CloseRounded } from '@mui/icons-material'; +import { subscribeNotices, hideNotice, NoticeItem } from '@/services/noticeService'; + +export const NoticeManager: React.FC = () => { + const [currentNotices, setCurrentNotices] = useState([]); + + useEffect(() => { + const unsubscribe = subscribeNotices((notices) => { + setCurrentNotices(notices); + }); + + return () => { + unsubscribe(); + }; + }, []); + + const handleClose = (id: number) => { + hideNotice(id); + }; + + return ( + + {currentNotices.map((notice) => ( + + handleClose(notice.id)} + > + + + } + > + {notice.message} + + + ))} + + ); +}; \ No newline at end of file diff --git a/src/components/base/base-notice.tsx b/src/components/base/base-notice.tsx deleted file mode 100644 index 5b4b4f65..00000000 --- a/src/components/base/base-notice.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { createRoot } from "react-dom/client"; -import { ReactNode, useState, useEffect } from "react"; -import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material"; -import { - CloseRounded, - CheckCircleRounded, - ErrorRounded, -} from "@mui/icons-material"; -import { useVerge } from "@/hooks/use-verge"; -import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; -const appWindow = getCurrentWebviewWindow(); -interface InnerProps { - type: string; - duration?: number; - message: ReactNode; - isDark?: boolean; - onClose: () => void; -} - -const NoticeInner = (props: InnerProps) => { - const { type, message, duration, onClose } = props; - const [visible, setVisible] = useState(true); - const [isDark, setIsDark] = useState(false); - const { verge } = useVerge(); - const { theme_mode } = verge ?? {}; - const onBtnClose = () => { - setVisible(false); - onClose(); - }; - const onAutoClose = (_e: any, reason: string) => { - if (reason !== "clickaway") onBtnClose(); - }; - - useEffect(() => { - const themeMode = ["light", "dark", "system"].includes(theme_mode!) - ? theme_mode! - : "light"; - - if (themeMode !== "system") { - setIsDark(themeMode === "dark"); - return; - } - - appWindow.theme().then((m) => m && setIsDark(m === "dark")); - const unlisten = appWindow.onThemeChanged((e) => - setIsDark(e.payload === "dark"), - ); - - return () => { - unlisten.then((fn) => fn()); - }; - }, [theme_mode]); - - const msgElement = - type === "info" ? ( - message - ) : ( - - {type === "error" && } - {type === "success" && } - - - {message} - - - ); - - return ( - } - transitionDuration={200} - action={ - - - - } - /> - ); -}; - -interface NoticeInstance { - (props: Omit): void; - - info(message: ReactNode, duration?: number, isDark?: boolean): void; - error(message: ReactNode, duration?: number, isDark?: boolean): void; - success(message: ReactNode, duration?: number, isDark?: boolean): void; -} - -let parent: HTMLDivElement = null!; - -export const Notice: NoticeInstance = (props) => { - const { type, message, duration } = props; - - // 验证必要的参数 - if (!message) { - return; - } - - if (!parent) { - parent = document.createElement("div"); - parent.setAttribute("id", "notice-container"); // 添加 id 便于调试 - document.body.appendChild(parent); - } - - const container = document.createElement("div"); - parent.appendChild(container); - - const root = createRoot(container); - - const onUnmount = () => { - root.unmount(); - if (parent && container.parentNode === parent) { - setTimeout(() => { - parent.removeChild(container); - }, 500); - } - }; - - root.render( - , - ); -}; - -const createNoticeTypeFactory = - (type: keyof NoticeInstance) => (message: ReactNode, duration?: number) => { - // 确保消息不为空 - if (!message) { - return; - } - - Notice({ - type, - message, - // 错误类型通知显示 8 秒,其他类型默认 1.5 秒 - duration: type === "error" ? 8000 : duration || 1500, - }); - }; - -Notice.info = createNoticeTypeFactory("info"); -Notice.error = createNoticeTypeFactory("error"); -Notice.success = createNoticeTypeFactory("success"); diff --git a/src/components/base/index.ts b/src/components/base/index.ts index 3d17d59a..e0e1afb2 100644 --- a/src/components/base/index.ts +++ b/src/components/base/index.ts @@ -3,6 +3,6 @@ export { BasePage } from "./base-page"; export { BaseEmpty } from "./base-empty"; export { BaseLoading } from "./base-loading"; export { BaseErrorBoundary } from "./base-error-boundary"; -export { Notice } from "./base-notice"; export { Switch } from "./base-switch"; export { BaseLoadingOverlay } from "./base-loading-overlay"; +export { NoticeManager } from "./NoticeManager"; diff --git a/src/components/home/home-profile-card.tsx b/src/components/home/home-profile-card.tsx index 52fb0409..1762002d 100644 --- a/src/components/home/home-profile-card.tsx +++ b/src/components/home/home-profile-card.tsx @@ -25,7 +25,7 @@ import parseTraffic from "@/utils/parse-traffic"; import { useMemo, useCallback, useState } from "react"; import { openWebUrl, updateProfile } from "@/services/cmds"; import { useLockFn } from "ahooks"; -import { Notice } from "@/components/base"; +import { showNotice } from "@/services/noticeService"; import { EnhancedCard } from "./enhanced-card"; import { useAppData } from "@/providers/app-data-provider"; @@ -272,7 +272,7 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr const { t } = useTranslation(); const navigate = useNavigate(); const { refreshAll } = useAppData(); - + // 更新当前订阅 const [updating, setUpdating] = useState(false); @@ -281,14 +281,14 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr setUpdating(true); try { - await updateProfile(current.uid); - Notice.success(t("Update subscription successfully")); + await updateProfile(current.uid, current.option); + showNotice('success', t("Update subscription successfully"), 1000); onProfileUpdated?.(); // 刷新首页数据 refreshAll(); } catch (err: any) { - Notice.error(err?.message || err.toString()); + showNotice('error', err.message || err.toString(), 3000); } finally { setUpdating(false); } diff --git a/src/components/home/proxy-tun-card.tsx b/src/components/home/proxy-tun-card.tsx index 720812d2..9c0e580c 100644 --- a/src/components/home/proxy-tun-card.tsx +++ b/src/components/home/proxy-tun-card.tsx @@ -11,7 +11,6 @@ import { } from "@mui/material"; import { useState, useMemo, memo, FC } from "react"; import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches"; -import { Notice } from "@/components/base"; import { ComputerRounded, TroubleshootRounded, @@ -20,6 +19,7 @@ import { } from "@mui/icons-material"; import { useVerge } from "@/hooks/use-verge"; import { useSystemState } from "@/hooks/use-system-state"; +import { showNotice } from "@/services/noticeService"; const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab"; @@ -151,7 +151,7 @@ export const ProxyTunCard: FC = () => { // 处理错误 const handleError = (err: Error) => { - Notice.error(err.message || err.toString(), 3000); + showNotice('error', err.message || err.toString(), 3000); }; // 处理标签切换并保存到localStorage diff --git a/src/components/home/system-info-card.tsx b/src/components/home/system-info-card.tsx index b68a2368..cd6c0f93 100644 --- a/src/components/home/system-info-card.tsx +++ b/src/components/home/system-info-card.tsx @@ -11,13 +11,13 @@ import { import { useVerge } from "@/hooks/use-verge"; import { EnhancedCard } from "./enhanced-card"; import useSWR from "swr"; -import { getSystemInfo, installService } from "@/services/cmds"; +import { getSystemInfo, installService, restartApp } from "@/services/cmds"; import { useNavigate } from "react-router-dom"; import { version as appVersion } from "@root/package.json"; import { useCallback, useEffect, useMemo, useState } from "react"; import { check as checkUpdate } from "@tauri-apps/plugin-updater"; import { useLockFn } from "ahooks"; -import { Notice } from "@/components/base"; +import { showNotice } from "@/services/noticeService"; import { useSystemState } from "@/hooks/use-system-state"; export const SystemInfoCard = () => { @@ -117,14 +117,14 @@ export const SystemInfoCard = () => { // 安装系统服务 const onInstallService = useLockFn(async () => { try { - Notice.info(t("Installing Service..."), 1000); + showNotice('info', t("Installing Service..."), 1000); await installService(); - Notice.success(t("Service Installed Successfully"), 2000); + showNotice('success', t("Service Installed Successfully"), 2000); - await mutateRunningMode(); + await mutateRunningMode(); } catch (err: any) { - Notice.error(err.message || err.toString(), 3000); + showNotice('error', err.message || err.toString(), 3000); } }); @@ -140,13 +140,13 @@ export const SystemInfoCard = () => { try { const info = await checkUpdate(); if (!info?.available) { - Notice.success(t("Currently on the Latest Version")); + showNotice('success', t("Currently on the Latest Version")); } else { - Notice.info(t("Update Available"), 2000); + showNotice('info', t("Update Available"), 2000); goToSettings(); } } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }); diff --git a/src/components/profile/editor-viewer.tsx b/src/components/profile/editor-viewer.tsx index 4554d705..b642fc9c 100644 --- a/src/components/profile/editor-viewer.tsx +++ b/src/components/profile/editor-viewer.tsx @@ -16,9 +16,9 @@ import { CloseFullscreenRounded, } from "@mui/icons-material"; import { useThemeMode } from "@/services/states"; -import { Notice } from "@/components/base"; import { nanoid } from "nanoid"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { showNotice } from "@/services/noticeService"; import getSystem from "@/utils/get-system"; import debounce from "@/utils/debounce"; @@ -127,7 +127,7 @@ export const EditorViewer = (props: Props) => { currData.current = value; onChange?.(prevData.current, currData.current); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }); @@ -136,7 +136,7 @@ export const EditorViewer = (props: Props) => { !readOnly && onSave?.(prevData.current, currData.current); onClose(); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }); @@ -144,7 +144,7 @@ export const EditorViewer = (props: Props) => { try { onClose(); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }); diff --git a/src/components/profile/groups-editor-viewer.tsx b/src/components/profile/groups-editor-viewer.tsx index 40771adf..9092471d 100644 --- a/src/components/profile/groups-editor-viewer.tsx +++ b/src/components/profile/groups-editor-viewer.tsx @@ -40,13 +40,14 @@ import { readProfileFile, saveProfileFile, } from "@/services/cmds"; -import { Notice, Switch } from "@/components/base"; +import { Switch } from "@/components/base"; import getSystem from "@/utils/get-system"; import { BaseSearchBox } from "../base/base-search-box"; import { Virtuoso } from "react-virtuoso"; import MonacoEditor from "react-monaco-editor"; import { useThemeMode } from "@/services/states"; import { Controller, useForm } from "react-hook-form"; +import { showNotice } from "@/services/noticeService"; interface Props { proxiesUid: string; @@ -285,10 +286,11 @@ export const GroupsEditorViewer = (props: Props) => { const handleSave = useLockFn(async () => { try { await saveProfileFile(property, currData); + showNotice('success', t("Saved Successfully")); onSave?.(prevData, currData); onClose(); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.toString()); } }); @@ -725,7 +727,7 @@ export const GroupsEditorViewer = (props: Props) => { } setPrependSeq([formIns.getValues(), ...prependSeq]); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }} > @@ -747,7 +749,7 @@ export const GroupsEditorViewer = (props: Props) => { } setAppendSeq([...appendSeq, formIns.getValues()]); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }} > diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 671cfab4..998ac2cf 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -24,7 +24,7 @@ import { saveProfileFile, getNextUpdateTime, } from "@/services/cmds"; -import { Notice } from "@/components/base"; +import { showNotice } from "@/services/noticeService"; import { GroupsEditorViewer } from "@/components/profile/groups-editor-viewer"; import { RulesEditorViewer } from "@/components/profile/rules-editor-viewer"; import { EditorViewer } from "@/components/profile/editor-viewer"; @@ -271,7 +271,7 @@ export const ProfileItem = (props: Props) => { try { await viewProfile(itemData.uid); } catch (err: any) { - Notice.error(err?.message || err.toString()); + showNotice('error', err?.message || err.toString()); } }); @@ -302,7 +302,7 @@ export const ProfileItem = (props: Props) => { await updateProfile(itemData.uid, option); // 更新成功,刷新列表 - Notice.success(t("Update subscription successfully")); + showNotice('success', t("Update subscription successfully")); mutate("getProfiles"); } catch (err: any) { // 更新完全失败(包括后端的回退尝试) diff --git a/src/components/profile/profile-more.tsx b/src/components/profile/profile-more.tsx index 95ffb89e..345dcc50 100644 --- a/src/components/profile/profile-more.tsx +++ b/src/components/profile/profile-more.tsx @@ -12,10 +12,10 @@ import { } from "@mui/material"; import { FeaturedPlayListRounded } from "@mui/icons-material"; import { viewProfile, readProfileFile, saveProfileFile } from "@/services/cmds"; -import { Notice } from "@/components/base"; import { EditorViewer } from "@/components/profile/editor-viewer"; import { ProfileBox } from "./profile-box"; import { LogViewer } from "./log-viewer"; +import { showNotice } from "@/services/noticeService"; interface Props { logInfo?: [string, string][]; @@ -43,7 +43,7 @@ export const ProfileMore = (props: Props) => { try { await viewProfile(id); } catch (err: any) { - Notice.error(err?.message || err.toString()); + showNotice('error', err?.message || err.toString()); } }); diff --git a/src/components/profile/profile-viewer.tsx b/src/components/profile/profile-viewer.tsx index a86a5a12..485ee781 100644 --- a/src/components/profile/profile-viewer.tsx +++ b/src/components/profile/profile-viewer.tsx @@ -19,10 +19,11 @@ import { TextField, } from "@mui/material"; import { createProfile, patchProfile } from "@/services/cmds"; -import { BaseDialog, Notice, Switch } from "@/components/base"; +import { BaseDialog, Switch } from "@/components/base"; import { version } from "@root/package.json"; import { FileInput } from "./file-input"; import { useProfiles } from "@/hooks/use-profiles"; +import { showNotice } from "@/services/noticeService"; interface Props { onChange: (isActivating?: boolean) => void; @@ -140,7 +141,7 @@ export const ProfileViewer = forwardRef( } } catch (err) { // 首次创建/更新失败,尝试使用自身代理 - Notice.info(t("Profile creation failed, retrying with Clash proxy...")); + showNotice('info', t("Profile creation failed, retrying with Clash proxy...")); // 使用自身代理的配置 const retryItem = { @@ -163,7 +164,7 @@ export const ProfileViewer = forwardRef( await patchProfile(form.uid, { option: originalOptions }); } - Notice.success(t("Profile creation succeeded with Clash proxy")); + showNotice('success', t("Profile creation succeeded with Clash proxy")); } } @@ -175,7 +176,7 @@ export const ProfileViewer = forwardRef( // 只传递当前配置激活状态,让父组件决定是否需要触发配置重载 props.onChange(isActivating); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } finally { setLoading(false); } diff --git a/src/components/profile/proxies-editor-viewer.tsx b/src/components/profile/proxies-editor-viewer.tsx index ca657090..7aca31e4 100644 --- a/src/components/profile/proxies-editor-viewer.tsx +++ b/src/components/profile/proxies-editor-viewer.tsx @@ -33,13 +33,13 @@ import { } from "@mui/icons-material"; import { ProxyItem } from "@/components/profile/proxy-item"; import { readProfileFile, saveProfileFile } from "@/services/cmds"; -import { Notice } from "@/components/base"; import getSystem from "@/utils/get-system"; import { BaseSearchBox } from "../base/base-search-box"; import { Virtuoso } from "react-virtuoso"; import MonacoEditor from "react-monaco-editor"; import { useThemeMode } from "@/services/states"; import parseUri from "@/utils/uri-parser"; +import { showNotice } from "@/services/noticeService"; interface Props { profileUid: string; @@ -150,7 +150,7 @@ export const ProxiesEditorViewer = (props: Props) => { names.push(proxy.name); } } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }); return proxies; @@ -212,10 +212,11 @@ export const ProxiesEditorViewer = (props: Props) => { const handleSave = useLockFn(async () => { try { await saveProfileFile(property, currData); + showNotice('success', t("Saved Successfully")); onSave?.(prevData, currData); onClose(); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.toString()); } }); diff --git a/src/components/profile/rules-editor-viewer.tsx b/src/components/profile/rules-editor-viewer.tsx index 92abf2f1..514e612e 100644 --- a/src/components/profile/rules-editor-viewer.tsx +++ b/src/components/profile/rules-editor-viewer.tsx @@ -34,13 +34,14 @@ import { VerticalAlignBottomRounded, } from "@mui/icons-material"; import { readProfileFile, saveProfileFile } from "@/services/cmds"; -import { Notice, Switch } from "@/components/base"; +import { Switch } from "@/components/base"; import getSystem from "@/utils/get-system"; import { RuleItem } from "@/components/profile/rule-item"; import { BaseSearchBox } from "../base/base-search-box"; import { Virtuoso } from "react-virtuoso"; import MonacoEditor from "react-monaco-editor"; import { useThemeMode } from "@/services/states"; +import { showNotice } from "@/services/noticeService"; interface Props { groupsUid: string; @@ -414,10 +415,11 @@ export const RulesEditorViewer = (props: Props) => { const handleSave = useLockFn(async () => { try { await saveProfileFile(property, currData); + showNotice('success', t("Saved Successfully")); onSave?.(prevData, currData); onClose(); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.toString()); } }); @@ -545,7 +547,7 @@ export const RulesEditorViewer = (props: Props) => { if (prependSeq.includes(raw)) return; setPrependSeq([raw, ...prependSeq]); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }} > @@ -563,7 +565,7 @@ export const RulesEditorViewer = (props: Props) => { if (appendSeq.includes(raw)) return; setAppendSeq([...appendSeq, raw]); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }} > diff --git a/src/components/proxy/provider-button.tsx b/src/components/proxy/provider-button.tsx index f949fbca..0f176c8c 100644 --- a/src/components/proxy/provider-button.tsx +++ b/src/components/proxy/provider-button.tsx @@ -21,7 +21,7 @@ import { useTranslation } from "react-i18next"; import { useLockFn } from "ahooks"; import { proxyProviderUpdate } from "@/services/api"; import { useAppData } from "@/providers/app-data-provider"; -import { Notice } from "@/components/base"; +import { showNotice } from "@/services/noticeService"; import { StorageOutlined, RefreshRounded } from "@mui/icons-material"; import dayjs from "dayjs"; import parseTraffic from "@/utils/parse-traffic"; @@ -81,9 +81,9 @@ export const ProviderButton = () => { await refreshProxy(); await refreshProxyProviders(); - Notice.success(`${name} 更新成功`); + showNotice('success', `${name} 更新成功`); } catch (err: any) { - Notice.error(`${name} 更新失败: ${err?.message || err.toString()}`); + showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`); } finally { // 清除更新状态 setUpdating(prev => ({ ...prev, [name]: false })); @@ -96,7 +96,7 @@ export const ProviderButton = () => { // 获取所有provider的名称 const allProviders = Object.keys(proxyProviders || {}); if (allProviders.length === 0) { - Notice.info("没有可更新的代理提供者"); + showNotice('info', "没有可更新的代理提供者"); return; } @@ -123,9 +123,9 @@ export const ProviderButton = () => { await refreshProxy(); await refreshProxyProviders(); - Notice.success("全部代理提供者更新成功"); + showNotice('success', "全部代理提供者更新成功"); } catch (err: any) { - Notice.error(`更新失败: ${err?.message || err.toString()}`); + showNotice('error', `更新失败: ${err?.message || err.toString()}`); } finally { // 清除所有更新状态 setUpdating({}); diff --git a/src/components/rule/provider-button.tsx b/src/components/rule/provider-button.tsx index c4e758d0..4044bb98 100644 --- a/src/components/rule/provider-button.tsx +++ b/src/components/rule/provider-button.tsx @@ -19,10 +19,10 @@ import { import { useTranslation } from "react-i18next"; import { useLockFn } from "ahooks"; import { ruleProviderUpdate } from "@/services/api"; -import { Notice } from "@/components/base"; import { StorageOutlined, RefreshRounded } from "@mui/icons-material"; import { useAppData } from "@/providers/app-data-provider"; import dayjs from "dayjs"; +import { showNotice } from "@/services/noticeService"; // 定义规则提供者类型 interface RuleProviderItem { @@ -67,9 +67,9 @@ export const ProviderButton = () => { await refreshRules(); await refreshRuleProviders(); - Notice.success(`${name} 更新成功`); + showNotice('success', `${name} 更新成功`); } catch (err: any) { - Notice.error(`${name} 更新失败: ${err?.message || err.toString()}`); + showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`); } finally { // 清除更新状态 setUpdating(prev => ({ ...prev, [name]: false })); @@ -82,7 +82,7 @@ export const ProviderButton = () => { // 获取所有provider的名称 const allProviders = Object.keys(ruleProviders || {}); if (allProviders.length === 0) { - Notice.info("没有可更新的规则提供者"); + showNotice('info', "没有可更新的规则提供者"); return; } @@ -109,9 +109,9 @@ export const ProviderButton = () => { await refreshRules(); await refreshRuleProviders(); - Notice.success("全部规则提供者更新成功"); + showNotice('success', "全部规则提供者更新成功"); } catch (err: any) { - Notice.error(`更新失败: ${err?.message || err.toString()}`); + showNotice('error', `更新失败: ${err?.message || err.toString()}`); } finally { // 清除所有更新状态 setUpdating({}); diff --git a/src/components/setting/mods/backup-config-viewer.tsx b/src/components/setting/mods/backup-config-viewer.tsx index 32da19c9..77edda03 100644 --- a/src/components/setting/mods/backup-config-viewer.tsx +++ b/src/components/setting/mods/backup-config-viewer.tsx @@ -2,7 +2,6 @@ import { useState, useRef, memo, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useForm } from "react-hook-form"; import { useVerge } from "@/hooks/use-verge"; -import { Notice } from "@/components/base"; import { isValidUrl } from "@/utils/helper"; import { useLockFn } from "ahooks"; import { @@ -17,6 +16,7 @@ import { import Visibility from "@mui/icons-material/Visibility"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; import { saveWebdavConfig, createWebdavBackup } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; export interface BackupConfigViewerProps { onBackupSuccess: () => Promise; @@ -83,21 +83,21 @@ export const BackupConfigViewer = memo( if (!url) { urlRef.current?.focus(); - Notice.error(t("WebDAV URL Required")); + showNotice('error', t("WebDAV URL Required")); throw new Error(t("WebDAV URL Required")); } else if (!isValidUrl(url)) { urlRef.current?.focus(); - Notice.error(t("Invalid WebDAV URL")); + showNotice('error', t("Invalid WebDAV URL")); throw new Error(t("Invalid WebDAV URL")); } if (!username) { usernameRef.current?.focus(); - Notice.error(t("WebDAV URL Required")); + showNotice('error', t("WebDAV URL Required")); throw new Error(t("Username Required")); } if (!password) { passwordRef.current?.focus(); - Notice.error(t("WebDAV URL Required")); + showNotice('error', t("WebDAV URL Required")); throw new Error(t("Password Required")); } }; @@ -111,11 +111,11 @@ export const BackupConfigViewer = memo( data.username.trim(), data.password, ).then(() => { - Notice.success(t("WebDAV Config Saved")); + showNotice('success', t("WebDAV Config Saved")); onSaveSuccess(); }); } catch (error) { - Notice.error(t("WebDAV Config Save Failed", { error }), 3000); + showNotice('error', t("WebDAV Config Save Failed", { error }), 3000); } finally { setLoading(false); } @@ -126,11 +126,11 @@ export const BackupConfigViewer = memo( try { setLoading(true); await createWebdavBackup().then(async () => { + showNotice('success', t("Backup Created")); await onBackupSuccess(); - Notice.success(t("Backup Created")); }); } catch (error) { - Notice.error(t("Backup Failed", { error })); + showNotice('error', t("Backup Failed", { error })); } finally { setLoading(false); } diff --git a/src/components/setting/mods/backup-table-viewer.tsx b/src/components/setting/mods/backup-table-viewer.tsx index 2dcf60ec..559f8c41 100644 --- a/src/components/setting/mods/backup-table-viewer.tsx +++ b/src/components/setting/mods/backup-table-viewer.tsx @@ -12,7 +12,6 @@ import { TableRow, TablePagination, } from "@mui/material"; -import { Notice } from "@/components/base"; import { Typography } from "@mui/material"; import { useLockFn } from "ahooks"; import { useTranslation } from "react-i18next"; @@ -24,6 +23,7 @@ import { } from "@/services/cmds"; import DeleteIcon from "@mui/icons-material/Delete"; import RestoreIcon from "@mui/icons-material/Restore"; +import { showNotice } from "@/services/noticeService"; export type BackupFile = IWebDavFile & { platform: string; @@ -61,7 +61,7 @@ export const BackupTableViewer = memo( const handleRestore = useLockFn(async (filename: string) => { await restoreWebDavBackup(filename).then(() => { - Notice.success(t("Restore Success, App will restart in 1s")); + showNotice('success', t("Restore Success, App will restart in 1s")); }); await restartApp(); }); diff --git a/src/components/setting/mods/clash-core-viewer.tsx b/src/components/setting/mods/clash-core-viewer.tsx index 969d3c81..fadd47e8 100644 --- a/src/components/setting/mods/clash-core-viewer.tsx +++ b/src/components/setting/mods/clash-core-viewer.tsx @@ -1,6 +1,6 @@ import { mutate } from "swr"; import { forwardRef, useImperativeHandle, useState } from "react"; -import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { BaseDialog, DialogRef } from "@/components/base"; import { useTranslation } from "react-i18next"; import { useVerge } from "@/hooks/use-verge"; import { useLockFn } from "ahooks"; @@ -19,6 +19,7 @@ import { } from "@mui/material"; import { changeClashCore, restartCore } from "@/services/cmds"; import { closeAllConnections, upgradeCore } from "@/services/api"; +import { showNotice } from "@/services/noticeService"; const VALID_CORE = [ { name: "Mihomo", core: "verge-mihomo", chip: "Release Version" }, @@ -48,7 +49,7 @@ export const ClashCoreViewer = forwardRef((props, ref) => { const errorMsg = await changeClashCore(core); if (errorMsg) { - Notice.error(errorMsg); + showNotice('error', errorMsg); return; } @@ -58,16 +59,16 @@ export const ClashCoreViewer = forwardRef((props, ref) => { mutate("getVersion"); }, 500); } catch (err: any) { - Notice.error(err?.message || err.toString()); + showNotice('error', err.message || err.toString()); } }); const onRestart = useLockFn(async () => { try { await restartCore(); - Notice.success(t(`Clash Core Restarted`), 1000); + showNotice('success', t(`Clash Core Restarted`), 1000); } catch (err: any) { - Notice.error(err?.message || err.toString()); + showNotice('error', err.message || err.toString()); } }); @@ -76,10 +77,10 @@ export const ClashCoreViewer = forwardRef((props, ref) => { setUpgrading(true); await upgradeCore(); setUpgrading(false); - Notice.success(t(`Core Version Updated`), 1000); + showNotice('success', t(`Core Version Updated`), 1000); } catch (err: any) { setUpgrading(false); - Notice.error(err?.response.data.message || err.toString()); + showNotice('error', err.response?.data?.message || err.toString()); } }); diff --git a/src/components/setting/mods/clash-port-viewer.tsx b/src/components/setting/mods/clash-port-viewer.tsx index 75a95365..39d20b3b 100644 --- a/src/components/setting/mods/clash-port-viewer.tsx +++ b/src/components/setting/mods/clash-port-viewer.tsx @@ -3,9 +3,10 @@ import { useTranslation } from "react-i18next"; import { useLockFn } from "ahooks"; import { List, ListItem, ListItemText, TextField } from "@mui/material"; import { useClashInfo } from "@/hooks/use-clash"; -import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base"; +import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { useVerge } from "@/hooks/use-verge"; import getSystem from "@/utils/get-system"; +import { showNotice } from "@/services/noticeService"; const OS = getSystem(); export const ClashPortViewer = forwardRef((props, ref) => { @@ -78,18 +79,18 @@ export const ClashPortViewer = forwardRef((props, ref) => { OS === "linux" && new Set([redirPort, tproxyPort, mixedPort, socksPort, port]).size !== 5 ) { - Notice.error(t("Port Conflict"), 4000); + showNotice('error', t("Port Conflict")); return; } if ( OS === "macos" && new Set([redirPort, mixedPort, socksPort, port]).size !== 4 ) { - Notice.error(t("Port Conflict"), 4000); + showNotice('error', t("Port Conflict")); return; } if (OS === "windows" && new Set([mixedPort, socksPort, port]).size !== 3) { - Notice.error(t("Port Conflict"), 4000); + showNotice('error', t("Port Conflict")); return; } try { @@ -145,9 +146,9 @@ export const ClashPortViewer = forwardRef((props, ref) => { }); } setOpen(false); - Notice.success(t("Clash Port Modified"), 1000); + showNotice('success', t("Clash Port Modified")); } catch (err: any) { - Notice.error(err.message || err.toString(), 4000); + showNotice('error', err.message || err.toString()); } }); diff --git a/src/components/setting/mods/controller-viewer.tsx b/src/components/setting/mods/controller-viewer.tsx index a52018cb..0a0d9b18 100644 --- a/src/components/setting/mods/controller-viewer.tsx +++ b/src/components/setting/mods/controller-viewer.tsx @@ -3,7 +3,8 @@ import { useLockFn } from "ahooks"; import { useTranslation } from "react-i18next"; import { List, ListItem, ListItemText, TextField } from "@mui/material"; import { useClashInfo } from "@/hooks/use-clash"; -import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { BaseDialog, DialogRef } from "@/components/base"; +import { showNotice } from "@/services/noticeService"; export const ControllerViewer = forwardRef((props, ref) => { const { t } = useTranslation(); @@ -26,10 +27,10 @@ export const ControllerViewer = forwardRef((props, ref) => { const onSave = useLockFn(async () => { try { await patchInfo({ "external-controller": controller, secret }); - Notice.success(t("External Controller Address Modified"), 1000); + showNotice('success', t("External Controller Address Modified"), 1000); setOpen(false); } catch (err: any) { - Notice.error(err.message || err.toString(), 4000); + showNotice('error', err.message || err.toString(), 4000); } }); diff --git a/src/components/setting/mods/dns-viewer.tsx b/src/components/setting/mods/dns-viewer.tsx index be41e3be..cbe00c16 100644 --- a/src/components/setting/mods/dns-viewer.tsx +++ b/src/components/setting/mods/dns-viewer.tsx @@ -17,12 +17,13 @@ import { } from "@mui/material"; import { RestartAltRounded } from "@mui/icons-material"; import { useClash } from "@/hooks/use-clash"; -import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { BaseDialog, DialogRef } from "@/components/base"; import yaml from "js-yaml"; import MonacoEditor from "react-monaco-editor"; import { useThemeMode } from "@/services/states"; import getSystem from "@/utils/get-system"; import { invoke } from "@tauri-apps/api/core"; +import { showNotice } from "@/services/noticeService"; const Item = styled(ListItem)(({ theme }) => ({ padding: "8px 0", @@ -374,7 +375,7 @@ export const DnsViewer = forwardRef((props, ref) => { formatNameserverPolicy(dnsConfig["nameserver-policy"]) || "", }); } catch (err: any) { - Notice.error(t("Invalid YAML format")); + showNotice('error', t("Invalid YAML format")); } }; @@ -526,9 +527,9 @@ export const DnsViewer = forwardRef((props, ref) => { } setOpen(false); - Notice.success(t("DNS settings saved")); + showNotice('success', t("DNS settings saved")); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }); diff --git a/src/components/setting/mods/hotkey-viewer.tsx b/src/components/setting/mods/hotkey-viewer.tsx index feaac70b..0cf93731 100644 --- a/src/components/setting/mods/hotkey-viewer.tsx +++ b/src/components/setting/mods/hotkey-viewer.tsx @@ -3,8 +3,9 @@ import { useTranslation } from "react-i18next"; import { useLockFn } from "ahooks"; import { styled, Typography, Switch } from "@mui/material"; import { useVerge } from "@/hooks/use-verge"; -import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { BaseDialog, DialogRef } from "@/components/base"; import { HotkeyInput } from "./hotkey-input"; +import { showNotice } from "@/services/noticeService"; const ItemWrapper = styled("div")` display: flex; @@ -79,7 +80,7 @@ export const HotkeyViewer = forwardRef((props, ref) => { }); setOpen(false); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.toString()); } }); diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx index b470b92e..fcb0ac81 100644 --- a/src/components/setting/mods/layout-viewer.tsx +++ b/src/components/setting/mods/layout-viewer.tsx @@ -11,7 +11,7 @@ import { Box, } from "@mui/material"; import { useVerge } from "@/hooks/use-verge"; -import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base"; +import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { GuardState } from "./guard-state"; import { open as openDialog } from "@tauri-apps/plugin-dialog"; import { convertFileSrc } from "@tauri-apps/api/core"; @@ -19,6 +19,7 @@ import { copyIconFile, getAppDir } from "@/services/cmds"; import { join } from "@tauri-apps/api/path"; import { exists } from "@tauri-apps/plugin-fs"; import getSystem from "@/utils/get-system"; +import { showNotice } from "@/services/noticeService"; const OS = getSystem(); @@ -87,7 +88,7 @@ export const LayoutViewer = forwardRef((props, ref) => { const onSwitchFormat = (_e: any, value: boolean) => value; const onError = (err: any) => { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); }; const onChangeData = (patch: Partial) => { mutateVerge({ ...verge, ...patch }, false); diff --git a/src/components/setting/mods/lite-mode-viewer.tsx b/src/components/setting/mods/lite-mode-viewer.tsx index a1e20a52..90517f02 100644 --- a/src/components/setting/mods/lite-mode-viewer.tsx +++ b/src/components/setting/mods/lite-mode-viewer.tsx @@ -10,9 +10,10 @@ import { InputAdornment, } from "@mui/material"; import { useVerge } from "@/hooks/use-verge"; -import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base"; +import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { entry_lightweight_mode } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; export const LiteModeViewer = forwardRef((props, ref) => { const { t } = useTranslation(); @@ -43,7 +44,7 @@ export const LiteModeViewer = forwardRef((props, ref) => { }); setOpen(false); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }); diff --git a/src/components/setting/mods/misc-viewer.tsx b/src/components/setting/mods/misc-viewer.tsx index 9abd34c3..02cda191 100644 --- a/src/components/setting/mods/misc-viewer.tsx +++ b/src/components/setting/mods/misc-viewer.tsx @@ -11,8 +11,9 @@ import { InputAdornment, } from "@mui/material"; import { useVerge } from "@/hooks/use-verge"; -import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base"; +import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; +import { showNotice } from "@/services/noticeService"; export const MiscViewer = forwardRef((props, ref) => { const { t } = useTranslation(); @@ -61,7 +62,7 @@ export const MiscViewer = forwardRef((props, ref) => { }); setOpen(false); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.toString()); } }); diff --git a/src/components/setting/mods/network-interface-viewer.tsx b/src/components/setting/mods/network-interface-viewer.tsx index bd574b9b..013423dc 100644 --- a/src/components/setting/mods/network-interface-viewer.tsx +++ b/src/components/setting/mods/network-interface-viewer.tsx @@ -1,10 +1,11 @@ import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { BaseDialog, DialogRef } from "@/components/base"; import { getNetworkInterfacesInfo } from "@/services/cmds"; import { alpha, Box, Button, Chip, IconButton } from "@mui/material"; import { ContentCopyRounded } from "@mui/icons-material"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; +import { showNotice } from "@/services/noticeService"; export const NetworkInterfaceViewer = forwardRef((props, ref) => { const { t } = useTranslation(); @@ -128,7 +129,7 @@ const AddressDisplay = (props: { label: string; content: string }) => { size="small" onClick={async () => { await writeText(props.content); - Notice.success(t("Copy Success")); + showNotice('success', t("Copy Success")); }} > diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx index a7b80257..0baf32e1 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -1,4 +1,4 @@ -import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base"; +import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { BaseFieldset } from "@/components/base/base-fieldset"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { EditorViewer } from "@/components/profile/editor-viewer"; @@ -36,6 +36,7 @@ import { } from "react"; import { useTranslation } from "react-i18next"; import { mutate } from "swr"; +import { showNotice } from "@/services/noticeService"; const DEFAULT_PAC = `function FindProxyForURL(url, host) { return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;"; }`; @@ -201,11 +202,11 @@ export const SysproxyViewer = forwardRef((props, ref) => { const onSave = useLockFn(async () => { if (value.duration < 1) { - Notice.error(t("Proxy Daemon Duration Cannot be Less than 1 Second")); + showNotice('error', t("Proxy Daemon Duration Cannot be Less than 1 Second")); return; } if (value.bypass && !validReg.test(value.bypass)) { - Notice.error(t("Invalid Bypass Format")); + showNotice('error', t("Invalid Bypass Format")); return; } @@ -222,7 +223,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { !ipv6Regex.test(value.proxy_host) && !hostnameRegex.test(value.proxy_host) ) { - Notice.error(t("Invalid Proxy Host Format")); + showNotice('error', t("Invalid Proxy Host Format")); return; } @@ -301,10 +302,10 @@ export const SysproxyViewer = forwardRef((props, ref) => { await Promise.all([mutate("getSystemProxy"), mutate("getAutotemProxy")]); } } - + setOpen(false); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.toString()); } finally { setSaving(false); } diff --git a/src/components/setting/mods/theme-viewer.tsx b/src/components/setting/mods/theme-viewer.tsx index bab22832..2ba1b980 100644 --- a/src/components/setting/mods/theme-viewer.tsx +++ b/src/components/setting/mods/theme-viewer.tsx @@ -12,9 +12,10 @@ import { } from "@mui/material"; import { useVerge } from "@/hooks/use-verge"; import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; -import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { BaseDialog, DialogRef } from "@/components/base"; import { EditorViewer } from "@/components/profile/editor-viewer"; import { EditRounded } from "@mui/icons-material"; +import { showNotice } from "@/services/noticeService"; export const ThemeViewer = forwardRef((props, ref) => { const { t } = useTranslation(); @@ -48,7 +49,7 @@ export const ThemeViewer = forwardRef((props, ref) => { await patchVerge({ theme_setting: theme }); setOpen(false); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.toString()); } }); diff --git a/src/components/setting/mods/tun-viewer.tsx b/src/components/setting/mods/tun-viewer.tsx index f2a1fa63..5c5d689e 100644 --- a/src/components/setting/mods/tun-viewer.tsx +++ b/src/components/setting/mods/tun-viewer.tsx @@ -11,10 +11,11 @@ import { TextField, } from "@mui/material"; import { useClash } from "@/hooks/use-clash"; -import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base"; +import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { StackModeSwitch } from "./stack-mode-switch"; import { enhanceProfiles } from "@/services/cmds"; import getSystem from "@/utils/get-system"; +import { showNotice } from "@/services/noticeService"; const OS = getSystem(); @@ -76,13 +77,13 @@ export const TunViewer = forwardRef((props, ref) => { ); try { await enhanceProfiles(); - Notice.success(t("Settings Applied"), 1000); + showNotice('success', t("Settings Applied")); } catch (err: any) { - Notice.error(err.message || err.toString(), 3000); + showNotice('error', err.message || err.toString()); } setOpen(false); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }); diff --git a/src/components/setting/mods/update-viewer.tsx b/src/components/setting/mods/update-viewer.tsx index 6eb8ef64..ff9ec117 100644 --- a/src/components/setting/mods/update-viewer.tsx +++ b/src/components/setting/mods/update-viewer.tsx @@ -5,13 +5,14 @@ import { Box, LinearProgress, Button } from "@mui/material"; import { useTranslation } from "react-i18next"; import { relaunch } from "@tauri-apps/plugin-process"; import { check as checkUpdate } from "@tauri-apps/plugin-updater"; -import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { BaseDialog, DialogRef } from "@/components/base"; import { useUpdateState, useSetUpdateState } from "@/services/states"; import { Event, UnlistenFn } from "@tauri-apps/api/event"; import { portableFlag } from "@/pages/_layout"; import { open as openUrl } from "@tauri-apps/plugin-shell"; import ReactMarkdown from "react-markdown"; import { useListen } from "@/hooks/use-listen"; +import { showNotice } from "@/services/noticeService"; let eventListener: UnlistenFn | null = null; @@ -55,12 +56,12 @@ export const UpdateViewer = forwardRef((props, ref) => { const onUpdate = useLockFn(async () => { if (portableFlag) { - Notice.error(t("Portable Updater Error")); + showNotice('error', t("Portable Updater Error")); return; } if (!updateInfo?.body) return; if (breakChangeFlag) { - Notice.error(t("Break Change Update Error")); + showNotice('error', t("Break Change Update Error")); return; } if (updateState) return; @@ -82,7 +83,7 @@ export const UpdateViewer = forwardRef((props, ref) => { await updateInfo.downloadAndInstall(); await relaunch(); } catch (err: any) { - Notice.error(err?.message || err.toString()); + showNotice('error', err?.message || err.toString()); } finally { setUpdateState(false); } diff --git a/src/components/setting/mods/web-ui-viewer.tsx b/src/components/setting/mods/web-ui-viewer.tsx index e2b1ee1e..35589955 100644 --- a/src/components/setting/mods/web-ui-viewer.tsx +++ b/src/components/setting/mods/web-ui-viewer.tsx @@ -4,9 +4,10 @@ import { useTranslation } from "react-i18next"; import { Button, Box, Typography } from "@mui/material"; import { useVerge } from "@/hooks/use-verge"; import { openWebUrl } from "@/services/cmds"; -import { BaseDialog, BaseEmpty, DialogRef, Notice } from "@/components/base"; +import { BaseDialog, BaseEmpty, DialogRef } from "@/components/base"; import { useClashInfo } from "@/hooks/use-clash"; import { WebUIItem } from "./web-ui-item"; +import { showNotice } from "@/services/noticeService"; export const WebUIViewer = forwardRef((props, ref) => { const { t } = useTranslation(); @@ -71,7 +72,7 @@ export const WebUIViewer = forwardRef((props, ref) => { await openWebUrl(url); } catch (e: any) { - Notice.error(e.message || e.toString()); + showNotice('error', e.message || e.toString()); } }); diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index 8de71ec1..ee13e961 100644 --- a/src/components/setting/setting-clash.tsx +++ b/src/components/setting/setting-clash.tsx @@ -6,7 +6,7 @@ import { ShuffleRounded, LanRounded, } from "@mui/icons-material"; -import { DialogRef, Notice, Switch } from "@/components/base"; +import { DialogRef, Switch } from "@/components/base"; import { useClash } from "@/hooks/use-clash"; import { GuardState } from "./mods/guard-state"; import { WebUIViewer } from "./mods/web-ui-viewer"; @@ -24,6 +24,7 @@ import { DnsViewer } from "./mods/dns-viewer"; import { invoke } from "@tauri-apps/api/core"; import { useLockFn } from "ahooks"; import { useListen } from "@/hooks/use-listen"; +import { showNotice } from "@/services/noticeService"; const isWIN = getSystem() === "windows"; @@ -77,9 +78,9 @@ const SettingClash = ({ onError }: Props) => { const onUpdateGeo = async () => { try { await updateGeoData(); - Notice.success(t("GeoData Updated")); + showNotice('success', t("GeoData Updated")); } catch (err: any) { - Notice.error(err?.response.data.message || err.toString()); + showNotice('error', err?.response.data.message || err.toString()); } }; @@ -100,7 +101,7 @@ const SettingClash = ({ onError }: Props) => { // 如果出错,恢复原始状态 setDnsSettingsEnabled(!enable); localStorage.setItem("dns_settings_enabled", String(!enable)); - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); await patchVerge({ enable_dns_settings: !enable }).catch(() => { // 忽略恢复状态时的错误 }); @@ -224,10 +225,7 @@ const SettingClash = ({ onError }: Props) => { color={enable_random_port ? "primary" : "inherit"} icon={ShuffleRounded} onClick={() => { - Notice.success( - t("Restart Application to Apply Modifications"), - 1000, - ); + showNotice('success', t("Restart Application to Apply Modifications"), 1000); onChangeVerge({ enable_random_port: !enable_random_port }); patchVerge({ enable_random_port: !enable_random_port }); }} diff --git a/src/components/setting/setting-system.tsx b/src/components/setting/setting-system.tsx index bb5d332a..29232c5f 100644 --- a/src/components/setting/setting-system.tsx +++ b/src/components/setting/setting-system.tsx @@ -9,7 +9,7 @@ import { BuildRounded, } from "@mui/icons-material"; import { useVerge } from "@/hooks/use-verge"; -import { DialogRef, Notice, Switch } from "@/components/base"; +import { DialogRef, Switch } from "@/components/base"; import { SettingList, SettingItem } from "./mods/setting-comp"; import { GuardState } from "./mods/guard-state"; import { SysproxyViewer } from "./mods/sysproxy-viewer"; @@ -25,6 +25,7 @@ import { useLockFn } from "ahooks"; import { Button, Tooltip } from "@mui/material"; import { useSystemState } from "@/hooks/use-system-state"; import { closeAllConnections } from "@/services/api"; +import { showNotice } from "@/services/noticeService"; interface Props { onError?: (err: Error) => void; @@ -86,13 +87,13 @@ const SettingSystem = ({ onError }: Props) => { // 安装系统服务 const onInstallService = useLockFn(async () => { try { - Notice.info(t("Installing Service..."), 1000); + showNotice('info', t("Installing Service..."), 1000); await installService(); - Notice.success(t("Service Installed Successfully"), 2000); + showNotice('success', t("Service Installed Successfully"), 2000); // 重新获取运行模式 await mutateRunningMode(); } catch (err: any) { - Notice.error(err.message || err.toString(), 3000); + showNotice('error', err.message || err.toString(), 3000); } }); @@ -144,7 +145,7 @@ const SettingSystem = ({ onError }: Props) => { onGuard={(e) => { // 当在sidecar模式下且非管理员模式时禁用切换 if (isSidecarMode && !isAdminMode) { - Notice.error(t("TUN requires Service Mode"), 2000); + showNotice('error', t("TUN requires Service Mode"), 2000); return Promise.reject(new Error(t("TUN requires Service Mode"))); } return patchVerge({ enable_tun_mode: e }); @@ -215,7 +216,7 @@ const SettingSystem = ({ onError }: Props) => { }} onGuard={async (e) => { if (isAdminMode) { - Notice.info(t("Administrator mode may not support auto launch"), 2000); + showNotice('info', t("Administrator mode may not support auto launch"), 2000); } try { diff --git a/src/components/setting/setting-verge-advanced.tsx b/src/components/setting/setting-verge-advanced.tsx index aee48d76..c9f95c06 100644 --- a/src/components/setting/setting-verge-advanced.tsx +++ b/src/components/setting/setting-verge-advanced.tsx @@ -12,7 +12,7 @@ import { import { check as checkUpdate } from "@tauri-apps/plugin-updater"; import { useVerge } from "@/hooks/use-verge"; import { version } from "@root/package.json"; -import { DialogRef, Notice } from "@/components/base"; +import { DialogRef } from "@/components/base"; import { SettingList, SettingItem } from "./mods/setting-comp"; import { ConfigViewer } from "./mods/config-viewer"; import { HotkeyViewer } from "./mods/hotkey-viewer"; @@ -24,6 +24,7 @@ import { BackupViewer } from "./mods/backup-viewer"; import { LiteModeViewer } from "./mods/lite-mode-viewer"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { ContentCopyRounded } from "@mui/icons-material"; +import { showNotice } from "@/services/noticeService"; interface Props { onError?: (err: Error) => void; @@ -46,18 +47,18 @@ const SettingVergeAdvanced = ({ onError }: Props) => { try { const info = await checkUpdate(); if (!info?.available) { - Notice.success(t("Currently on the Latest Version")); + showNotice('success', t("Currently on the Latest Version")); } else { updateRef.current?.open(); } } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); } }; const onExportDiagnosticInfo = useCallback(async () => { await exportDiagnosticInfo(); - Notice.success(t("Copy Success"), 1000); + showNotice('success', t("Copy Success"), 1000); }, []); return ( diff --git a/src/components/setting/setting-verge-basic.tsx b/src/components/setting/setting-verge-basic.tsx index 0fadef0e..346507fe 100644 --- a/src/components/setting/setting-verge-basic.tsx +++ b/src/components/setting/setting-verge-basic.tsx @@ -4,7 +4,7 @@ import { open } from "@tauri-apps/plugin-dialog"; import { Button, MenuItem, Select, Input } from "@mui/material"; import { copyClashEnv } from "@/services/cmds"; import { useVerge } from "@/hooks/use-verge"; -import { DialogRef, Notice } from "@/components/base"; +import { DialogRef } from "@/components/base"; import { SettingList, SettingItem } from "./mods/setting-comp"; import { ThemeModeSwitch } from "./mods/theme-mode-switch"; import { ConfigViewer } from "./mods/config-viewer"; @@ -20,6 +20,7 @@ import { routers } from "@/pages/_routers"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { ContentCopyRounded } from "@mui/icons-material"; import { languages } from "@/services/i18n"; +import { showNotice } from "@/services/noticeService"; interface Props { onError?: (err: Error) => void; @@ -66,7 +67,7 @@ const SettingVergeBasic = ({ onError }: Props) => { const onCopyClashEnv = useCallback(async () => { await copyClashEnv(); - Notice.success(t("Copy Success"), 1000); + showNotice('success', t("Copy Success"), 1000); }, []); return ( diff --git a/src/components/shared/ProxyControlSwitches.tsx b/src/components/shared/ProxyControlSwitches.tsx index d00d45f1..a59a7386 100644 --- a/src/components/shared/ProxyControlSwitches.tsx +++ b/src/components/shared/ProxyControlSwitches.tsx @@ -15,7 +15,7 @@ import { alpha, useTheme, } from "@mui/material"; -import { DialogRef, Notice, Switch } from "@/components/base"; +import { DialogRef, Switch } from "@/components/base"; import { GuardState } from "@/components/setting/mods/guard-state"; import { SysproxyViewer } from "@/components/setting/mods/sysproxy-viewer"; import { TunViewer } from "@/components/setting/mods/tun-viewer"; @@ -28,6 +28,7 @@ import { } from "@/services/cmds"; import { useLockFn } from "ahooks"; import { closeAllConnections } from "@/services/api"; +import { showNotice } from "@/services/noticeService"; interface ProxySwitchProps { label?: string; @@ -78,13 +79,13 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { // 安装系统服务 const onInstallService = useLockFn(async () => { try { - Notice.info(t("Installing Service..."), 1000); + showNotice('info', t("Installing Service..."), 1000); await installService(); - Notice.success(t("Service Installed Successfully"), 2000); + showNotice('success', t("Service Installed Successfully"), 2000); // 重新获取运行模式 await mutateRunningMode(); } catch (err: any) { - Notice.error(err.message || err.toString(), 3000); + showNotice('error', err.message || err.toString(), 3000); } }); @@ -258,13 +259,18 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { onFormat={onSwitchFormat} onChange={(e) => { // 当在sidecar模式下禁用切换 - if (isSidecarMode) return; + if (isSidecarMode) { + showNotice('error', t("TUN requires Service Mode"), 2000); + return Promise.reject( + new Error(t("TUN requires Service Mode")), + ); + } onChangeData({ enable_tun_mode: e }); }} onGuard={(e) => { // 当在sidecar模式下禁用切换 if (isSidecarMode) { - Notice.error(t("TUN requires Service Mode"), 2000); + showNotice('error', t("TUN requires Service Mode"), 2000); return Promise.reject( new Error(t("TUN requires Service Mode")), ); diff --git a/src/components/test/test-item.tsx b/src/components/test/test-item.tsx index 2a1fc8ee..81829c80 100644 --- a/src/components/test/test-item.tsx +++ b/src/components/test/test-item.tsx @@ -6,13 +6,14 @@ import { CSS } from "@dnd-kit/utilities"; import { Box, Divider, MenuItem, Menu, styled, alpha } from "@mui/material"; import { BaseLoading } from "@/components/base"; import { LanguageRounded } from "@mui/icons-material"; -import { Notice } from "@/components/base"; +import { showNotice } from "@/services/noticeService"; import { TestBox } from "./test-box"; import delayManager from "@/services/delay"; import { cmdTestDelay, downloadIconCache } from "@/services/cmds"; import { UnlistenFn } from "@tauri-apps/api/event"; import { convertFileSrc } from "@tauri-apps/api/core"; import { useListen } from "@/hooks/use-listen"; + interface Props { id: string; itemData: IVergeTestItem; @@ -73,7 +74,7 @@ export const TestItem = (props: Props) => { try { onDeleteItem(uid); } catch (err: any) { - Notice.error(err?.message || err.toString()); + showNotice('error', err.message || err.toString()); } }); diff --git a/src/components/test/test-viewer.tsx b/src/components/test/test-viewer.tsx index 053ee619..a629a94f 100644 --- a/src/components/test/test-viewer.tsx +++ b/src/components/test/test-viewer.tsx @@ -4,8 +4,9 @@ import { useTranslation } from "react-i18next"; import { useForm, Controller } from "react-hook-form"; import { TextField } from "@mui/material"; import { useVerge } from "@/hooks/use-verge"; -import { BaseDialog, Notice } from "@/components/base"; +import { BaseDialog } from "@/components/base"; import { nanoid } from "nanoid"; +import { showNotice } from "@/services/noticeService"; interface Props { onChange: (uid: string, patch?: Partial) => void; @@ -99,7 +100,7 @@ export const TestViewer = forwardRef((props, ref) => { setLoading(false); setTimeout(() => formIns.reset(), 500); } catch (err: any) { - Notice.error(err.message || err.toString()); + showNotice('error', err.message || err.toString()); setLoading(false); } }), diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index ade06a47..ee8aa321 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -14,7 +14,6 @@ import LogoSvg from "@/assets/image/logo.svg?react"; import iconLight from "@/assets/image/icon_light.svg?react"; import iconDark from "@/assets/image/icon_dark.svg?react"; import { useThemeMode, useEnableLog } from "@/services/states"; -import { Notice } from "@/components/base"; import { LayoutItem } from "@/components/layout/layout-item"; import { LayoutControl } from "@/components/layout/layout-control"; import { LayoutTraffic } from "@/components/layout/layout-traffic"; @@ -31,6 +30,8 @@ import { listen } from "@tauri-apps/api/event"; 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"; const appWindow = getCurrentWebviewWindow(); export let portableFlag = false; @@ -46,92 +47,95 @@ const handleNoticeMessage = ( t: (key: string) => string, navigate: (path: string, options?: any) => void, ) => { - console.log("[通知监听] 收到消息:", status, msg); + console.log("[通知监听 V2] 收到消息:", status, msg); switch (status) { case "import_sub_url::ok": navigate("/profile", { state: { current: msg } }); - Notice.success(t("Import Subscription Successful")); + showNotice('success', t("Import Subscription Successful")); break; case "import_sub_url::error": navigate("/profile"); - Notice.error(msg); + showNotice('error', msg); break; case "set_config::error": - Notice.error(msg); + showNotice('error', msg); break; case "update_with_clash_proxy": - Notice.success(`${t("Update with Clash proxy successfully")} ${msg}`); + showNotice('success', `${t("Update with Clash proxy successfully")} ${msg}`); break; case "update_retry_with_clash": - Notice.info(t("Update failed, retrying with Clash proxy...")); + showNotice('info', t("Update failed, retrying with Clash proxy...")); break; case "update_failed_even_with_clash": - Notice.error(`${t("Update failed even with Clash proxy")}: ${msg}`); + showNotice('error', `${t("Update failed even with Clash proxy")}: ${msg}`); break; case "update_failed": - Notice.error(msg); + showNotice('error', msg); break; case "config_validate::boot_error": - Notice.error(`${t("Boot Config Validation Failed")} ${msg}`); + showNotice('error', `${t("Boot Config Validation Failed")} ${msg}`); break; case "config_validate::core_change": - Notice.error(`${t("Core Change Config Validation Failed")} ${msg}`); + showNotice('error', `${t("Core Change Config Validation Failed")} ${msg}`); break; case "config_validate::error": - Notice.error(`${t("Config Validation Failed")} ${msg}`); + showNotice('error', `${t("Config Validation Failed")} ${msg}`); break; case "config_validate::process_terminated": - Notice.error(t("Config Validation Process Terminated")); + showNotice('error', t("Config Validation Process Terminated")); break; case "config_validate::stdout_error": - Notice.error(`${t("Config Validation Failed")} ${msg}`); + showNotice('error', `${t("Config Validation Failed")} ${msg}`); break; case "config_validate::script_error": - Notice.error(`${t("Script File Error")} ${msg}`); + showNotice('error', `${t("Script File Error")} ${msg}`); break; case "config_validate::script_syntax_error": - Notice.error(`${t("Script Syntax Error")} ${msg}`); + showNotice('error', `${t("Script Syntax Error")} ${msg}`); break; case "config_validate::script_missing_main": - Notice.error(`${t("Script Missing Main")} ${msg}`); + showNotice('error', `${t("Script Missing Main")} ${msg}`); break; case "config_validate::file_not_found": - Notice.error(`${t("File Not Found")} ${msg}`); + showNotice('error', `${t("File Not Found")} ${msg}`); break; case "config_validate::yaml_syntax_error": - Notice.error(`${t("YAML Syntax Error")} ${msg}`); + showNotice('error', `${t("YAML Syntax Error")} ${msg}`); break; case "config_validate::yaml_read_error": - Notice.error(`${t("YAML Read Error")} ${msg}`); + showNotice('error', `${t("YAML Read Error")} ${msg}`); break; case "config_validate::yaml_mapping_error": - Notice.error(`${t("YAML Mapping Error")} ${msg}`); + showNotice('error', `${t("YAML Mapping Error")} ${msg}`); break; case "config_validate::yaml_key_error": - Notice.error(`${t("YAML Key Error")} ${msg}`); + showNotice('error', `${t("YAML Key Error")} ${msg}`); break; case "config_validate::yaml_error": - Notice.error(`${t("YAML Error")} ${msg}`); + showNotice('error', `${t("YAML Error")} ${msg}`); break; case "config_validate::merge_syntax_error": - Notice.error(`${t("Merge File Syntax Error")} ${msg}`); + showNotice('error', `${t("Merge File Syntax Error")} ${msg}`); break; case "config_validate::merge_mapping_error": - Notice.error(`${t("Merge File Mapping Error")} ${msg}`); + showNotice('error', `${t("Merge File Mapping Error")} ${msg}`); break; case "config_validate::merge_key_error": - Notice.error(`${t("Merge File Key Error")} ${msg}`); + showNotice('error', `${t("Merge File Key Error")} ${msg}`); break; case "config_validate::merge_error": - Notice.error(`${t("Merge File Error")} ${msg}`); + showNotice('error', `${t("Merge File Error")} ${msg}`); break; case "config_core::change_success": - Notice.success(`${t("Core Changed Successfully")}: ${msg}`); + showNotice('success', `${t("Core Changed Successfully")}: ${msg}`); break; case "config_core::change_error": - Notice.error(`${t("Failed to Change Core")}: ${msg}`); + showNotice('error', `${t("Failed to Change Core")}: ${msg}`); break; + default: // Optional: Log unhandled statuses + console.warn(`[通知监听 V2] 未处理的状态: ${status}`); + break; } }; @@ -276,6 +280,8 @@ const Layout = () => { return ( + + { const { t } = useTranslation(); @@ -79,7 +80,7 @@ const ProfilePage = () => { for (let file of paths) { if (!file.endsWith(".yaml") && !file.endsWith(".yml")) { - Notice.error(t("Only YAML Files Supported")); + showNotice('error', t("Only YAML Files Supported")); continue; } const item = { @@ -144,14 +145,14 @@ const ProfilePage = () => { try { // 尝试正常导入 await importProfile(url); - Notice.success(t("Profile Imported Successfully")); + showNotice('success', t("Profile Imported Successfully")); setUrl(""); mutateProfiles(); await onEnhance(false); } catch (err: any) { // 首次导入失败,尝试使用自身代理 const errmsg = err.message || err.toString(); - Notice.info(t("Import failed, retrying with Clash proxy...")); + showNotice('info', t("Import failed, retrying with Clash proxy...")); try { // 使用自身代理尝试导入 @@ -161,16 +162,14 @@ const ProfilePage = () => { }); // 回退导入成功 - Notice.success(t("Profile Imported with Clash proxy")); + showNotice('success', t("Profile Imported with Clash proxy")); setUrl(""); mutateProfiles(); await onEnhance(false); } catch (retryErr: any) { // 回退导入也失败 const retryErrmsg = retryErr?.message || retryErr.toString(); - Notice.error( - `${t("Import failed even with Clash proxy")}: ${retryErrmsg}`, - ); + showNotice('error', `${t("Import failed even with Clash proxy")}: ${retryErrmsg}`); } } finally { setDisabled(false); @@ -200,10 +199,10 @@ const ProfilePage = () => { closeAllConnections(); await activateSelected(); if (notifySuccess && success) { - Notice.success(t("Profile Switched"), 1000); + showNotice('success', t("Profile Switched"), 1000); } } catch (err: any) { - Notice.error(err?.message || err.toString(), 4000); + showNotice('error', err?.message || err.toString(), 4000); } finally { clearTimeout(reset); setActivatings([]); @@ -229,10 +228,10 @@ const ProfilePage = () => { await enhanceProfiles(); mutateLogs(); if (notifySuccess) { - Notice.success(t("Profile Reactivated"), 1000); + showNotice('success', t("Profile Reactivated"), 1000); } } catch (err: any) { - Notice.error(err.message || err.toString(), 3000); + showNotice('error', err.message || err.toString(), 3000); } finally { setActivatings([]); } @@ -247,7 +246,7 @@ const ProfilePage = () => { mutateLogs(); current && (await onEnhance(false)); } catch (err: any) { - Notice.error(err?.message || err.toString()); + showNotice('error', err?.message || err.toString()); } finally { setActivatings([]); } diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 47cdb91d..bbf8e97b 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -2,7 +2,7 @@ import { Box, ButtonGroup, IconButton, Select, MenuItem } from "@mui/material"; import Grid from "@mui/material/Grid2"; import { useLockFn } from "ahooks"; import { useTranslation } from "react-i18next"; -import { BasePage, Notice } from "@/components/base"; +import { BasePage } from "@/components/base"; import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material"; import { openWebUrl } from "@/services/cmds"; import SettingVergeBasic from "@/components/setting/setting-verge-basic"; @@ -10,12 +10,13 @@ import SettingVergeAdvanced from "@/components/setting/setting-verge-advanced"; import SettingClash from "@/components/setting/setting-clash"; import SettingSystem from "@/components/setting/setting-system"; import { useThemeMode } from "@/services/states"; +import { showNotice } from "@/services/noticeService"; const SettingPage = () => { const { t } = useTranslation(); const onError = (err: any) => { - Notice.error(err?.message || err.toString()); + showNotice('error', err?.message || err.toString()); }; const toGithubRepo = useLockFn(() => { diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 96e7522f..911b7258 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -1,5 +1,5 @@ import { invoke } from "@tauri-apps/api/core"; -import { Notice } from "@/components/base"; +import { showNotice } from "@/services/noticeService"; export async function copyClashEnv() { return invoke("copy_clash_env"); @@ -145,25 +145,29 @@ export async function getAppDir() { export async function openAppDir() { return invoke("open_app_dir").catch((err) => - Notice.error(err?.message || err.toString(), 1500), + showNotice('error', err?.message || err.toString()), ); } export async function openCoreDir() { return invoke("open_core_dir").catch((err) => - Notice.error(err?.message || err.toString(), 1500), + showNotice('error', err?.message || err.toString()), ); } export async function openLogsDir() { return invoke("open_logs_dir").catch((err) => - Notice.error(err?.message || err.toString(), 1500), + showNotice('error', err?.message || err.toString()), ); } -export async function openWebUrl(url: string) { - return invoke("open_web_url", { url }); -} +export const openWebUrl = async (url: string) => { + try { + await invoke("open_web_url", { url }); + } catch (err: any) { + showNotice('error', err.toString()); + } +}; export async function cmdGetProxyDelay( name: string, @@ -214,7 +218,7 @@ export async function cmdTestDelay(url: string) { export async function invoke_uwp_tool() { return invoke("invoke_uwp_tool").catch((err) => - Notice.error(err?.message || err.toString(), 1500), + showNotice('error', err?.message || err.toString(), 1500), ); } diff --git a/src/services/noticeService.ts b/src/services/noticeService.ts new file mode 100644 index 00000000..e9b9f9cd --- /dev/null +++ b/src/services/noticeService.ts @@ -0,0 +1,80 @@ +import { ReactNode } from 'react'; + +export interface NoticeItem { + id: number; + type: 'success' | 'error' | 'info'; + message: ReactNode; + duration: number; + timerId?: ReturnType; +} + +type Listener = (notices: NoticeItem[]) => void; + +let nextId = 0; +let notices: NoticeItem[] = []; +const listeners: Set = 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); + } + + + 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: Listener): () => void { + listeners.add(listener); + listener([...notices]); + return () => { + listeners.delete(listener); + }; +} + +// Function to clear all notices at once +export function clearAllNotices() { + notices.forEach(n => { + if (n.timerId) clearTimeout(n.timerId); + }); + notices = []; + notifyListeners(); +} \ No newline at end of file