mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-02-10 11:40:28 +08:00
refactor: extract common config context logic into factory function
This commit is contained in:
parent
38389e0c3c
commit
2c638f56c0
72
src/renderer/src/hooks/create-config-context.tsx
Normal file
72
src/renderer/src/hooks/create-config-context.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { createContext, useContext, ReactNode, useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { showError } from '@renderer/utils/error-display'
|
||||
import useSWR, { KeyedMutator } from 'swr'
|
||||
|
||||
interface ConfigContextValue<T> {
|
||||
config: T | undefined
|
||||
mutate: KeyedMutator<T>
|
||||
}
|
||||
|
||||
interface CreateConfigContextOptions<T> {
|
||||
swrKey: string
|
||||
fetcher: () => Promise<T>
|
||||
ipcEvent: string
|
||||
}
|
||||
|
||||
export function createConfigContext<T>(options: CreateConfigContextOptions<T>) {
|
||||
const { swrKey, fetcher, ipcEvent } = options
|
||||
const Context = createContext<ConfigContextValue<T> | undefined>(undefined)
|
||||
|
||||
const Provider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const { data: config, mutate } = useSWR(swrKey, fetcher)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (): void => {
|
||||
mutate()
|
||||
}
|
||||
window.electron.ipcRenderer.on(ipcEvent, handler)
|
||||
return () => {
|
||||
window.electron.ipcRenderer.removeListener(ipcEvent, handler)
|
||||
}
|
||||
}, [mutate])
|
||||
|
||||
return <Context.Provider value={{ config, mutate }}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
const useConfig = (): ConfigContextValue<T> => {
|
||||
const context = useContext(Context)
|
||||
if (!context) {
|
||||
throw new Error(`useConfig must be used within Provider`)
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
return { Provider, useConfig, Context }
|
||||
}
|
||||
|
||||
interface ActionOptions {
|
||||
errorKey: string
|
||||
updateTray?: boolean
|
||||
}
|
||||
|
||||
export function useConfigAction<T>(
|
||||
mutate: KeyedMutator<T>,
|
||||
action: () => Promise<void>,
|
||||
options: ActionOptions
|
||||
): () => Promise<void> {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useCallback(async () => {
|
||||
try {
|
||||
await action()
|
||||
} catch (e) {
|
||||
await showError(e, t(options.errorKey))
|
||||
} finally {
|
||||
mutate()
|
||||
if (options.updateTray) {
|
||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||
}
|
||||
}
|
||||
}, [mutate, action, t, options.errorKey, options.updateTray])
|
||||
}
|
||||
@ -1,51 +1,60 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react'
|
||||
import React, { ReactNode, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { showError } from '@renderer/utils/error-display'
|
||||
import useSWR from 'swr'
|
||||
import { createConfigContext } from './create-config-context'
|
||||
import { getAppConfig, patchAppConfig as patch } from '@renderer/utils/ipc'
|
||||
|
||||
const { Provider, useConfig } = createConfigContext<IAppConfig>({
|
||||
swrKey: 'getAppConfig',
|
||||
fetcher: getAppConfig,
|
||||
ipcEvent: 'appConfigUpdated'
|
||||
})
|
||||
|
||||
interface AppConfigContextType {
|
||||
appConfig: IAppConfig | undefined
|
||||
mutateAppConfig: () => void
|
||||
patchAppConfig: (value: Partial<IAppConfig>) => Promise<void>
|
||||
}
|
||||
|
||||
const AppConfigContext = createContext<AppConfigContextType | undefined>(undefined)
|
||||
|
||||
export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<Provider>
|
||||
<AppConfigContextWrapper>{children}</AppConfigContextWrapper>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const AppConfigContextWrapper: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const { config, mutate } = useConfig()
|
||||
const { t } = useTranslation()
|
||||
const { data: appConfig, mutate: mutateAppConfig } = useSWR('getConfig', () => getAppConfig())
|
||||
|
||||
const patchAppConfig = async (value: Partial<IAppConfig>): Promise<void> => {
|
||||
try {
|
||||
await patch(value)
|
||||
} catch (e) {
|
||||
await showError(e, t('common.error.updateAppConfigFailed'))
|
||||
} finally {
|
||||
mutateAppConfig()
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const handler = (): void => {
|
||||
mutateAppConfig()
|
||||
}
|
||||
window.electron.ipcRenderer.on('appConfigUpdated', handler)
|
||||
return (): void => {
|
||||
window.electron.ipcRenderer.removeListener('appConfigUpdated', handler)
|
||||
}
|
||||
}, [])
|
||||
const patchAppConfig = useCallback(
|
||||
async (value: Partial<IAppConfig>): Promise<void> => {
|
||||
try {
|
||||
await patch(value)
|
||||
} catch (e) {
|
||||
await showError(e, t('common.error.updateAppConfigFailed'))
|
||||
} finally {
|
||||
mutate()
|
||||
}
|
||||
},
|
||||
[mutate, t]
|
||||
)
|
||||
|
||||
return (
|
||||
<AppConfigContext.Provider value={{ appConfig, mutateAppConfig, patchAppConfig }}>
|
||||
<AppConfigContext.Provider
|
||||
value={{ appConfig: config, mutateAppConfig: mutate, patchAppConfig }}
|
||||
>
|
||||
{children}
|
||||
</AppConfigContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const AppConfigContext = React.createContext<AppConfigContextType | undefined>(undefined)
|
||||
|
||||
export const useAppConfig = (): AppConfigContextType => {
|
||||
const context = useContext(AppConfigContext)
|
||||
if (context === undefined) {
|
||||
const context = React.useContext(AppConfigContext)
|
||||
if (!context) {
|
||||
throw new Error('useAppConfig must be used within an AppConfigProvider')
|
||||
}
|
||||
return context
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react'
|
||||
import React, { ReactNode, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { showError } from '@renderer/utils/error-display'
|
||||
import useSWR from 'swr'
|
||||
import { createConfigContext } from './create-config-context'
|
||||
import {
|
||||
getOverrideConfig,
|
||||
setOverrideConfig as set,
|
||||
@ -10,6 +10,12 @@ import {
|
||||
updateOverrideItem as update
|
||||
} from '@renderer/utils/ipc'
|
||||
|
||||
const { Provider, useConfig } = createConfigContext<IOverrideConfig>({
|
||||
swrKey: 'getOverrideConfig',
|
||||
fetcher: getOverrideConfig,
|
||||
ipcEvent: 'overrideConfigUpdated'
|
||||
})
|
||||
|
||||
interface OverrideConfigContextType {
|
||||
overrideConfig: IOverrideConfig | undefined
|
||||
setOverrideConfig: (config: IOverrideConfig) => Promise<void>
|
||||
@ -19,60 +25,59 @@ interface OverrideConfigContextType {
|
||||
removeOverrideItem: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
const OverrideConfigContext = createContext<OverrideConfigContextType | undefined>(undefined)
|
||||
const OverrideConfigContext = React.createContext<OverrideConfigContextType | undefined>(undefined)
|
||||
|
||||
export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<Provider>
|
||||
<OverrideConfigContextWrapper>{children}</OverrideConfigContextWrapper>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const OverrideConfigContextWrapper: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const { config, mutate } = useConfig()
|
||||
const { t } = useTranslation()
|
||||
const { data: overrideConfig, mutate: mutateOverrideConfig } = useSWR('getOverrideConfig', () =>
|
||||
getOverrideConfig()
|
||||
|
||||
const withErrorHandling = useCallback(
|
||||
(action: () => Promise<void>, errorKey: string) => async () => {
|
||||
try {
|
||||
await action()
|
||||
} catch (e) {
|
||||
await showError(e, t(errorKey))
|
||||
} finally {
|
||||
mutate()
|
||||
}
|
||||
},
|
||||
[mutate, t]
|
||||
)
|
||||
|
||||
const setOverrideConfig = async (config: IOverrideConfig): Promise<void> => {
|
||||
try {
|
||||
await set(config)
|
||||
} catch (e) {
|
||||
await showError(e, t('common.error.saveOverrideConfigFailed'))
|
||||
} finally {
|
||||
mutateOverrideConfig()
|
||||
}
|
||||
}
|
||||
const setOverrideConfig = useCallback(
|
||||
(cfg: IOverrideConfig) => withErrorHandling(() => set(cfg), 'common.error.saveOverrideConfigFailed')(),
|
||||
[withErrorHandling]
|
||||
)
|
||||
|
||||
const addOverrideItem = async (item: Partial<IOverrideItem>): Promise<void> => {
|
||||
try {
|
||||
await add(item)
|
||||
} catch (e) {
|
||||
await showError(e, t('common.error.addOverrideFailed'))
|
||||
} finally {
|
||||
mutateOverrideConfig()
|
||||
}
|
||||
}
|
||||
const addOverrideItem = useCallback(
|
||||
(item: Partial<IOverrideItem>) => withErrorHandling(() => add(item), 'common.error.addOverrideFailed')(),
|
||||
[withErrorHandling]
|
||||
)
|
||||
|
||||
const removeOverrideItem = async (id: string): Promise<void> => {
|
||||
try {
|
||||
await remove(id)
|
||||
} catch (e) {
|
||||
await showError(e, t('common.error.deleteOverrideFailed'))
|
||||
} finally {
|
||||
mutateOverrideConfig()
|
||||
}
|
||||
}
|
||||
const removeOverrideItem = useCallback(
|
||||
(id: string) => withErrorHandling(() => remove(id), 'common.error.deleteOverrideFailed')(),
|
||||
[withErrorHandling]
|
||||
)
|
||||
|
||||
const updateOverrideItem = async (item: IOverrideItem): Promise<void> => {
|
||||
try {
|
||||
await update(item)
|
||||
} catch (e) {
|
||||
await showError(e, t('common.error.updateOverrideFailed'))
|
||||
} finally {
|
||||
mutateOverrideConfig()
|
||||
}
|
||||
}
|
||||
const updateOverrideItem = useCallback(
|
||||
(item: IOverrideItem) => withErrorHandling(() => update(item), 'common.error.updateOverrideFailed')(),
|
||||
[withErrorHandling]
|
||||
)
|
||||
|
||||
return (
|
||||
<OverrideConfigContext.Provider
|
||||
value={{
|
||||
overrideConfig,
|
||||
overrideConfig: config,
|
||||
setOverrideConfig,
|
||||
mutateOverrideConfig,
|
||||
mutateOverrideConfig: mutate,
|
||||
addOverrideItem,
|
||||
removeOverrideItem,
|
||||
updateOverrideItem
|
||||
@ -84,8 +89,8 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil
|
||||
}
|
||||
|
||||
export const useOverrideConfig = (): OverrideConfigContextType => {
|
||||
const context = useContext(OverrideConfigContext)
|
||||
if (context === undefined) {
|
||||
const context = React.useContext(OverrideConfigContext)
|
||||
if (!context) {
|
||||
throw new Error('useOverrideConfig must be used within an OverrideConfigProvider')
|
||||
}
|
||||
return context
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { createContext, ReactNode, useContext } from 'react'
|
||||
import React, { ReactNode, useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { showError } from '@renderer/utils/error-display'
|
||||
import useSWR from 'swr'
|
||||
import { createConfigContext } from './create-config-context'
|
||||
import {
|
||||
addProfileItem as add,
|
||||
changeCurrentProfile as change,
|
||||
@ -11,6 +11,12 @@ import {
|
||||
updateProfileItem as update
|
||||
} from '@renderer/utils/ipc'
|
||||
|
||||
const { Provider, useConfig } = createConfigContext<IProfileConfig>({
|
||||
swrKey: 'getProfileConfig',
|
||||
fetcher: getProfileConfig,
|
||||
ipcEvent: 'profileConfigUpdated'
|
||||
})
|
||||
|
||||
interface ProfileConfigContextType {
|
||||
profileConfig: IProfileConfig | undefined
|
||||
setProfileConfig: (config: IProfileConfig) => Promise<void>
|
||||
@ -21,80 +27,64 @@ interface ProfileConfigContextType {
|
||||
changeCurrentProfile: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
const ProfileConfigContext = createContext<ProfileConfigContextType | undefined>(undefined)
|
||||
const ProfileConfigContext = React.createContext<ProfileConfigContextType | undefined>(undefined)
|
||||
|
||||
export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: profileConfig, mutate: mutateProfileConfig } = useSWR('getProfileConfig', () =>
|
||||
getProfileConfig()
|
||||
return (
|
||||
<Provider>
|
||||
<ProfileConfigContextWrapper>{children}</ProfileConfigContextWrapper>
|
||||
</Provider>
|
||||
)
|
||||
const targetProfileId = React.useRef<string | null>(null)
|
||||
const pendingTask = React.useRef<Promise<void> | null>(null)
|
||||
}
|
||||
|
||||
const setProfileConfig = async (config: IProfileConfig): Promise<void> => {
|
||||
try {
|
||||
await set(config)
|
||||
} catch (e) {
|
||||
await showError(e, t('common.error.saveProfileConfigFailed'))
|
||||
} finally {
|
||||
mutateProfileConfig()
|
||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||
}
|
||||
}
|
||||
const ProfileConfigContextWrapper: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const { config, mutate } = useConfig()
|
||||
const { t } = useTranslation()
|
||||
const targetProfileId = useRef<string | null>(null)
|
||||
const pendingTask = useRef<Promise<void> | null>(null)
|
||||
|
||||
const addProfileItem = async (item: Partial<IProfileItem>): Promise<void> => {
|
||||
try {
|
||||
await add(item)
|
||||
} catch (e) {
|
||||
await showError(e, t('common.error.addProfileFailed'))
|
||||
} finally {
|
||||
mutateProfileConfig()
|
||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||
}
|
||||
}
|
||||
const withErrorHandling = useCallback(
|
||||
(action: () => Promise<void>, errorKey: string, updateTray = true) =>
|
||||
async () => {
|
||||
try {
|
||||
await action()
|
||||
} catch (e) {
|
||||
await showError(e, t(errorKey))
|
||||
} finally {
|
||||
mutate()
|
||||
if (updateTray) {
|
||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||
}
|
||||
}
|
||||
},
|
||||
[mutate, t]
|
||||
)
|
||||
|
||||
const removeProfileItem = async (id: string): Promise<void> => {
|
||||
try {
|
||||
await remove(id)
|
||||
} catch (e) {
|
||||
await showError(e, t('common.error.deleteProfileFailed'))
|
||||
} finally {
|
||||
mutateProfileConfig()
|
||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||
}
|
||||
}
|
||||
const setProfileConfig = useCallback(
|
||||
(cfg: IProfileConfig) =>
|
||||
withErrorHandling(() => set(cfg), 'common.error.saveProfileConfigFailed')(),
|
||||
[withErrorHandling]
|
||||
)
|
||||
|
||||
const updateProfileItem = async (item: IProfileItem): Promise<void> => {
|
||||
try {
|
||||
await update(item)
|
||||
} catch (e) {
|
||||
await showError(e, t('common.error.updateProfileFailed'))
|
||||
} finally {
|
||||
mutateProfileConfig()
|
||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||
}
|
||||
}
|
||||
const addProfileItem = useCallback(
|
||||
(item: Partial<IProfileItem>) =>
|
||||
withErrorHandling(() => add(item), 'common.error.addProfileFailed')(),
|
||||
[withErrorHandling]
|
||||
)
|
||||
|
||||
const changeCurrentProfile = async (id: string): Promise<void> => {
|
||||
if (targetProfileId.current === id) {
|
||||
return
|
||||
}
|
||||
const removeProfileItem = useCallback(
|
||||
(id: string) => withErrorHandling(() => remove(id), 'common.error.deleteProfileFailed')(),
|
||||
[withErrorHandling]
|
||||
)
|
||||
|
||||
// 立即更新 UI 状态和托盘菜单,提供即时反馈
|
||||
if (profileConfig) {
|
||||
const optimisticUpdate = { ...profileConfig, current: id }
|
||||
mutateProfileConfig(optimisticUpdate, false)
|
||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||
}
|
||||
const updateProfileItem = useCallback(
|
||||
(item: IProfileItem) =>
|
||||
withErrorHandling(() => update(item), 'common.error.updateProfileFailed')(),
|
||||
[withErrorHandling]
|
||||
)
|
||||
|
||||
targetProfileId.current = id
|
||||
await processChange()
|
||||
}
|
||||
|
||||
const processChange = async () => {
|
||||
if (pendingTask.current) {
|
||||
return
|
||||
}
|
||||
const processChange = useCallback(async () => {
|
||||
if (pendingTask.current) return
|
||||
|
||||
while (targetProfileId.current) {
|
||||
const targetId = targetProfileId.current
|
||||
@ -102,41 +92,48 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
|
||||
|
||||
pendingTask.current = change(targetId)
|
||||
try {
|
||||
// 异步执行后台切换,不阻塞 UI
|
||||
await pendingTask.current
|
||||
} catch (e) {
|
||||
const errorMsg = (e as { message?: string })?.message || String(e)
|
||||
// 处理 IPC 超时错误
|
||||
if (errorMsg.includes('reply was never sent')) {
|
||||
setTimeout(() => mutateProfileConfig(), 1000)
|
||||
setTimeout(() => mutate(), 1000)
|
||||
} else {
|
||||
await showError(errorMsg, t('common.error.switchProfileFailed'))
|
||||
mutateProfileConfig()
|
||||
mutate()
|
||||
}
|
||||
} finally {
|
||||
pendingTask.current = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [mutate, t])
|
||||
|
||||
const changeCurrentProfile = useCallback(
|
||||
async (id: string) => {
|
||||
if (targetProfileId.current === id) return
|
||||
|
||||
if (config) {
|
||||
mutate({ ...config, current: id }, false)
|
||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||
}
|
||||
|
||||
targetProfileId.current = id
|
||||
await processChange()
|
||||
},
|
||||
[config, mutate, processChange]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const handler = (): void => {
|
||||
mutateProfileConfig()
|
||||
}
|
||||
window.electron.ipcRenderer.on('profileConfigUpdated', handler)
|
||||
return (): void => {
|
||||
// 清理待处理任务,防止内存泄漏
|
||||
return () => {
|
||||
targetProfileId.current = null
|
||||
window.electron.ipcRenderer.removeListener('profileConfigUpdated', handler)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ProfileConfigContext.Provider
|
||||
value={{
|
||||
profileConfig,
|
||||
profileConfig: config,
|
||||
setProfileConfig,
|
||||
mutateProfileConfig,
|
||||
mutateProfileConfig: mutate,
|
||||
addProfileItem,
|
||||
removeProfileItem,
|
||||
updateProfileItem,
|
||||
@ -149,8 +146,8 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
|
||||
}
|
||||
|
||||
export const useProfileConfig = (): ProfileConfigContextType => {
|
||||
const context = useContext(ProfileConfigContext)
|
||||
if (context === undefined) {
|
||||
const context = React.useContext(ProfileConfigContext)
|
||||
if (!context) {
|
||||
throw new Error('useProfileConfig must be used within a ProfileConfigProvider')
|
||||
}
|
||||
return context
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user