From 5fb67cf9213afb73ee89aa8b89539f72882bd917 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sun, 2 Nov 2025 15:56:44 +0800 Subject: [PATCH] refactor(notice): unify showNotice API --- src/components/base/NoticeManager.tsx | 56 +++- src/components/home/home-profile-card.tsx | 4 +- src/components/home/proxy-tun-card.tsx | 4 +- src/components/home/system-info-card.tsx | 10 +- src/components/profile/editor-viewer.tsx | 14 +- .../profile/groups-editor-viewer.tsx | 22 +- src/components/profile/profile-item.tsx | 6 +- src/components/profile/profile-more.tsx | 6 +- src/components/profile/profile-viewer.tsx | 6 +- .../profile/proxies-editor-viewer.tsx | 10 +- .../profile/rules-editor-viewer.tsx | 22 +- .../setting/mods/clash-core-viewer.tsx | 14 +- .../setting/mods/controller-viewer.tsx | 13 +- src/components/setting/mods/dns-viewer.tsx | 17 +- src/components/setting/mods/hotkey-viewer.tsx | 6 +- src/components/setting/mods/layout-viewer.tsx | 4 +- .../setting/mods/lite-mode-viewer.tsx | 6 +- src/components/setting/mods/misc-viewer.tsx | 6 +- .../setting/mods/sysproxy-viewer.tsx | 10 +- src/components/setting/mods/theme-viewer.tsx | 6 +- src/components/setting/mods/tun-viewer.tsx | 10 +- src/components/setting/mods/update-viewer.tsx | 4 +- src/components/setting/mods/web-ui-viewer.tsx | 4 +- src/components/setting/setting-clash.tsx | 6 +- .../setting/setting-verge-advanced.tsx | 4 +- .../shared/ProxyControlSwitches.tsx | 8 +- src/components/test/test-item.tsx | 4 +- src/components/test/test-viewer.tsx | 4 +- src/hooks/useServiceInstaller.ts | 8 +- src/hooks/useServiceUninstaller.ts | 8 +- src/pages/_layout/notificationHandlers.ts | 86 ++--- src/pages/profiles.tsx | 45 +-- src/pages/settings.tsx | 4 +- src/services/cmds.ts | 18 +- src/services/noticeService.ts | 303 ++++++++++++++---- 35 files changed, 450 insertions(+), 308 deletions(-) diff --git a/src/components/base/NoticeManager.tsx b/src/components/base/NoticeManager.tsx index 60b7db51..a34f2584 100644 --- a/src/components/base/NoticeManager.tsx +++ b/src/components/base/NoticeManager.tsx @@ -63,10 +63,58 @@ export const NoticeManager: React.FC = () => { } > {notice.i18n - ? t(notice.i18n.i18nKey, { - defaultValue: notice.i18n.fallback, - ...(notice.i18n.params ?? {}), - }) + ? (() => { + const params = (notice.i18n.params ?? {}) as Record< + string, + unknown + >; + const { + prefixKey, + prefixParams, + prefix, + message, + ...restParams + } = params; + + const prefixKeyParams = + prefixParams && + typeof prefixParams === "object" && + prefixParams !== null + ? (prefixParams as Record) + : undefined; + + const resolvedPrefix = + typeof prefixKey === "string" + ? t(prefixKey, { + defaultValue: prefixKey, + ...(prefixKeyParams ?? {}), + }) + : typeof prefix === "string" + ? prefix + : undefined; + + const finalParams: Record = { + ...restParams, + }; + if (resolvedPrefix !== undefined) { + finalParams.prefix = resolvedPrefix; + } + if (typeof message === "string") { + finalParams.message = message; + } + + const defaultValue = + resolvedPrefix && typeof message === "string" + ? `${resolvedPrefix} ${message}` + : typeof message === "string" + ? message + : undefined; + + return t(notice.i18n.key, { + defaultValue, + ...finalParams, + }); + })() : notice.message} diff --git a/src/components/home/home-profile-card.tsx b/src/components/home/home-profile-card.tsx index fdcbebeb..fa1e01e3 100644 --- a/src/components/home/home-profile-card.tsx +++ b/src/components/home/home-profile-card.tsx @@ -26,7 +26,7 @@ import { useNavigate } from "react-router"; import { useAppData } from "@/providers/app-data-context"; import { openWebUrl, updateProfile } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import parseTraffic from "@/utils/parse-traffic"; import { EnhancedCard } from "./enhanced-card"; @@ -297,7 +297,7 @@ export const HomeProfileCard = ({ // 刷新首页数据 refreshAll(); } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString()), 3000); + 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 7d2598a1..c775f87a 100644 --- a/src/components/home/proxy-tun-card.tsx +++ b/src/components/home/proxy-tun-card.tsx @@ -21,7 +21,7 @@ import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches"; import { useSystemProxyState } from "@/hooks/use-system-proxy-state"; import { useSystemState } from "@/hooks/use-system-state"; import { useVerge } from "@/hooks/use-verge"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab"; @@ -148,7 +148,7 @@ export const ProxyTunCard: FC = () => { const { enable_tun_mode } = verge ?? {}; const handleError = (err: Error) => { - showNotice.error(createRawNotice(err.message || err.toString())); + showNotice.error(err.message || err.toString()); }; const handleTabChange = (tab: string) => { diff --git a/src/components/home/system-info-card.tsx b/src/components/home/system-info-card.tsx index 5d2e2b40..74a52957 100644 --- a/src/components/home/system-info-card.tsx +++ b/src/components/home/system-info-card.tsx @@ -24,7 +24,7 @@ import { useSystemState } from "@/hooks/use-system-state"; import { useVerge } from "@/hooks/use-verge"; import { useServiceInstaller } from "@/hooks/useServiceInstaller"; import { getSystemInfo } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { checkUpdateSafe as checkUpdate } from "@/services/update"; import { version as appVersion } from "@root/package.json"; @@ -174,15 +174,13 @@ export const SystemInfoCard = () => { try { const info = await checkUpdate(); if (!info?.available) { - showNotice.success({ - i18nKey: "Currently on the Latest Version", - }); + showNotice.success("Currently on the Latest Version"); } else { - showNotice.info({ i18nKey: "Update Available" }, 2000); + showNotice.info("Update Available", 2000); goToSettings(); } } catch (err: any) { - showNotice.error(createRawNotice(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 99cae488..4b8ea35f 100644 --- a/src/components/profile/editor-viewer.tsx +++ b/src/components/profile/editor-viewer.tsx @@ -25,7 +25,7 @@ import { useTranslation } from "react-i18next"; import MonacoEditor from "react-monaco-editor"; import pac from "types-pac/pac.d.ts?raw"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; import debounce from "@/utils/debounce"; import getSystem from "@/utils/get-system"; @@ -132,8 +132,8 @@ export const EditorViewer = (props: Props) => { try { currData.current = value; onChange?.(prevData.current, currData.current); - } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString())); + } catch (err) { + showNotice.error(err); } }); @@ -143,16 +143,16 @@ export const EditorViewer = (props: Props) => { onSave?.(prevData.current, currData.current); } onClose(); - } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString())); + } catch (err) { + showNotice.error(err); } }); const handleClose = useLockFn(async () => { try { onClose(); - } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString())); + } catch (err) { + showNotice.error(err); } }); diff --git a/src/components/profile/groups-editor-viewer.tsx b/src/components/profile/groups-editor-viewer.tsx index ef6f4b7f..6e6cc986 100644 --- a/src/components/profile/groups-editor-viewer.tsx +++ b/src/components/profile/groups-editor-viewer.tsx @@ -55,7 +55,7 @@ import { readProfileFile, saveProfileFile, } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; import getSystem from "@/utils/get-system"; @@ -384,14 +384,12 @@ export const GroupsEditorViewer = (props: Props) => { } await saveProfileFile(property, nextData); - showNotice.success({ - i18nKey: "components.profile.notifications.saved", - }); + showNotice.success("components.profile.notifications.saved"); setPrevData(nextData); onSave?.(prevData, nextData); onClose(); - } catch (err: any) { - showNotice.error(createRawNotice(err.toString())); + } catch (err) { + showNotice.error(err); } }); @@ -921,10 +919,8 @@ export const GroupsEditorViewer = (props: Props) => { } } setPrependSeq([formIns.getValues(), ...prependSeq]); - } catch (err: any) { - showNotice.error( - createRawNotice(err.message || err.toString()), - ); + } catch (err) { + showNotice.error(err); } }} > @@ -949,10 +945,8 @@ export const GroupsEditorViewer = (props: Props) => { } } setAppendSeq([...appendSeq, formIns.getValues()]); - } catch (err: any) { - showNotice.error( - createRawNotice(err.message || err.toString()), - ); + } catch (err) { + showNotice.error(err); } }} > diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index e3052144..81a04cde 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -34,7 +34,7 @@ import { saveProfileFile, getNextUpdateTime, } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { useLoadingCache, useSetLoadingCache } from "@/services/states"; import parseTraffic from "@/utils/parse-traffic"; @@ -320,8 +320,8 @@ export const ProfileItem = (props: Props) => { setAnchorEl(null); try { await viewProfile(itemData.uid); - } catch (err: any) { - showNotice.error(createRawNotice(err?.message || err.toString())); + } catch (err) { + showNotice.error(err); } }); diff --git a/src/components/profile/profile-more.tsx b/src/components/profile/profile-more.tsx index 87313ba6..21d1341f 100644 --- a/src/components/profile/profile-more.tsx +++ b/src/components/profile/profile-more.tsx @@ -14,7 +14,7 @@ import { useTranslation } from "react-i18next"; import { EditorViewer } from "@/components/profile/editor-viewer"; import { viewProfile, readProfileFile, saveProfileFile } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { LogViewer } from "./log-viewer"; import { ProfileBox } from "./profile-box"; @@ -47,8 +47,8 @@ export const ProfileMore = (props: Props) => { setAnchorEl(null); try { await viewProfile(id); - } catch (err: any) { - showNotice.error(createRawNotice(err?.message || err.toString())); + } catch (err) { + showNotice.error(err); } }); diff --git a/src/components/profile/profile-viewer.tsx b/src/components/profile/profile-viewer.tsx index e67a6b93..b872ec07 100644 --- a/src/components/profile/profile-viewer.tsx +++ b/src/components/profile/profile-viewer.tsx @@ -17,7 +17,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, Switch } from "@/components/base"; import { useProfiles } from "@/hooks/use-profiles"; import { createProfile, patchProfile } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { version } from "@root/package.json"; import { FileInput } from "./file-input"; @@ -184,8 +184,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) { setTimeout(() => { onChange(isActivating); }, 0); - } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString())); + } catch (err) { + showNotice.error(err); } finally { setLoading(false); } diff --git a/src/components/profile/proxies-editor-viewer.tsx b/src/components/profile/proxies-editor-viewer.tsx index 44f0495b..7aa41576 100644 --- a/src/components/profile/proxies-editor-viewer.tsx +++ b/src/components/profile/proxies-editor-viewer.tsx @@ -42,7 +42,7 @@ import { Virtuoso } from "react-virtuoso"; import { ProxyItem } from "@/components/profile/proxy-item"; import { readProfileFile, saveProfileFile } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; import getSystem from "@/utils/get-system"; import parseUri from "@/utils/uri-parser"; @@ -263,13 +263,11 @@ export const ProxiesEditorViewer = (props: Props) => { const handleSave = useLockFn(async () => { try { await saveProfileFile(property, currData); - showNotice.success({ - i18nKey: "components.profile.notifications.saved", - }); + showNotice.success("components.profile.notifications.saved"); onSave?.(prevData, currData); onClose(); - } catch (err: any) { - showNotice.error(createRawNotice(err.toString())); + } catch (err) { + showNotice.error(err); } }); diff --git a/src/components/profile/rules-editor-viewer.tsx b/src/components/profile/rules-editor-viewer.tsx index ee7569a8..9f55dbb6 100644 --- a/src/components/profile/rules-editor-viewer.tsx +++ b/src/components/profile/rules-editor-viewer.tsx @@ -45,7 +45,7 @@ import { Virtuoso } from "react-virtuoso"; import { Switch } from "@/components/base"; import { RuleItem } from "@/components/profile/rule-item"; import { readProfileFile, saveProfileFile } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; import getSystem from "@/utils/get-system"; @@ -350,10 +350,8 @@ export const RulesEditorViewer = (props: Props) => { { forceQuotes: true }, ), ); - } catch (e: any) { - showNotice.error( - createRawNotice(e?.message || e?.toString() || "YAML dump error"), - ); + } catch (error) { + showNotice.error(error ?? "YAML dump error"); } }; let idleId: number | undefined; @@ -481,13 +479,11 @@ export const RulesEditorViewer = (props: Props) => { const handleSave = useLockFn(async () => { try { await saveProfileFile(property, currData); - showNotice.success({ - i18nKey: "components.profile.notifications.saved", - }); + showNotice.success("components.profile.notifications.saved"); onSave?.(prevData, currData); onClose(); } catch (err: any) { - showNotice.error(createRawNotice(err.toString())); + showNotice.error(err); } }); @@ -630,9 +626,7 @@ export const RulesEditorViewer = (props: Props) => { if (prependSeq.includes(raw)) return; setPrependSeq([raw, ...prependSeq]); } catch (err: any) { - showNotice.error( - createRawNotice(err.message || err.toString()), - ); + showNotice.error(err); } }} > @@ -650,9 +644,7 @@ export const RulesEditorViewer = (props: Props) => { if (appendSeq.includes(raw)) return; setAppendSeq([...appendSeq, raw]); } catch (err: any) { - showNotice.error( - createRawNotice(err.message || err.toString()), - ); + showNotice.error(err); } }} > diff --git a/src/components/setting/mods/clash-core-viewer.tsx b/src/components/setting/mods/clash-core-viewer.tsx index fb6bc28c..8c87667f 100644 --- a/src/components/setting/mods/clash-core-viewer.tsx +++ b/src/components/setting/mods/clash-core-viewer.tsx @@ -21,7 +21,7 @@ import { closeAllConnections, upgradeCore } from "tauri-plugin-mihomo-api"; import { BaseDialog, DialogRef } from "@/components/base"; import { useVerge } from "@/hooks/use-verge"; import { changeClashCore, restartCore } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; const VALID_CORE = [ { name: "Mihomo", core: "verge-mihomo", chip: "Release Version" }, @@ -54,7 +54,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref }) { const errorMsg = await changeClashCore(core); if (errorMsg) { - showNotice.error(createRawNotice(errorMsg)); + showNotice.error(errorMsg); setChangingCore(null); return; } @@ -65,9 +65,9 @@ export function ClashCoreViewer({ ref }: { ref?: Ref }) { mutate("getVersion"); setChangingCore(null); }, 500); - } catch (err: any) { + } catch (err) { setChangingCore(null); - showNotice.error(createRawNotice(err.message || err.toString())); + showNotice.error(err); } }); @@ -77,9 +77,9 @@ export function ClashCoreViewer({ ref }: { ref?: Ref }) { await restartCore(); showNotice.success({ i18nKey: "Clash Core Restarted" }); setRestarting(false); - } catch (err: any) { + } catch (err) { setRestarting(false); - showNotice.error(createRawNotice(err.message || err.toString())); + showNotice.error(err); } }); @@ -95,7 +95,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref }) { const showMsg = errMsg.includes("already using latest version") ? "Already Using Latest Core Version" : errMsg; - showNotice.error({ i18nKey: showMsg, fallback: showMsg }); + showNotice.error(showMsg); } }); diff --git a/src/components/setting/mods/controller-viewer.tsx b/src/components/setting/mods/controller-viewer.tsx index 3523decb..02d205dd 100644 --- a/src/components/setting/mods/controller-viewer.tsx +++ b/src/components/setting/mods/controller-viewer.tsx @@ -18,7 +18,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { useClashInfo } from "@/hooks/use-clash"; import { useVerge } from "@/hooks/use-verge"; -import { createPrefixedNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; export function ControllerViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); @@ -81,12 +81,11 @@ export function ControllerViewer({ ref }: { ref?: Ref }) { setOpen(false); } catch (err: any) { const message = err?.message || err?.toString?.(); - showNotice.error( - message - ? createPrefixedNotice(t("Failed to save configuration"), message) - : { i18nKey: "Failed to save configuration" }, - 4000, - ); + if (message) { + showNotice.error(t("Failed to save configuration"), message, 4000); + } else { + showNotice.error("Failed to save configuration", 4000); + } } finally { setIsSaving(false); } diff --git a/src/components/setting/mods/dns-viewer.tsx b/src/components/setting/mods/dns-viewer.tsx index 0b0db888..109888bc 100644 --- a/src/components/setting/mods/dns-viewer.tsx +++ b/src/components/setting/mods/dns-viewer.tsx @@ -29,11 +29,7 @@ import MonacoEditor from "react-monaco-editor"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { useClash } from "@/hooks/use-clash"; -import { - createPrefixedNotice, - createRawNotice, - showNotice, -} from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; import getSystem from "@/utils/get-system"; @@ -551,12 +547,7 @@ export function DnsViewer({ ref }: { ref?: Ref }) { } } - showNotice.error( - createPrefixedNotice( - `${t("DNS configuration error")}:`, - cleanErrorMsg, - ), - ); + showNotice.error(`${t("DNS configuration error")}:`, cleanErrorMsg); return; } @@ -568,8 +559,8 @@ export function DnsViewer({ ref }: { ref?: Ref }) { setOpen(false); showNotice.success({ i18nKey: "DNS settings saved" }); - } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString())); + } catch (err) { + showNotice.error(err); } }); diff --git a/src/components/setting/mods/hotkey-viewer.tsx b/src/components/setting/mods/hotkey-viewer.tsx index 0286bab4..bf311c92 100644 --- a/src/components/setting/mods/hotkey-viewer.tsx +++ b/src/components/setting/mods/hotkey-viewer.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { useVerge } from "@/hooks/use-verge"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { HotkeyInput } from "./hotkey-input"; @@ -81,8 +81,8 @@ export const HotkeyViewer = forwardRef((props, ref) => { enable_global_hotkey: enableGlobalHotkey, }); setOpen(false); - } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString())); + } catch (err) { + showNotice.error(err); } }); diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx index ed79de22..70a8774b 100644 --- a/src/components/setting/mods/layout-viewer.tsx +++ b/src/components/setting/mods/layout-viewer.tsx @@ -23,7 +23,7 @@ import { DEFAULT_HOVER_DELAY } from "@/components/proxy/proxy-group-navigator"; import { useVerge } from "@/hooks/use-verge"; import { useWindowDecorations } from "@/hooks/use-window"; import { copyIconFile, getAppDir } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import getSystem from "@/utils/get-system"; import { GuardState } from "./guard-state"; @@ -104,7 +104,7 @@ export const LayoutViewer = forwardRef((_, ref) => { const onSwitchFormat = (_e: any, value: boolean) => value; const onError = (err: any) => { - showNotice.error(createRawNotice(err.message || err.toString())); + showNotice.error(err); }; 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 94799001..107e50df 100644 --- a/src/components/setting/mods/lite-mode-viewer.tsx +++ b/src/components/setting/mods/lite-mode-viewer.tsx @@ -15,7 +15,7 @@ import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useVerge } from "@/hooks/use-verge"; import { entry_lightweight_mode } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; export function LiteModeViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); @@ -45,8 +45,8 @@ export function LiteModeViewer({ ref }: { ref?: Ref }) { auto_light_weight_minutes: values.autoEnterLiteModeDelay, }); setOpen(false); - } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString())); + } catch (err) { + showNotice.error(err); } }); diff --git a/src/components/setting/mods/misc-viewer.tsx b/src/components/setting/mods/misc-viewer.tsx index afb500b1..a64c9d65 100644 --- a/src/components/setting/mods/misc-viewer.tsx +++ b/src/components/setting/mods/misc-viewer.tsx @@ -14,7 +14,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useVerge } from "@/hooks/use-verge"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; export const MiscViewer = forwardRef((props, ref) => { const { t } = useTranslation(); @@ -69,8 +69,8 @@ export const MiscViewer = forwardRef((props, ref) => { auto_log_clean: values.autoLogClean as any, }); setOpen(false); - } catch (err: any) { - showNotice.error(createRawNotice(err.toString())); + } catch (err) { + showNotice.error(err); } }); diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx index c1ba951a..a021fefd 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -36,7 +36,7 @@ import { getSystemProxy, patchVergeConfig, } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import getSystem from "@/utils/get-system"; const sleep = (ms: number) => @@ -160,8 +160,8 @@ export const SysproxyViewer = forwardRef((props, ref) => { mutate("getAutotemProxy"), ]); } - } catch (err: any) { - showNotice.error(createRawNotice(err.toString())); + } catch (err) { + showNotice.error(err); } }; @@ -404,10 +404,10 @@ export const SysproxyViewer = forwardRef((props, ref) => { console.warn("代理状态更新失败:", err); } }, 50); - } catch (err: any) { + } catch (err) { console.error("配置保存失败:", err); mutateVerge(); - showNotice.error(createRawNotice(err.toString())); + showNotice.error(err); // setOpen(true); } }); diff --git a/src/components/setting/mods/theme-viewer.tsx b/src/components/setting/mods/theme-viewer.tsx index e997a0d1..cb87abe9 100644 --- a/src/components/setting/mods/theme-viewer.tsx +++ b/src/components/setting/mods/theme-viewer.tsx @@ -16,7 +16,7 @@ import { BaseDialog, DialogRef } from "@/components/base"; import { EditorViewer } from "@/components/profile/editor-viewer"; import { useVerge } from "@/hooks/use-verge"; import { defaultDarkTheme, defaultTheme } from "@/pages/_theme"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; export function ThemeViewer(props: { ref?: React.Ref }) { const { ref } = props; @@ -50,8 +50,8 @@ export function ThemeViewer(props: { ref?: React.Ref }) { try { await patchVerge({ theme_setting: theme }); setOpen(false); - } catch (err: any) { - showNotice.error(createRawNotice(err.toString())); + } catch (err) { + showNotice.error(err); } }); diff --git a/src/components/setting/mods/tun-viewer.tsx b/src/components/setting/mods/tun-viewer.tsx index 0d305c51..53b09863 100644 --- a/src/components/setting/mods/tun-viewer.tsx +++ b/src/components/setting/mods/tun-viewer.tsx @@ -15,7 +15,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { useClash } from "@/hooks/use-clash"; import { enhanceProfiles } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import getSystem from "@/utils/get-system"; import { StackModeSwitch } from "./stack-mode-switch"; @@ -80,15 +80,13 @@ export function TunViewer({ ref }: { ref?: Ref }) { ); try { await enhanceProfiles(); - showNotice.success({ - i18nKey: "components.settings.tun.messages.applied", - }); + showNotice.success("components.settings.tun.messages.applied"); } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString())); + showNotice.error(err); } setOpen(false); } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString())); + showNotice.error(err); } }); diff --git a/src/components/setting/mods/update-viewer.tsx b/src/components/setting/mods/update-viewer.tsx index 9dc47e8f..c4d59c1c 100644 --- a/src/components/setting/mods/update-viewer.tsx +++ b/src/components/setting/mods/update-viewer.tsx @@ -12,7 +12,7 @@ import useSWR from "swr"; import { BaseDialog, DialogRef } from "@/components/base"; import { useListen } from "@/hooks/use-listen"; import { portableFlag } from "@/pages/_layout"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { useSetUpdateState, useUpdateState } from "@/services/states"; import { checkUpdateSafe as checkUpdate } from "@/services/update"; @@ -91,7 +91,7 @@ export function UpdateViewer({ ref }: { ref?: Ref }) { await updateInfo.downloadAndInstall(); await relaunch(); } catch (err: any) { - showNotice.error(createRawNotice(err?.message || err.toString())); + showNotice.error(err); } finally { setUpdateState(false); if (progressListener) { diff --git a/src/components/setting/mods/web-ui-viewer.tsx b/src/components/setting/mods/web-ui-viewer.tsx index 2ebdce78..5b917d6a 100644 --- a/src/components/setting/mods/web-ui-viewer.tsx +++ b/src/components/setting/mods/web-ui-viewer.tsx @@ -8,7 +8,7 @@ import { BaseDialog, BaseEmpty, DialogRef } from "@/components/base"; import { useClashInfo } from "@/hooks/use-clash"; import { useVerge } from "@/hooks/use-verge"; import { openWebUrl } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { WebUIItem } from "./web-ui-item"; @@ -92,7 +92,7 @@ export function WebUIViewer({ ref }: { ref?: Ref }) { await openWebUrl(url); } catch (e: any) { - showNotice.error(createRawNotice(e.message || e.toString())); + showNotice.error(e); } }); diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index 42fb798c..c2537239 100644 --- a/src/components/setting/setting-clash.tsx +++ b/src/components/setting/setting-clash.tsx @@ -11,7 +11,7 @@ import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useClash } from "@/hooks/use-clash"; import { useVerge } from "@/hooks/use-verge"; import { invoke_uwp_tool } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { useClashLog } from "@/services/states"; import getSystem from "@/utils/get-system"; @@ -71,7 +71,7 @@ const SettingClash = ({ onError }: Props) => { t("components.settings.clash.messages.geoDataUpdated"), ); } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString())); + showNotice.error(err); } }; @@ -88,7 +88,7 @@ const SettingClash = ({ onError }: Props) => { } catch (err: any) { setDnsSettingsEnabled(!enable); localStorage.setItem("dns_settings_enabled", String(!enable)); - showNotice.error(createRawNotice(err.message || err.toString())); + showNotice.error(err); await patchVerge({ enable_dns_settings: !enable }).catch(() => {}); throw err; } diff --git a/src/components/setting/setting-verge-advanced.tsx b/src/components/setting/setting-verge-advanced.tsx index c9f3afe0..46a3aafd 100644 --- a/src/components/setting/setting-verge-advanced.tsx +++ b/src/components/setting/setting-verge-advanced.tsx @@ -13,7 +13,7 @@ import { openDevTools, openLogsDir, } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { checkUpdateSafe as checkUpdate } from "@/services/update"; import { version } from "@root/package.json"; @@ -54,7 +54,7 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => { updateRef.current?.open(); } } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString())); + showNotice.error(err); } }; diff --git a/src/components/shared/ProxyControlSwitches.tsx b/src/components/shared/ProxyControlSwitches.tsx index d290f309..f7591cbc 100644 --- a/src/components/shared/ProxyControlSwitches.tsx +++ b/src/components/shared/ProxyControlSwitches.tsx @@ -21,7 +21,7 @@ import { useSystemState } from "@/hooks/use-system-state"; import { useVerge } from "@/hooks/use-verge"; import { useServiceInstaller } from "@/hooks/useServiceInstaller"; import { useServiceUninstaller } from "@/hooks/useServiceUninstaller"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; interface ProxySwitchProps { label?: string; @@ -126,7 +126,7 @@ const ProxyControlSwitches = ({ const { enable_tun_mode, enable_system_proxy } = verge ?? {}; const showErrorNotice = useCallback( - (msg: string) => showNotice.error({ i18nKey: msg }), + (msg: string) => showNotice.error(msg), [], ); @@ -145,7 +145,7 @@ const ProxyControlSwitches = ({ await installServiceAndRestartCore(); await mutateSystemState(); } catch (err) { - showNotice.error(createRawNotice((err as Error).message || String(err))); + showNotice.error(err); } }); @@ -157,7 +157,7 @@ const ProxyControlSwitches = ({ await uninstallServiceAndRestartCore(); await mutateSystemState(); } catch (err) { - showNotice.error(createRawNotice((err as Error).message || String(err))); + showNotice.error(err); } }); diff --git a/src/components/test/test-item.tsx b/src/components/test/test-item.tsx index f1bd21c6..6407cc18 100644 --- a/src/components/test/test-item.tsx +++ b/src/components/test/test-item.tsx @@ -12,7 +12,7 @@ import { BaseLoading } from "@/components/base"; import { useListen } from "@/hooks/use-listen"; import { cmdTestDelay, downloadIconCache } from "@/services/cmds"; import delayManager from "@/services/delay"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { TestBox } from "./test-box"; @@ -82,7 +82,7 @@ export const TestItem = ({ try { removeTest(uid); } catch (err: any) { - showNotice.error(createRawNotice(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 3bc6bc47..42386a4c 100644 --- a/src/components/test/test-viewer.tsx +++ b/src/components/test/test-viewer.tsx @@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next"; import { BaseDialog } from "@/components/base"; import { useVerge } from "@/hooks/use-verge"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; interface Props { onChange: (uid: string, patch?: Partial) => void; @@ -101,7 +101,7 @@ export const TestViewer = forwardRef((props, ref) => { setLoading(false); setTimeout(() => formIns.reset(), 500); } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString())); + showNotice.error(err.message || err.toString()); setLoading(false); } }), diff --git a/src/hooks/useServiceInstaller.ts b/src/hooks/useServiceInstaller.ts index f6d08809..39d56443 100644 --- a/src/hooks/useServiceInstaller.ts +++ b/src/hooks/useServiceInstaller.ts @@ -1,7 +1,7 @@ import { useCallback } from "react"; import { installService, restartCore } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { useSystemState } from "./use-system-state"; @@ -11,14 +11,14 @@ const executeWithErrorHandling = async ( successMessage?: string, ) => { try { - showNotice.info({ i18nKey: loadingMessage }); + showNotice.info(loadingMessage); await operation(); if (successMessage) { - showNotice.success({ i18nKey: successMessage }); + showNotice.success(successMessage); } } catch (err) { const msg = (err as Error)?.message || String(err); - showNotice.error(createRawNotice(msg)); + showNotice.error(msg); throw err; } }; diff --git a/src/hooks/useServiceUninstaller.ts b/src/hooks/useServiceUninstaller.ts index 7e1d7a0f..546c3f59 100644 --- a/src/hooks/useServiceUninstaller.ts +++ b/src/hooks/useServiceUninstaller.ts @@ -1,7 +1,7 @@ import { useCallback } from "react"; import { restartCore, stopCore, uninstallService } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { useSystemState } from "./use-system-state"; @@ -11,14 +11,14 @@ const executeWithErrorHandling = async ( successMessage?: string, ) => { try { - showNotice.info({ i18nKey: loadingMessage }); + showNotice.info(loadingMessage); await operation(); if (successMessage) { - showNotice.success({ i18nKey: successMessage }); + showNotice.success(successMessage); } } catch (err) { const msg = (err as Error)?.message || String(err); - showNotice.error(createRawNotice(msg)); + showNotice.error(msg); throw err; } }; diff --git a/src/pages/_layout/notificationHandlers.ts b/src/pages/_layout/notificationHandlers.ts index 378093d3..78596d81 100644 --- a/src/pages/_layout/notificationHandlers.ts +++ b/src/pages/_layout/notificationHandlers.ts @@ -1,8 +1,4 @@ -import { - createPrefixedNotice, - createRawNotice, - showNotice, -} from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; type NavigateFunction = (path: string, options?: any) => void; type TranslateFunction = (key: string) => string; @@ -16,85 +12,59 @@ export const handleNoticeMessage = ( const handlers: Record void> = { "import_sub_url::ok": () => { navigate("/profile", { state: { current: msg } }); - showNotice.success({ i18nKey: "Import Subscription Successful" }); + showNotice.success("Import Subscription Successful"); }, "import_sub_url::error": () => { navigate("/profile"); - showNotice.error(createRawNotice(msg)); + showNotice.error(msg); }, - "set_config::error": () => showNotice.error(createRawNotice(msg)), + "set_config::error": () => showNotice.error(msg), update_with_clash_proxy: () => - showNotice.success( - createPrefixedNotice(t("Update with Clash proxy successfully"), msg), - ), + showNotice.success(t("Update with Clash proxy successfully"), msg), update_retry_with_clash: () => - showNotice.info({ - i18nKey: "Update failed, retrying with Clash proxy...", - }), + showNotice.info("Update failed, retrying with Clash proxy..."), update_failed_even_with_clash: () => - showNotice.error( - createPrefixedNotice( - `${t("Update failed even with Clash proxy")}:`, - msg, - ), - ), - update_failed: () => showNotice.error(createRawNotice(msg)), + showNotice.error(`${t("Update failed even with Clash proxy")}:`, msg), + update_failed: () => showNotice.error(msg), "config_validate::boot_error": () => - showNotice.error( - createPrefixedNotice(t("Boot Config Validation Failed"), msg), - ), + showNotice.error(t("Boot Config Validation Failed"), msg), "config_validate::core_change": () => - showNotice.error( - createPrefixedNotice(t("Core Change Config Validation Failed"), msg), - ), + showNotice.error(t("Core Change Config Validation Failed"), msg), "config_validate::error": () => - showNotice.error( - createPrefixedNotice(t("Config Validation Failed"), msg), - ), + showNotice.error(t("Config Validation Failed"), msg), "config_validate::process_terminated": () => - showNotice.error({ - i18nKey: "Config Validation Process Terminated", - }), + showNotice.error("Config Validation Process Terminated"), "config_validate::stdout_error": () => - showNotice.error( - createPrefixedNotice(t("Config Validation Failed"), msg), - ), + showNotice.error(t("Config Validation Failed"), msg), "config_validate::script_error": () => - showNotice.error(createPrefixedNotice(t("Script File Error"), msg)), + showNotice.error(t("Script File Error"), msg), "config_validate::script_syntax_error": () => - showNotice.error(createPrefixedNotice(t("Script Syntax Error"), msg)), + showNotice.error(t("Script Syntax Error"), msg), "config_validate::script_missing_main": () => - showNotice.error(createPrefixedNotice(t("Script Missing Main"), msg)), + showNotice.error(t("Script Missing Main"), msg), "config_validate::file_not_found": () => - showNotice.error(createPrefixedNotice(t("File Not Found"), msg)), + showNotice.error(t("File Not Found"), msg), "config_validate::yaml_syntax_error": () => - showNotice.error(createPrefixedNotice(t("YAML Syntax Error"), msg)), + showNotice.error(t("YAML Syntax Error"), msg), "config_validate::yaml_read_error": () => - showNotice.error(createPrefixedNotice(t("YAML Read Error"), msg)), + showNotice.error(t("YAML Read Error"), msg), "config_validate::yaml_mapping_error": () => - showNotice.error(createPrefixedNotice(t("YAML Mapping Error"), msg)), + showNotice.error(t("YAML Mapping Error"), msg), "config_validate::yaml_key_error": () => - showNotice.error(createPrefixedNotice(t("YAML Key Error"), msg)), - "config_validate::yaml_error": () => - showNotice.error(createPrefixedNotice(t("YAML Error"), msg)), + showNotice.error(t("YAML Key Error"), msg), + "config_validate::yaml_error": () => showNotice.error(t("YAML Error"), msg), "config_validate::merge_syntax_error": () => - showNotice.error(createPrefixedNotice(t("Merge File Syntax Error"), msg)), + showNotice.error(t("Merge File Syntax Error"), msg), "config_validate::merge_mapping_error": () => - showNotice.error( - createPrefixedNotice(t("Merge File Mapping Error"), msg), - ), + showNotice.error(t("Merge File Mapping Error"), msg), "config_validate::merge_key_error": () => - showNotice.error(createPrefixedNotice(t("Merge File Key Error"), msg)), + showNotice.error(t("Merge File Key Error"), msg), "config_validate::merge_error": () => - showNotice.error(createPrefixedNotice(t("Merge File Error"), msg)), + showNotice.error(t("Merge File Error"), msg), "config_core::change_success": () => - showNotice.success( - createPrefixedNotice(`${t("Core Changed Successfully")}:`, msg), - ), + showNotice.success(`${t("Core Changed Successfully")}:`, msg), "config_core::change_error": () => - showNotice.error( - createPrefixedNotice(`${t("Failed to Change Core")}:`, msg), - ), + showNotice.error(`${t("Failed to Change Core")}:`, msg), }; const handler = handlers[status]; diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 0a5f7e7d..4dc7fece 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -58,11 +58,7 @@ import { reorderProfile, updateProfile, } from "@/services/cmds"; -import { - createPrefixedNotice, - createRawNotice, - showNotice, -} from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { useSetLoadingCache, useThemeMode } from "@/services/states"; // 记录profile切换状态 @@ -150,10 +146,8 @@ const ProfilePage = () => { setActivatings((prev) => prev.filter((id) => id !== previousSwitching)); showNotice.info( - createPrefixedNotice( - t("pages.profiles.notifications.switchInterrupted"), - `${previousSwitching} → ${newProfile}`, - ), + t("pages.profiles.notifications.switchInterrupted"), + `${previousSwitching} → ${newProfile}`, 3000, ); }, @@ -197,9 +191,7 @@ const ProfilePage = () => { for (const file of paths) { if (!file.endsWith(".yaml") && !file.endsWith(".yml")) { - showNotice.error({ - i18nKey: "pages.profiles.errors.onlyYaml", - }); + showNotice.error("pages.profiles.errors.onlyYaml"); continue; } const item = { @@ -247,18 +239,13 @@ const ProfilePage = () => { await new Promise((resolve) => setTimeout(resolve, 500)); await onEnhance(false); - showNotice.success( - { i18nKey: "pages.profiles.notices.forceRefreshCompleted" }, - 2000, - ); + showNotice.success("pages.profiles.notices.forceRefreshCompleted", 2000); } catch (error: any) { console.error("[紧急刷新] 失败:", error); const message = error?.message || String(error); showNotice.error( - { - i18nKey: "pages.profiles.notices.emergencyRefreshFailed", - params: { message }, - }, + "pages.profiles.notices.emergencyRefreshFailed", + { message }, 4000, ); } @@ -289,15 +276,13 @@ const ProfilePage = () => { if (!url) return; // 校验url是否为http/https if (!/^https?:\/\//i.test(url)) { - showNotice.error({ - i18nKey: "pages.profiles.errors.invalidUrl", - }); + showNotice.error("pages.profiles.errors.invalidUrl"); return; } setLoading(true); const handleImportSuccess = async (noticeKey: string) => { - showNotice.success({ i18nKey: noticeKey }); + showNotice.success(noticeKey); setUrl(""); await performRobustRefresh(); }; @@ -309,9 +294,7 @@ const ProfilePage = () => { } catch (initialErr) { console.warn("[订阅导入] 首次导入失败:", initialErr); - showNotice.info({ - i18nKey: "pages.profiles.notifications.importRetry", - }); + showNotice.info("pages.profiles.notifications.importRetry"); try { // 使用自身代理尝试导入 await importProfile(url, { @@ -527,7 +510,7 @@ const ProfilePage = () => { } console.error(`[Profile] 切换失败:`, err); - showNotice.error(createRawNotice(err?.message || err.toString()), 4000); + showNotice.error(err, 4000); } finally { // 只有当前profile仍然是正在切换的profile且序列号匹配时才清理状态 if ( @@ -599,7 +582,7 @@ const ProfilePage = () => { ); } } catch (err: any) { - showNotice.error(createRawNotice(err.message || err.toString()), 3000); + showNotice.error(err, 3000); } finally { // 保留正在切换的profile,清除其他状态 setActivatings((prev) => @@ -619,7 +602,7 @@ const ProfilePage = () => { await onEnhance(false); } } catch (err: any) { - showNotice.error(createRawNotice(err?.message || err.toString())); + showNotice.error(err); } finally { setActivatings([]); } @@ -739,7 +722,7 @@ const ProfilePage = () => { i18nKey: "pages.profiles.notifications.batchDeleted", }); } catch (err: any) { - showNotice.error(createRawNotice(err?.message || err.toString())); + showNotice.error(err); } finally { setActivatings([]); } diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 58507b25..7392b27b 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -9,14 +9,14 @@ import SettingSystem from "@/components/setting/setting-system"; import SettingVergeAdvanced from "@/components/setting/setting-verge-advanced"; import SettingVergeBasic from "@/components/setting/setting-verge-basic"; import { openWebUrl } from "@/services/cmds"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; const SettingPage = () => { const { t } = useTranslation(); const onError = (err: any) => { - showNotice.error(createRawNotice(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 a518b77f..738b16a8 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -2,7 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import dayjs from "dayjs"; import { getProxies, getProxyProviders } from "tauri-plugin-mihomo-api"; -import { createRawNotice, showNotice } from "@/services/noticeService"; +import { showNotice } from "@/services/noticeService"; export async function copyClashEnv() { return invoke("copy_clash_env"); @@ -314,28 +314,22 @@ export async function getAppDir() { } export async function openAppDir() { - return invoke("open_app_dir").catch((err) => - showNotice.error(createRawNotice(err?.message || err.toString())), - ); + return invoke("open_app_dir").catch((err) => showNotice.error(err)); } export async function openCoreDir() { - return invoke("open_core_dir").catch((err) => - showNotice.error(createRawNotice(err?.message || err.toString())), - ); + return invoke("open_core_dir").catch((err) => showNotice.error(err)); } export async function openLogsDir() { - return invoke("open_logs_dir").catch((err) => - showNotice.error(createRawNotice(err?.message || err.toString())), - ); + return invoke("open_logs_dir").catch((err) => showNotice.error(err)); } export const openWebUrl = async (url: string) => { try { await invoke("open_web_url", { url }); } catch (err: any) { - showNotice.error(createRawNotice(err.toString())); + showNotice.error(err); } }; @@ -377,7 +371,7 @@ export async function cmdTestDelay(url: string) { export async function invoke_uwp_tool() { return invoke("invoke_uwp_tool").catch((err) => - showNotice.error(createRawNotice(err?.message || err.toString()), 1500), + showNotice.error(err, 1500), ); } diff --git a/src/services/noticeService.ts b/src/services/noticeService.ts index f764689a..f86c5b46 100644 --- a/src/services/noticeService.ts +++ b/src/services/noticeService.ts @@ -1,4 +1,5 @@ -import { ReactNode } from "react"; +import i18n from "i18next"; +import { ReactNode, isValidElement } from "react"; type NoticeType = "success" | "error" | "info"; @@ -6,16 +7,13 @@ type NoticeType = "success" | "error" | "info"; * Descriptor used when the notice content should be resolved through i18n. */ export interface NoticeTranslationDescriptor { - i18nKey: string; + key: string; params?: Record; - fallback?: string; } /** - * Notification payload that either renders raw React content or defers to i18n. + * Runtime representation of a notice entry queued for rendering. */ -type NoticeMessage = ReactNode | NoticeTranslationDescriptor; - interface NoticeItem { readonly id: number; readonly type: NoticeType; @@ -25,12 +23,19 @@ interface NoticeItem { timerId?: ReturnType; } -type NoticeShortcut = (message: NoticeMessage, duration?: number) => number; +type NoticeContent = unknown; + +type NoticeExtra = unknown; + +type NoticeShortcut = ( + message: NoticeContent, + ...extras: NoticeExtra[] +) => number; type ShowNotice = (( type: NoticeType, - message: NoticeMessage, - duration?: number, + message: NoticeContent, + ...extras: NoticeExtra[] ) => number) & { success: NoticeShortcut; error: NoticeShortcut; @@ -53,6 +58,54 @@ function notifySubscribers() { subscribers.forEach((subscriber) => subscriber()); } +interface ParsedNoticeExtras { + params?: Record; + raw?: unknown; + duration?: number; +} + +function parseNoticeExtras(extras: NoticeExtra[]): ParsedNoticeExtras { + let params: Record | undefined; + let raw: unknown; + let duration: number | undefined; + + for (const extra of extras) { + if (extra === undefined) continue; + + if (typeof extra === "number" && duration === undefined) { + duration = extra; + continue; + } + + if (isPlainRecord(extra)) { + if (!params) { + params = extra; + continue; + } + if (!raw) { + raw = extra; + continue; + } + } + + if (!raw) { + raw = extra; + continue; + } + + if (!params && isPlainRecord(extra)) { + params = extra; + continue; + } + + if (duration === undefined && typeof extra === "number") { + duration = extra; + } + } + + return { params, raw, duration }; +} + function resolveDuration(type: NoticeType, override?: number) { return override ?? DEFAULT_DURATIONS[type]; } @@ -60,44 +113,201 @@ function resolveDuration(type: NoticeType, override?: number) { function buildNotice( id: number, type: NoticeType, - message: NoticeMessage, duration: number, + payload: { message?: ReactNode; i18n?: NoticeTranslationDescriptor }, timerId?: ReturnType, ): NoticeItem { - if (isTranslationDescriptor(message)) { - return { - id, - type, - duration, - timerId, - i18n: message, - }; - } - return { id, type, duration, timerId, - message, + ...payload, }; } +function isMaybeTranslationDescriptor( + message: unknown, +): message is NoticeTranslationDescriptor { + if ( + typeof message === "object" && + message !== null && + !Array.isArray(message) && + !isValidElement(message) + ) { + return typeof (message as Record).key === "string"; + } + return false; +} + +function isPlainRecord(value: unknown): value is Record { + if ( + typeof value !== "object" || + value === null || + Array.isArray(value) || + value instanceof Error || + isValidElement(value) + ) { + return false; + } + + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} + +function createRawDescriptor(message: string): NoticeTranslationDescriptor { + return { + key: "common.notices.raw", + params: { message }, + }; +} + +function shouldUseTranslationKey( + key: string, + params?: Record, +) { + if (params && Object.keys(params).length > 0) return true; + if (i18n.isInitialized) { + return i18n.exists(key); + } + return false; +} + +function extractDisplayText(input: unknown): string | undefined { + if (input === null || input === undefined) return undefined; + if (typeof input === "string") return input; + if (typeof input === "number" || typeof input === "boolean") { + return String(input); + } + if (input instanceof Error) { + return input.message || input.name; + } + if ( + typeof input === "object" && + input !== null && + "message" in input && + typeof (input as { message?: unknown }).message === "string" + ) { + return (input as { message?: string }).message; + } + try { + return JSON.stringify(input); + } catch { + return String(input); + } +} + +function normalizeNoticeMessage( + message: NoticeContent, + params?: Record, + raw?: unknown, +): { message?: ReactNode; i18n?: NoticeTranslationDescriptor } { + const rawText = raw !== undefined ? extractDisplayText(raw) : undefined; + + if (isValidElement(message)) { + return { message }; + } + + if (isMaybeTranslationDescriptor(message)) { + const originalParams = message.params ?? {}; + const mergedParams = Object.keys(params ?? {}).length + ? { ...originalParams, ...params } + : { ...originalParams }; + + if (rawText !== undefined) { + return { + i18n: { + key: "common.notices.prefixedRaw", + params: { + ...mergedParams, + prefixKey: message.key, + prefixParams: originalParams, + message: rawText, + }, + }, + }; + } + + return { + i18n: { + key: message.key, + params: Object.keys(mergedParams).length ? mergedParams : undefined, + }, + }; + } + + if (typeof message === "string") { + if (rawText !== undefined) { + if (shouldUseTranslationKey(message, params)) { + return { + i18n: { + key: "common.notices.prefixedRaw", + params: { + ...(params ?? {}), + prefixKey: message, + message: rawText, + }, + }, + }; + } + return { + i18n: { + key: "common.notices.prefixedRaw", + params: { + ...(params ?? {}), + prefix: message, + message: rawText, + }, + }, + }; + } + + if (shouldUseTranslationKey(message, params)) { + return { + i18n: { + key: message, + params: params && Object.keys(params).length ? params : undefined, + }, + }; + } + return { i18n: createRawDescriptor(message) }; + } + + if (rawText !== undefined) { + return { i18n: createRawDescriptor(rawText) }; + } + + const extracted = extractDisplayText(message); + if (extracted !== undefined) { + return { i18n: createRawDescriptor(extracted) }; + } + + return { i18n: createRawDescriptor("") }; +} + /** * Imperative entry point for users to display new notices. */ const baseShowNotice = ( type: NoticeType, - message: NoticeMessage, - duration?: number, + message: NoticeContent, + ...extras: NoticeExtra[] ): number => { const id = nextId++; + const { params, raw, duration } = parseNoticeExtras(extras); const effectiveDuration = resolveDuration(type, duration); const timerId = effectiveDuration > 0 ? setTimeout(() => hideNotice(id), effectiveDuration) : undefined; - const notice = buildNotice(id, type, message, effectiveDuration, timerId); + const normalizedMessage = normalizeNoticeMessage(message, params, raw); + const notice = buildNotice( + id, + type, + effectiveDuration, + normalizedMessage, + timerId, + ); notices = [...notices, notice]; notifySubscribers(); @@ -105,31 +315,12 @@ const baseShowNotice = ( }; export const showNotice: ShowNotice = Object.assign(baseShowNotice, { - success: (message: NoticeMessage, duration?: number) => - baseShowNotice("success", message, duration), - error: (message: NoticeMessage, duration?: number) => - baseShowNotice("error", message, duration), - info: (message: NoticeMessage, duration?: number) => - baseShowNotice("info", message, duration), -}); - -export const createRawNotice = ( - message: string, - fallback?: string, -): NoticeTranslationDescriptor => ({ - i18nKey: "common.notices.raw", - params: { message }, - fallback: fallback ?? message, -}); - -export const createPrefixedNotice = ( - prefix: string, - message: string, - fallback?: string, -): NoticeTranslationDescriptor => ({ - i18nKey: "common.notices.prefixedRaw", - params: { prefix, message }, - fallback: fallback ?? `${prefix} ${message}`, + success: (message: NoticeContent, ...extras: NoticeExtra[]) => + baseShowNotice("success", message, ...extras), + error: (message: NoticeContent, ...extras: NoticeExtra[]) => + baseShowNotice("error", message, ...extras), + info: (message: NoticeContent, ...extras: NoticeExtra[]) => + baseShowNotice("info", message, ...extras), }); export function hideNotice(id: number) { @@ -159,17 +350,3 @@ export function clearAllNotices() { notices = []; notifySubscribers(); } - -function isTranslationDescriptor( - message: NoticeMessage, -): message is NoticeTranslationDescriptor { - if ( - typeof message === "object" && - message !== null && - Object.prototype.hasOwnProperty.call(message, "i18nKey") - ) { - const descriptor = message as NoticeTranslationDescriptor; - return typeof descriptor.i18nKey === "string"; - } - return false; -}