fix(fonts): hydrate custom fonts on library page, closes #4178 (#4191)

Custom fonts vanished from the Font panel after an app restart unless a
book was opened first. The custom-font store is hydrated only by the
reader's FoliateViewer (on book open) or by useReplicaPull (gated on a
signed-in user), so opening Settings straight from the library left the
store empty.

Add a useCustomFonts hook that loads persisted custom fonts on mount,
unconditional of auth or book state, and mount it on the library page.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin 2026-05-17 02:19:45 +08:00 committed by GitHub
parent d2ff47029c
commit 2d30868d23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 90 additions and 0 deletions

View file

@ -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<void>>(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();
});
});

View file

@ -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',
);

View file

@ -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]);
};