Compare commits

..

No commits in common. "ddd0077a616cfa827faa61f69bb6e51c45a0dbf9" and "7619b4d3e516407917e59327007a112eb214b102" have entirely different histories.

47 changed files with 301 additions and 492 deletions

View File

@ -40,7 +40,7 @@ export async function addOverrideItem(item: Partial<IOverrideItem>): Promise<voi
const config = await getOverrideConfig() const config = await getOverrideConfig()
const newItem = await createOverride(item) const newItem = await createOverride(item)
if (await getOverrideItem(item.id)) { if (await getOverrideItem(item.id)) {
await updateOverrideItem(newItem) updateOverrideItem(newItem)
} else { } else {
config.items.push(newItem) config.items.push(newItem)
} }
@ -84,7 +84,7 @@ export async function createOverride(item: Partial<IOverrideItem>): Promise<IOve
} }
case 'local': { case 'local': {
const data = item.file || '' const data = item.file || ''
await setOverride(id, newItem.ext, data) setOverride(id, newItem.ext, data)
break break
} }
} }

View File

@ -339,6 +339,11 @@ export async function createSmartOverride(): Promise<void> {
smartCollectorSize smartCollectorSize
) )
// 检查是否已存在 Smart 覆写配置
const existingOverride = await getOverrideItem(SMART_OVERRIDE_ID)
if (existingOverride) {
// 如果已存在,更新配置
await addOverrideItem({ await addOverrideItem({
id: SMART_OVERRIDE_ID, id: SMART_OVERRIDE_ID,
name: 'Smart Core Override', name: 'Smart Core Override',
@ -347,6 +352,17 @@ export async function createSmartOverride(): Promise<void> {
global: true, global: true,
file: template file: template
}) })
} else {
// 如果不存在,创建新的覆写配置
await addOverrideItem({
id: SMART_OVERRIDE_ID,
name: 'Smart Core Override',
type: 'local',
ext: 'js',
global: true,
file: template
})
}
} catch (error) { } catch (error) {
await overrideLogger.error('Failed to create Smart override', error) await overrideLogger.error('Failed to create Smart override', error)
throw error throw error

View File

@ -499,7 +499,24 @@ async function checkProfile(): Promise<void> {
} }
export async function checkTunPermissions(): Promise<boolean> { export async function checkTunPermissions(): Promise<boolean> {
return checkMihomoCorePermissions() 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
} }
export async function grantTunPermissions(): Promise<void> { export async function grantTunPermissions(): Promise<void> {

View File

@ -278,7 +278,7 @@ const mihomoTraffic = async (): Promise<void> => {
mihomoTrafficWs.onclose = (): void => { mihomoTrafficWs.onclose = (): void => {
if (trafficRetry) { if (trafficRetry) {
trafficRetry-- trafficRetry--
setTimeout(mihomoTraffic, 1000) mihomoTraffic()
} }
} }
@ -325,7 +325,7 @@ const mihomoMemory = async (): Promise<void> => {
mihomoMemoryWs.onclose = (): void => { mihomoMemoryWs.onclose = (): void => {
if (memoryRetry) { if (memoryRetry) {
memoryRetry-- memoryRetry--
setTimeout(mihomoMemory, 1000) mihomoMemory()
} }
} }
@ -373,7 +373,7 @@ const mihomoLogs = async (): Promise<void> => {
mihomoLogsWs.onclose = (): void => { mihomoLogsWs.onclose = (): void => {
if (logsRetry) { if (logsRetry) {
logsRetry-- logsRetry--
setTimeout(mihomoLogs, 1000) mihomoLogs()
} }
} }
@ -419,7 +419,7 @@ const mihomoConnections = async (): Promise<void> => {
mihomoConnectionsWs.onclose = (): void => { mihomoConnectionsWs.onclose = (): void => {
if (connectionsRetry) { if (connectionsRetry) {
connectionsRetry-- connectionsRetry--
setTimeout(mihomoConnections, 1000) mihomoConnections()
} }
} }

View File

@ -7,7 +7,7 @@ import { quitWithoutCore, startCore, stopCore, checkAdminRestartForTun, checkHig
import { triggerSysProxy } from './sys/sysproxy' import { triggerSysProxy } from './sys/sysproxy'
import icon from '../../resources/icon.png?asset' import icon from '../../resources/icon.png?asset'
import { createTray, hideDockIcon, showDockIcon } from './resolve/tray' import { createTray, hideDockIcon, showDockIcon } from './resolve/tray'
import { init, initBasic, safeShowErrorBox } from './utils/init' import { init, initBasic } from './utils/init'
import { join } from 'path' import { join } from 'path'
import { initShortcut } from './resolve/shortcut' import { initShortcut } from './resolve/shortcut'
import { spawn, exec } from 'child_process' import { spawn, exec } from 'child_process'
@ -23,6 +23,24 @@ import i18next from 'i18next'
import { logger } from './utils/logger' import { logger } from './utils/logger'
import { initWebdavBackupScheduler } from './resolve/backup' 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<string, { zh: string; en: string }> = {
'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<void> { async function fixUserDataPermissions(): Promise<void> {
if (process.platform !== 'darwin') return if (process.platform !== 'darwin') return
@ -62,7 +80,7 @@ async function initApp(): Promise<void> {
initApp() initApp()
.catch((e) => { .catch((e) => {
safeShowErrorBox('common.error.initFailed', `${e}`) showSafeErrorBox('common.error.initFailed', `${e}`)
app.quit() app.quit()
}) })
@ -132,7 +150,7 @@ async function checkHighPrivilegeCoreEarly(): Promise<void> {
await restartAsAdmin(false) await restartAsAdmin(false)
process.exit(0) process.exit(0)
} catch (error) { } catch (error) {
safeShowErrorBox('common.error.adminRequired', `${error}`) showSafeErrorBox('common.error.adminRequired', `${error}`)
process.exit(1) process.exit(1)
} }
} else { } else {
@ -216,7 +234,7 @@ app.whenReady().then(async () => {
} }
await initI18n({ lng: appConfig.language }) await initI18n({ lng: appConfig.language })
} catch (e) { } catch (e) {
safeShowErrorBox('common.error.initFailed', `${e}`) showSafeErrorBox('common.error.initFailed', `${e}`)
app.quit() app.quit()
} }
@ -229,7 +247,7 @@ app.whenReady().then(async () => {
await checkAdminRestartForTun() await checkAdminRestartForTun()
}) })
} catch (e) { } catch (e) {
safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`)
} }
try { try {
await startMonitor() await startMonitor()
@ -285,7 +303,7 @@ async function handleDeepLink(url: string): Promise<void> {
new Notification({ title: i18next.t('profiles.notification.importSuccess') }).show() new Notification({ title: i18next.t('profiles.notification.importSuccess') }).show()
break break
} catch (e) { } catch (e) {
safeShowErrorBox('profiles.error.importFailed', `${url}\n${e}`) showSafeErrorBox('profiles.error.importFailed', `${url}\n${e}`)
} }
} }
} }

View File

@ -21,13 +21,7 @@ import i18next from 'i18next'
let backupCronJob: Cron | null = null let backupCronJob: Cron | null = null
interface WebDAVContext { export async function webdavBackup(): Promise<boolean> {
client: ReturnType<Awaited<typeof import('webdav/dist/node/index.js')>['createClient']>
webdavDir: string
webdavMaxBackups: number
}
async function getWebDAVClient(): Promise<WebDAVContext> {
const { createClient } = await import('webdav/dist/node/index.js') const { createClient } = await import('webdav/dist/node/index.js')
const { const {
webdavUrl = '', webdavUrl = '',
@ -36,17 +30,6 @@ async function getWebDAVClient(): Promise<WebDAVContext> {
webdavDir = 'clash-party', webdavDir = 'clash-party',
webdavMaxBackups = 0 webdavMaxBackups = 0
} = await getAppConfig() } = await getAppConfig()
const client = createClient(webdavUrl, {
username: webdavUsername,
password: webdavPassword
})
return { client, webdavDir, webdavMaxBackups }
}
export async function webdavBackup(): Promise<boolean> {
const { client, webdavDir, webdavMaxBackups } = await getWebDAVClient()
const zip = new AdmZip() const zip = new AdmZip()
zip.addLocalFile(appConfigPath()) zip.addLocalFile(appConfigPath())
@ -61,6 +44,10 @@ export async function webdavBackup(): Promise<boolean> {
const date = new Date() const date = new Date()
const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip` const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
const client = createClient(webdavUrl, {
username: webdavUsername,
password: webdavPassword
})
try { try {
await client.createDirectory(webdavDir) await client.createDirectory(webdavDir)
} catch { } catch {
@ -105,14 +92,36 @@ export async function webdavBackup(): Promise<boolean> {
} }
export async function webdavRestore(filename: string): Promise<void> { export async function webdavRestore(filename: string): Promise<void> {
const { client, webdavDir } = await getWebDAVClient() 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 zipData = await client.getFileContents(`${webdavDir}/${filename}`) const zipData = await client.getFileContents(`${webdavDir}/${filename}`)
const zip = new AdmZip(zipData as Buffer) const zip = new AdmZip(zipData as Buffer)
zip.extractAllTo(dataDir(), true) zip.extractAllTo(dataDir(), true)
} }
export async function listWebdavBackups(): Promise<string[]> { export async function listWebdavBackups(): Promise<string[]> {
const { client, webdavDir } = await getWebDAVClient() 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 files = await client.getDirectoryContents(webdavDir, { glob: '*.zip' }) const files = await client.getDirectoryContents(webdavDir, { glob: '*.zip' })
if (Array.isArray(files)) { if (Array.isArray(files)) {
return files.map((file) => file.basename) return files.map((file) => file.basename)
@ -122,7 +131,18 @@ export async function listWebdavBackups(): Promise<string[]> {
} }
export async function webdavDelete(filename: string): Promise<void> { export async function webdavDelete(filename: string): Promise<void> {
const { client, webdavDir } = await getWebDAVClient() 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
})
await client.deleteFile(`${webdavDir}/${filename}`) await client.deleteFile(`${webdavDir}/${filename}`)
} }

View File

@ -42,7 +42,7 @@ import {
} from '../config' } from '../config'
import { app, dialog } from 'electron' import { app, dialog } from 'electron'
import { startSSIDCheck } from '../sys/ssid' import { startSSIDCheck } from '../sys/ssid'
import i18next, { resources } from '../../shared/i18n' import i18next from '../../shared/i18n'
import { initLogger } from './logger' import { initLogger } from './logger'
let isInitBasicCompleted = false let isInitBasicCompleted = false
@ -54,9 +54,11 @@ export function safeShowErrorBox(titleKey: string, message: string): void {
title = i18next.t(titleKey) title = i18next.t(titleKey)
if (!title || title === titleKey) throw new Error('Translation not ready') if (!title || title === titleKey) throw new Error('Translation not ready')
} catch { } catch {
const isZh = app.getLocale().startsWith('zh') const isZh = process.env.LANG?.startsWith('zh') || process.env.LC_ALL?.startsWith('zh')
const lang = isZh ? resources['zh-CN'].translation : resources['en-US'].translation const fallbacks: Record<string, { zh: string; en: string }> = {
title = lang[titleKey] || (isZh ? '错误' : 'Error') 'mihomo.error.coreStartFailed': { zh: '内核启动出错', en: 'Core start failed' }
}
title = fallbacks[titleKey] ? (isZh ? fallbacks[titleKey].zh : fallbacks[titleKey].en) : (isZh ? '错误' : 'Error')
} }
dialog.showErrorBox(title, message) dialog.showErrorBox(title, message)
} }

View File

@ -9,7 +9,6 @@ export const defaultConfig: IAppConfig = {
appTheme: 'system', appTheme: 'system',
useWindowFrame: false, useWindowFrame: false,
proxyInTray: true, proxyInTray: true,
showCurrentProxyInTray: false,
disableTrayIconColor: false, disableTrayIconColor: false,
maxLogDays: 7, maxLogDays: 7,
proxyCols: 'auto', proxyCols: 'auto',

View File

@ -148,48 +148,3 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; 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%;
}
}

View File

@ -1,157 +0,0 @@
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<ToastType, { icon: React.ReactNode; bg: string; iconBg: string }> = {
success: {
icon: <IoCheckmark className="text-white text-sm" />,
bg: 'bg-content1',
iconBg: 'bg-success'
},
error: {
icon: <IoClose className="text-white text-sm" />,
bg: 'bg-content1',
iconBg: 'bg-danger'
},
warning: {
icon: <IoAlertSharp className="text-white text-sm" />,
bg: 'bg-content1',
iconBg: 'bg-warning'
},
info: {
icon: <IoInformationSharp className="text-white text-sm" />,
bg: 'bg-content1',
iconBg: 'bg-primary'
}
}
const { icon, iconBg } = theme[data.type]
const duration = data.duration || 3500
return (
<div
className={`
relative overflow-hidden
flex items-center gap-3 p-3
bg-content1/80 rounded-large
shadow-large border border-default-200/50
backdrop-blur-xl backdrop-saturate-150
${data.exiting ? 'toast-exit' : 'toast-enter'}
`}
style={{ width: 340 }}
>
<div className={`flex-shrink-0 w-7 h-7 ${iconBg} rounded-full flex items-center justify-center`}>
{icon}
</div>
<div className="flex-1 min-w-0">
{data.title && (
<p className="text-sm font-medium text-foreground">{data.title}</p>
)}
<p className="text-sm text-foreground-500 break-words select-text">
{data.message}
</p>
</div>
<button
onClick={() => {
markExiting(data.id)
setTimeout(() => onRemove(data.id), 150)
}}
className="flex-shrink-0 p-1 rounded-full hover:bg-default-200/60 transition-colors"
>
<IoClose className="text-base text-foreground-400" />
</button>
<div
className={`absolute bottom-0 left-0 h-[2px] ${iconBg} toast-progress`}
style={{ animationDuration: `${duration}ms` }}
/>
</div>
)
}
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [currentToasts, setCurrentToasts] = useState<ToastData[]>([])
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(
<div className="fixed top-[60px] right-4 z-[9999] flex flex-col gap-2">
{currentToasts.map((t) => (
<ToastItem key={t.id} data={t} onRemove={handleRemove} />
))}
</div>,
document.body
)}
</>
)
}

View File

@ -1,19 +1,26 @@
import { Button, Card, CardFooter, CardHeader, Chip } from '@heroui/react' import { Button, Card, CardFooter, CardHeader, Chip } from '@heroui/react'
import { calcTraffic } from '@renderer/utils/calc' import { calcTraffic } from '@renderer/utils/calc'
import dayjs from '@renderer/utils/dayjs' import dayjs from '@renderer/utils/dayjs'
import React from 'react' import React, { useEffect } from 'react'
import { CgClose, CgTrash } from 'react-icons/cg' import { CgClose, CgTrash } from 'react-icons/cg'
interface Props { interface Props {
index: number index: number
info: IMihomoConnectionDetail info: IMihomoConnectionDetail
selected: IMihomoConnectionDetail | undefined
setSelected: React.Dispatch<React.SetStateAction<IMihomoConnectionDetail | undefined>> setSelected: React.Dispatch<React.SetStateAction<IMihomoConnectionDetail | undefined>>
setIsDetailModalOpen: React.Dispatch<React.SetStateAction<boolean>> setIsDetailModalOpen: React.Dispatch<React.SetStateAction<boolean>>
close: (id: string) => void close: (id: string) => void
} }
const ConnectionItem: React.FC<Props> = (props) => { const ConnectionItem: React.FC<Props> = (props) => {
const { index, info, close, setSelected, setIsDetailModalOpen } = props const { index, info, close, selected, setSelected, setIsDetailModalOpen } = props
useEffect(() => {
if (selected?.id === info.id) {
setSelected(info)
}
}, [info])
return ( return (
<div className={`px-2 pb-2 ${index === 0 ? 'pt-2' : ''}`}> <div className={`px-2 pb-2 ${index === 0 ? 'pt-2' : ''}`}>
@ -30,7 +37,7 @@ const ConnectionItem: React.FC<Props> = (props) => {
<div className="w-full pr-12"> <div className="w-full pr-12">
<CardHeader className="pb-0 gap-1"> <CardHeader className="pb-0 gap-1">
<Chip <Chip
color={info.isActive ? 'primary' : 'danger'} color={`${info.isActive ? 'primary' : 'danger'}`}
size="sm" size="sm"
radius="sm" radius="sm"
variant="dot" variant="dot"
@ -77,7 +84,7 @@ const ConnectionItem: React.FC<Props> = (props) => {
</div> </div>
</Card> </Card>
<Button <Button
color={info.isActive ? 'warning' : 'danger'} color={`${info.isActive ? 'warning' : 'danger'}`}
variant="light" variant="light"
isIconOnly isIconOnly
className="absolute right-2 top-1/2 -translate-y-1/2" className="absolute right-2 top-1/2 -translate-y-1/2"

View File

@ -14,7 +14,7 @@ const LogItem: React.FC<IMihomoLogInfo & { index: number }> = (props) => {
<Card> <Card>
<CardHeader className="pb-0 pt-1"> <CardHeader className="pb-0 pt-1">
<div className={`mr-2 text-lg font-bold text-${colorMap[type]}`}> <div className={`mr-2 text-lg font-bold text-${colorMap[type]}`}>
{type.toUpperCase()} {props.type.toUpperCase()}
</div> </div>
<small className="text-foreground-500">{time}</small> <small className="text-foreground-500">{time}</small>
</CardHeader> </CardHeader>
@ -24,4 +24,4 @@ const LogItem: React.FC<IMihomoLogInfo & { index: number }> = (props) => {
) )
} }
export default React.memo(LogItem) export default LogItem

View File

@ -1,5 +1,4 @@
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react' import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
import { toast } from '@renderer/components/base/toast'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { BaseEditor } from '../base/base-editor' import { BaseEditor } from '../base/base-editor'
import { getOverride, restartCore, setOverride } from '@renderer/utils/ipc' import { getOverride, restartCore, setOverride } from '@renderer/utils/ipc'
@ -59,7 +58,7 @@ const EditFileModal: React.FC<Props> = (props) => {
await restartCore() await restartCore()
onClose() onClose()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
}} }}
> >

View File

@ -8,7 +8,6 @@ import {
DropdownMenu, DropdownMenu,
DropdownTrigger DropdownTrigger
} from '@heroui/react' } from '@heroui/react'
import { toast } from '@renderer/components/base/toast'
import { IoMdMore, IoMdRefresh } from 'react-icons/io' import { IoMdMore, IoMdRefresh } from 'react-icons/io'
import dayjs from '@renderer/utils/dayjs' import dayjs from '@renderer/utils/dayjs'
import React, { Key, useMemo, useState } from 'react' import React, { Key, useMemo, useState } from 'react'
@ -198,7 +197,7 @@ const OverrideItem: React.FC<Props> = (props) => {
await addOverrideItem(info) await addOverrideItem(info)
await restartCore() await restartCore()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
setUpdating(false) setUpdating(false)
} }

View File

@ -13,7 +13,6 @@ import {
DropdownMenu, DropdownMenu,
DropdownItem DropdownItem
} from '@heroui/react' } from '@heroui/react'
import { toast } from '@renderer/components/base/toast'
import React, { useState } from 'react' import React, { useState } from 'react'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { useOverrideConfig } from '@renderer/hooks/use-override-config' import { useOverrideConfig } from '@renderer/hooks/use-override-config'
@ -50,7 +49,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
await restartCore() await restartCore()
onClose() onClose()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
} }

View File

@ -22,7 +22,6 @@ import yaml from 'js-yaml'
import { IoMdTrash, IoMdArrowUp, IoMdArrowDown, IoMdUndo } from 'react-icons/io' import { IoMdTrash, IoMdArrowUp, IoMdArrowDown, IoMdUndo } from 'react-icons/io'
import { MdVerticalAlignTop, MdVerticalAlignBottom } from 'react-icons/md' import { MdVerticalAlignTop, MdVerticalAlignBottom } from 'react-icons/md'
import { platform } from '@renderer/utils/init' import { platform } from '@renderer/utils/init'
import { toast } from '@renderer/components/base/toast'
interface Props { interface Props {
id: string id: string
@ -643,7 +642,7 @@ const EditRulesModal: React.FC<Props> = (props) => {
await setRuleStr(id, ruleYaml); await setRuleStr(id, ruleYaml);
onClose(); onClose();
} catch (e) { } catch (e) {
toast.error(t('profiles.editRules.saveError') + ': ' + (e instanceof Error ? e.message : String(e))); alert(t('profiles.editRules.saveError') + ': ' + (e instanceof Error ? e.message : String(e)));
} }
}, [prependRules, deletedRules, rules, appendRules, id, onClose, t]) }, [prependRules, deletedRules, rules, appendRules, id, onClose, t])
@ -689,7 +688,7 @@ const EditRulesModal: React.FC<Props> = (props) => {
} }
if (newRule.type !== 'MATCH' && newRule.payload.trim() !== '' && !validateRulePayload(newRule.type, newRule.payload)) { if (newRule.type !== 'MATCH' && newRule.payload.trim() !== '' && !validateRulePayload(newRule.type, newRule.payload)) {
toast.error(t('profiles.editRules.invalidPayload') + ': ' + getRuleExample(newRule.type)); alert(t('profiles.editRules.invalidPayload') + ': ' + getRuleExample(newRule.type));
return; return;
} }

View File

@ -1,5 +1,4 @@
import { Button, Input, Switch, Tab, Tabs } from '@heroui/react' 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 SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item' import SettingItem from '@renderer/components/base/base-setting-item'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
@ -124,7 +123,7 @@ const GeoData: React.FC = () => {
await mihomoUpgradeGeo() await mihomoUpgradeGeo()
new Notification(t('resources.geoData.updateSuccess')) new Notification(t('resources.geoData.updateSuccess'))
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
setUpdating(false) setUpdating(false)
} }

View File

@ -9,7 +9,6 @@ import useSWR from 'swr'
import SettingCard from '../base/base-setting-card' import SettingCard from '../base/base-setting-card'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { Button, Chip } from '@heroui/react' import { Button, Chip } from '@heroui/react'
import { toast } from '@renderer/components/base/toast'
import { IoMdRefresh } from 'react-icons/io' import { IoMdRefresh } from 'react-icons/io'
import { CgLoadbarDoc } from 'react-icons/cg' import { CgLoadbarDoc } from 'react-icons/cg'
import { MdEditDocument } from 'react-icons/md' import { MdEditDocument } from 'react-icons/md'
@ -68,7 +67,7 @@ const ProxyProvider: React.FC = () => {
await mihomoUpdateProxyProviders(name) await mihomoUpdateProxyProviders(name)
mutate() mutate()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
setUpdating((prev) => { setUpdating((prev) => {
prev[index] = false prev[index] = false

View File

@ -10,7 +10,6 @@ import useSWR from 'swr'
import SettingCard from '../base/base-setting-card' import SettingCard from '../base/base-setting-card'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { Button, Chip } from '@heroui/react' import { Button, Chip } from '@heroui/react'
import { toast } from '@renderer/components/base/toast'
import { IoMdRefresh } from 'react-icons/io' import { IoMdRefresh } from 'react-icons/io'
import { CgLoadbarDoc } from 'react-icons/cg' import { CgLoadbarDoc } from 'react-icons/cg'
import { MdEditDocument } from 'react-icons/md' import { MdEditDocument } from 'react-icons/md'
@ -72,7 +71,7 @@ const RuleProvider: React.FC = () => {
await mihomoUpdateRuleProviders(name) await mihomoUpdateRuleProviders(name)
mutate() mutate()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
setUpdating((prev) => { setUpdating((prev) => {
prev[index] = false prev[index] = false

View File

@ -1,6 +1,5 @@
import { Button, Tooltip } from '@heroui/react' import { Button, Tooltip } from '@heroui/react'
import SettingCard from '../base/base-setting-card' import SettingCard from '../base/base-setting-card'
import { toast } from '@renderer/components/base/toast'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { import {
checkUpdate, checkUpdate,
@ -70,7 +69,7 @@ const Actions: React.FC = () => {
}) })
} }
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
setCheckingUpdate(false) setCheckingUpdate(false)
} }

View File

@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import SettingCard from '../base/base-setting-card' import SettingCard from '../base/base-setting-card'
import { toast } from '@renderer/components/base/toast'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { Button, Input, Select, SelectItem, Switch, Tab, Tabs, Tooltip } from '@heroui/react' import { Button, Input, Select, SelectItem, Switch, Tab, Tabs, Tooltip } from '@heroui/react'
import { BiCopy, BiSolidFileImport } from 'react-icons/bi' import { BiCopy, BiSolidFileImport } from 'react-icons/bi'
@ -106,7 +105,7 @@ const GeneralConfig: React.FC = () => {
await patchAppConfig({ disableHardwareAcceleration: pendingHardwareAccelValue }) await patchAppConfig({ disableHardwareAcceleration: pendingHardwareAccelValue })
await relaunchApp() await relaunchApp()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
setIsRelaunching(false) setIsRelaunching(false)
} }
}} }}
@ -152,7 +151,7 @@ const GeneralConfig: React.FC = () => {
await disableAutoRun() await disableAutoRun()
} }
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
mutateEnable() mutateEnable()
} }
@ -249,7 +248,7 @@ const GeneralConfig: React.FC = () => {
envType: Array.from(v) as ('bash' | 'cmd' | 'powershell')[] envType: Array.from(v) as ('bash' | 'cmd' | 'powershell')[]
}) })
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
}} }}
> >
@ -414,7 +413,7 @@ const GeneralConfig: React.FC = () => {
await patchAppConfig({ useWindowFrame: v }) await patchAppConfig({ useWindowFrame: v })
await relaunchApp() await relaunchApp()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
setIsRelaunching(false) setIsRelaunching(false)
} }
}, 1000)} }, 1000)}
@ -504,7 +503,7 @@ const GeneralConfig: React.FC = () => {
await fetchThemes() await fetchThemes()
setCustomThemes(await resolveThemes()) setCustomThemes(await resolveThemes())
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
setFetching(false) setFetching(false)
} }
@ -524,7 +523,7 @@ const GeneralConfig: React.FC = () => {
await importThemes(files) await importThemes(files)
setCustomThemes(await resolveThemes()) setCustomThemes(await resolveThemes())
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
}} }}
> >
@ -556,7 +555,7 @@ const GeneralConfig: React.FC = () => {
try { try {
await patchAppConfig({ customTheme: v.currentKey as string }) await patchAppConfig({ customTheme: v.currentKey as string })
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
}} }}
> >

View File

@ -1,6 +1,5 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import SettingCard from '../base/base-setting-card' import SettingCard from '../base/base-setting-card'
import { toast } from '@renderer/components/base/toast'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { Button, useDisclosure } from '@heroui/react' import { Button, useDisclosure } from '@heroui/react'
import { exportLocalBackup, importLocalBackup } from '@renderer/utils/ipc' import { exportLocalBackup, importLocalBackup } from '@renderer/utils/ipc'
@ -23,7 +22,7 @@ const LocalBackupConfig: React.FC = () => {
}) })
} }
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
setExporting(false) setExporting(false)
} }
@ -39,7 +38,7 @@ const LocalBackupConfig: React.FC = () => {
}) })
} }
} catch (e) { } catch (e) {
toast.error(t('common.error.importFailed', { error: e })) alert(t('common.error.importFailed', { error: e }))
} finally { } finally {
setImporting(false) setImporting(false)
onClose() onClose()

View File

@ -1,6 +1,5 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import SettingCard from '../base/base-setting-card' import SettingCard from '../base/base-setting-card'
import { toast } from '@renderer/components/base/toast'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { Button, Input, Select, SelectItem, Switch, Tooltip } from '@heroui/react' import { Button, Input, Select, SelectItem, Switch, Tooltip } from '@heroui/react'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
@ -123,7 +122,7 @@ const MihomoConfig: React.FC = () => {
await navigator.clipboard.writeText(`${url}/raw/clash-party.yaml`) await navigator.clipboard.writeText(`${url}/raw/clash-party.yaml`)
} }
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
}} }}
> >
@ -177,7 +176,7 @@ const MihomoConfig: React.FC = () => {
}) })
await restartCore() await restartCore()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
}} }}
> >
@ -209,7 +208,7 @@ const MihomoConfig: React.FC = () => {
await patchAppConfig({ diffWorkDir: v }) await patchAppConfig({ diffWorkDir: v })
await restartCore() await restartCore()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
}} }}
/> />

View File

@ -1,6 +1,5 @@
import { Button, Input } from '@heroui/react' import { Button, Input } from '@heroui/react'
import SettingCard from '../base/base-setting-card' import SettingCard from '../base/base-setting-card'
import { toast } from '@renderer/components/base/toast'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
import React, { KeyboardEvent, useState } from 'react' import React, { KeyboardEvent, useState } from 'react'
@ -214,10 +213,10 @@ const ShortcutInput: React.FC<{
await patchAppConfig({ [action]: inputValue }) await patchAppConfig({ [action]: inputValue })
window.electron.ipcRenderer.send('updateTrayMenu') window.electron.ipcRenderer.send('updateTrayMenu')
} else { } else {
toast.error(t('common.error.shortcutRegistrationFailed')) alert(t('common.error.shortcutRegistrationFailed'))
} }
} catch (e) { } catch (e) {
toast.error(t('common.error.shortcutRegistrationFailedWithError', { error: e })) alert(t('common.error.shortcutRegistrationFailedWithError', { error: e }))
} }
}} }}
> >

View File

@ -1,6 +1,5 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import SettingCard from '@renderer/components/base/base-setting-card' 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 SettingItem from '@renderer/components/base/base-setting-item'
import { Button, Input, Switch } from '@heroui/react' import { Button, Input, Switch } from '@heroui/react'
import { import {
@ -56,7 +55,7 @@ const SubStoreConfig: React.FC = () => {
await stopSubStoreBackendServer() await stopSubStoreBackendServer()
} }
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
}} }}
/> />
@ -77,7 +76,7 @@ const SubStoreConfig: React.FC = () => {
await startSubStoreFrontendServer() await startSubStoreFrontendServer()
await startSubStoreBackendServer() await startSubStoreBackendServer()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
}} }}
/> />
@ -95,7 +94,7 @@ const SubStoreConfig: React.FC = () => {
await startSubStoreBackendServer() await startSubStoreBackendServer()
} }
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
}} }}
/> />
@ -124,7 +123,7 @@ const SubStoreConfig: React.FC = () => {
await patchAppConfig({ useProxyInSubStore: v }) await patchAppConfig({ useProxyInSubStore: v })
await startSubStoreBackendServer() await startSubStoreBackendServer()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
}} }}
/> />
@ -145,7 +144,7 @@ const SubStoreConfig: React.FC = () => {
}) })
new Notification(t('common.notification.restartRequired')) new Notification(t('common.notification.restartRequired'))
} else { } else {
toast.warning(t('common.error.invalidCron')) alert(t('common.error.invalidCron'))
} }
}} }}
> >
@ -178,7 +177,7 @@ const SubStoreConfig: React.FC = () => {
}) })
new Notification(t('common.notification.restartRequired')) new Notification(t('common.notification.restartRequired'))
} else { } else {
toast.warning(t('common.error.invalidCron')) alert(t('common.error.invalidCron'))
} }
}} }}
> >
@ -211,7 +210,7 @@ const SubStoreConfig: React.FC = () => {
}) })
new Notification(t('common.notification.restartRequired')) new Notification(t('common.notification.restartRequired'))
} else { } else {
toast.warning(t('common.error.invalidCron')) alert(t('common.error.invalidCron'))
} }
}} }}
> >

View File

@ -1,6 +1,5 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import SettingCard from '../base/base-setting-card' import SettingCard from '../base/base-setting-card'
import { toast } from '@renderer/components/base/toast'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { Button, Input, Select, SelectItem } from '@heroui/react' import { Button, Input, Select, SelectItem } from '@heroui/react'
import { listWebdavBackups, webdavBackup, reinitWebdavBackupScheduler } from '@renderer/utils/ipc' import { listWebdavBackups, webdavBackup, reinitWebdavBackupScheduler } from '@renderer/utils/ipc'
@ -48,7 +47,7 @@ const WebdavConfig: React.FC = () => {
body: t('webdav.notification.backupSuccess.body') body: t('webdav.notification.backupSuccess.body')
}) })
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
setBackuping(false) setBackuping(false)
} }
@ -61,7 +60,7 @@ const WebdavConfig: React.FC = () => {
setFilenames(filenames) setFilenames(filenames)
setRestoreOpen(true) setRestoreOpen(true)
} catch (e) { } catch (e) {
toast.error(t('common.error.getBackupListFailed', { error: e })) alert(t('common.error.getBackupListFailed', { error: e }))
} finally { } finally {
setRestoring(false) setRestoring(false)
} }
@ -157,7 +156,7 @@ const WebdavConfig: React.FC = () => {
new Notification(t('webdav.notification.cronUpdateFailed')) new Notification(t('webdav.notification.cronUpdateFailed'))
} }
} else { } else {
toast.warning(t('common.error.invalidCron')) alert(t('common.error.invalidCron'))
} }
}} }}
> >

View File

@ -1,5 +1,4 @@
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react' 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 { relaunchApp, webdavDelete, webdavRestore } from '@renderer/utils/ipc'
import React, { useState } from 'react' import React, { useState } from 'react'
import { MdDeleteForever } from 'react-icons/md' import { MdDeleteForever } from 'react-icons/md'
@ -44,7 +43,7 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
await webdavRestore(filename) await webdavRestore(filename)
await relaunchApp() await relaunchApp()
} catch (e) { } catch (e) {
toast.error(t('common.error.restoreFailed', { error: e })) alert(t('common.error.restoreFailed', { error: e }))
} finally { } finally {
setRestoring(false) setRestoring(false)
} }
@ -62,7 +61,7 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
await webdavDelete(filename) await webdavDelete(filename)
setFilenames(filenames.filter((name) => name !== filename)) setFilenames(filenames.filter((name) => name !== filename))
} catch (e) { } catch (e) {
toast.error(t('common.error.deleteFailed', { error: e })) alert(t('common.error.deleteFailed', { error: e }))
} }
}} }}
> >

View File

@ -1,5 +1,4 @@
import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react' 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 { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import BorderSwitch from '@renderer/components/base/border-swtich' import BorderSwitch from '@renderer/components/base/border-swtich'
import { LuServer } from 'react-icons/lu' import { LuServer } from 'react-icons/lu'
@ -40,7 +39,7 @@ const DNSCard: React.FC<Props> = (props) => {
await patchControledMihomoConfig({}) await patchControledMihomoConfig({})
await restartCore() await restartCore()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
} }

View File

@ -1,5 +1,4 @@
import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react' import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
import { toast } from '@renderer/components/base/toast'
import { calcTraffic } from '@renderer/utils/calc' import { calcTraffic } from '@renderer/utils/calc'
import { mihomoVersion, restartCore } from '@renderer/utils/ipc' import { mihomoVersion, restartCore } from '@renderer/utils/ipc'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
@ -113,7 +112,7 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
try { try {
await restartCore() await restartCore()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
mutate() mutate()
} }

View File

@ -1,5 +1,4 @@
import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react' 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 BorderSwitch from '@renderer/components/base/border-swtich'
import { RiScan2Fill } from 'react-icons/ri' import { RiScan2Fill } from 'react-icons/ri'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
@ -40,7 +39,7 @@ const SniffCard: React.FC<Props> = (props) => {
await patchControledMihomoConfig({}) await patchControledMihomoConfig({})
await restartCore() await restartCore()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
} }

View File

@ -1,5 +1,4 @@
import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react' 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 BorderSwitch from '@renderer/components/base/border-swtich'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
@ -55,7 +54,7 @@ const SysproxySwitcher: React.FC<Props> = (props) => {
await patchAppConfig({ sysProxy: { enable: previousState } }) await patchAppConfig({ sysProxy: { enable: previousState } })
// 回滚图标 // 回滚图标
updateTrayIconImmediate(previousState, tunEnabled) updateTrayIconImmediate(previousState, tunEnabled)
toast.error(String(e)) alert(e)
} }
} }

View File

@ -1,5 +1,12 @@
import { Button, Code, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/react' import {
import { toast } from '@renderer/components/base/toast' Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Button,
Code
} from '@heroui/react'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import React, { useState } from 'react' import React, { useState } from 'react'
import { downloadAndInstallUpdate } from '@renderer/utils/ipc' import { downloadAndInstallUpdate } from '@renderer/utils/ipc'
@ -20,7 +27,7 @@ const UpdaterModal: React.FC<Props> = (props) => {
try { try {
await downloadAndInstallUpdate(version) await downloadAndInstallUpdate(version)
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
} }
@ -75,7 +82,7 @@ const UpdaterModal: React.FC<Props> = (props) => {
await onUpdate() await onUpdate()
onClose() onClose()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
setDownloading(false) setDownloading(false)
} }

View File

@ -1,5 +1,4 @@
import React, { createContext, useContext, ReactNode } from 'react' import React, { createContext, useContext, ReactNode } from 'react'
import { toast } from '@renderer/components/base/toast'
import useSWR from 'swr' import useSWR from 'swr'
import { getAppConfig, patchAppConfig as patch } from '@renderer/utils/ipc' import { getAppConfig, patchAppConfig as patch } from '@renderer/utils/ipc'
@ -18,7 +17,7 @@ export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children
try { try {
await patch(value) await patch(value)
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
mutateAppConfig() mutateAppConfig()
} }

View File

@ -1,5 +1,4 @@
import React, { createContext, useContext, ReactNode } from 'react' import React, { createContext, useContext, ReactNode } from 'react'
import { toast } from '@renderer/components/base/toast'
import useSWR from 'swr' import useSWR from 'swr'
import { getControledMihomoConfig, patchControledMihomoConfig as patch } from '@renderer/utils/ipc' import { getControledMihomoConfig, patchControledMihomoConfig as patch } from '@renderer/utils/ipc'
@ -23,7 +22,7 @@ export const ControledMihomoConfigProvider: React.FC<{ children: ReactNode }> =
try { try {
await patch(value) await patch(value)
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
mutateControledMihomoConfig() mutateControledMihomoConfig()
} }

View File

@ -1,5 +1,4 @@
import React, { createContext, useContext, ReactNode } from 'react' import React, { createContext, useContext, ReactNode } from 'react'
import { toast } from '@renderer/components/base/toast'
import useSWR from 'swr' import useSWR from 'swr'
import { import {
getOverrideConfig, getOverrideConfig,
@ -29,7 +28,7 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil
try { try {
await set(config) await set(config)
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
mutateOverrideConfig() mutateOverrideConfig()
} }
@ -39,7 +38,7 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil
try { try {
await add(item) await add(item)
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
mutateOverrideConfig() mutateOverrideConfig()
} }
@ -49,7 +48,7 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil
try { try {
await remove(id) await remove(id)
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
mutateOverrideConfig() mutateOverrideConfig()
} }
@ -59,7 +58,7 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil
try { try {
await update(item) await update(item)
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
mutateOverrideConfig() mutateOverrideConfig()
} }

View File

@ -1,5 +1,4 @@
import React, { createContext, ReactNode, useContext } from 'react' import React, { createContext, ReactNode, useContext } from 'react'
import { toast } from '@renderer/components/base/toast'
import useSWR from 'swr' import useSWR from 'swr'
import { import {
addProfileItem as add, addProfileItem as add,
@ -33,7 +32,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
try { try {
await set(config) await set(config)
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
mutateProfileConfig() mutateProfileConfig()
window.electron.ipcRenderer.send('updateTrayMenu') window.electron.ipcRenderer.send('updateTrayMenu')
@ -44,7 +43,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
try { try {
await add(item) await add(item)
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
mutateProfileConfig() mutateProfileConfig()
window.electron.ipcRenderer.send('updateTrayMenu') window.electron.ipcRenderer.send('updateTrayMenu')
@ -55,7 +54,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
try { try {
await remove(id) await remove(id)
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
mutateProfileConfig() mutateProfileConfig()
window.electron.ipcRenderer.send('updateTrayMenu') window.electron.ipcRenderer.send('updateTrayMenu')
@ -66,7 +65,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
try { try {
await update(item) await update(item)
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
mutateProfileConfig() mutateProfileConfig()
window.electron.ipcRenderer.send('updateTrayMenu') window.electron.ipcRenderer.send('updateTrayMenu')
@ -108,7 +107,7 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
if (errorMsg.includes('reply was never sent')) { if (errorMsg.includes('reply was never sent')) {
setTimeout(() => mutateProfileConfig(), 1000) setTimeout(() => mutateProfileConfig(), 1000)
} else { } else {
toast.error(errorMsg, '切换配置失败') alert(`切换 Profile 失败: ${errorMsg}`)
mutateProfileConfig() mutateProfileConfig()
} }
} finally { } finally {

View File

@ -14,7 +14,6 @@ import { OverrideConfigProvider } from './hooks/use-override-config'
import { ProfileConfigProvider } from './hooks/use-profile-config' import { ProfileConfigProvider } from './hooks/use-profile-config'
import { RulesProvider } from './hooks/use-rules' import { RulesProvider } from './hooks/use-rules'
import { GroupsProvider } from './hooks/use-groups' import { GroupsProvider } from './hooks/use-groups'
import { ToastProvider } from './components/base/toast'
import './i18n' import './i18n'
let F12Count = 0 let F12Count = 0
@ -54,9 +53,7 @@ init().then(() => {
<OverrideConfigProvider> <OverrideConfigProvider>
<GroupsProvider> <GroupsProvider>
<RulesProvider> <RulesProvider>
<ToastProvider>
<App /> <App />
</ToastProvider>
</RulesProvider> </RulesProvider>
</GroupsProvider> </GroupsProvider>
</OverrideConfigProvider> </OverrideConfigProvider>

View File

@ -1,6 +1,6 @@
import BasePage from '@renderer/components/base/base-page' import BasePage from '@renderer/components/base/base-page'
import { mihomoCloseAllConnections, mihomoCloseConnection } from '@renderer/utils/ipc' import { mihomoCloseAllConnections, mihomoCloseConnection } from '@renderer/utils/ipc'
import { Key, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Key, useCallback, useEffect, useMemo, useState } from 'react'
import { Badge, Button, Divider, Input, Select, SelectItem, Tab, Tabs } from '@heroui/react' import { Badge, Button, Divider, Input, Select, SelectItem, Tab, Tabs } from '@heroui/react'
import { calcTraffic } from '@renderer/utils/calc' import { calcTraffic } from '@renderer/utils/calc'
import ConnectionItem from '@renderer/components/connections/connection-item' import ConnectionItem from '@renderer/components/connections/connection-item'
@ -48,22 +48,6 @@ const Connections: React.FC = () => {
const [viewMode, setViewMode] = useState<'list' | 'table'>(connectionViewMode) const [viewMode, setViewMode] = useState<'list' | 'table'>(connectionViewMode)
const [visibleColumns, setVisibleColumns] = useState<Set<string>>(new Set(connectionTableColumns)) const [visibleColumns, setVisibleColumns] = useState<Set<string>>(new Set(connectionTableColumns))
const activeConnectionsRef = useRef(activeConnections)
const allConnectionsRef = useRef(allConnections)
useEffect(() => {
activeConnectionsRef.current = activeConnections
allConnectionsRef.current = allConnections
}, [activeConnections, allConnections])
const selectedConnection = useMemo(() => {
if (!selected) return undefined
return (
activeConnections.find((c) => c.id === selected.id) ||
closedConnections.find((c) => c.id === selected.id) ||
selected
)
}, [selected, activeConnections, closedConnections])
const handleColumnWidthChange = useCallback(async (widths: Record<string, number>) => { const handleColumnWidthChange = useCallback(async (widths: Record<string, number>) => {
await patchAppConfig({ connectionTableColumnWidths: widths }) await patchAppConfig({ connectionTableColumnWidths: widths })
}, [patchAppConfig]) }, [patchAppConfig])
@ -114,80 +98,71 @@ const Connections: React.FC = () => {
const closeAllConnections = useCallback((): void => { const closeAllConnections = useCallback((): void => {
tab === 'active' ? mihomoCloseAllConnections() : trashAllClosedConnection() tab === 'active' ? mihomoCloseAllConnections() : trashAllClosedConnection()
}, [tab]) }, [tab, closedConnections])
const closeConnection = useCallback((id: string): void => { const closeConnection = useCallback((id: string): void => {
tab === 'active' ? mihomoCloseConnection(id) : trashClosedConnection(id) tab === 'active' ? mihomoCloseConnection(id) : trashClosedConnection(id)
}, [tab]) }, [tab])
const trashAllClosedConnection = (): void => { const trashAllClosedConnection = (): void => {
setClosedConnections((closedConns) => { const trashIds = closedConnections.map((conn) => conn.id)
const trashIds = new Set(closedConns.map((conn) => conn.id)) setAllConnections((allConns) => allConns.filter((conn) => !trashIds.includes(conn.id)))
setAllConnections((allConns) => { setClosedConnections([])
const filtered = allConns.filter((conn) => !trashIds.has(conn.id))
cachedConnections = filtered cachedConnections = allConnections
return filtered
})
return []
})
} }
const trashClosedConnection = (id: string): void => { const trashClosedConnection = (id: string): void => {
setAllConnections((allConns) => { setAllConnections((allConns) => allConns.filter((conn) => conn.id != id))
const filtered = allConns.filter((conn) => conn.id !== id) setClosedConnections((closedConns) => closedConns.filter((conn) => conn.id != id))
cachedConnections = filtered
return filtered cachedConnections = allConnections
})
setClosedConnections((closedConns) => closedConns.filter((conn) => conn.id !== id))
} }
useEffect(() => { useEffect(() => {
const handler = (_e: unknown, info: IMihomoConnectionsInfo): void => { if (isPaused) return
window.electron.ipcRenderer.on('mihomoConnections', (_e, info: IMihomoConnectionsInfo) => {
setConnectionsInfo(info) setConnectionsInfo(info)
if (!info.connections) return if (!info.connections) return
const allConns = unionWith( const allConns = unionWith(activeConnections, allConnections, (a, b) => a.id === b.id)
activeConnectionsRef.current,
allConnectionsRef.current,
(a, b) => a.id === b.id
)
const prevConnMap = new Map(activeConnectionsRef.current.map((c) => [c.id, c]))
const activeConns = info.connections.map((conn) => { const activeConns = info.connections.map((conn) => {
const preConn = prevConnMap.get(conn.id) const preConn = activeConnections.find((c) => c.id === conn.id)
const downloadSpeed = preConn ? conn.download - preConn.download : 0
const uploadSpeed = preConn ? conn.upload - preConn.upload : 0
return { return {
...conn, ...conn,
isActive: true, isActive: true,
downloadSpeed: preConn ? conn.download - preConn.download : 0, downloadSpeed: downloadSpeed,
uploadSpeed: preConn ? conn.upload - preConn.upload : 0 uploadSpeed: uploadSpeed
} }
}) })
const closedConns = differenceWith(allConns, activeConns, (a, b) => a.id === b.id).map( const closedConns = differenceWith(allConns, activeConns, (a, b) => a.id === b.id).map(
(conn) => ({ (conn) => {
return {
...conn, ...conn,
isActive: false, isActive: false,
downloadSpeed: 0, downloadSpeed: 0,
uploadSpeed: 0 uploadSpeed: 0
}) }
}
) )
setActiveConnections(activeConns) setActiveConnections(activeConns)
setClosedConnections(closedConns) setClosedConnections(closedConns)
setAllConnections(allConns.slice(-(activeConns.length + 200))) setAllConnections(allConns.slice(-(activeConns.length + 200)))
cachedConnections = allConns
}
if (!isPaused) { cachedConnections = allConnections
window.electron.ipcRenderer.on('mihomoConnections', handler) })
}
return (): void => { return (): void => {
window.electron.ipcRenderer.removeAllListeners('mihomoConnections') window.electron.ipcRenderer.removeAllListeners('mihomoConnections')
} }
}, [isPaused]) }, [allConnections, activeConnections, closedConnections, isPaused])
const togglePause = useCallback(() => { const togglePause = () => {
setIsPaused((prev) => !prev) setIsPaused(!isPaused)
}, []) }
return ( return (
<BasePage <BasePage
@ -207,7 +182,7 @@ const Connections: React.FC = () => {
color="primary" color="primary"
variant="flat" variant="flat"
showOutline={false} showOutline={false}
content={filteredConnections.length} content={`${filteredConnections.length}`}
> >
<Button <Button
className="app-nodrag ml-1" className="app-nodrag ml-1"
@ -255,14 +230,14 @@ const Connections: React.FC = () => {
</div> </div>
} }
> >
{isDetailModalOpen && selectedConnection && ( {isDetailModalOpen && selected && (
<ConnectionDetailModal onClose={() => setIsDetailModalOpen(false)} connection={selectedConnection} /> <ConnectionDetailModal onClose={() => setIsDetailModalOpen(false)} connection={selected} />
)} )}
<div className="overflow-x-auto sticky top-0 z-40"> <div className="overflow-x-auto sticky top-0 z-40">
<div className="flex p-2 gap-2"> <div className="flex p-2 gap-2">
<Tabs <Tabs
size="sm" size="sm"
color={tab === 'active' ? 'primary' : 'danger'} color={`${tab === 'active' ? 'primary' : 'danger'}`}
selectedKey={tab} selectedKey={tab}
variant="underlined" variant="underlined"
className="w-fit h-[32px]" className="w-fit h-[32px]"
@ -274,7 +249,7 @@ const Connections: React.FC = () => {
key="active" key="active"
title={ title={
<Badge <Badge
color={tab === 'active' ? 'primary' : 'default'} color={`${tab === 'active' ? 'primary' : 'default'}`}
size="sm" size="sm"
shape="circle" shape="circle"
variant="flat" variant="flat"
@ -289,7 +264,7 @@ const Connections: React.FC = () => {
key="closed" key="closed"
title={ title={
<Badge <Badge
color={tab === 'closed' ? 'danger' : 'default'} color={`${tab === 'closed' ? 'danger' : 'default'}`}
size="sm" size="sm"
shape="circle" shape="circle"
variant="flat" variant="flat"
@ -364,7 +339,7 @@ const Connections: React.FC = () => {
size="sm" size="sm"
className="w-[180px] min-w-[131px]" className="w-[180px] min-w-[131px]"
aria-label={t('connections.orderBy')} aria-label={t('connections.orderBy')}
selectedKeys={[connectionOrderBy]} selectedKeys={new Set([connectionOrderBy])}
disallowEmptySelection={true} disallowEmptySelection={true}
onSelectionChange={async (v) => { onSelectionChange={async (v) => {
await patchAppConfig({ await patchAppConfig({
@ -387,7 +362,7 @@ const Connections: React.FC = () => {
size="sm" size="sm"
isIconOnly isIconOnly
className="bg-content2" className="bg-content2"
onPress={() => { onPress={async () => {
patchAppConfig({ patchAppConfig({
connectionDirection: connectionDirection === 'asc' ? 'desc' : 'asc' connectionDirection: connectionDirection === 'asc' ? 'desc' : 'asc'
}) })
@ -412,6 +387,7 @@ const Connections: React.FC = () => {
<ConnectionItem <ConnectionItem
setSelected={setSelected} setSelected={setSelected}
setIsDetailModalOpen={setIsDetailModalOpen} setIsDetailModalOpen={setIsDetailModalOpen}
selected={selected}
close={closeConnection} close={closeConnection}
index={i} index={i}
key={connection.id} key={connection.id}

View File

@ -1,6 +1,5 @@
import { Button, Tab, Input, Switch, Tabs, Divider } from '@heroui/react' import { Button, Tab, Input, Switch, Tabs, Divider } from '@heroui/react'
import BasePage from '@renderer/components/base/base-page' import BasePage from '@renderer/components/base/base-page'
import { toast } from '@renderer/components/base/toast'
import { MdDeleteForever } from 'react-icons/md' import { MdDeleteForever } from 'react-icons/md'
import SettingCard from '@renderer/components/base/base-setting-card' import SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item' import SettingItem from '@renderer/components/base/base-setting-item'
@ -146,7 +145,7 @@ const DNS: React.FC = () => {
await restartCore() await restartCore()
} }
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
} }

View File

@ -121,14 +121,17 @@ const Logs: React.FC = () => {
<Virtuoso <Virtuoso
ref={virtuosoRef} ref={virtuosoRef}
data={filteredLogs} data={filteredLogs}
itemContent={(i, log) => ( itemContent={(i, log) => {
return (
<LogItem <LogItem
index={i} index={i}
key={log.payload + i}
time={log.time} time={log.time}
type={log.type} type={log.type}
payload={log.payload} payload={log.payload}
/> />
)} )
}}
/> />
</div> </div>
</BasePage> </BasePage>

View File

@ -1,6 +1,5 @@
import { Button, Divider, Input, Select, SelectItem, Switch, Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure, Spinner, Chip } from '@heroui/react' 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 BasePage from '@renderer/components/base/base-page'
import { toast } from '@renderer/components/base/toast'
import SettingCard from '@renderer/components/base/base-setting-card' import SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item' import SettingItem from '@renderer/components/base/base-setting-item'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
@ -45,6 +44,10 @@ const Mihomo: React.FC = () => {
smartCollectorSize = 100, smartCollectorSize = 100,
maxLogDays = 7, maxLogDays = 7,
sysProxy, sysProxy,
disableLoopbackDetector,
disableEmbedCA,
disableSystemCA,
skipSafePathCheck,
showMixedPort, showMixedPort,
enableMixedPort = true, enableMixedPort = true,
showSocksPort, showSocksPort,
@ -309,7 +312,7 @@ const Mihomo: React.FC = () => {
if (errorMessage.includes('配置检查失败') || errorMessage.includes('Profile Check Failed')) { if (errorMessage.includes('配置检查失败') || errorMessage.includes('Profile Check Failed')) {
await showDetailedError(t('mihomo.error.profileCheckFailed'), errorMessage) await showDetailedError(t('mihomo.error.profileCheckFailed'), errorMessage)
} else { } else {
toast.error(errorMessage) alert(errorMessage)
} }
} finally { } finally {
PubSub.publish('mihomo-core-changed') PubSub.publish('mihomo-core-changed')
@ -325,7 +328,7 @@ const Mihomo: React.FC = () => {
} catch (error) { } catch (error) {
console.error('Failed to fetch tags:', error) console.error('Failed to fetch tags:', error)
setTags([]) setTags([])
toast.error(t('mihomo.error.fetchTagsFailed')) alert(t('mihomo.error.fetchTagsFailed'))
} finally { } finally {
setLoadingTags(false) setLoadingTags(false)
} }
@ -356,7 +359,7 @@ const Mihomo: React.FC = () => {
new Notification(t('mihomo.coreUpgradeSuccess')) new Notification(t('mihomo.coreUpgradeSuccess'))
} catch (error) { } catch (error) {
console.error('Failed to install specific core:', error) console.error('Failed to install specific core:', error)
toast.error(t('mihomo.error.installCoreFailed')) alert(t('mihomo.error.installCoreFailed'))
} finally { } finally {
setInstalling(false) setInstalling(false)
} }
@ -494,7 +497,7 @@ const Mihomo: React.FC = () => {
if (typeof e === 'string' && e.includes('already using latest version')) { if (typeof e === 'string' && e.includes('already using latest version')) {
new Notification(t('mihomo.alreadyLatestVersion')) new Notification(t('mihomo.alreadyLatestVersion'))
} else { } else {
toast.error(String(e)) alert(e)
} }
} finally { } finally {
setUpgrading(false) setUpgrading(false)

View File

@ -8,7 +8,6 @@ import {
Input Input
} from '@heroui/react' } from '@heroui/react'
import BasePage from '@renderer/components/base/base-page' import BasePage from '@renderer/components/base/base-page'
import { toast } from '@renderer/components/base/toast'
import { getFilePath, readTextFile } from '@renderer/utils/ipc' import { getFilePath, readTextFile } from '@renderer/utils/ipc'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { MdContentPaste } from 'react-icons/md' import { MdContentPaste } from 'react-icons/md'
@ -76,29 +75,18 @@ const Override: React.FC = () => {
} }
} }
const addOverrideItemRef = useRef(addOverrideItem)
addOverrideItemRef.current = addOverrideItem
const tRef = useRef(t)
tRef.current = t
useEffect(() => { useEffect(() => {
const element = pageRef.current pageRef.current?.addEventListener('dragover', (e) => {
if (!element) return
const handleDragOver = (e: DragEvent): void => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
setFileOver(true) setFileOver(true)
} })
pageRef.current?.addEventListener('dragleave', (e) => {
const handleDragLeave = (e: DragEvent): void => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
setFileOver(false) setFileOver(false)
} })
pageRef.current?.addEventListener('drop', async (event) => {
const handleDrop = async (event: DragEvent): Promise<void> => {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
if (event.dataTransfer?.files) { if (event.dataTransfer?.files) {
@ -106,7 +94,7 @@ const Override: React.FC = () => {
if (file.name.endsWith('.js') || file.name.endsWith('.yaml')) { if (file.name.endsWith('.js') || file.name.endsWith('.yaml')) {
const content = await readTextFile((file as File & { path: string }).path) const content = await readTextFile((file as File & { path: string }).path)
try { try {
await addOverrideItemRef.current({ await addOverrideItem({
name: file.name, name: file.name,
type: 'local', type: 'local',
file: content, file: content,
@ -116,20 +104,15 @@ const Override: React.FC = () => {
setFileOver(false) setFileOver(false)
} }
} else { } else {
toast.warning(tRef.current('override.unsupportedFileType')) alert(t('override.unsupportedFileType'))
} }
} }
setFileOver(false) setFileOver(false)
} })
element.addEventListener('dragover', handleDragOver)
element.addEventListener('dragleave', handleDragLeave)
element.addEventListener('drop', handleDrop)
return (): void => { return (): void => {
element.removeEventListener('dragover', handleDragOver) pageRef.current?.removeEventListener('dragover', () => {})
element.removeEventListener('dragleave', handleDragLeave) pageRef.current?.removeEventListener('dragleave', () => {})
element.removeEventListener('drop', handleDrop) pageRef.current?.removeEventListener('drop', () => {})
} }
}, []) }, [])
@ -223,7 +206,7 @@ const Override: React.FC = () => {
}) })
} }
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
} else if (key === 'new-yaml') { } else if (key === 'new-yaml') {
await addOverrideItem({ await addOverrideItem({

View File

@ -11,7 +11,6 @@ import {
Tooltip Tooltip
} from '@heroui/react' } from '@heroui/react'
import BasePage from '@renderer/components/base/base-page' import BasePage from '@renderer/components/base/base-page'
import { toast } from '@renderer/components/base/toast'
import ProfileItem from '@renderer/components/profiles/profile-item' import ProfileItem from '@renderer/components/profiles/profile-item'
import { useProfileConfig } from '@renderer/hooks/use-profile-config' import { useProfileConfig } from '@renderer/hooks/use-profile-config'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
@ -157,37 +156,26 @@ const Profiles: React.FC = () => {
} }
} }
const handleImportRef = useRef(handleImport) const handleInputKeyUp = useCallback(
handleImportRef.current = handleImport (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Enter' || isUrlEmpty) return
const addProfileItemRef = useRef(addProfileItem) handleImport()
addProfileItemRef.current = addProfileItem },
[isUrlEmpty]
const tRef = useRef(t) )
tRef.current = t
const handleInputKeyUp = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Enter' || e.currentTarget.value.trim() === '') return
handleImportRef.current()
}, [])
useEffect(() => { useEffect(() => {
const element = pageRef.current pageRef.current?.addEventListener('dragover', (e) => {
if (!element) return
const handleDragOver = (e: DragEvent): void => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
setFileOver(true) setFileOver(true)
} })
pageRef.current?.addEventListener('dragleave', (e) => {
const handleDragLeave = (e: DragEvent): void => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
setFileOver(false) setFileOver(false)
} })
pageRef.current?.addEventListener('drop', async (event) => {
const handleDrop = async (event: DragEvent): Promise<void> => {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
if (event.dataTransfer?.files) { if (event.dataTransfer?.files) {
@ -196,25 +184,20 @@ const Profiles: React.FC = () => {
try { try {
const path = window.api.webUtils.getPathForFile(file) const path = window.api.webUtils.getPathForFile(file)
const content = await readTextFile(path) const content = await readTextFile(path)
await addProfileItemRef.current({ name: file.name, type: 'local', file: content }) await addProfileItem({ name: file.name, type: 'local', file: content })
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
} else { } else {
toast.warning(tRef.current('profiles.error.unsupportedFileType')) alert(t('profiles.error.unsupportedFileType'))
} }
} }
setFileOver(false) setFileOver(false)
} })
element.addEventListener('dragover', handleDragOver)
element.addEventListener('dragleave', handleDragLeave)
element.addEventListener('drop', handleDrop)
return (): void => { return (): void => {
element.removeEventListener('dragover', handleDragOver) pageRef.current?.removeEventListener('dragover', () => {})
element.removeEventListener('dragleave', handleDragLeave) pageRef.current?.removeEventListener('dragleave', () => {})
element.removeEventListener('drop', handleDrop) pageRef.current?.removeEventListener('drop', () => {})
} }
}, []) }, [])
@ -346,7 +329,7 @@ const Profiles: React.FC = () => {
useProxy useProxy
}) })
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
setSubStoreImporting(false) setSubStoreImporting(false)
} }
@ -367,7 +350,7 @@ const Profiles: React.FC = () => {
useProxy useProxy
}) })
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
setSubStoreImporting(false) setSubStoreImporting(false)
} }
@ -399,7 +382,7 @@ const Profiles: React.FC = () => {
await addProfileItem({ name: fileName, type: 'local', file: content }) await addProfileItem({ name: fileName, type: 'local', file: content })
} }
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
} else if (key === 'new') { } else if (key === 'new') {
await addProfileItem({ await addProfileItem({

View File

@ -1,6 +1,5 @@
import { Button, Divider, Input, Switch } from '@heroui/react' import { Button, Divider, Input, Switch } from '@heroui/react'
import BasePage from '@renderer/components/base/base-page' 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 SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item' import SettingItem from '@renderer/components/base/base-setting-item'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
@ -71,7 +70,7 @@ const Sniffer: React.FC = () => {
await restartCore() await restartCore()
} }
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
} }

View File

@ -1,6 +1,5 @@
import { Button, Input, Tab, Tabs } from '@heroui/react' import { Button, Input, Tab, Tabs } from '@heroui/react'
import BasePage from '@renderer/components/base/base-page' 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 SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item' import SettingItem from '@renderer/components/base/base-setting-item'
import PacEditorModal from '@renderer/components/sysproxy/pac-editor-modal' import PacEditorModal from '@renderer/components/sysproxy/pac-editor-modal'
@ -106,7 +105,7 @@ const Sysproxy: React.FC = () => {
} catch (e) { } catch (e) {
setValues({ ...values, enable: previousState }) setValues({ ...values, enable: previousState })
setChanged(true) setChanged(true)
toast.error(String(e)) alert(e)
await patchAppConfig({ sysProxy: { enable: false } }) await patchAppConfig({ sysProxy: { enable: false } })
} }

View File

@ -1,6 +1,5 @@
import { Button, Input, Switch, Tab, Tabs } from '@heroui/react' import { Button, Input, Switch, Tab, Tabs } from '@heroui/react'
import BasePage from '@renderer/components/base/base-page' 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 SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item' import SettingItem from '@renderer/components/base/base-setting-item'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
@ -113,7 +112,7 @@ const Tun: React.FC = () => {
new Notification(t('tun.notifications.firewallResetSuccess')) new Notification(t('tun.notifications.firewallResetSuccess'))
await restartCore() await restartCore()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -134,7 +133,7 @@ const Tun: React.FC = () => {
new Notification(t('tun.notifications.coreAuthSuccess')) new Notification(t('tun.notifications.coreAuthSuccess'))
await restartCore() await restartCore()
} catch (e) { } catch (e) {
toast.error(String(e)) alert(e)
} }
}} }}
> >

View File

@ -556,3 +556,10 @@ export async function getRuleStr(id: string): Promise<string> {
export async function setRuleStr(id: string, str: string): Promise<void> { export async function setRuleStr(id: string, str: string): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setRuleStr', id, str)) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setRuleStr', id, str))
} }
async function alert<T>(msg: T): Promise<void> {
const msgStr = typeof msg === 'string' ? msg : JSON.stringify(msg)
return await window.electron.ipcRenderer.invoke('alert', msgStr)
}
window.alert = alert