mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-23 01:50:13 +08:00
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
This commit is contained in:
parent
2c78f5f906
commit
d35d7029f7
@ -351,6 +351,8 @@ export interface PreferenceSchemas {
|
|||||||
'feature.translate.model_prompt': string
|
'feature.translate.model_prompt': string
|
||||||
// redux/settings/targetLanguage
|
// redux/settings/targetLanguage
|
||||||
'feature.translate.target_language': string
|
'feature.translate.target_language': string
|
||||||
|
// redux/ocr/imageProviderId
|
||||||
|
'ocr.settings.image_provider_id': string | null
|
||||||
// redux/shortcuts/shortcuts.exit_fullscreen
|
// redux/shortcuts/shortcuts.exit_fullscreen
|
||||||
'shortcut.app.exit_fullscreen': Record<string, unknown>
|
'shortcut.app.exit_fullscreen': Record<string, unknown>
|
||||||
// redux/shortcuts/shortcuts.search_message
|
// redux/shortcuts/shortcuts.search_message
|
||||||
@ -612,6 +614,7 @@ export const DefaultPreferences: PreferenceSchemas = {
|
|||||||
'feature.selection.trigger_mode': PreferenceTypes.SelectionTriggerMode.Selected,
|
'feature.selection.trigger_mode': PreferenceTypes.SelectionTriggerMode.Selected,
|
||||||
'feature.translate.model_prompt': TRANSLATE_PROMPT,
|
'feature.translate.model_prompt': TRANSLATE_PROMPT,
|
||||||
'feature.translate.target_language': 'en-us',
|
'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.exit_fullscreen': { editable: false, enabled: true, key: ['Escape'], system: true },
|
||||||
'shortcut.app.search_message': {
|
'shortcut.app.search_message': {
|
||||||
editable: true,
|
editable: true,
|
||||||
|
|||||||
@ -1,16 +1,17 @@
|
|||||||
import { Avatar } from '@cherrystudio/ui'
|
import { Avatar } from '@cherrystudio/ui'
|
||||||
|
import { usePreference } from '@data/hooks/usePreference'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import IntelLogo from '@renderer/assets/images/providers/intel.png'
|
import IntelLogo from '@renderer/assets/images/providers/intel.png'
|
||||||
import PaddleocrLogo from '@renderer/assets/images/providers/paddleocr.png'
|
import PaddleocrLogo from '@renderer/assets/images/providers/paddleocr.png'
|
||||||
import TesseractLogo from '@renderer/assets/images/providers/Tesseract.js.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 { getBuiltinOcrProviderLabel } from '@renderer/i18n/label'
|
||||||
import { useAppSelector } from '@renderer/store'
|
import { useAppSelector } from '@renderer/store'
|
||||||
import { addOcrProvider, removeOcrProvider, setImageOcrProviderId, updateOcrProviderConfig } from '@renderer/store/ocr'
|
import { addOcrProvider, removeOcrProvider, updateOcrProviderConfig } from '@renderer/store/ocr'
|
||||||
import type { ImageOcrProvider, OcrProvider, OcrProviderConfig } from '@renderer/types'
|
import type { OcrProvider, OcrProviderConfig } from '@renderer/types'
|
||||||
import { isBuiltinOcrProvider, isBuiltinOcrProviderId, isImageOcrProvider } from '@renderer/types'
|
import { isBuiltinOcrProvider, isBuiltinOcrProviderId, isImageOcrProvider } from '@renderer/types'
|
||||||
import { FileQuestionMarkIcon, MonitorIcon } from 'lucide-react'
|
import { FileQuestionMarkIcon, MonitorIcon } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
@ -19,8 +20,10 @@ const logger = loggerService.withContext('useOcrProvider')
|
|||||||
export const useOcrProviders = () => {
|
export const useOcrProviders = () => {
|
||||||
const providers = useAppSelector((state) => state.ocr.providers)
|
const providers = useAppSelector((state) => state.ocr.providers)
|
||||||
const imageProviders = providers.filter(isImageOcrProvider)
|
const imageProviders = providers.filter(isImageOcrProvider)
|
||||||
const imageProviderId = useAppSelector((state) => state.ocr.imageProviderId)
|
const [imageProviderId, setImageProviderId] = usePreference('ocr.settings.image_provider_id')
|
||||||
const [imageProvider, setImageProvider] = useState<ImageOcrProvider>(DEFAULT_OCR_PROVIDER.image)
|
const imageProvider = useMemo(() => {
|
||||||
|
return imageProviders.find((p) => p.id === imageProviderId)
|
||||||
|
}, [imageProviderId, imageProviders])
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -58,13 +61,6 @@ export const useOcrProviders = () => {
|
|||||||
dispatch(removeOcrProvider(id))
|
dispatch(removeOcrProvider(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
const setImageProviderId = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
dispatch(setImageOcrProviderId(id))
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
const getOcrProviderName = (p: OcrProvider) => {
|
const getOcrProviderName = (p: OcrProvider) => {
|
||||||
return isBuiltinOcrProvider(p) ? getBuiltinOcrProviderLabel(p.id) : p.name
|
return isBuiltinOcrProvider(p) ? getBuiltinOcrProviderLabel(p.id) : p.name
|
||||||
}
|
}
|
||||||
@ -85,21 +81,6 @@ export const useOcrProviders = () => {
|
|||||||
return <FileQuestionMarkIcon size={size} />
|
return <FileQuestionMarkIcon size={size} />
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
providers,
|
providers,
|
||||||
imageProvider,
|
imageProvider,
|
||||||
|
|||||||
@ -3,11 +3,11 @@ import { loggerService } from '@logger'
|
|||||||
import { ErrorTag } from '@renderer/components/Tags/ErrorTag'
|
import { ErrorTag } from '@renderer/components/Tags/ErrorTag'
|
||||||
import { isMac, isWin } from '@renderer/config/constant'
|
import { isMac, isWin } from '@renderer/config/constant'
|
||||||
import { useOcrProviders } from '@renderer/hooks/useOcrProvider'
|
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 { BuiltinOcrProviderIds, isImageOcrProvider } from '@renderer/types'
|
||||||
import { getErrorMessage } from '@renderer/utils'
|
import { getErrorMessage } from '@renderer/utils'
|
||||||
import { Select } from 'antd'
|
import { Select } from 'antd'
|
||||||
import { useCallback, useEffect, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import useSWRImmutable from 'swr/immutable'
|
import useSWRImmutable from 'swr/immutable'
|
||||||
|
|
||||||
@ -15,11 +15,7 @@ import { SettingRow, SettingRowTitle } from '..'
|
|||||||
|
|
||||||
const logger = loggerService.withContext('OcrImageSettings')
|
const logger = loggerService.withContext('OcrImageSettings')
|
||||||
|
|
||||||
type Props = {
|
const OcrImageSettings = () => {
|
||||||
setProvider: (provider: OcrProvider) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const OcrImageSettings = ({ setProvider }: Props) => {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { providers, imageProvider, getOcrProviderName, setImageProviderId } = useOcrProviders()
|
const { providers, imageProvider, getOcrProviderName, setImageProviderId } = useOcrProviders()
|
||||||
const fetcher = useCallback(() => {
|
const fetcher = useCallback(() => {
|
||||||
@ -30,12 +26,6 @@ const OcrImageSettings = ({ setProvider }: Props) => {
|
|||||||
|
|
||||||
const imageProviders = providers.filter((p) => isImageOcrProvider(p))
|
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 setImageProvider = (id: string) => {
|
||||||
const provider = imageProviders.find((p) => p.id === id)
|
const provider = imageProviders.find((p) => p.id === id)
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
@ -44,7 +34,6 @@ const OcrImageSettings = ({ setProvider }: Props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setProvider(provider)
|
|
||||||
setImageProviderId(id)
|
setImageProviderId(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +51,11 @@ const OcrImageSettings = ({ setProvider }: Props) => {
|
|||||||
}))
|
}))
|
||||||
}, [getOcrProviderName, imageProviders, platformSupport, validProviders])
|
}, [getOcrProviderName, imageProviders, platformSupport, validProviders])
|
||||||
|
|
||||||
const isSystem = imageProvider.id === BuiltinOcrProviderIds.system
|
const isSystem = imageProvider?.id === BuiltinOcrProviderIds.system
|
||||||
|
|
||||||
|
if (!imageProvider) {
|
||||||
|
return <Alert color="danger" title={t('ocr.error.provider.not_found')} />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -18,14 +18,14 @@ import { OcrTesseractSettings } from './OcrTesseractSettings'
|
|||||||
// const logger = loggerService.withContext('OcrTesseractSettings')
|
// const logger = loggerService.withContext('OcrTesseractSettings')
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
provider: OcrProvider
|
provider: OcrProvider | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const OcrProviderSettings = ({ provider }: Props) => {
|
const OcrProviderSettings = ({ provider }: Props) => {
|
||||||
const { theme: themeMode } = useTheme()
|
const { theme: themeMode } = useTheme()
|
||||||
const { OcrProviderLogo, getOcrProviderName } = useOcrProviders()
|
const { OcrProviderLogo, getOcrProviderName } = useOcrProviders()
|
||||||
|
|
||||||
if (!isWin && !isMac && isOcrSystemProvider(provider)) {
|
if (!provider || (!isWin && !isMac && isOcrSystemProvider(provider))) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,40 +1,84 @@
|
|||||||
import { PictureOutlined } from '@ant-design/icons'
|
import { PictureOutlined } from '@ant-design/icons'
|
||||||
|
import { cn, Tabs, TabsContent, TabsList, TabsTrigger } from '@cherrystudio/ui'
|
||||||
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useOcrProviders } from '@renderer/hooks/useOcrProvider'
|
import { useOcrProviders } from '@renderer/hooks/useOcrProvider'
|
||||||
import type { OcrProvider } from '@renderer/types'
|
import type { FC, ReactNode } from 'react'
|
||||||
import type { TabsProps } from 'antd'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import { Tabs } from 'antd'
|
|
||||||
import type { FC } from 'react'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
import { SettingDivider, SettingGroup, SettingTitle } from '..'
|
import { SettingDivider, SettingGroup, SettingTitle } from '..'
|
||||||
import OcrImageSettings from './OcrImageSettings'
|
import OcrImageSettings from './OcrImageSettings'
|
||||||
import OcrProviderSettings from './OcrProviderSettings'
|
import OcrProviderSettings from './OcrProviderSettings'
|
||||||
|
|
||||||
|
const TabSchema = z.enum(['image'])
|
||||||
|
type Tab = z.infer<typeof TabSchema>
|
||||||
|
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 OcrSettings: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { theme: themeMode } = useTheme()
|
const { theme: themeMode } = useTheme()
|
||||||
const { imageProvider } = useOcrProviders()
|
const { imageProvider } = useOcrProviders()
|
||||||
const [provider, setProvider] = useState<OcrProvider>(imageProvider) // since default to image provider
|
const [activeTab, setActiveTab] = useState<Tab>('image')
|
||||||
|
const provider = useMemo(() => {
|
||||||
const tabs: TabsProps['items'] = [
|
switch (activeTab) {
|
||||||
{
|
case 'image':
|
||||||
key: 'image',
|
return imageProvider
|
||||||
label: t('settings.tool.ocr.image.title'),
|
|
||||||
icon: <PictureOutlined />,
|
|
||||||
children: <OcrImageSettings setProvider={setProvider} />
|
|
||||||
}
|
}
|
||||||
]
|
}, [imageProvider, activeTab])
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: t('settings.tool.ocr.image.title'),
|
||||||
|
value: 'image',
|
||||||
|
icon: <PictureOutlined />,
|
||||||
|
content: <OcrImageSettings />
|
||||||
|
}
|
||||||
|
] satisfies TabItem[]
|
||||||
|
|
||||||
|
const handleTabChange = useCallback((value: string) => {
|
||||||
|
if (isValidTab(value)) {
|
||||||
|
setActiveTab(value)
|
||||||
|
} else {
|
||||||
|
window.toast.error('Unexpected behavior: Not a valid tab.')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<SettingGroup theme={themeMode}>
|
<SettingGroup theme={themeMode}>
|
||||||
<SettingTitle>{t('settings.tool.ocr.title')}</SettingTitle>
|
<SettingTitle>{t('settings.tool.ocr.title')}</SettingTitle>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<Tabs defaultActiveKey="image" items={tabs} />
|
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
||||||
|
<TabsList>
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
return (
|
||||||
|
<TabsTrigger key={tab.value} value={tab.value} className="cursor-pointer">
|
||||||
|
<div className={cn('flex items-center gap-1', tab.value === activeTab && 'text-primary')}>
|
||||||
|
{tab.icon}
|
||||||
|
{tab.name}
|
||||||
|
</div>
|
||||||
|
</TabsTrigger>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TabsList>
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
return (
|
||||||
|
<TabsContent key={tab.value} value={tab.value} className="pl-1">
|
||||||
|
{tab.content}
|
||||||
|
</TabsContent>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<OcrProviderSettings provider={provider} />
|
<OcrProviderSettings provider={provider} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user