This release adds the audio player as a first-class package. It also includes one breaking change for all existing StatefulReaderWrapper users.
StatefulReaderWrapper and usePublication now detect and handle the "audio" profile automatically. No changes are required to existing EPUB or WebPub integrations.
const { publication, profile, localDataKey } = usePublication({ url });
// profile may now be "epub" | "webPub" | "audio" | null
<StatefulReaderWrapper
profile={ profile }
publication={ publication }
localDataKey={ localDataKey }
/>ReaderPlugins now accepts an audio factory alongside epub and webPub. Use createAudioDefaultPlugin to get the built-in audio plugin set, the same way createDefaultPlugin works for EPUB and WebPub:
import { createDefaultPlugin, createAudioDefaultPlugin } from "@edrlab/thorium-web/reader";
<StatefulReaderWrapper
profile={ profile }
publication={ publication }
localDataKey={ localDataKey }
plugins={{
epub: () => [createDefaultPlugin()],
audio: () => [createAudioDefaultPlugin()]
}}
/>The wrapper resolves only the factory matching the active profile — unused factories are never called.
StatefulReaderWrapper accepts a preferences prop to pass initial preferences or a custom adapter to the underlying provider. The shape is profile-specific:
import { createAudioPreferences } from "@edrlab/thorium-web/audio";
import { createPreferences } from "@edrlab/thorium-web/reader";
// Audio
<StatefulReaderWrapper
profile="audio"
publication={ publication }
localDataKey={ localDataKey }
preferences={{
initialPreferences: createAudioPreferences({ /* ... */ }),
adapter: myAudioAdapter // optional
}}
/>
// EPUB / WebPub
<StatefulReaderWrapper
profile="epub"
publication={ publication }
localDataKey={ localDataKey }
preferences={{
initialPreferences: createPreferences({ /* ... */ }),
adapter: myAdapter // optional
}}
/>If you use StatefulPlayer from @edrlab/thorium-web/audio instead of StatefulReaderWrapper, you are responsible for wrapping it with the required providers. ThAudioPreferencesProvider is separate from ThPreferencesProvider — do not use the EPUB/WebPub one for audio.
import {
StatefulPlayer,
ThStoreProvider,
ThAudioPreferencesProvider,
ThI18nProvider,
createAudioPreferences
} from "@edrlab/thorium-web/audio";
const myPreferences = createAudioPreferences({ /* ... */ });
<ThStoreProvider>
<ThAudioPreferencesProvider initialPreferences={ myPreferences }>
<ThI18nProvider>
<StatefulPlayer
publication={ publication }
localDataKey={ localDataKey }
/>
</ThI18nProvider>
</ThAudioPreferencesProvider>
</ThStoreProvider>Caution
All components from @edrlab/thorium-web/audio must use the providers from that same path. Using providers from @edrlab/thorium-web/reader or @edrlab/thorium-web/epub will create a separate context that the audio components cannot access.
See the Audio package documentation for the full guide.
StatefulReaderWrapper now mounts the appropriate preferences provider (ThPreferencesProvider for EPUB/WebPub, ThAudioPreferencesProvider for audio) and ThI18nProvider internally. Remove these from your outer component tree. Keeping them will result in nested duplicate providers.
Before (1.2.0):
<ThStoreProvider>
<ThPreferencesProvider initialPreferences={ myPreferences }>
<ThI18nProvider>
<StatefulReaderWrapper
profile={ profile }
publication={ publication }
localDataKey={ localDataKey }
/>
</ThI18nProvider>
</ThPreferencesProvider>
</ThStoreProvider>After (1.3.0):
<ThStoreProvider>
<StatefulReaderWrapper
profile={ profile }
publication={ publication }
localDataKey={ localDataKey }
preferences={{ initialPreferences: myPreferences }}
/>
</ThStoreProvider>If you need to pass i18next options (extra namespaces, a custom backend path, etc.) that you previously passed directly to <ThI18nProvider>, use the i18n prop on StatefulReaderWrapper:
<StatefulReaderWrapper
profile={ profile }
publication={ publication }
localDataKey={ localDataKey }
i18n={{
ns: ["thorium-shared", "thorium-web", "my-app"],
defaultNS: ["my-app", "thorium-web", "thorium-shared"]
}}
/>Important
Array fields like ns and defaultNS are replaced, not merged with defaults. Always include "thorium-shared" and "thorium-web" when you extend them.
useNavigator no longer returns a flat navigator interface. The return value is now an object with three namespaced accessors — visual, media, and unified — reflecting the introduction of the audio navigator as a first-class citizen alongside the existing visual (EPUB/WebPub) navigator.
const { submitPreferences, getSetting, goLink, play, pause, ... } = useNavigator();All methods were returned on a single flat object regardless of navigator type.
const navigator = useNavigator();
// Visual navigator (EPUB / WebPub) — reading system settings, preferences, frames
const { submitPreferences, getSetting, preferencesEditor, getCframes, ... } = navigator.visual;
// Media navigator (Audio) — playback, seeking, track navigation
const { play, pause, seek, skipForward, skipBackward, submitPreferences, ... } = navigator.media;
// Unified interface — works across both navigator types
const { go, goLink, goForward, goBackward, currentLocator, previousLocator, nextLocator, isVisual } = navigator.unified;Accessing .visual inside an audio player context (or .media inside an EPUB reader context) will throw at runtime. Use .unified for components that are shared across both profiles.
| Situation | Accessor |
|---|---|
| EPUB/WebPub-only component (settings, spacing, themes, zoom…) | .visual |
| Audio-only component (play/pause, progress bar, volume, sleep timer…) | .media |
| Component shared across both profiles (TOC navigation, position jumping…) | .unified |
.unified prefers the visual navigator when both are present, falling back to media. It exposes the lowest-common-denominator API:
interface UnifiedNavigator {
go(locator: Locator, animated: boolean, callback: (ok: boolean) => void): void;
goLink(link: Link, animated: boolean, callback: (ok: boolean) => void): void;
goForward(animated: boolean, callback: (ok: boolean) => void): void;
goBackward(animated: boolean, callback: (ok: boolean) => void): void;
currentLocator(): Locator | undefined;
previousLocator(): Locator | null;
nextLocator(): Locator | null;
isVisual(): boolean;
getCframes?(): (FrameManager | FXLFrameManager | WebPubFrameManager | undefined)[] | undefined;
underlying: VisualNavigator | MediaNavigator; // escape hatch to the raw navigator
}| Before | After |
|---|---|
useNavigator().submitPreferences(…) |
useNavigator().visual.submitPreferences(…) |
useNavigator().getSetting(key) |
useNavigator().visual.getSetting(key) |
useNavigator().preferencesEditor |
useNavigator().visual.preferencesEditor |
useNavigator().getCframes() |
useNavigator().visual.getCframes() |
useNavigator().goLink(…) |
useNavigator().unified.goLink(…) |
useNavigator().go(…) |
useNavigator().unified.go(…) |
useNavigator().currentLocator() |
useNavigator().unified.currentLocator() |
useTheming has been extended with cover-based automatic theme generation. All new properties are optional.
New input props:
coverUrl: cover image URL to extract a theme fromautoThemeSource:"cover"or"system"onCoverThemeGenerated: callback fired when cover theme tokens are ready
New return values:
coverThemeTokens: the generatedThemeTokens, ornullif not yet readythemeResolved:falsewhile cover theme extraction is in progress — use this to defer rendering
A new core component combining a slider with a preset radio group, used internally by the audio player and available for custom integrations.
import { ThSliderWithPresets } from "@edrlab/thorium-web/core/components";See Core Settings Components for the full API.