diff --git a/src/services/notice-handlers.ts b/src/services/notice-handlers.ts index c87c5a9f2..669853a4c 100644 --- a/src/services/notice-handlers.ts +++ b/src/services/notice-handlers.ts @@ -32,230 +32,221 @@ export const forgetShownStartupError = () => { * type makes a backend status without a frontend entry a compile error (and * contract regeneration keeps the union in sync). */ +const handlers: Record< + NoticeStatus, + (msg: string, navigate: NavigateFunction) => void +> = { + info: (msg) => { + if (msg) showNotice.info(msg) + }, + 'import_sub_url::ok': (_msg, navigate) => { + // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。 + // 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符 + // navigate("/profile", { state: { current: msg } }); + navigate('/profile') + showNotice.success( + 'shared.feedback.notifications.importSubscriptionSuccess', + ) + }, + 'import_sub_url::error': (msg, navigate) => { + navigate('/profile') + showNotice.error(msg) + }, + 'set_config::error': (msg) => showNotice.error(msg), + 'core_start::error': () => { + const read = ++startupErrorReads + void getCoreStartupError() + .then(async (failure) => { + if (read < settledStartupErrorRead) return + settledStartupErrorRead = read + if (!failure) { + shownStartupError = null + return + } + const window = getCurrentWindow() + const [visible, minimized] = await Promise.all([ + window.isVisible(), + window.isMinimized(), + ]) + const shown = `${failure.kind}:${failure.detail}` + if ( + read === settledStartupErrorRead && + visible && + !minimized && + shownStartupError !== shown + ) { + shownStartupError = shown + showNotice.error( + failure.kind === 'serviceCoreStopped' + ? 'settings.feedback.errors.clash.serviceCoreStopped' + : 'settings.feedback.errors.clash.startFailed', + failure.detail, + ) + } + }) + .catch((error) => { + console.error('Failed to read the pending core startup error', error) + }) + }, + 'service_core::repair_required': () => { + void takeServiceRepairNotice() + .then((pending) => { + if (pending) { + requestService({ reason: 'serviceLocationRefused' }) + } + }) + .catch((error) => { + console.error('Failed to read the pending service repair notice', error) + }) + }, + 'service_core::app_data_not_owned': () => { + void takeServiceOwnerNotice() + .then((command) => { + if (command) { + if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice) + shownOwnerNotice = showNotice.warning( + 'settings.feedback.notifications.clashService.appDataNotOwned', + { command }, + 0, + ) + } + }) + .catch((error) => { + console.error('Failed to read the pending service owner notice', error) + }) + }, + 'service_core::sidecar_fallback': () => { + void takeServiceFallbackNotice() + .then((notice) => { + if (notice?.kind === 'coreRejected') { + showNotice.warning( + 'settings.feedback.notifications.clashService.permissionFallback', + { reason: notice.reason }, + 0, + ) + } else if (notice?.kind === 'notAutoStarted') { + requestService({ reason: 'serviceNotAutoStarted' }) + } else if (notice) { + showNotice.warning( + 'settings.feedback.notifications.clashService.sidecarFallback', + ) + } + }) + .catch((error) => { + console.error( + 'Failed to read the pending service fallback notice', + error, + ) + }) + }, + 'dns_override::auto_disabled': () => { + void takeDnsOverrideNotice() + .then((pending) => { + if (pending) { + showNotice.info('settings.modals.dns.protection.autoDisabled') + } + }) + .catch((error) => { + console.error('Failed to read the pending DNS override notice', error) + }) + }, + 'enhance::discarded_keys': () => { + void takeDiscardedKeysNotice() + .then((keys) => { + if (keys) { + showNotice.warning('profiles.page.feedback.notices.discardedKeys', { + keys, + }) + } + }) + .catch((error) => { + console.error('Failed to read the pending discarded keys notice', error) + }) + }, + 'tun_mode::auto_disabled': () => + showNotice.info( + 'settings.sections.system.notifications.tunMode.autoDisabled', + ), + 'tun_mode::auto_disable_failed': () => + showNotice.error( + 'settings.sections.system.notifications.tunMode.autoDisableFailed', + ), + 'app_restart::core_stop_failed': (msg) => + showNotice.error( + 'layout.feedback.errors.restartCoreStopFailed', + msg || undefined, + ), + 'app_quit::core_stop_failed': (msg) => + showNotice.error( + 'layout.feedback.errors.quitCoreStopFailed', + msg || undefined, + ), + update_with_clash_proxy: (msg) => + showNotice.success( + 'settings.feedback.notifications.updater.withClashProxySuccess', + msg, + ), + update_failed_even_with_clash: (msg) => + showNotice.error( + 'settings.feedback.notifications.updater.withClashProxyFailed', + msg, + ), + 'reactivate_profiles::error': (msg) => showNotice.error(msg), + update_failed: (msg) => showNotice.error(msg), + 'config_validate::boot_error': (msg) => + showNotice.error('shared.feedback.validation.config.bootFailed', msg), + 'config_validate::error': (msg) => + showNotice.error('shared.feedback.validation.config.failed', msg), + 'config_validate::process_terminated': () => + showNotice.error('shared.feedback.validation.config.processTerminated'), + 'config_validate::script_error': (msg) => + showNotice.error('shared.feedback.validation.script.fileError', msg), + 'config_validate::script_syntax_error': (msg) => + showNotice.error('shared.feedback.validation.script.syntaxError', msg), + 'config_validate::script_missing_main': (msg) => + showNotice.error('shared.feedback.validation.script.missingMain', msg), + 'config_validate::file_not_found': (msg) => + showNotice.error('shared.feedback.validation.script.fileNotFound', msg), + 'config_validate::yaml_syntax_error': (msg) => + showNotice.error('shared.feedback.validation.yaml.syntaxError', msg), + 'config_validate::yaml_read_error': (msg) => + showNotice.error('shared.feedback.validation.yaml.readError', msg), + 'config_validate::yaml_mapping_error': (msg) => + showNotice.error('shared.feedback.validation.yaml.mappingError', msg), + 'config_validate::merge_syntax_error': (msg) => + showNotice.error('shared.feedback.validation.merge.syntaxError', msg), + 'config_validate::merge_mapping_error': (msg) => + showNotice.error('shared.feedback.validation.merge.mappingError', msg), + 'config_core::change_success': (msg) => + showNotice.success( + 'settings.feedback.notifications.clash.changeSuccess', + msg, + ), + 'config_core::change_error': (msg) => + showNotice.error('settings.feedback.notifications.clash.changeFailed', msg), + 'mixed_port::fallback': (msg) => { + const [oldPort, newPort] = msg.split(',') + showNotice.info('settings.modals.clashPort.messages.automaticFallback', { + oldPort, + newPort, + }) + }, + 'mixed_port::fallback_error': (msg) => + showNotice.error( + 'settings.modals.clashPort.messages.automaticFallbackFailed', + { error: msg }, + ), +} + export const handleNoticeMessage = ( status: string, msg: string, t: TranslateFunction, navigate: NavigateFunction, ) => { - const handlers: Record void> = { - info: () => { - if (msg) showNotice.info(msg) - }, - 'import_sub_url::ok': () => { - // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。 - // 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符 - // navigate("/profile", { state: { current: msg } }); - navigate('/profile') - showNotice.success( - 'shared.feedback.notifications.importSubscriptionSuccess', - ) - }, - 'import_sub_url::error': () => { - navigate('/profile') - showNotice.error(msg) - }, - 'set_config::error': () => showNotice.error(msg), - 'core_start::error': () => { - const read = ++startupErrorReads - void getCoreStartupError() - .then(async (failure) => { - if (read < settledStartupErrorRead) return - settledStartupErrorRead = read - if (!failure) { - shownStartupError = null - return - } - const window = getCurrentWindow() - const [visible, minimized] = await Promise.all([ - window.isVisible(), - window.isMinimized(), - ]) - const shown = `${failure.kind}:${failure.detail}` - if ( - read === settledStartupErrorRead && - visible && - !minimized && - shownStartupError !== shown - ) { - shownStartupError = shown - showNotice.error( - failure.kind === 'serviceCoreStopped' - ? 'settings.feedback.errors.clash.serviceCoreStopped' - : 'settings.feedback.errors.clash.startFailed', - failure.detail, - ) - } - }) - .catch((error) => { - console.error('Failed to read the pending core startup error', error) - }) - }, - 'service_core::repair_required': () => { - void takeServiceRepairNotice() - .then((pending) => { - if (pending) { - requestService({ reason: 'serviceLocationRefused' }) - } - }) - .catch((error) => { - console.error( - 'Failed to read the pending service repair notice', - error, - ) - }) - }, - 'service_core::app_data_not_owned': () => { - void takeServiceOwnerNotice() - .then((command) => { - if (command) { - if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice) - shownOwnerNotice = showNotice.warning( - 'settings.feedback.notifications.clashService.appDataNotOwned', - { command }, - 0, - ) - } - }) - .catch((error) => { - console.error( - 'Failed to read the pending service owner notice', - error, - ) - }) - }, - 'service_core::sidecar_fallback': () => { - void takeServiceFallbackNotice() - .then((notice) => { - if (notice?.kind === 'coreRejected') { - showNotice.warning( - 'settings.feedback.notifications.clashService.permissionFallback', - { reason: notice.reason }, - 0, - ) - } else if (notice?.kind === 'notAutoStarted') { - requestService({ reason: 'serviceNotAutoStarted' }) - } else if (notice) { - showNotice.warning( - 'settings.feedback.notifications.clashService.sidecarFallback', - ) - } - }) - .catch((error) => { - console.error( - 'Failed to read the pending service fallback notice', - error, - ) - }) - }, - 'dns_override::auto_disabled': () => { - void takeDnsOverrideNotice() - .then((pending) => { - if (pending) { - showNotice.info('settings.modals.dns.protection.autoDisabled') - } - }) - .catch((error) => { - console.error('Failed to read the pending DNS override notice', error) - }) - }, - 'enhance::discarded_keys': () => { - void takeDiscardedKeysNotice() - .then((keys) => { - if (keys) { - showNotice.warning('profiles.page.feedback.notices.discardedKeys', { - keys, - }) - } - }) - .catch((error) => { - console.error( - 'Failed to read the pending discarded keys notice', - error, - ) - }) - }, - 'tun_mode::auto_disabled': () => - showNotice.info( - 'settings.sections.system.notifications.tunMode.autoDisabled', - ), - 'tun_mode::auto_disable_failed': () => - showNotice.error( - 'settings.sections.system.notifications.tunMode.autoDisableFailed', - ), - 'app_restart::core_stop_failed': () => - showNotice.error( - 'layout.feedback.errors.restartCoreStopFailed', - msg || undefined, - ), - 'app_quit::core_stop_failed': () => - showNotice.error( - 'layout.feedback.errors.quitCoreStopFailed', - msg || undefined, - ), - update_with_clash_proxy: () => - showNotice.success( - 'settings.feedback.notifications.updater.withClashProxySuccess', - msg, - ), - update_failed_even_with_clash: () => - showNotice.error( - 'settings.feedback.notifications.updater.withClashProxyFailed', - msg, - ), - 'reactivate_profiles::error': () => showNotice.error(msg), - update_failed: () => showNotice.error(msg), - 'config_validate::boot_error': () => - showNotice.error('shared.feedback.validation.config.bootFailed', msg), - 'config_validate::error': () => - showNotice.error('shared.feedback.validation.config.failed', msg), - 'config_validate::process_terminated': () => - showNotice.error('shared.feedback.validation.config.processTerminated'), - 'config_validate::script_error': () => - showNotice.error('shared.feedback.validation.script.fileError', msg), - 'config_validate::script_syntax_error': () => - showNotice.error('shared.feedback.validation.script.syntaxError', msg), - 'config_validate::script_missing_main': () => - showNotice.error('shared.feedback.validation.script.missingMain', msg), - 'config_validate::file_not_found': () => - showNotice.error('shared.feedback.validation.script.fileNotFound', msg), - 'config_validate::yaml_syntax_error': () => - showNotice.error('shared.feedback.validation.yaml.syntaxError', msg), - 'config_validate::yaml_read_error': () => - showNotice.error('shared.feedback.validation.yaml.readError', msg), - 'config_validate::yaml_mapping_error': () => - showNotice.error('shared.feedback.validation.yaml.mappingError', msg), - 'config_validate::merge_syntax_error': () => - showNotice.error('shared.feedback.validation.merge.syntaxError', msg), - 'config_validate::merge_mapping_error': () => - showNotice.error('shared.feedback.validation.merge.mappingError', msg), - 'config_core::change_success': () => - showNotice.success( - 'settings.feedback.notifications.clash.changeSuccess', - msg, - ), - 'config_core::change_error': () => - showNotice.error( - 'settings.feedback.notifications.clash.changeFailed', - msg, - ), - 'mixed_port::fallback': () => { - const [oldPort, newPort] = msg.split(',') - showNotice.info('settings.modals.clashPort.messages.automaticFallback', { - oldPort, - newPort, - }) - }, - 'mixed_port::fallback_error': () => - showNotice.error( - 'settings.modals.clashPort.messages.automaticFallbackFailed', - { error: msg }, - ), - } - const handler = handlers[status as NoticeStatus] if (handler) { - handler() + handler(msg, navigate) } else { console.warn(`未处理的通知状态: ${status}`) } diff --git a/src/store/app-state.ts b/src/store/app-state.ts index c0e143308..ad2fe6cd1 100644 --- a/src/store/app-state.ts +++ b/src/store/app-state.ts @@ -66,6 +66,7 @@ const withLoading = ( next.delete(uid) } } + if (next.size === state.profileUpdates.loading.size) return state return { ...state, profileUpdates: { ...state.profileUpdates, loading: next },