@caipira/vue-reader
v0.0.15
Published
Headless e-book reader toolkit for vue
Readme
@caipira/vue-reader
Headless reader toolkit for Vue 3.
This package provides a set of tools for creating your own custom reader. It currently supports:
- Raw text
- EPUB
What is included
- Headless epub reader via
useEpubReader,useEpubReaderStateanduseEpubReaderNavigation - Colors hook
setReaderColors - Dictionary integration hook
setReaderDictionary
Install
npm install @caipira/vue-readerUsage
Script:
import { useTemplateRef } from 'vue';
import {
useEpubReader,
setReaderColors,
useEpubReaderState,
useEpubReaderNavigation,
} from '@caipira/vue-reader';
const epubSrc = 'https://example.com/book.epub';
const containerRef = useTemplateRef<HTMLDivElement>('containerRef');
setReaderColors({
background: '#0f172a',
text: '#f8fafc',
highlight: '#facc15',
});
const reader = useEpubReader(() => epubSrc, containerRef);
const { error, loading } = useEpubReaderState();
const { goForward, goBackward } = useEpubReaderNavigation();Template:
<section class="w-full flex justify-between">
<button
icon="chevron-left"
@click="goBackward"
/>
<button
icon="chevron-right"
@click="goForward"
/>
</section>
<div
ref="containerRef"
class="h-full w-full overflow-hidden"
:class="{ 'opacity-0': loading || error }"
/>Dictionary API
The reader uses your dictionary adapter to classify words as known or unknown.
Unknown words are highlighted automatically in the viewport.
Implement and register a dictionary adapter:
import type {
ReaderDictionaryApi,
ReaderDictionaryWordStatus,
} from '@caipira/vue-reader';
import { setReaderDictionary } from '@caipira/vue-reader';
function createDictionary(): ReaderDictionaryApi {
const cache = new Map<string, ReaderDictionaryWordStatus>();
return {
async classifyWords(language, lemmas, contextKey) {
// Load/sync dictionary data for `language` once, then classify lemmas.
// `contextKey` can be used as a cache partition key.
const result = new Map<string, ReaderDictionaryWordStatus>();
for (const lemma of lemmas) {
const status = cache.get(lemma) ?? 'unknown';
result.set(lemma, status);
}
return result;
},
clear() {
cache.clear();
},
};
}
setReaderDictionary(createDictionary());Contract
classifyWords(language, lemmas, contextKey)Returns aMap<lemma, 'known' | 'unknown'>.clear()Clears adapter caches when reader state is torn down.
Word highlighting and lookup flow
When a dictionary is registered with setReaderDictionary(...):
- Unknown words are highlighted automatically.
- Selecting a single word triggers a lookup, whatever the device raises the selection with: a double click, a long press, a drag. A selection holding more than one word is left alone.
You can handle lookup events by extending your dictionary object with an optional onWordLookup callback (matching the UI integration):
import type { ReaderDictionaryApi } from '@caipira/vue-reader';
import { setReaderDictionary } from '@caipira/vue-reader';
const dictionary = createDictionary() as ReaderDictionaryApi & {
onWordLookup?: (payload: { language: string; word: string; context: string }) => void;
};
dictionary.onWordLookup = ({ language, word, context }) => {
// Open your dictionary UI (drawer/modal/popover) and query definitions.
openDictionaryDrawer({ language, word, context });
};
setReaderDictionary(dictionary);The context value contains nearby text around the clicked word, useful for disambiguation.
Public API
useEpubReader(src, containerRef, options)useEpubReaderState()useEpubReaderNavigation()setReaderColors({ background, text, highlight })setReaderDictionary(dictionary)useReaderDictionary()
Adapters
Preferences adapter
Use preferences to load and persist reader settings from your app-specific store.
Storage adapter
Use storage to override default sessionStorage-based locator persistence.
import type { Locator } from '@readium/shared';
import type { ReaderStorageAdapter } from '@caipira/vue-reader';
const saved = new Map<string, { locator?: Locator; percentage?: number }>();
const storage: ReaderStorageAdapter = {
savePosition(key, locator, progress) {
// `progress.percentage` is 0..1, not 0..100.
saved.set(key, { locator, percentage: progress?.percentage });
},
restorePosition(key, publication, positions) {
const state = saved.get(key);
if (state?.locator) {
return state.locator;
}
if (state?.percentage === undefined) {
return undefined;
}
// A percentage synced from a device: the nearest resource start is where it is.
let nearest: Locator | undefined;
let shortest = Infinity;
for (const position of positions) {
const at = position.locations.totalProgression;
if (at === undefined) continue;
const distance = Math.abs(at - state.percentage);
if (distance < shortest) {
shortest = distance;
nearest = position;
}
}
return nearest;
},
};Contract
savePosition(key, locator, progress?)Called whenever the reading position changes.progress.percentageis how far through the whole book the reader is, on a 0..1 scale — not 0..100. This is the scale devices such as KOReader keep their own progress on, so a position saved here and one synced from a device are directly comparable. Store it as-is and multiply only for display.locator.locations.totalProgressionis that same number, andlocator.locations.progressionis how far through its own resource the reader is.
restorePosition(key, publication, positions)Called once per book opened, before the navigator is created; return the locator to open at, orundefinedto open the book at its start.positionsis the publication's generated positions list. An adapter that holds nothing but a percentage — a position that came from a device rather than from this reader — uses it to map that percentage back onto a locator.- The list holds one locator per resource in the reading order, each carrying
that resource's
positionand, inlocations.totalProgression, where the resource starts on the same weighted 0..1 scalesavePositionhands a percentage out on. Invert a percentage by taking the locator whosetotalProgressionis nearest it, as above — that places a reader in a resource rather than on a page. - A resource the reader cannot place carries no
totalProgression. Skip those rather than reading them as the start of the book.
Both calls are synchronous. An adapter backed by a server should load its state before the reader mounts and write behind, so nothing the reader does waits on the network.
Build
npm run typecheck
npm run build