diff --git a/src/main/config/override.ts b/src/main/config/override.ts index 7e7e1ea..16db1a0 100644 --- a/src/main/config/override.ts +++ b/src/main/config/override.ts @@ -40,7 +40,7 @@ export async function addOverrideItem(item: Partial): Promise): Promise { smartCollectorSize ) - // 检查是否已存在 Smart 覆写配置 - const existingOverride = await getOverrideItem(SMART_OVERRIDE_ID) - - if (existingOverride) { - // 如果已存在,更新配置 - await addOverrideItem({ - id: SMART_OVERRIDE_ID, - name: 'Smart Core Override', - type: 'local', - ext: 'js', - global: true, - file: template - }) - } else { - // 如果不存在,创建新的覆写配置 - await addOverrideItem({ - id: SMART_OVERRIDE_ID, - name: 'Smart Core Override', - type: 'local', - ext: 'js', - global: true, - file: template - }) - } + await addOverrideItem({ + id: SMART_OVERRIDE_ID, + name: 'Smart Core Override', + type: 'local', + ext: 'js', + global: true, + file: template + }) } catch (error) { await overrideLogger.error('Failed to create Smart override', error) throw error diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index bc7dbd2..d076930 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -499,24 +499,7 @@ async function checkProfile(): Promise { } export async function checkTunPermissions(): Promise { - const { core = 'mihomo' } = await getAppConfig() - const corePath = mihomoCorePath(core) - - try { - if (process.platform === 'win32') { - return await checkAdminPrivileges() - } - - if (process.platform === 'darwin' || process.platform === 'linux') { - const { stat } = await import('fs/promises') - const stats = await stat(corePath) - return (stats.mode & 0o4000) !== 0 && stats.uid === 0 - } - } catch { - return false - } - - return false + return checkMihomoCorePermissions() } export async function grantTunPermissions(): Promise { diff --git a/src/main/core/mihomoApi.ts b/src/main/core/mihomoApi.ts index b66c922..6cad3df 100644 --- a/src/main/core/mihomoApi.ts +++ b/src/main/core/mihomoApi.ts @@ -278,7 +278,7 @@ const mihomoTraffic = async (): Promise => { mihomoTrafficWs.onclose = (): void => { if (trafficRetry) { trafficRetry-- - mihomoTraffic() + setTimeout(mihomoTraffic, 1000) } } @@ -325,7 +325,7 @@ const mihomoMemory = async (): Promise => { mihomoMemoryWs.onclose = (): void => { if (memoryRetry) { memoryRetry-- - mihomoMemory() + setTimeout(mihomoMemory, 1000) } } @@ -373,7 +373,7 @@ const mihomoLogs = async (): Promise => { mihomoLogsWs.onclose = (): void => { if (logsRetry) { logsRetry-- - mihomoLogs() + setTimeout(mihomoLogs, 1000) } } @@ -419,7 +419,7 @@ const mihomoConnections = async (): Promise => { mihomoConnectionsWs.onclose = (): void => { if (connectionsRetry) { connectionsRetry-- - mihomoConnections() + setTimeout(mihomoConnections, 1000) } } diff --git a/src/main/index.ts b/src/main/index.ts index 12711bb..4ba2fb3 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -7,7 +7,7 @@ import { quitWithoutCore, startCore, stopCore, checkAdminRestartForTun, checkHig import { triggerSysProxy } from './sys/sysproxy' import icon from '../../resources/icon.png?asset' import { createTray, hideDockIcon, showDockIcon } from './resolve/tray' -import { init, initBasic } from './utils/init' +import { init, initBasic, safeShowErrorBox } from './utils/init' import { join } from 'path' import { initShortcut } from './resolve/shortcut' import { spawn, exec } from 'child_process' @@ -23,24 +23,6 @@ import i18next from 'i18next' import { logger } from './utils/logger' import { initWebdavBackupScheduler } from './resolve/backup' -// 错误处理 -function showSafeErrorBox(titleKey: string, message: string): void { - let title: string - try { - title = i18next.t(titleKey) - if (!title || title === titleKey) throw new Error('Translation not ready') - } catch { - const isZh = app.getLocale().startsWith('zh') - const fallbacks: Record = { - 'common.error.initFailed': { zh: '应用初始化失败', en: 'Application initialization failed' }, - 'mihomo.error.coreStartFailed': { zh: '内核启动出错', en: 'Core start failed' }, - 'profiles.error.importFailed': { zh: '配置导入失败', en: 'Profile import failed' }, - 'common.error.adminRequired': { zh: '需要管理员权限', en: 'Administrator privileges required' } - } - title = fallbacks[titleKey] ? (isZh ? fallbacks[titleKey].zh : fallbacks[titleKey].en) : (isZh ? '错误' : 'Error') - } - dialog.showErrorBox(title, message) -} async function fixUserDataPermissions(): Promise { if (process.platform !== 'darwin') return @@ -80,7 +62,7 @@ async function initApp(): Promise { initApp() .catch((e) => { - showSafeErrorBox('common.error.initFailed', `${e}`) + safeShowErrorBox('common.error.initFailed', `${e}`) app.quit() }) @@ -150,7 +132,7 @@ async function checkHighPrivilegeCoreEarly(): Promise { await restartAsAdmin(false) process.exit(0) } catch (error) { - showSafeErrorBox('common.error.adminRequired', `${error}`) + safeShowErrorBox('common.error.adminRequired', `${error}`) process.exit(1) } } else { @@ -234,7 +216,7 @@ app.whenReady().then(async () => { } await initI18n({ lng: appConfig.language }) } catch (e) { - showSafeErrorBox('common.error.initFailed', `${e}`) + safeShowErrorBox('common.error.initFailed', `${e}`) app.quit() } @@ -247,7 +229,7 @@ app.whenReady().then(async () => { await checkAdminRestartForTun() }) } catch (e) { - showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`) + safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) } try { await startMonitor() @@ -303,7 +285,7 @@ async function handleDeepLink(url: string): Promise { new Notification({ title: i18next.t('profiles.notification.importSuccess') }).show() break } catch (e) { - showSafeErrorBox('profiles.error.importFailed', `${url}\n${e}`) + safeShowErrorBox('profiles.error.importFailed', `${url}\n${e}`) } } } diff --git a/src/main/resolve/backup.ts b/src/main/resolve/backup.ts index 1eb3bdf..639b484 100644 --- a/src/main/resolve/backup.ts +++ b/src/main/resolve/backup.ts @@ -21,7 +21,13 @@ import i18next from 'i18next' let backupCronJob: Cron | null = null -export async function webdavBackup(): Promise { +interface WebDAVContext { + client: ReturnType['createClient']> + webdavDir: string + webdavMaxBackups: number +} + +async function getWebDAVClient(): Promise { const { createClient } = await import('webdav/dist/node/index.js') const { webdavUrl = '', @@ -30,6 +36,17 @@ export async function webdavBackup(): Promise { webdavDir = 'clash-party', webdavMaxBackups = 0 } = await getAppConfig() + + const client = createClient(webdavUrl, { + username: webdavUsername, + password: webdavPassword + }) + + return { client, webdavDir, webdavMaxBackups } +} + +export async function webdavBackup(): Promise { + const { client, webdavDir, webdavMaxBackups } = await getWebDAVClient() const zip = new AdmZip() zip.addLocalFile(appConfigPath()) @@ -44,10 +61,6 @@ export async function webdavBackup(): Promise { const date = new Date() const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip` - const client = createClient(webdavUrl, { - username: webdavUsername, - password: webdavPassword - }) try { await client.createDirectory(webdavDir) } catch { @@ -92,36 +105,14 @@ export async function webdavBackup(): Promise { } export async function webdavRestore(filename: string): Promise { - const { createClient } = await import('webdav/dist/node/index.js') - const { - webdavUrl = '', - webdavUsername = '', - webdavPassword = '', - webdavDir = 'clash-party' - } = await getAppConfig() - - const client = createClient(webdavUrl, { - username: webdavUsername, - password: webdavPassword - }) + const { client, webdavDir } = await getWebDAVClient() const zipData = await client.getFileContents(`${webdavDir}/${filename}`) const zip = new AdmZip(zipData as Buffer) zip.extractAllTo(dataDir(), true) } export async function listWebdavBackups(): Promise { - const { createClient } = await import('webdav/dist/node/index.js') - const { - webdavUrl = '', - webdavUsername = '', - webdavPassword = '', - webdavDir = 'clash-party' - } = await getAppConfig() - - const client = createClient(webdavUrl, { - username: webdavUsername, - password: webdavPassword - }) + const { client, webdavDir } = await getWebDAVClient() const files = await client.getDirectoryContents(webdavDir, { glob: '*.zip' }) if (Array.isArray(files)) { return files.map((file) => file.basename) @@ -131,18 +122,7 @@ export async function listWebdavBackups(): Promise { } export async function webdavDelete(filename: string): Promise { - const { createClient } = await import('webdav/dist/node/index.js') - const { - webdavUrl = '', - webdavUsername = '', - webdavPassword = '', - webdavDir = 'clash-party' - } = await getAppConfig() - - const client = createClient(webdavUrl, { - username: webdavUsername, - password: webdavPassword - }) + const { client, webdavDir } = await getWebDAVClient() await client.deleteFile(`${webdavDir}/${filename}`) } diff --git a/src/main/utils/init.ts b/src/main/utils/init.ts index 844d021..1bad56c 100644 --- a/src/main/utils/init.ts +++ b/src/main/utils/init.ts @@ -42,7 +42,7 @@ import { } from '../config' import { app, dialog } from 'electron' import { startSSIDCheck } from '../sys/ssid' -import i18next from '../../shared/i18n' +import i18next, { resources } from '../../shared/i18n' import { initLogger } from './logger' let isInitBasicCompleted = false @@ -54,11 +54,9 @@ export function safeShowErrorBox(titleKey: string, message: string): void { title = i18next.t(titleKey) if (!title || title === titleKey) throw new Error('Translation not ready') } catch { - const isZh = process.env.LANG?.startsWith('zh') || process.env.LC_ALL?.startsWith('zh') - const fallbacks: Record = { - 'mihomo.error.coreStartFailed': { zh: '内核启动出错', en: 'Core start failed' } - } - title = fallbacks[titleKey] ? (isZh ? fallbacks[titleKey].zh : fallbacks[titleKey].en) : (isZh ? '错误' : 'Error') + const isZh = app.getLocale().startsWith('zh') + const lang = isZh ? resources['zh-CN'].translation : resources['en-US'].translation + title = lang[titleKey] || (isZh ? '错误' : 'Error') } dialog.showErrorBox(title, message) } diff --git a/src/main/utils/template.ts b/src/main/utils/template.ts index cb7db1c..53648f2 100644 --- a/src/main/utils/template.ts +++ b/src/main/utils/template.ts @@ -9,6 +9,7 @@ export const defaultConfig: IAppConfig = { appTheme: 'system', useWindowFrame: false, proxyInTray: true, + showCurrentProxyInTray: false, disableTrayIconColor: false, maxLogDays: 7, proxyCols: 'auto', 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/connections/connection-item.tsx b/src/renderer/src/components/connections/connection-item.tsx index 5b37994..49bdbd9 100644 --- a/src/renderer/src/components/connections/connection-item.tsx +++ b/src/renderer/src/components/connections/connection-item.tsx @@ -1,26 +1,19 @@ import { Button, Card, CardFooter, CardHeader, Chip } from '@heroui/react' import { calcTraffic } from '@renderer/utils/calc' import dayjs from '@renderer/utils/dayjs' -import React, { useEffect } from 'react' +import React from 'react' import { CgClose, CgTrash } from 'react-icons/cg' interface Props { index: number info: IMihomoConnectionDetail - selected: IMihomoConnectionDetail | undefined setSelected: React.Dispatch> setIsDetailModalOpen: React.Dispatch> close: (id: string) => void } const ConnectionItem: React.FC = (props) => { - const { index, info, close, selected, setSelected, setIsDetailModalOpen } = props - - useEffect(() => { - if (selected?.id === info.id) { - setSelected(info) - } - }, [info]) + const { index, info, close, setSelected, setIsDetailModalOpen } = props return (
@@ -37,7 +30,7 @@ const ConnectionItem: React.FC = (props) => {
= (props) => {
} > - {isDetailModalOpen && selected && ( - setIsDetailModalOpen(false)} connection={selected} /> + {isDetailModalOpen && selectedConnection && ( + setIsDetailModalOpen(false)} connection={selectedConnection} /> )}
{ key="active" title={ { key="closed" title={ { size="sm" className="w-[180px] min-w-[131px]" aria-label={t('connections.orderBy')} - selectedKeys={new Set([connectionOrderBy])} + selectedKeys={[connectionOrderBy]} disallowEmptySelection={true} onSelectionChange={async (v) => { await patchAppConfig({ @@ -362,7 +387,7 @@ const Connections: React.FC = () => { size="sm" isIconOnly className="bg-content2" - onPress={async () => { + onPress={() => { patchAppConfig({ connectionDirection: connectionDirection === 'asc' ? 'desc' : 'asc' }) @@ -387,7 +412,6 @@ const Connections: React.FC = () => { { await restartCore() } } catch (e) { - alert(e) + toast.error(String(e)) } } diff --git a/src/renderer/src/pages/logs.tsx b/src/renderer/src/pages/logs.tsx index f21ab1e..7b87c4d 100644 --- a/src/renderer/src/pages/logs.tsx +++ b/src/renderer/src/pages/logs.tsx @@ -121,17 +121,14 @@ const Logs: React.FC = () => { { - return ( - - ) - }} + itemContent={(i, log) => ( + + )} />
diff --git a/src/renderer/src/pages/mihomo.tsx b/src/renderer/src/pages/mihomo.tsx index a07e455..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' @@ -44,10 +45,6 @@ const Mihomo: React.FC = () => { smartCollectorSize = 100, maxLogDays = 7, sysProxy, - disableLoopbackDetector, - disableEmbedCA, - disableSystemCA, - skipSafePathCheck, showMixedPort, enableMixedPort = true, showSocksPort, @@ -312,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') @@ -328,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) } @@ -359,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) } @@ -497,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 c69b5bc..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' @@ -75,18 +76,29 @@ const Override: React.FC = () => { } } + const addOverrideItemRef = useRef(addOverrideItem) + addOverrideItemRef.current = addOverrideItem + + const tRef = useRef(t) + tRef.current = t + useEffect(() => { - pageRef.current?.addEventListener('dragover', (e) => { + const element = pageRef.current + if (!element) return + + const handleDragOver = (e: DragEvent): void => { e.preventDefault() e.stopPropagation() setFileOver(true) - }) - pageRef.current?.addEventListener('dragleave', (e) => { + } + + const handleDragLeave = (e: DragEvent): void => { e.preventDefault() e.stopPropagation() setFileOver(false) - }) - pageRef.current?.addEventListener('drop', async (event) => { + } + + const handleDrop = async (event: DragEvent): Promise => { event.preventDefault() event.stopPropagation() if (event.dataTransfer?.files) { @@ -94,7 +106,7 @@ const Override: React.FC = () => { if (file.name.endsWith('.js') || file.name.endsWith('.yaml')) { const content = await readTextFile((file as File & { path: string }).path) try { - await addOverrideItem({ + await addOverrideItemRef.current({ name: file.name, type: 'local', file: content, @@ -104,15 +116,20 @@ const Override: React.FC = () => { setFileOver(false) } } else { - alert(t('override.unsupportedFileType')) + toast.warning(tRef.current('override.unsupportedFileType')) } } setFileOver(false) - }) + } + + element.addEventListener('dragover', handleDragOver) + element.addEventListener('dragleave', handleDragLeave) + element.addEventListener('drop', handleDrop) + return (): void => { - pageRef.current?.removeEventListener('dragover', () => {}) - pageRef.current?.removeEventListener('dragleave', () => {}) - pageRef.current?.removeEventListener('drop', () => {}) + element.removeEventListener('dragover', handleDragOver) + element.removeEventListener('dragleave', handleDragLeave) + element.removeEventListener('drop', handleDrop) } }, []) @@ -206,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 9b469ab..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' @@ -156,26 +157,37 @@ const Profiles: React.FC = () => { } } - const handleInputKeyUp = useCallback( - (e: KeyboardEvent) => { - if (e.key !== 'Enter' || isUrlEmpty) return - handleImport() - }, - [isUrlEmpty] - ) + const handleImportRef = useRef(handleImport) + handleImportRef.current = handleImport + + const addProfileItemRef = useRef(addProfileItem) + addProfileItemRef.current = addProfileItem + + const tRef = useRef(t) + tRef.current = t + + const handleInputKeyUp = useCallback((e: KeyboardEvent) => { + if (e.key !== 'Enter' || e.currentTarget.value.trim() === '') return + handleImportRef.current() + }, []) useEffect(() => { - pageRef.current?.addEventListener('dragover', (e) => { + const element = pageRef.current + if (!element) return + + const handleDragOver = (e: DragEvent): void => { e.preventDefault() e.stopPropagation() setFileOver(true) - }) - pageRef.current?.addEventListener('dragleave', (e) => { + } + + const handleDragLeave = (e: DragEvent): void => { e.preventDefault() e.stopPropagation() setFileOver(false) - }) - pageRef.current?.addEventListener('drop', async (event) => { + } + + const handleDrop = async (event: DragEvent): Promise => { event.preventDefault() event.stopPropagation() if (event.dataTransfer?.files) { @@ -184,20 +196,25 @@ const Profiles: React.FC = () => { try { const path = window.api.webUtils.getPathForFile(file) const content = await readTextFile(path) - await addProfileItem({ name: file.name, type: 'local', file: content }) + await addProfileItemRef.current({ name: file.name, type: 'local', file: content }) } catch (e) { - alert(e) + toast.error(String(e)) } } else { - alert(t('profiles.error.unsupportedFileType')) + toast.warning(tRef.current('profiles.error.unsupportedFileType')) } } setFileOver(false) - }) + } + + element.addEventListener('dragover', handleDragOver) + element.addEventListener('dragleave', handleDragLeave) + element.addEventListener('drop', handleDrop) + return (): void => { - pageRef.current?.removeEventListener('dragover', () => {}) - pageRef.current?.removeEventListener('dragleave', () => {}) - pageRef.current?.removeEventListener('drop', () => {}) + element.removeEventListener('dragover', handleDragOver) + element.removeEventListener('dragleave', handleDragLeave) + element.removeEventListener('drop', handleDrop) } }, []) @@ -329,7 +346,7 @@ const Profiles: React.FC = () => { useProxy }) } catch (e) { - alert(e) + toast.error(String(e)) } finally { setSubStoreImporting(false) } @@ -350,7 +367,7 @@ const Profiles: React.FC = () => { useProxy }) } catch (e) { - alert(e) + toast.error(String(e)) } finally { setSubStoreImporting(false) } @@ -382,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