From d35d7029f731c50c93bedc1bafd2626d8c6f3da0 Mon Sep 17 00:00:00 2001 From: icarus Date: Sun, 19 Oct 2025 22:32:00 +0800 Subject: [PATCH] refactor(ocr): simplify image provider state management - Remove unnecessary state propagation between components - Store image provider ID in preferences instead of redux - Add null checks for provider existence - Update tab navigation to use new ui components --- .../data/preference/preferenceSchemas.ts | 3 + src/renderer/src/hooks/useOcrProvider.tsx | 37 +++------- .../DocProcessSettings/OcrImageSettings.tsx | 23 ++---- .../OcrProviderSettings.tsx | 4 +- .../DocProcessSettings/OcrSettings.tsx | 74 +++++++++++++++---- 5 files changed, 81 insertions(+), 60 deletions(-) diff --git a/packages/shared/data/preference/preferenceSchemas.ts b/packages/shared/data/preference/preferenceSchemas.ts index 6b758c0ff5..43ddfd6655 100644 --- a/packages/shared/data/preference/preferenceSchemas.ts +++ b/packages/shared/data/preference/preferenceSchemas.ts @@ -351,6 +351,8 @@ export interface PreferenceSchemas { 'feature.translate.model_prompt': string // redux/settings/targetLanguage 'feature.translate.target_language': string + // redux/ocr/imageProviderId + 'ocr.settings.image_provider_id': string | null // redux/shortcuts/shortcuts.exit_fullscreen 'shortcut.app.exit_fullscreen': Record // redux/shortcuts/shortcuts.search_message @@ -612,6 +614,7 @@ export const DefaultPreferences: PreferenceSchemas = { 'feature.selection.trigger_mode': PreferenceTypes.SelectionTriggerMode.Selected, 'feature.translate.model_prompt': TRANSLATE_PROMPT, 'feature.translate.target_language': 'en-us', + 'ocr.settings.image_provider_id': null, 'shortcut.app.exit_fullscreen': { editable: false, enabled: true, key: ['Escape'], system: true }, 'shortcut.app.search_message': { editable: true, diff --git a/src/renderer/src/hooks/useOcrProvider.tsx b/src/renderer/src/hooks/useOcrProvider.tsx index 34f99c5e50..4a32dda2d3 100644 --- a/src/renderer/src/hooks/useOcrProvider.tsx +++ b/src/renderer/src/hooks/useOcrProvider.tsx @@ -1,16 +1,17 @@ import { Avatar } from '@cherrystudio/ui' +import { usePreference } from '@data/hooks/usePreference' import { loggerService } from '@logger' import IntelLogo from '@renderer/assets/images/providers/intel.png' import PaddleocrLogo from '@renderer/assets/images/providers/paddleocr.png' import TesseractLogo from '@renderer/assets/images/providers/Tesseract.js.png' -import { BUILTIN_OCR_PROVIDERS_MAP, DEFAULT_OCR_PROVIDER } from '@renderer/config/ocr' +import { BUILTIN_OCR_PROVIDERS_MAP } from '@renderer/config/ocr' import { getBuiltinOcrProviderLabel } from '@renderer/i18n/label' import { useAppSelector } from '@renderer/store' -import { addOcrProvider, removeOcrProvider, setImageOcrProviderId, updateOcrProviderConfig } from '@renderer/store/ocr' -import type { ImageOcrProvider, OcrProvider, OcrProviderConfig } from '@renderer/types' +import { addOcrProvider, removeOcrProvider, updateOcrProviderConfig } from '@renderer/store/ocr' +import type { OcrProvider, OcrProviderConfig } from '@renderer/types' import { isBuiltinOcrProvider, isBuiltinOcrProviderId, isImageOcrProvider } from '@renderer/types' import { FileQuestionMarkIcon, MonitorIcon } from 'lucide-react' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' @@ -19,8 +20,10 @@ const logger = loggerService.withContext('useOcrProvider') export const useOcrProviders = () => { const providers = useAppSelector((state) => state.ocr.providers) const imageProviders = providers.filter(isImageOcrProvider) - const imageProviderId = useAppSelector((state) => state.ocr.imageProviderId) - const [imageProvider, setImageProvider] = useState(DEFAULT_OCR_PROVIDER.image) + const [imageProviderId, setImageProviderId] = usePreference('ocr.settings.image_provider_id') + const imageProvider = useMemo(() => { + return imageProviders.find((p) => p.id === imageProviderId) + }, [imageProviderId, imageProviders]) const dispatch = useDispatch() const { t } = useTranslation() @@ -58,13 +61,6 @@ export const useOcrProviders = () => { dispatch(removeOcrProvider(id)) } - const setImageProviderId = useCallback( - (id: string) => { - dispatch(setImageOcrProviderId(id)) - }, - [dispatch] - ) - const getOcrProviderName = (p: OcrProvider) => { return isBuiltinOcrProvider(p) ? getBuiltinOcrProviderLabel(p.id) : p.name } @@ -85,21 +81,6 @@ export const useOcrProviders = () => { return } - useEffect(() => { - const actualImageProvider = imageProviders.find((p) => p.id === imageProviderId) - if (!actualImageProvider) { - if (isBuiltinOcrProviderId(imageProviderId)) { - logger.warn(`Builtin ocr provider ${imageProviderId} not exist. Will add it to providers.`) - addProvider(BUILTIN_OCR_PROVIDERS_MAP[imageProviderId]) - } - setImageProviderId(DEFAULT_OCR_PROVIDER.image.id) - setImageProvider(DEFAULT_OCR_PROVIDER.image) - } else { - setImageProviderId(actualImageProvider.id) - setImageProvider(actualImageProvider) - } - }, [addProvider, imageProviderId, imageProviders, setImageProviderId]) - return { providers, imageProvider, diff --git a/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx b/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx index 06908e80ee..5206104b87 100644 --- a/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx +++ b/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx @@ -3,11 +3,11 @@ import { loggerService } from '@logger' import { ErrorTag } from '@renderer/components/Tags/ErrorTag' import { isMac, isWin } from '@renderer/config/constant' import { useOcrProviders } from '@renderer/hooks/useOcrProvider' -import type { ImageOcrProvider, OcrProvider } from '@renderer/types' +import type { ImageOcrProvider } from '@renderer/types' import { BuiltinOcrProviderIds, isImageOcrProvider } from '@renderer/types' import { getErrorMessage } from '@renderer/utils' import { Select } from 'antd' -import { useCallback, useEffect, useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import useSWRImmutable from 'swr/immutable' @@ -15,11 +15,7 @@ import { SettingRow, SettingRowTitle } from '..' const logger = loggerService.withContext('OcrImageSettings') -type Props = { - setProvider: (provider: OcrProvider) => void -} - -const OcrImageSettings = ({ setProvider }: Props) => { +const OcrImageSettings = () => { const { t } = useTranslation() const { providers, imageProvider, getOcrProviderName, setImageProviderId } = useOcrProviders() const fetcher = useCallback(() => { @@ -30,12 +26,6 @@ const OcrImageSettings = ({ setProvider }: Props) => { const imageProviders = providers.filter((p) => isImageOcrProvider(p)) - // 挂载时更新外部状态 - // FIXME: Just keep the imageProvider always valid, so we don't need update it in this component. - useEffect(() => { - setProvider(imageProvider) - }, [imageProvider, setProvider]) - const setImageProvider = (id: string) => { const provider = imageProviders.find((p) => p.id === id) if (!provider) { @@ -44,7 +34,6 @@ const OcrImageSettings = ({ setProvider }: Props) => { return } - setProvider(provider) setImageProviderId(id) } @@ -62,7 +51,11 @@ const OcrImageSettings = ({ setProvider }: Props) => { })) }, [getOcrProviderName, imageProviders, platformSupport, validProviders]) - const isSystem = imageProvider.id === BuiltinOcrProviderIds.system + const isSystem = imageProvider?.id === BuiltinOcrProviderIds.system + + if (!imageProvider) { + return + } return ( <> diff --git a/src/renderer/src/pages/settings/DocProcessSettings/OcrProviderSettings.tsx b/src/renderer/src/pages/settings/DocProcessSettings/OcrProviderSettings.tsx index a99f91d096..5d2b909dc5 100644 --- a/src/renderer/src/pages/settings/DocProcessSettings/OcrProviderSettings.tsx +++ b/src/renderer/src/pages/settings/DocProcessSettings/OcrProviderSettings.tsx @@ -18,14 +18,14 @@ import { OcrTesseractSettings } from './OcrTesseractSettings' // const logger = loggerService.withContext('OcrTesseractSettings') type Props = { - provider: OcrProvider + provider: OcrProvider | undefined } const OcrProviderSettings = ({ provider }: Props) => { const { theme: themeMode } = useTheme() const { OcrProviderLogo, getOcrProviderName } = useOcrProviders() - if (!isWin && !isMac && isOcrSystemProvider(provider)) { + if (!provider || (!isWin && !isMac && isOcrSystemProvider(provider))) { return null } diff --git a/src/renderer/src/pages/settings/DocProcessSettings/OcrSettings.tsx b/src/renderer/src/pages/settings/DocProcessSettings/OcrSettings.tsx index 077451dd4d..c26b4045f0 100644 --- a/src/renderer/src/pages/settings/DocProcessSettings/OcrSettings.tsx +++ b/src/renderer/src/pages/settings/DocProcessSettings/OcrSettings.tsx @@ -1,40 +1,84 @@ import { PictureOutlined } from '@ant-design/icons' +import { cn, Tabs, TabsContent, TabsList, TabsTrigger } from '@cherrystudio/ui' import { ErrorBoundary } from '@renderer/components/ErrorBoundary' import { useTheme } from '@renderer/context/ThemeProvider' import { useOcrProviders } from '@renderer/hooks/useOcrProvider' -import type { OcrProvider } from '@renderer/types' -import type { TabsProps } from 'antd' -import { Tabs } from 'antd' -import type { FC } from 'react' -import { useState } from 'react' +import type { FC, ReactNode } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import * as z from 'zod' import { SettingDivider, SettingGroup, SettingTitle } from '..' import OcrImageSettings from './OcrImageSettings' import OcrProviderSettings from './OcrProviderSettings' +const TabSchema = z.enum(['image']) +type Tab = z.infer +const isValidTab = (value: string): value is Tab => TabSchema.safeParse(value).success +type TabItem = { + name: string + value: Tab + icon: ReactNode + content: ReactNode +} + const OcrSettings: FC = () => { const { t } = useTranslation() const { theme: themeMode } = useTheme() const { imageProvider } = useOcrProviders() - const [provider, setProvider] = useState(imageProvider) // since default to image provider - - const tabs: TabsProps['items'] = [ - { - key: 'image', - label: t('settings.tool.ocr.image.title'), - icon: , - children: + const [activeTab, setActiveTab] = useState('image') + const provider = useMemo(() => { + switch (activeTab) { + case 'image': + return imageProvider } - ] + }, [imageProvider, activeTab]) + + const tabs = [ + { + name: t('settings.tool.ocr.image.title'), + value: 'image', + icon: , + content: + } + ] satisfies TabItem[] + + const handleTabChange = useCallback((value: string) => { + if (isValidTab(value)) { + setActiveTab(value) + } else { + window.toast.error('Unexpected behavior: Not a valid tab.') + } + }, []) return ( {t('settings.tool.ocr.title')} - + + + {tabs.map((tab) => { + return ( + +
+ {tab.icon} + {tab.name} +
+
+ ) + })} +
+ {tabs.map((tab) => { + return ( + + {tab.content} + + ) + })} +
+