refactor(ocr): restructure ocr provider settings and hooks

- Simplify useOcrImageProvider by directly using useOcrProvider
- Make useOcrProvider handle null provider IDs
- Update provider settings components to use passed props
- Remove styled-components in favor of tailwind classes
This commit is contained in:
icarus 2025-10-20 09:10:04 +08:00
parent 741bb94c8b
commit d47c3b1d63
5 changed files with 60 additions and 65 deletions

View File

@ -1,16 +1,9 @@
import { usePreference } from '@data/hooks/usePreference' import { usePreference } from '@data/hooks/usePreference'
import type { ImageOcrProvider } from '@renderer/types'
import { isImageOcrProvider } from '@renderer/types'
import { useMemo } from 'react'
import { useOcrProviders } from './useOcrProviders' import { useOcrProvider } from './useOcrProvider'
export const useOcrImageProvider = () => { export const useOcrImageProvider = () => {
const { providers, loading, error } = useOcrProviders()
const imageProviders: ImageOcrProvider[] | undefined = providers?.filter((p) => isImageOcrProvider(p))
const [imageProviderId, setImageProviderId] = usePreference('ocr.settings.image_provider_id') const [imageProviderId, setImageProviderId] = usePreference('ocr.settings.image_provider_id')
const imageProvider = useMemo(() => { const { provider: imageProvider, mutating, loading, error, updateConfig } = useOcrProvider(imageProviderId)
return imageProviders?.find((p) => p.id === imageProviderId) return { imageProvider, loading, mutating, error, updateConfig, imageProviderId, setImageProviderId }
}, [imageProviderId, imageProviders])
return { imageProvider, loading, error, imageProviderId, setImageProviderId }
} }

View File

@ -7,15 +7,16 @@ import { useTranslation } from 'react-i18next'
// const logger = loggerService.withContext('useOcrProvider') // const logger = loggerService.withContext('useOcrProvider')
export const useOcrProvider = (id: string) => { export const useOcrProvider = (id: string | null) => {
const { t } = useTranslation() const { t } = useTranslation()
const path: ConcreteApiPaths = `/ocr/providers/${id}` const path: ConcreteApiPaths = `/ocr/providers/${id}`
const { data, loading, error } = useQuery(path, undefined) const { data, loading, error } = useQuery(path)
const { mutate, loading: mutating } = useMutation('PATCH', path) const { mutate, loading: mutating } = useMutation('PATCH', path)
const updateConfig = useCallback( const updateConfig = useCallback(
async (update: Partial<OcrProviderConfig>) => { async (update: Partial<OcrProviderConfig>) => {
if (!id) return
try { try {
await mutate({ body: { id, config: update } }) await mutate({ body: { id, config: update } })
} catch (e) { } catch (e) {
@ -26,7 +27,8 @@ export const useOcrProvider = (id: string) => {
) )
return { return {
provider: data?.data, /** undefined: loading; null: invalid, id is null */
provider: id ? data?.data : null,
loading, loading,
mutating, mutating,
error, error,

View File

@ -7,11 +7,17 @@ import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
import { isMac, isWin } from '@renderer/config/constant' import { isMac, isWin } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useOcrProviders } from '@renderer/hooks/ocr/useOcrProviders' import { useOcrProviders } from '@renderer/hooks/ocr/useOcrProviders'
import type { OcrProvider } from '@renderer/types' import type { OcrProvider, OcrProviderConfig } from '@renderer/types'
import { isBuiltinOcrProvider, isOcrSystemProvider } from '@renderer/types' import {
isBuiltinOcrProvider,
isOcrOVProvider,
isOcrPpocrProvider,
isOcrSystemProvider,
isOcrTesseractProvider
} from '@renderer/types'
import { Divider } from 'antd' import { Divider } from 'antd'
import { FileQuestionMarkIcon, MonitorIcon } from 'lucide-react' import { FileQuestionMarkIcon, MonitorIcon } from 'lucide-react'
import styled from 'styled-components' import { useMemo } from 'react'
import { SettingGroup, SettingTitle } from '..' import { SettingGroup, SettingTitle } from '..'
import { OcrOVSettings } from './OcrOVSettings' import { OcrOVSettings } from './OcrOVSettings'
@ -22,34 +28,37 @@ import { OcrTesseractSettings } from './OcrTesseractSettings'
// const logger = loggerService.withContext('OcrTesseractSettings') // const logger = loggerService.withContext('OcrTesseractSettings')
type Props = { type Props = {
provider: OcrProvider | undefined provider: OcrProvider | undefined | null
updateConfig: (config: Partial<OcrProviderConfig>) => Promise<void>
} }
const OcrProviderSettings = ({ provider }: Props) => { const OcrProviderSettings = ({ provider, updateConfig }: Props) => {
const { theme: themeMode } = useTheme() const { theme: themeMode } = useTheme()
const { getOcrProviderName } = useOcrProviders() const { getOcrProviderName } = useOcrProviders()
if (!provider || (!isWin && !isMac && isOcrSystemProvider(provider))) { const settings = useMemo(() => {
return null if (!provider) return null
}
const ProviderSettings = () => {
if (isBuiltinOcrProvider(provider)) { if (isBuiltinOcrProvider(provider)) {
switch (provider.id) { if (isOcrTesseractProvider(provider)) {
case 'tesseract': return <OcrTesseractSettings provider={provider} updateConfig={updateConfig} />
return <OcrTesseractSettings />
case 'system':
return <OcrSystemSettings />
case 'paddleocr':
return <OcrPpocrSettings />
case 'ovocr':
return <OcrOVSettings />
default:
return null
} }
if (isOcrSystemProvider(provider)) {
return <OcrSystemSettings />
}
if (isOcrPpocrProvider(provider)) {
return <OcrPpocrSettings />
}
if (isOcrOVProvider(provider)) {
return <OcrOVSettings />
}
return null
} else { } else {
throw new Error('Not supported OCR provider') throw new Error('Not supported OCR provider')
} }
}, [provider, updateConfig])
if (!provider || (!isWin && !isMac && isOcrSystemProvider(provider))) {
return null
} }
return ( return (
@ -57,22 +66,15 @@ const OcrProviderSettings = ({ provider }: Props) => {
<SettingTitle> <SettingTitle>
<Flex className="items-center gap-2"> <Flex className="items-center gap-2">
<OcrProviderLogo provider={provider} /> <OcrProviderLogo provider={provider} />
<ProviderName> {getOcrProviderName(provider)}</ProviderName> <span className="font-semibold text-sm"> {getOcrProviderName(provider)}</span>
</Flex> </Flex>
</SettingTitle> </SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} /> <Divider style={{ width: '100%', margin: '10px 0' }} />
<ErrorBoundary> <ErrorBoundary>{settings}</ErrorBoundary>
<ProviderSettings />
</ErrorBoundary>
</SettingGroup> </SettingGroup>
) )
} }
const ProviderName = styled.span`
font-size: 14px;
font-weight: 500;
`
const OcrProviderLogo = ({ provider: p, size = 14 }: { provider: OcrProvider; size?: number }) => { const OcrProviderLogo = ({ provider: p, size = 14 }: { provider: OcrProvider; size?: number }) => {
if (isBuiltinOcrProvider(p)) { if (isBuiltinOcrProvider(p)) {
switch (p.id) { switch (p.id) {

View File

@ -21,7 +21,7 @@ import OcrProviderSettings from './OcrProviderSettings'
const OcrSettings: FC = () => { const OcrSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme: themeMode } = useTheme() const { theme: themeMode } = useTheme()
const { imageProvider: provider } = useOcrImageProvider() const { imageProvider: provider, updateConfig } = useOcrImageProvider()
// const [activeTab, setActiveTab] = useState<Tab>('image') // const [activeTab, setActiveTab] = useState<Tab>('image')
// const provider = useMemo(() => { // const provider = useMemo(() => {
// switch (activeTab) { // switch (activeTab) {
@ -82,7 +82,7 @@ const OcrSettings: FC = () => {
</SettingGroup> </SettingGroup>
<ErrorBoundary> <ErrorBoundary>
<OcrProviderSettings provider={provider} /> <OcrProviderSettings provider={provider} updateConfig={updateConfig} />
</ErrorBoundary> </ErrorBoundary>
</ErrorBoundary> </ErrorBoundary>
) )

View File

@ -2,10 +2,9 @@
import { Flex } from '@cherrystudio/ui' import { Flex } from '@cherrystudio/ui'
import { InfoTooltip } from '@cherrystudio/ui' import { InfoTooltip } from '@cherrystudio/ui'
import CustomTag from '@renderer/components/Tags/CustomTag' import CustomTag from '@renderer/components/Tags/CustomTag'
import { useOcrProvider } from '@renderer/hooks/ocr/useOcrProvider'
import useTranslate from '@renderer/hooks/useTranslate' import useTranslate from '@renderer/hooks/useTranslate'
import type { TesseractLangCode } from '@renderer/types' import type { OcrProviderConfig, OcrTesseractConfig, OcrTesseractProvider, TesseractLangCode } from '@renderer/types'
import { BuiltinOcrProviderIdMap, isOcrTesseractProvider } from '@renderer/types' import { objectEntries } from '@renderer/types'
import { TESSERACT_LANG_MAP } from '@shared/config/ocr' import { TESSERACT_LANG_MAP } from '@shared/config/ocr'
import { Select } from 'antd' import { Select } from 'antd'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
@ -15,15 +14,16 @@ import { SettingRow, SettingRowTitle } from '..'
// const logger = loggerService.withContext('OcrTesseractSettings') // const logger = loggerService.withContext('OcrTesseractSettings')
export const OcrTesseractSettings = () => { export const OcrTesseractSettings = ({
provider,
updateConfig
}: {
provider: OcrTesseractProvider
updateConfig: (config: Partial<OcrProviderConfig>) => Promise<void>
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const { provider, config, updateConfig } = useOcrProvider(BuiltinOcrProviderIdMap.tesseract)
if (!isOcrTesseractProvider(provider)) { const [langs, setLangs] = useState<OcrTesseractConfig['langs'] | undefined>(provider?.config.langs)
throw new Error('Not tesseract provider.')
}
const [langs, setLangs] = useState<Partial<Record<TesseractLangCode, boolean>>>(config?.langs ?? {})
const { translateLanguages } = useTranslate() const { translateLanguages } = useTranslate()
const options = useMemo( const options = useMemo(
@ -37,14 +37,12 @@ export const OcrTesseractSettings = () => {
[translateLanguages] [translateLanguages]
) )
// TODO: type safe objectKeys const selectedLangs = useMemo(() => {
const value = useMemo( if (!langs) return
() => return objectEntries(langs)
Object.entries(langs) .filter(([, enabled]) => enabled)
.filter(([, enabled]) => enabled) .map(([lang]) => lang) as TesseractLangCode[]
.map(([lang]) => lang) as TesseractLangCode[], }, [langs])
[langs]
)
const onChange = useCallback((values: TesseractLangCode[]) => { const onChange = useCallback((values: TesseractLangCode[]) => {
setLangs(() => { setLangs(() => {
@ -69,11 +67,11 @@ export const OcrTesseractSettings = () => {
<InfoTooltip content={t('settings.tool.ocr.tesseract.langs_tooltip')} /> <InfoTooltip content={t('settings.tool.ocr.tesseract.langs_tooltip')} />
</Flex> </Flex>
</SettingRowTitle> </SettingRowTitle>
<div style={{ display: 'flex', gap: '8px' }}> <div className="flex gap-2">
<Select <Select
mode="multiple" mode="multiple"
style={{ minWidth: 200 }} style={{ minWidth: 200 }}
value={value} value={selectedLangs}
options={options} options={options}
maxTagCount={1} maxTagCount={1}
onChange={onChange} onChange={onChange}