diff --git a/apps/readest-app/src/__tests__/hooks/useCustomFonts.test.tsx b/apps/readest-app/src/__tests__/hooks/useCustomFonts.test.tsx new file mode 100644 index 00000000..580ed531 --- /dev/null +++ b/apps/readest-app/src/__tests__/hooks/useCustomFonts.test.tsx @@ -0,0 +1,56 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { cleanup, renderHook } from '@testing-library/react'; + +const loadCustomFontsSpy = vi.fn<(...args: unknown[]) => Promise>(async () => {}); + +let envValue: { envConfig: unknown; appService: unknown } = { + envConfig: { name: 'env' }, + appService: { name: 'svc' }, +}; +let settingsValue: { settings: { customFonts?: unknown[] } } = { + settings: { customFonts: [] }, +}; + +vi.mock('@/context/EnvContext', () => ({ + useEnv: () => envValue, +})); + +vi.mock('@/store/settingsStore', () => ({ + useSettingsStore: () => settingsValue, +})); + +vi.mock('@/store/customFontStore', () => ({ + useCustomFontStore: () => ({ loadCustomFonts: loadCustomFontsSpy }), +})); + +import { useCustomFonts } from '@/hooks/useCustomFonts'; + +beforeEach(() => { + loadCustomFontsSpy.mockClear(); + envValue = { envConfig: { name: 'env' }, appService: { name: 'svc' } }; + settingsValue = { settings: { customFonts: [] } }; +}); + +afterEach(() => { + cleanup(); +}); + +describe('useCustomFonts', () => { + test('hydrates the custom font store on mount', () => { + renderHook(() => useCustomFonts()); + expect(loadCustomFontsSpy).toHaveBeenCalledTimes(1); + expect(loadCustomFontsSpy).toHaveBeenCalledWith(envValue.envConfig); + }); + + test('waits for the app service before hydrating', () => { + envValue = { envConfig: { name: 'env' }, appService: null }; + renderHook(() => useCustomFonts()); + expect(loadCustomFontsSpy).not.toHaveBeenCalled(); + }); + + test('skips hydration when settings carry no customFonts field', () => { + settingsValue = { settings: {} }; + renderHook(() => useCustomFonts()); + expect(loadCustomFontsSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index 47663250..a7e52680 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -81,6 +81,7 @@ import LibraryEmptyState from './components/LibraryEmptyState'; import GroupHeader from './components/GroupHeader'; import useShortcuts from '@/hooks/useShortcuts'; import { useReplicaPull } from '@/hooks/useReplicaPull'; +import { useCustomFonts } from '@/hooks/useCustomFonts'; import DropIndicator from '@/components/DropIndicator'; import SettingsDialog from '@/components/settings/SettingsDialog'; import ModalPortal from '@/components/ModalPortal'; @@ -124,6 +125,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP useReplicaPull({ kinds: ['dictionary', 'font', 'texture', 'opds_catalog', 'settings'], }); + // Hydrate the custom-font store from persisted settings so the Font + // panel sees imported fonts even when opened straight from the + // library — the replica pull above is auth-gated and the reader's + // FoliateViewer hydration never runs without a book open. + useCustomFonts(); const [showCatalogManager, setShowCatalogManager] = useState( searchParams?.get('opds') === 'true', ); diff --git a/apps/readest-app/src/hooks/useCustomFonts.ts b/apps/readest-app/src/hooks/useCustomFonts.ts new file mode 100644 index 00000000..19b803c7 --- /dev/null +++ b/apps/readest-app/src/hooks/useCustomFonts.ts @@ -0,0 +1,28 @@ +import { useEffect } from 'react'; +import { useEnv } from '@/context/EnvContext'; +import { useSettingsStore } from '@/store/settingsStore'; +import { useCustomFontStore } from '@/store/customFontStore'; + +/** + * Hydrate the custom-font store from persisted `settings.customFonts`. + * + * The reader hydrates the store inside FoliateViewer when a book opens, + * and `useReplicaPull` hydrates it during a sync — but that pull is + * gated on a signed-in user. Without this hook, opening Settings + * straight from the library (no book opened, no account) leaves the + * store empty, so imported custom fonts vanish from the Font panel + * after an app restart until a book is opened. Mount this on the + * library page so the panel always sees the persisted fonts. + */ +export const useCustomFonts = () => { + const { envConfig, appService } = useEnv(); + const { settings } = useSettingsStore(); + const { loadCustomFonts } = useCustomFontStore(); + + useEffect(() => { + if (!appService) return; + if (!settings?.customFonts) return; + void loadCustomFonts(envConfig); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [appService, settings?.customFonts, envConfig]); +};