import clsx from 'clsx'; import React, { useEffect, useState } from 'react'; import { MdChevronRight } from 'react-icons/md'; import { RiBookOpenLine, RiRssLine, RiBookReadLine, RiBook3Line } from 'react-icons/ri'; import { useTranslation } from '@/hooks/useTranslation'; import { useSettingsStore } from '@/store/settingsStore'; import { useCustomOPDSStore } from '@/store/customOPDSStore'; import { CatalogManager } from '@/app/opds/components/CatalogManager'; import KOSyncForm from './integrations/KOSyncForm'; import ReadwiseForm from './integrations/ReadwiseForm'; import HardcoverForm from './integrations/HardcoverForm'; import SubPageHeader from './SubPageHeader'; import { SectionTitle, SettingLabel } from './primitives'; type SubPage = 'kosync' | 'readwise' | 'hardcover' | 'opds' | null; /** * Integrations panel — single point of discovery for external service config: * KOReader Sync, Readwise, Hardcover, and OPDS Catalogs. * * Pattern: boxed list of NavigationRows. Each row pushes the panel into an * inline sub-page (with breadcrumb back-navigation matching the Dictionaries * pattern) — no nested modals. * * TODO(design-system): Once we extract BoxedList / NavigationRow primitives, * this panel and CustomDictionaries should both consume them instead of * inlining the chassis. */ const IntegrationsPanel: React.FC = () => { const _ = useTranslation(); const { settings, requestedSubPage, setRequestedSubPage } = useSettingsStore(); const opdsCatalogs = useCustomOPDSStore((s) => s.catalogs); const opdsCount = opdsCatalogs.filter((c) => !c.deletedAt).length; const [subPage, setSubPage] = useState(null); // Deep-link consumption: when a caller (e.g. OPDS browser close handler) // sets `requestedSubPage` in the store before opening the dialog, drill // straight into that sub-page on mount and clear the request so it doesn't // stick to the next open. Recognised values match the SubPage union. useEffect(() => { if (!requestedSubPage) return; if ( requestedSubPage === 'kosync' || requestedSubPage === 'readwise' || requestedSubPage === 'hardcover' || requestedSubPage === 'opds' ) { setSubPage(requestedSubPage); } setRequestedSubPage(null); }, [requestedSubPage, setRequestedSubPage]); // Sub-page wrapper matches the list-view's `my-4 w-full` so the // SubPageHeader's "Integrations" label lands at the exact same Y position // as the list-view's h2 — clicking a row reads as a navigation morph // rather than a layout shift. if (subPage === 'kosync') return (
setSubPage(null)} />
); if (subPage === 'readwise') return (
setSubPage(null)} />
); if (subPage === 'hardcover') return (
setSubPage(null)} />
); if (subPage === 'opds') return (
setSubPage(null)} />
); const koSyncStatus = settings.kosync?.enabled ? settings.kosync.username ? _('Connected as {{user}}', { user: settings.kosync.username }) : _('Connected') : _('Not connected'); const readwiseStatus = settings.readwise?.enabled ? _('Connected') : _('Not connected'); const hardcoverStatus = settings.hardcover?.enabled ? _('Connected') : _('Not connected'); const opdsStatus = opdsCount > 0 ? _('{{count}} catalog', { count: opdsCount }) : _('No catalogs'); return (

{_('Integrations')}

{_('Connect Readest to external services for sync, highlights, and catalogs.')}

{_('Reading Sync')}
setSubPage('kosync')} /> setSubPage('readwise')} /> setSubPage('hardcover')} />
{_('Content Sources')}
setSubPage('opds')} />
); }; interface IntegrationRowProps { icon: React.ElementType; title: string; status: string; onClick: () => void; } const IntegrationRow: React.FC = ({ icon: Icon, title, status, onClick }) => { return ( ); }; export default IntegrationsPanel;