diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 2ce804d..5a00f49 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -147,4 +147,49 @@ text-overflow: ellipsis; white-space: nowrap; overflow: hidden; +} + +/* Toast */ +.toast-enter { + animation: toast-in 0.2s ease-out forwards; +} + +.toast-exit { + animation: toast-out 0.15s ease-in forwards; +} + +@keyframes toast-in { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes toast-out { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(20px); + } +} + +.toast-progress { + animation: progress-shrink linear forwards; + width: 100%; +} + +@keyframes progress-shrink { + from { + width: 100%; + } + to { + width: 0%; + } } \ No newline at end of file diff --git a/src/renderer/src/components/base/toast.tsx b/src/renderer/src/components/base/toast.tsx new file mode 100644 index 0000000..f985bdb --- /dev/null +++ b/src/renderer/src/components/base/toast.tsx @@ -0,0 +1,157 @@ +import React, { useEffect, useState, useCallback } from 'react' +import { createPortal } from 'react-dom' +import { IoCheckmark, IoClose, IoAlertSharp, IoInformationSharp } from 'react-icons/io5' + +type ToastType = 'success' | 'error' | 'warning' | 'info' + +interface ToastData { + id: string + type: ToastType + title?: string + message: string + duration?: number + exiting?: boolean +} + +type ToastListener = (toasts: ToastData[]) => void + +let toasts: ToastData[] = [] +let listeners: ToastListener[] = [] + +const notifyListeners = (): void => { + listeners.forEach((listener) => listener([...toasts])) +} + +const addToast = (type: ToastType, message: string, title?: string, duration = 1500): void => { + const id = `${Date.now()}-${Math.random().toString(36).slice(2)}` + toasts = [...toasts.slice(-4), { id, type, message, title, duration }] + notifyListeners() +} + +const markExiting = (id: string): void => { + toasts = toasts.map((t) => (t.id === id ? { ...t, exiting: true } : t)) + notifyListeners() +} + +const removeToast = (id: string): void => { + toasts = toasts.filter((t) => t.id !== id) + notifyListeners() +} + +export const toast = { + success: (message: string, title?: string): void => addToast('success', message, title), + error: (message: string, title?: string): void => addToast('error', message, title, 1800), + warning: (message: string, title?: string): void => addToast('warning', message, title), + info: (message: string, title?: string): void => addToast('info', message, title) +} + +const ToastItem: React.FC<{ + data: ToastData + onRemove: (id: string) => void +}> = ({ data, onRemove }) => { + useEffect(() => { + const duration = data.duration || 3500 + const exitTimer = setTimeout(() => markExiting(data.id), duration - 200) + const removeTimer = setTimeout(() => onRemove(data.id), duration) + return () => { + clearTimeout(exitTimer) + clearTimeout(removeTimer) + } + }, [data.id, data.duration, onRemove]) + + const theme: Record = { + success: { + icon: , + bg: 'bg-content1', + iconBg: 'bg-success' + }, + error: { + icon: , + bg: 'bg-content1', + iconBg: 'bg-danger' + }, + warning: { + icon: , + bg: 'bg-content1', + iconBg: 'bg-warning' + }, + info: { + icon: , + bg: 'bg-content1', + iconBg: 'bg-primary' + } + } + + const { icon, iconBg } = theme[data.type] + const duration = data.duration || 3500 + + return ( +
+
+ {icon} +
+
+ {data.title && ( +

{data.title}

+ )} +

+ {data.message} +

+
+ +
+
+ ) +} + +export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [currentToasts, setCurrentToasts] = useState([]) + + const handleRemove = useCallback((id: string) => { + removeToast(id) + }, []) + + useEffect(() => { + const listener: ToastListener = (newToasts) => setCurrentToasts(newToasts) + listeners.push(listener) + return () => { + listeners = listeners.filter((l) => l !== listener) + } + }, []) + + return ( + <> + {children} + {currentToasts.length > 0 && + createPortal( +
+ {currentToasts.map((t) => ( + + ))} +
, + document.body + )} + + ) +} diff --git a/src/renderer/src/components/override/edit-file-modal.tsx b/src/renderer/src/components/override/edit-file-modal.tsx index 5f51284..d5b949e 100644 --- a/src/renderer/src/components/override/edit-file-modal.tsx +++ b/src/renderer/src/components/override/edit-file-modal.tsx @@ -1,4 +1,5 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react' +import { toast } from '@renderer/components/base/toast' import React, { useEffect, useState } from 'react' import { BaseEditor } from '../base/base-editor' import { getOverride, restartCore, setOverride } from '@renderer/utils/ipc' @@ -58,7 +59,7 @@ const EditFileModal: React.FC = (props) => { await restartCore() onClose() } catch (e) { - alert(e) + toast.error(String(e)) } }} > diff --git a/src/renderer/src/components/override/override-item.tsx b/src/renderer/src/components/override/override-item.tsx index 500de82..708ac76 100644 --- a/src/renderer/src/components/override/override-item.tsx +++ b/src/renderer/src/components/override/override-item.tsx @@ -8,6 +8,7 @@ import { DropdownMenu, DropdownTrigger } from '@heroui/react' +import { toast } from '@renderer/components/base/toast' import { IoMdMore, IoMdRefresh } from 'react-icons/io' import dayjs from '@renderer/utils/dayjs' import React, { Key, useMemo, useState } from 'react' @@ -197,7 +198,7 @@ const OverrideItem: React.FC = (props) => { await addOverrideItem(info) await restartCore() } catch (e) { - alert(e) + toast.error(String(e)) } finally { setUpdating(false) } diff --git a/src/renderer/src/components/profiles/edit-info-modal.tsx b/src/renderer/src/components/profiles/edit-info-modal.tsx index 354afa3..3caf3ff 100644 --- a/src/renderer/src/components/profiles/edit-info-modal.tsx +++ b/src/renderer/src/components/profiles/edit-info-modal.tsx @@ -13,6 +13,7 @@ import { DropdownMenu, DropdownItem } from '@heroui/react' +import { toast } from '@renderer/components/base/toast' import React, { useState } from 'react' import SettingItem from '../base/base-setting-item' import { useOverrideConfig } from '@renderer/hooks/use-override-config' @@ -49,7 +50,7 @@ const EditInfoModal: React.FC = (props) => { await restartCore() onClose() } catch (e) { - alert(e) + toast.error(String(e)) } } diff --git a/src/renderer/src/components/profiles/edit-rules-modal.tsx b/src/renderer/src/components/profiles/edit-rules-modal.tsx index 2387c54..f5a9f31 100644 --- a/src/renderer/src/components/profiles/edit-rules-modal.tsx +++ b/src/renderer/src/components/profiles/edit-rules-modal.tsx @@ -22,6 +22,7 @@ import yaml from 'js-yaml' import { IoMdTrash, IoMdArrowUp, IoMdArrowDown, IoMdUndo } from 'react-icons/io' import { MdVerticalAlignTop, MdVerticalAlignBottom } from 'react-icons/md' import { platform } from '@renderer/utils/init' +import { toast } from '@renderer/components/base/toast' interface Props { id: string @@ -642,7 +643,7 @@ const EditRulesModal: React.FC = (props) => { await setRuleStr(id, ruleYaml); onClose(); } catch (e) { - alert(t('profiles.editRules.saveError') + ': ' + (e instanceof Error ? e.message : String(e))); + toast.error(t('profiles.editRules.saveError') + ': ' + (e instanceof Error ? e.message : String(e))); } }, [prependRules, deletedRules, rules, appendRules, id, onClose, t]) @@ -688,7 +689,7 @@ const EditRulesModal: React.FC = (props) => { } if (newRule.type !== 'MATCH' && newRule.payload.trim() !== '' && !validateRulePayload(newRule.type, newRule.payload)) { - alert(t('profiles.editRules.invalidPayload') + ': ' + getRuleExample(newRule.type)); + toast.error(t('profiles.editRules.invalidPayload') + ': ' + getRuleExample(newRule.type)); return; } diff --git a/src/renderer/src/components/resources/geo-data.tsx b/src/renderer/src/components/resources/geo-data.tsx index 34d3b36..b30cff0 100644 --- a/src/renderer/src/components/resources/geo-data.tsx +++ b/src/renderer/src/components/resources/geo-data.tsx @@ -1,4 +1,5 @@ import { Button, Input, Switch, Tab, Tabs } from '@heroui/react' +import { toast } from '@renderer/components/base/toast' import SettingCard from '@renderer/components/base/base-setting-card' import SettingItem from '@renderer/components/base/base-setting-item' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' @@ -123,7 +124,7 @@ const GeoData: React.FC = () => { await mihomoUpgradeGeo() new Notification(t('resources.geoData.updateSuccess')) } catch (e) { - alert(e) + toast.error(String(e)) } finally { setUpdating(false) } diff --git a/src/renderer/src/components/resources/proxy-provider.tsx b/src/renderer/src/components/resources/proxy-provider.tsx index f970014..9bf9058 100644 --- a/src/renderer/src/components/resources/proxy-provider.tsx +++ b/src/renderer/src/components/resources/proxy-provider.tsx @@ -9,6 +9,7 @@ import useSWR from 'swr' import SettingCard from '../base/base-setting-card' import SettingItem from '../base/base-setting-item' import { Button, Chip } from '@heroui/react' +import { toast } from '@renderer/components/base/toast' import { IoMdRefresh } from 'react-icons/io' import { CgLoadbarDoc } from 'react-icons/cg' import { MdEditDocument } from 'react-icons/md' @@ -67,7 +68,7 @@ const ProxyProvider: React.FC = () => { await mihomoUpdateProxyProviders(name) mutate() } catch (e) { - alert(e) + toast.error(String(e)) } finally { setUpdating((prev) => { prev[index] = false diff --git a/src/renderer/src/components/resources/rule-provider.tsx b/src/renderer/src/components/resources/rule-provider.tsx index 21604ee..2656cfe 100644 --- a/src/renderer/src/components/resources/rule-provider.tsx +++ b/src/renderer/src/components/resources/rule-provider.tsx @@ -10,6 +10,7 @@ import useSWR from 'swr' import SettingCard from '../base/base-setting-card' import SettingItem from '../base/base-setting-item' import { Button, Chip } from '@heroui/react' +import { toast } from '@renderer/components/base/toast' import { IoMdRefresh } from 'react-icons/io' import { CgLoadbarDoc } from 'react-icons/cg' import { MdEditDocument } from 'react-icons/md' @@ -71,7 +72,7 @@ const RuleProvider: React.FC = () => { await mihomoUpdateRuleProviders(name) mutate() } catch (e) { - alert(e) + toast.error(String(e)) } finally { setUpdating((prev) => { prev[index] = false diff --git a/src/renderer/src/components/settings/actions.tsx b/src/renderer/src/components/settings/actions.tsx index b5dedf1..379293a 100644 --- a/src/renderer/src/components/settings/actions.tsx +++ b/src/renderer/src/components/settings/actions.tsx @@ -1,5 +1,6 @@ import { Button, Tooltip } from '@heroui/react' import SettingCard from '../base/base-setting-card' +import { toast } from '@renderer/components/base/toast' import SettingItem from '../base/base-setting-item' import { checkUpdate, @@ -69,7 +70,7 @@ const Actions: React.FC = () => { }) } } catch (e) { - alert(e) + toast.error(String(e)) } finally { setCheckingUpdate(false) } diff --git a/src/renderer/src/components/settings/general-config.tsx b/src/renderer/src/components/settings/general-config.tsx index dfc193e..92b7a18 100644 --- a/src/renderer/src/components/settings/general-config.tsx +++ b/src/renderer/src/components/settings/general-config.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react' import SettingCard from '../base/base-setting-card' +import { toast } from '@renderer/components/base/toast' import SettingItem from '../base/base-setting-item' import { Button, Input, Select, SelectItem, Switch, Tab, Tabs, Tooltip } from '@heroui/react' import { BiCopy, BiSolidFileImport } from 'react-icons/bi' @@ -105,7 +106,7 @@ const GeneralConfig: React.FC = () => { await patchAppConfig({ disableHardwareAcceleration: pendingHardwareAccelValue }) await relaunchApp() } catch (e) { - alert(e) + toast.error(String(e)) setIsRelaunching(false) } }} @@ -151,7 +152,7 @@ const GeneralConfig: React.FC = () => { await disableAutoRun() } } catch (e) { - alert(e) + toast.error(String(e)) } finally { mutateEnable() } @@ -248,7 +249,7 @@ const GeneralConfig: React.FC = () => { envType: Array.from(v) as ('bash' | 'cmd' | 'powershell')[] }) } catch (e) { - alert(e) + toast.error(String(e)) } }} > @@ -413,7 +414,7 @@ const GeneralConfig: React.FC = () => { await patchAppConfig({ useWindowFrame: v }) await relaunchApp() } catch (e) { - alert(e) + toast.error(String(e)) setIsRelaunching(false) } }, 1000)} @@ -503,7 +504,7 @@ const GeneralConfig: React.FC = () => { await fetchThemes() setCustomThemes(await resolveThemes()) } catch (e) { - alert(e) + toast.error(String(e)) } finally { setFetching(false) } @@ -523,7 +524,7 @@ const GeneralConfig: React.FC = () => { await importThemes(files) setCustomThemes(await resolveThemes()) } catch (e) { - alert(e) + toast.error(String(e)) } }} > @@ -555,7 +556,7 @@ const GeneralConfig: React.FC = () => { try { await patchAppConfig({ customTheme: v.currentKey as string }) } catch (e) { - alert(e) + toast.error(String(e)) } }} > diff --git a/src/renderer/src/components/settings/local-backup-config.tsx b/src/renderer/src/components/settings/local-backup-config.tsx index 9cb2915..bd4fe4d 100644 --- a/src/renderer/src/components/settings/local-backup-config.tsx +++ b/src/renderer/src/components/settings/local-backup-config.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' import SettingCard from '../base/base-setting-card' +import { toast } from '@renderer/components/base/toast' import SettingItem from '../base/base-setting-item' import { Button, useDisclosure } from '@heroui/react' import { exportLocalBackup, importLocalBackup } from '@renderer/utils/ipc' @@ -22,7 +23,7 @@ const LocalBackupConfig: React.FC = () => { }) } } catch (e) { - alert(e) + toast.error(String(e)) } finally { setExporting(false) } @@ -38,7 +39,7 @@ const LocalBackupConfig: React.FC = () => { }) } } catch (e) { - alert(t('common.error.importFailed', { error: e })) + toast.error(t('common.error.importFailed', { error: e })) } finally { setImporting(false) onClose() diff --git a/src/renderer/src/components/settings/mihomo-config.tsx b/src/renderer/src/components/settings/mihomo-config.tsx index 239ba5b..9b1020b 100644 --- a/src/renderer/src/components/settings/mihomo-config.tsx +++ b/src/renderer/src/components/settings/mihomo-config.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' import SettingCard from '../base/base-setting-card' +import { toast } from '@renderer/components/base/toast' import SettingItem from '../base/base-setting-item' import { Button, Input, Select, SelectItem, Switch, Tooltip } from '@heroui/react' import { useAppConfig } from '@renderer/hooks/use-app-config' @@ -122,7 +123,7 @@ const MihomoConfig: React.FC = () => { await navigator.clipboard.writeText(`${url}/raw/clash-party.yaml`) } } catch (e) { - alert(e) + toast.error(String(e)) } }} > @@ -176,7 +177,7 @@ const MihomoConfig: React.FC = () => { }) await restartCore() } catch (e) { - alert(e) + toast.error(String(e)) } }} > @@ -208,7 +209,7 @@ const MihomoConfig: React.FC = () => { await patchAppConfig({ diffWorkDir: v }) await restartCore() } catch (e) { - alert(e) + toast.error(String(e)) } }} /> diff --git a/src/renderer/src/components/settings/shortcut-config.tsx b/src/renderer/src/components/settings/shortcut-config.tsx index c5e2404..2e3286b 100644 --- a/src/renderer/src/components/settings/shortcut-config.tsx +++ b/src/renderer/src/components/settings/shortcut-config.tsx @@ -1,5 +1,6 @@ import { Button, Input } from '@heroui/react' import SettingCard from '../base/base-setting-card' +import { toast } from '@renderer/components/base/toast' import SettingItem from '../base/base-setting-item' import { useAppConfig } from '@renderer/hooks/use-app-config' import React, { KeyboardEvent, useState } from 'react' @@ -213,10 +214,10 @@ const ShortcutInput: React.FC<{ await patchAppConfig({ [action]: inputValue }) window.electron.ipcRenderer.send('updateTrayMenu') } else { - alert(t('common.error.shortcutRegistrationFailed')) + toast.error(t('common.error.shortcutRegistrationFailed')) } } catch (e) { - alert(t('common.error.shortcutRegistrationFailedWithError', { error: e })) + toast.error(t('common.error.shortcutRegistrationFailedWithError', { error: e })) } }} > diff --git a/src/renderer/src/components/settings/substore-config.tsx b/src/renderer/src/components/settings/substore-config.tsx index 61e3d2d..7ad8477 100644 --- a/src/renderer/src/components/settings/substore-config.tsx +++ b/src/renderer/src/components/settings/substore-config.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' import SettingCard from '@renderer/components/base/base-setting-card' +import { toast } from '@renderer/components/base/toast' import SettingItem from '@renderer/components/base/base-setting-item' import { Button, Input, Switch } from '@heroui/react' import { @@ -55,7 +56,7 @@ const SubStoreConfig: React.FC = () => { await stopSubStoreBackendServer() } } catch (e) { - alert(e) + toast.error(String(e)) } }} /> @@ -76,7 +77,7 @@ const SubStoreConfig: React.FC = () => { await startSubStoreFrontendServer() await startSubStoreBackendServer() } catch (e) { - alert(e) + toast.error(String(e)) } }} /> @@ -94,7 +95,7 @@ const SubStoreConfig: React.FC = () => { await startSubStoreBackendServer() } } catch (e) { - alert(e) + toast.error(String(e)) } }} /> @@ -123,7 +124,7 @@ const SubStoreConfig: React.FC = () => { await patchAppConfig({ useProxyInSubStore: v }) await startSubStoreBackendServer() } catch (e) { - alert(e) + toast.error(String(e)) } }} /> @@ -144,7 +145,7 @@ const SubStoreConfig: React.FC = () => { }) new Notification(t('common.notification.restartRequired')) } else { - alert(t('common.error.invalidCron')) + toast.warning(t('common.error.invalidCron')) } }} > @@ -177,7 +178,7 @@ const SubStoreConfig: React.FC = () => { }) new Notification(t('common.notification.restartRequired')) } else { - alert(t('common.error.invalidCron')) + toast.warning(t('common.error.invalidCron')) } }} > @@ -210,7 +211,7 @@ const SubStoreConfig: React.FC = () => { }) new Notification(t('common.notification.restartRequired')) } else { - alert(t('common.error.invalidCron')) + toast.warning(t('common.error.invalidCron')) } }} > diff --git a/src/renderer/src/components/settings/webdav-config.tsx b/src/renderer/src/components/settings/webdav-config.tsx index 6575827..5c28785 100644 --- a/src/renderer/src/components/settings/webdav-config.tsx +++ b/src/renderer/src/components/settings/webdav-config.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' import SettingCard from '../base/base-setting-card' +import { toast } from '@renderer/components/base/toast' import SettingItem from '../base/base-setting-item' import { Button, Input, Select, SelectItem } from '@heroui/react' import { listWebdavBackups, webdavBackup, reinitWebdavBackupScheduler } from '@renderer/utils/ipc' @@ -47,7 +48,7 @@ const WebdavConfig: React.FC = () => { body: t('webdav.notification.backupSuccess.body') }) } catch (e) { - alert(e) + toast.error(String(e)) } finally { setBackuping(false) } @@ -60,7 +61,7 @@ const WebdavConfig: React.FC = () => { setFilenames(filenames) setRestoreOpen(true) } catch (e) { - alert(t('common.error.getBackupListFailed', { error: e })) + toast.error(t('common.error.getBackupListFailed', { error: e })) } finally { setRestoring(false) } @@ -156,7 +157,7 @@ const WebdavConfig: React.FC = () => { new Notification(t('webdav.notification.cronUpdateFailed')) } } else { - alert(t('common.error.invalidCron')) + toast.warning(t('common.error.invalidCron')) } }} > diff --git a/src/renderer/src/components/settings/webdav-restore-modal.tsx b/src/renderer/src/components/settings/webdav-restore-modal.tsx index 787eafc..23fb322 100644 --- a/src/renderer/src/components/settings/webdav-restore-modal.tsx +++ b/src/renderer/src/components/settings/webdav-restore-modal.tsx @@ -1,4 +1,5 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react' +import { toast } from '@renderer/components/base/toast' import { relaunchApp, webdavDelete, webdavRestore } from '@renderer/utils/ipc' import React, { useState } from 'react' import { MdDeleteForever } from 'react-icons/md' @@ -43,7 +44,7 @@ const WebdavRestoreModal: React.FC = (props) => { await webdavRestore(filename) await relaunchApp() } catch (e) { - alert(t('common.error.restoreFailed', { error: e })) + toast.error(t('common.error.restoreFailed', { error: e })) } finally { setRestoring(false) } @@ -61,7 +62,7 @@ const WebdavRestoreModal: React.FC = (props) => { await webdavDelete(filename) setFilenames(filenames.filter((name) => name !== filename)) } catch (e) { - alert(t('common.error.deleteFailed', { error: e })) + toast.error(t('common.error.deleteFailed', { error: e })) } }} > diff --git a/src/renderer/src/components/sider/dns-card.tsx b/src/renderer/src/components/sider/dns-card.tsx index a11b35d..06a5691 100644 --- a/src/renderer/src/components/sider/dns-card.tsx +++ b/src/renderer/src/components/sider/dns-card.tsx @@ -1,4 +1,5 @@ import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react' +import { toast } from '@renderer/components/base/toast' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' import BorderSwitch from '@renderer/components/base/border-swtich' import { LuServer } from 'react-icons/lu' @@ -39,7 +40,7 @@ const DNSCard: React.FC = (props) => { await patchControledMihomoConfig({}) await restartCore() } catch (e) { - alert(e) + toast.error(String(e)) } } diff --git a/src/renderer/src/components/sider/mihomo-core-card.tsx b/src/renderer/src/components/sider/mihomo-core-card.tsx index 2f1d898..dea4d9f 100644 --- a/src/renderer/src/components/sider/mihomo-core-card.tsx +++ b/src/renderer/src/components/sider/mihomo-core-card.tsx @@ -1,4 +1,5 @@ import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react' +import { toast } from '@renderer/components/base/toast' import { calcTraffic } from '@renderer/utils/calc' import { mihomoVersion, restartCore } from '@renderer/utils/ipc' import React, { useEffect, useState } from 'react' @@ -112,7 +113,7 @@ const MihomoCoreCard: React.FC = (props) => { try { await restartCore() } catch (e) { - alert(e) + toast.error(String(e)) } finally { mutate() } diff --git a/src/renderer/src/components/sider/sniff-card.tsx b/src/renderer/src/components/sider/sniff-card.tsx index 4bd4ef4..51dc805 100644 --- a/src/renderer/src/components/sider/sniff-card.tsx +++ b/src/renderer/src/components/sider/sniff-card.tsx @@ -1,4 +1,5 @@ import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react' +import { toast } from '@renderer/components/base/toast' import BorderSwitch from '@renderer/components/base/border-swtich' import { RiScan2Fill } from 'react-icons/ri' import { useLocation, useNavigate } from 'react-router-dom' @@ -39,7 +40,7 @@ const SniffCard: React.FC = (props) => { await patchControledMihomoConfig({}) await restartCore() } catch (e) { - alert(e) + toast.error(String(e)) } } diff --git a/src/renderer/src/components/sider/sysproxy-switcher.tsx b/src/renderer/src/components/sider/sysproxy-switcher.tsx index 25490b0..d44d059 100644 --- a/src/renderer/src/components/sider/sysproxy-switcher.tsx +++ b/src/renderer/src/components/sider/sysproxy-switcher.tsx @@ -1,4 +1,5 @@ import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react' +import { toast } from '@renderer/components/base/toast' import BorderSwitch from '@renderer/components/base/border-swtich' import { useLocation, useNavigate } from 'react-router-dom' import { useAppConfig } from '@renderer/hooks/use-app-config' @@ -54,7 +55,7 @@ const SysproxySwitcher: React.FC = (props) => { await patchAppConfig({ sysProxy: { enable: previousState } }) // 回滚图标 updateTrayIconImmediate(previousState, tunEnabled) - alert(e) + toast.error(String(e)) } } diff --git a/src/renderer/src/components/updater/updater-modal.tsx b/src/renderer/src/components/updater/updater-modal.tsx index e31c1a8..623f532 100644 --- a/src/renderer/src/components/updater/updater-modal.tsx +++ b/src/renderer/src/components/updater/updater-modal.tsx @@ -1,12 +1,5 @@ -import { - Modal, - ModalContent, - ModalHeader, - ModalBody, - ModalFooter, - Button, - Code -} from '@heroui/react' +import { Button, Code, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/react' +import { toast } from '@renderer/components/base/toast' import ReactMarkdown from 'react-markdown' import React, { useState } from 'react' import { downloadAndInstallUpdate } from '@renderer/utils/ipc' @@ -27,7 +20,7 @@ const UpdaterModal: React.FC = (props) => { try { await downloadAndInstallUpdate(version) } catch (e) { - alert(e) + toast.error(String(e)) } } @@ -82,7 +75,7 @@ const UpdaterModal: React.FC = (props) => { await onUpdate() onClose() } catch (e) { - alert(e) + toast.error(String(e)) } finally { setDownloading(false) } diff --git a/src/renderer/src/hooks/use-app-config.tsx b/src/renderer/src/hooks/use-app-config.tsx index 236ab2a..5ad1912 100644 --- a/src/renderer/src/hooks/use-app-config.tsx +++ b/src/renderer/src/hooks/use-app-config.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext, ReactNode } from 'react' +import { toast } from '@renderer/components/base/toast' import useSWR from 'swr' import { getAppConfig, patchAppConfig as patch } from '@renderer/utils/ipc' @@ -17,7 +18,7 @@ export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children try { await patch(value) } catch (e) { - alert(e) + toast.error(String(e)) } finally { mutateAppConfig() } diff --git a/src/renderer/src/hooks/use-controled-mihomo-config.tsx b/src/renderer/src/hooks/use-controled-mihomo-config.tsx index 091941c..882b3fe 100644 --- a/src/renderer/src/hooks/use-controled-mihomo-config.tsx +++ b/src/renderer/src/hooks/use-controled-mihomo-config.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext, ReactNode } from 'react' +import { toast } from '@renderer/components/base/toast' import useSWR from 'swr' import { getControledMihomoConfig, patchControledMihomoConfig as patch } from '@renderer/utils/ipc' @@ -22,7 +23,7 @@ export const ControledMihomoConfigProvider: React.FC<{ children: ReactNode }> = try { await patch(value) } catch (e) { - alert(e) + toast.error(String(e)) } finally { mutateControledMihomoConfig() } diff --git a/src/renderer/src/hooks/use-override-config.tsx b/src/renderer/src/hooks/use-override-config.tsx index 82c094e..75b087f 100644 --- a/src/renderer/src/hooks/use-override-config.tsx +++ b/src/renderer/src/hooks/use-override-config.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext, ReactNode } from 'react' +import { toast } from '@renderer/components/base/toast' import useSWR from 'swr' import { getOverrideConfig, @@ -28,7 +29,7 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil try { await set(config) } catch (e) { - alert(e) + toast.error(String(e)) } finally { mutateOverrideConfig() } @@ -38,7 +39,7 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil try { await add(item) } catch (e) { - alert(e) + toast.error(String(e)) } finally { mutateOverrideConfig() } @@ -48,7 +49,7 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil try { await remove(id) } catch (e) { - alert(e) + toast.error(String(e)) } finally { mutateOverrideConfig() } @@ -58,7 +59,7 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil try { await update(item) } catch (e) { - alert(e) + toast.error(String(e)) } finally { mutateOverrideConfig() } diff --git a/src/renderer/src/hooks/use-profile-config.tsx b/src/renderer/src/hooks/use-profile-config.tsx index a6d0cb7..f1954f8 100644 --- a/src/renderer/src/hooks/use-profile-config.tsx +++ b/src/renderer/src/hooks/use-profile-config.tsx @@ -1,4 +1,5 @@ import React, { createContext, ReactNode, useContext } from 'react' +import { toast } from '@renderer/components/base/toast' import useSWR from 'swr' import { addProfileItem as add, @@ -32,7 +33,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child try { await set(config) } catch (e) { - alert(e) + toast.error(String(e)) } finally { mutateProfileConfig() window.electron.ipcRenderer.send('updateTrayMenu') @@ -43,7 +44,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child try { await add(item) } catch (e) { - alert(e) + toast.error(String(e)) } finally { mutateProfileConfig() window.electron.ipcRenderer.send('updateTrayMenu') @@ -54,7 +55,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child try { await remove(id) } catch (e) { - alert(e) + toast.error(String(e)) } finally { mutateProfileConfig() window.electron.ipcRenderer.send('updateTrayMenu') @@ -65,7 +66,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child try { await update(item) } catch (e) { - alert(e) + toast.error(String(e)) } finally { mutateProfileConfig() window.electron.ipcRenderer.send('updateTrayMenu') @@ -107,7 +108,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child if (errorMsg.includes('reply was never sent')) { setTimeout(() => mutateProfileConfig(), 1000) } else { - alert(`切换 Profile 失败: ${errorMsg}`) + toast.error(errorMsg, '切换配置失败') mutateProfileConfig() } } finally { diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index a391180..2f83753 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -14,6 +14,7 @@ import { OverrideConfigProvider } from './hooks/use-override-config' import { ProfileConfigProvider } from './hooks/use-profile-config' import { RulesProvider } from './hooks/use-rules' import { GroupsProvider } from './hooks/use-groups' +import { ToastProvider } from './components/base/toast' import './i18n' let F12Count = 0 @@ -53,7 +54,9 @@ init().then(() => { - + + + diff --git a/src/renderer/src/pages/dns.tsx b/src/renderer/src/pages/dns.tsx index a96209b..7cc4c14 100644 --- a/src/renderer/src/pages/dns.tsx +++ b/src/renderer/src/pages/dns.tsx @@ -1,5 +1,6 @@ import { Button, Tab, Input, Switch, Tabs, Divider } from '@heroui/react' import BasePage from '@renderer/components/base/base-page' +import { toast } from '@renderer/components/base/toast' import { MdDeleteForever } from 'react-icons/md' import SettingCard from '@renderer/components/base/base-setting-card' import SettingItem from '@renderer/components/base/base-setting-item' @@ -145,7 +146,7 @@ const DNS: React.FC = () => { await restartCore() } } catch (e) { - alert(e) + toast.error(String(e)) } } diff --git a/src/renderer/src/pages/mihomo.tsx b/src/renderer/src/pages/mihomo.tsx index 6ce119c..0cc1bb7 100644 --- a/src/renderer/src/pages/mihomo.tsx +++ b/src/renderer/src/pages/mihomo.tsx @@ -1,5 +1,6 @@ import { Button, Divider, Input, Select, SelectItem, Switch, Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure, Spinner, Chip } from '@heroui/react' import BasePage from '@renderer/components/base/base-page' +import { toast } from '@renderer/components/base/toast' import SettingCard from '@renderer/components/base/base-setting-card' import SettingItem from '@renderer/components/base/base-setting-item' import { useAppConfig } from '@renderer/hooks/use-app-config' @@ -308,7 +309,7 @@ const Mihomo: React.FC = () => { if (errorMessage.includes('配置检查失败') || errorMessage.includes('Profile Check Failed')) { await showDetailedError(t('mihomo.error.profileCheckFailed'), errorMessage) } else { - alert(errorMessage) + toast.error(errorMessage) } } finally { PubSub.publish('mihomo-core-changed') @@ -324,7 +325,7 @@ const Mihomo: React.FC = () => { } catch (error) { console.error('Failed to fetch tags:', error) setTags([]) - alert(t('mihomo.error.fetchTagsFailed')) + toast.error(t('mihomo.error.fetchTagsFailed')) } finally { setLoadingTags(false) } @@ -355,7 +356,7 @@ const Mihomo: React.FC = () => { new Notification(t('mihomo.coreUpgradeSuccess')) } catch (error) { console.error('Failed to install specific core:', error) - alert(t('mihomo.error.installCoreFailed')) + toast.error(t('mihomo.error.installCoreFailed')) } finally { setInstalling(false) } @@ -493,7 +494,7 @@ const Mihomo: React.FC = () => { if (typeof e === 'string' && e.includes('already using latest version')) { new Notification(t('mihomo.alreadyLatestVersion')) } else { - alert(e) + toast.error(String(e)) } } finally { setUpgrading(false) diff --git a/src/renderer/src/pages/override.tsx b/src/renderer/src/pages/override.tsx index 4f0ff1a..6117b52 100644 --- a/src/renderer/src/pages/override.tsx +++ b/src/renderer/src/pages/override.tsx @@ -8,6 +8,7 @@ import { Input } from '@heroui/react' import BasePage from '@renderer/components/base/base-page' +import { toast } from '@renderer/components/base/toast' import { getFilePath, readTextFile } from '@renderer/utils/ipc' import { useEffect, useRef, useState } from 'react' import { MdContentPaste } from 'react-icons/md' @@ -115,7 +116,7 @@ const Override: React.FC = () => { setFileOver(false) } } else { - alert(tRef.current('override.unsupportedFileType')) + toast.warning(tRef.current('override.unsupportedFileType')) } } setFileOver(false) @@ -222,7 +223,7 @@ const Override: React.FC = () => { }) } } catch (e) { - alert(e) + toast.error(String(e)) } } else if (key === 'new-yaml') { await addOverrideItem({ diff --git a/src/renderer/src/pages/profiles.tsx b/src/renderer/src/pages/profiles.tsx index 1355ec9..6570b6f 100644 --- a/src/renderer/src/pages/profiles.tsx +++ b/src/renderer/src/pages/profiles.tsx @@ -11,6 +11,7 @@ import { Tooltip } from '@heroui/react' import BasePage from '@renderer/components/base/base-page' +import { toast } from '@renderer/components/base/toast' import ProfileItem from '@renderer/components/profiles/profile-item' import { useProfileConfig } from '@renderer/hooks/use-profile-config' import { useAppConfig } from '@renderer/hooks/use-app-config' @@ -197,10 +198,10 @@ const Profiles: React.FC = () => { const content = await readTextFile(path) await addProfileItemRef.current({ name: file.name, type: 'local', file: content }) } catch (e) { - alert(e) + toast.error(String(e)) } } else { - alert(tRef.current('profiles.error.unsupportedFileType')) + toast.warning(tRef.current('profiles.error.unsupportedFileType')) } } setFileOver(false) @@ -345,7 +346,7 @@ const Profiles: React.FC = () => { useProxy }) } catch (e) { - alert(e) + toast.error(String(e)) } finally { setSubStoreImporting(false) } @@ -366,7 +367,7 @@ const Profiles: React.FC = () => { useProxy }) } catch (e) { - alert(e) + toast.error(String(e)) } finally { setSubStoreImporting(false) } @@ -398,7 +399,7 @@ const Profiles: React.FC = () => { await addProfileItem({ name: fileName, type: 'local', file: content }) } } catch (e) { - alert(e) + toast.error(String(e)) } } else if (key === 'new') { await addProfileItem({ diff --git a/src/renderer/src/pages/sniffer.tsx b/src/renderer/src/pages/sniffer.tsx index 3bf13c6..f59cc41 100644 --- a/src/renderer/src/pages/sniffer.tsx +++ b/src/renderer/src/pages/sniffer.tsx @@ -1,5 +1,6 @@ import { Button, Divider, Input, Switch } from '@heroui/react' import BasePage from '@renderer/components/base/base-page' +import { toast } from '@renderer/components/base/toast' import SettingCard from '@renderer/components/base/base-setting-card' import SettingItem from '@renderer/components/base/base-setting-item' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' @@ -70,7 +71,7 @@ const Sniffer: React.FC = () => { await restartCore() } } catch (e) { - alert(e) + toast.error(String(e)) } } diff --git a/src/renderer/src/pages/sysproxy.tsx b/src/renderer/src/pages/sysproxy.tsx index 4c5d2d7..612f7ef 100644 --- a/src/renderer/src/pages/sysproxy.tsx +++ b/src/renderer/src/pages/sysproxy.tsx @@ -1,5 +1,6 @@ import { Button, Input, Tab, Tabs } from '@heroui/react' import BasePage from '@renderer/components/base/base-page' +import { toast } from '@renderer/components/base/toast' import SettingCard from '@renderer/components/base/base-setting-card' import SettingItem from '@renderer/components/base/base-setting-item' import PacEditorModal from '@renderer/components/sysproxy/pac-editor-modal' @@ -105,7 +106,7 @@ const Sysproxy: React.FC = () => { } catch (e) { setValues({ ...values, enable: previousState }) setChanged(true) - alert(e) + toast.error(String(e)) await patchAppConfig({ sysProxy: { enable: false } }) } diff --git a/src/renderer/src/pages/tun.tsx b/src/renderer/src/pages/tun.tsx index 1fde5f0..362d511 100644 --- a/src/renderer/src/pages/tun.tsx +++ b/src/renderer/src/pages/tun.tsx @@ -1,5 +1,6 @@ import { Button, Input, Switch, Tab, Tabs } from '@heroui/react' import BasePage from '@renderer/components/base/base-page' +import { toast } from '@renderer/components/base/toast' import SettingCard from '@renderer/components/base/base-setting-card' import SettingItem from '@renderer/components/base/base-setting-item' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' @@ -112,7 +113,7 @@ const Tun: React.FC = () => { new Notification(t('tun.notifications.firewallResetSuccess')) await restartCore() } catch (e) { - alert(e) + toast.error(String(e)) } finally { setLoading(false) } @@ -133,7 +134,7 @@ const Tun: React.FC = () => { new Notification(t('tun.notifications.coreAuthSuccess')) await restartCore() } catch (e) { - alert(e) + toast.error(String(e)) } }} > diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 736797b..772f09d 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -556,10 +556,3 @@ export async function getRuleStr(id: string): Promise { export async function setRuleStr(id: string, str: string): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setRuleStr', id, str)) } - -async function alert(msg: T): Promise { - const msgStr = typeof msg === 'string' ? msg : JSON.stringify(msg) - return await window.electron.ipcRenderer.invoke('alert', msgStr) -} - -window.alert = alert \ No newline at end of file