refactor(reader): implement page-based pagination with progress restoration
Complete rewrite of ebook reader initialization to use page-based pagination with CFI position tracking: Progress restoration: - Fetch saved reading progress on initialization via getReadingProgress - Restore position using saved CFI or fallback to page number - Maintain accurate position across reflowable content changes Pagination system: - Calculate pagination based on user settings and viewport - Load user settings for font size, line height, and margins - Generate page map for accurate page-to-content mapping Viewport responsiveness: - Implement resize handler with 300ms debounce - Recalculate pagination on viewport changes - Preserve reading position during recalculation Architecture improvements: - Update feature module imports to new directory structure - Add pagination and position to UniversalReader interface - Implement renderPage for initial content display - Update UI components with accurate page/progress data This provides the foundation for robust EPUB reading with accurate position tracking and responsive pagination.
This commit is contained in:
+291
-37
@@ -1,12 +1,12 @@
|
|||||||
import { Alpine } from "../alpine";
|
import { Alpine } from "../alpine";
|
||||||
import { getReaderMetadata } from "../api";
|
import { getReaderMetadata, getReadingProgress } from "../api";
|
||||||
import "./comic/panel-editor";
|
import "./formats/comic/panel-editor";
|
||||||
import { createReaderContext, type ReaderContext } from "./core/reader-context";
|
import { createReaderContext, type ReaderContext } from "./core/reader-context";
|
||||||
import { getState, setState } from "./core/reader-state";
|
import { getState, setState } from "./core/reader-state";
|
||||||
import { createNavigationAPI } from "./core/reader-navigation";
|
import { createNavigationAPI } from "./core/reader-navigation";
|
||||||
import { readerEvents } from "./core/reader-events";
|
import { readerEvents } from "./core/reader-events";
|
||||||
import { renderSpineItem } from "./core/reader-navigation";
|
import { renderSpineItem } from "./core/reader-navigation";
|
||||||
import { parseReflowable } from "./formats/reflowable/parser";
|
import { loadSettings } from "./settings-manager";
|
||||||
import {
|
import {
|
||||||
calculatePagination,
|
calculatePagination,
|
||||||
shouldRecalculate,
|
shouldRecalculate,
|
||||||
@@ -14,8 +14,9 @@ import {
|
|||||||
import { restorePosition } from "./formats/reflowable/progress-tracker";
|
import { restorePosition } from "./formats/reflowable/progress-tracker";
|
||||||
import { applyPaginatedStyles } from "./formats/reflowable/content-renderer";
|
import { applyPaginatedStyles } from "./formats/reflowable/content-renderer";
|
||||||
import type {
|
import type {
|
||||||
ReflowableBook,
|
|
||||||
PaginationSettings,
|
PaginationSettings,
|
||||||
|
PaginationData,
|
||||||
|
ReadingPosition,
|
||||||
} from "./formats/reflowable/types";
|
} from "./formats/reflowable/types";
|
||||||
import { updatePageDisplay, updateProgressBar } from "./ui/page-display";
|
import { updatePageDisplay, updateProgressBar } from "./ui/page-display";
|
||||||
|
|
||||||
@@ -25,6 +26,8 @@ export interface UniversalReader {
|
|||||||
currentSpineIndex: number;
|
currentSpineIndex: number;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
pageCalculationResult?: any;
|
pageCalculationResult?: any;
|
||||||
|
pagination?: PaginationData;
|
||||||
|
position?: ReadingPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PDFReader {
|
interface PDFReader {
|
||||||
@@ -59,31 +62,39 @@ type FeatureInit = (context: ReaderContext) => void | Promise<void>;
|
|||||||
|
|
||||||
const featureModules: FeatureInit[] = [
|
const featureModules: FeatureInit[] = [
|
||||||
// Core features
|
// Core features
|
||||||
(ctx) => import("./features/gestures").then((m) => m.init(ctx)),
|
(ctx) => import("./ui/gestures").then((m) => m.init(ctx)),
|
||||||
(ctx) => import("./features/keyboard-shortcuts").then((m) => m.init(ctx)),
|
(ctx) => import("./ui/keyboard-shortcuts").then((m) => m.init(ctx)),
|
||||||
(ctx) => import("./features/panel-dock-system").then((m) => m.init(ctx)),
|
(ctx) => import("./ui/panel-dock-system").then((m) => m.init(ctx)),
|
||||||
(ctx) => import("./features/navigator-panel").then((m) => m.init(ctx)),
|
(ctx) => import("./ui/navigator-panel").then((m) => m.init(ctx)),
|
||||||
(ctx) => import("./features/offline-manager").then((m) => m.init(ctx)),
|
(ctx) => import("./ui/offline-manager").then((m) => m.init(ctx)),
|
||||||
(ctx) => import("./features/reading-speed-tracker").then((m) => m.init(ctx)),
|
(ctx) => import("./ui/reading-speed-tracker").then((m) => m.init(ctx)),
|
||||||
(ctx) => import("./features/progress-indicator").then((m) => m.init(ctx)),
|
(ctx) => import("./ui/progress-indicator").then((m) => m.init(ctx)),
|
||||||
|
|
||||||
// Comic features
|
// Comic features
|
||||||
(ctx) => import("./comic/background-color").then((m) => m.init?.(ctx)),
|
(ctx) =>
|
||||||
(ctx) => import("./comic/chapter-markers").then((m) => m.init?.(ctx)),
|
import("./formats/comic/background-color").then((m) => m.init?.(ctx)),
|
||||||
(ctx) => import("./comic/page-cache").then((m) => m.init?.(ctx)),
|
(ctx) => import("./formats/comic/chapter-markers").then((m) => m.init?.(ctx)),
|
||||||
|
(ctx) => import("./formats/comic/page-cache").then((m) => m.init?.(ctx)),
|
||||||
|
|
||||||
// PDF features
|
// PDF features
|
||||||
(ctx) => import("./pdf/pdf-navigation").then((m) => m.init?.(ctx)),
|
(ctx) => import("./formats/pdf/pdf-navigation").then((m) => m.init?.(ctx)),
|
||||||
(ctx) => import("./pdf/pdf-text-selection").then((m) => m.init?.(ctx)),
|
(ctx) =>
|
||||||
(ctx) => import("./pdf/annotation-layer").then((m) => m.init?.(ctx)),
|
import("./formats/pdf/pdf-text-selection").then((m) => m.init?.(ctx)),
|
||||||
|
(ctx) => import("./formats/pdf/annotation-layer").then((m) => m.init?.(ctx)),
|
||||||
|
|
||||||
// Ebook features
|
// Ebook features
|
||||||
(ctx) => import("./ebook/copy-handler").then((m) => m.init?.(ctx)),
|
(ctx) =>
|
||||||
(ctx) => import("./ebook/font-loader").then((m) => m.init?.(ctx)),
|
import("./formats/reflowable/ebook/copy-handler").then((m) =>
|
||||||
|
m.init?.(ctx),
|
||||||
|
),
|
||||||
|
(ctx) =>
|
||||||
|
import("./formats/reflowable/ebook/font-loader").then((m) => m.init?.(ctx)),
|
||||||
|
|
||||||
// Manga features
|
// Manga features
|
||||||
(ctx) => import("./manga/reading-direction").then((m) => m.init?.(ctx)),
|
(ctx) =>
|
||||||
(ctx) => import("./manga/vertical-scroll-mode").then((m) => m.init?.(ctx)),
|
import("./formats/manga/reading-direction").then((m) => m.init?.(ctx)),
|
||||||
|
(ctx) =>
|
||||||
|
import("./formats/manga/vertical-scroll-mode").then((m) => m.init?.(ctx)),
|
||||||
];
|
];
|
||||||
|
|
||||||
async function initializeFeatures(context: ReaderContext): Promise<void> {
|
async function initializeFeatures(context: ReaderContext): Promise<void> {
|
||||||
@@ -174,14 +185,11 @@ async function initializeReader(): Promise<void> {
|
|||||||
async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
|
async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
|
||||||
const { parseEbook, requiresServerParsing } =
|
const { parseEbook, requiresServerParsing } =
|
||||||
await import("./core/parser-manager");
|
await import("./core/parser-manager");
|
||||||
|
|
||||||
const needsServer = requiresServerParsing(
|
const needsServer = requiresServerParsing(
|
||||||
metadata.mime_type,
|
metadata.mime_type,
|
||||||
getFileExtension(metadata.file_path),
|
getFileExtension(metadata.file_path),
|
||||||
);
|
);
|
||||||
|
|
||||||
let ebookFile: Blob;
|
let ebookFile: Blob;
|
||||||
|
|
||||||
if (needsServer) {
|
if (needsServer) {
|
||||||
const response = await fetch(`/readers/${metadata.media_item_id}/parse`, {
|
const response = await fetch(`/readers/${metadata.media_item_id}/parse`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -191,17 +199,14 @@ async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
|
|||||||
file_path: metadata.file_path,
|
file_path: metadata.file_path,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Server parsing failed: ${response.statusText}`);
|
throw new Error(`Server parsing failed: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
ebookFile = await response.blob();
|
ebookFile = await response.blob();
|
||||||
} else {
|
} else {
|
||||||
const response = await fetch(metadata.file_path);
|
const response = await fetch(metadata.file_path);
|
||||||
ebookFile = await response.blob();
|
ebookFile = await response.blob();
|
||||||
}
|
}
|
||||||
|
|
||||||
const cif = await parseEbook(
|
const cif = await parseEbook(
|
||||||
ebookFile,
|
ebookFile,
|
||||||
metadata.mime_type,
|
metadata.mime_type,
|
||||||
@@ -213,27 +218,134 @@ async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
|
|||||||
tocLength: cif.toc?.length,
|
tocLength: cif.toc?.length,
|
||||||
keys: Object.keys(cif),
|
keys: Object.keys(cif),
|
||||||
});
|
});
|
||||||
console.log("Spine items detail:", JSON.stringify(cif.spine));
|
// ============================================================
|
||||||
|
// Fetch saved progress from backend
|
||||||
|
// ============================================================
|
||||||
|
const savedProgress = await getReadingProgress(metadata.media_item_id);
|
||||||
|
const savedCFI = savedProgress?.epubcfi;
|
||||||
|
const savedPage = savedProgress?.current_page;
|
||||||
|
console.log("Saved progress:", { savedCFI, savedPage });
|
||||||
|
// ============================================================
|
||||||
|
// Calculate pagination
|
||||||
|
// ============================================================
|
||||||
|
const userSettings = await loadSettings().catch(() => ({
|
||||||
|
font_size: 16,
|
||||||
|
line_height: 1.6,
|
||||||
|
margin_width: 20,
|
||||||
|
}));
|
||||||
|
const settings: PaginationSettings = {
|
||||||
|
fontSize: userSettings.font_size,
|
||||||
|
lineHeight: userSettings.line_height,
|
||||||
|
viewportWidth: window.innerWidth,
|
||||||
|
viewportHeight: window.innerHeight - 120,
|
||||||
|
wordsPerPage: 250,
|
||||||
|
};
|
||||||
|
console.log("Using settings:", {
|
||||||
|
fontSize: userSettings.font_size,
|
||||||
|
lineHeight: userSettings.line_height,
|
||||||
|
marginWidth: userSettings.margin_width,
|
||||||
|
});
|
||||||
|
const spineItems = cif.spine.map((s: any) => ({
|
||||||
|
id: s.id || s.href,
|
||||||
|
type: "html" as const,
|
||||||
|
content: s.href,
|
||||||
|
href: s.href,
|
||||||
|
}));
|
||||||
|
const pagination = await calculatePagination(
|
||||||
|
spineItems,
|
||||||
|
cif.resources,
|
||||||
|
settings,
|
||||||
|
);
|
||||||
|
console.log("Pagination calculated:", {
|
||||||
|
totalPages: pagination.totalPages,
|
||||||
|
spines: pagination.spines.length,
|
||||||
|
});
|
||||||
|
// ============================================================
|
||||||
|
// Restore saved position or start at page 1
|
||||||
|
// ============================================================
|
||||||
|
const position: ReadingPosition = restorePosition(
|
||||||
|
{
|
||||||
|
type: "epub",
|
||||||
|
spine: spineItems,
|
||||||
|
resources: cif.resources,
|
||||||
|
toc: cif.toc || [],
|
||||||
|
metadata: {
|
||||||
|
title: cif.metadata.title || metadata.title,
|
||||||
|
author: cif.metadata.author || metadata.author,
|
||||||
|
identifier: metadata.id,
|
||||||
|
},
|
||||||
|
pagination,
|
||||||
|
position: {
|
||||||
|
currentPage: 1,
|
||||||
|
spineIndex: 0,
|
||||||
|
localPageIndex: 0,
|
||||||
|
cfi: "",
|
||||||
|
progress: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
savedCFI || "",
|
||||||
|
savedPage,
|
||||||
|
);
|
||||||
|
console.log("Restored position:", position);
|
||||||
|
// ============================================================
|
||||||
|
// Apply paginated styles to container
|
||||||
|
// ============================================================
|
||||||
|
const container = document.getElementById("reader-content");
|
||||||
|
if (container) {
|
||||||
|
applyPaginatedStyles();
|
||||||
|
}
|
||||||
|
// ============================================================
|
||||||
|
// Render the first page
|
||||||
|
// ============================================================
|
||||||
|
const reflowableNav = await import("./formats/reflowable/navigation");
|
||||||
|
const { success, content: pageContent } = reflowableNav.goToPage(
|
||||||
|
{
|
||||||
|
type: "epub",
|
||||||
|
spine: spineItems,
|
||||||
|
resources: cif.resources,
|
||||||
|
toc: cif.toc || [],
|
||||||
|
metadata: {
|
||||||
|
title: cif.metadata.title || metadata.title,
|
||||||
|
author: cif.metadata.author || metadata.author,
|
||||||
|
identifier: metadata.id,
|
||||||
|
},
|
||||||
|
pagination,
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
position.currentPage,
|
||||||
|
);
|
||||||
|
if (success && container) {
|
||||||
|
const { renderPage } =
|
||||||
|
await import("./formats/reflowable/content-renderer");
|
||||||
|
renderPage(container, pageContent);
|
||||||
|
// Update UI
|
||||||
|
updatePageDisplay(container, position.currentPage, pagination.totalPages);
|
||||||
|
updateProgressBar(container, Math.round(position.progress * 100));
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
type: "ebook",
|
type: "ebook",
|
||||||
cif,
|
cif,
|
||||||
currentSpineIndex: 0,
|
currentSpineIndex: position.spineIndex,
|
||||||
currentPage: 1,
|
currentPage: position.currentPage,
|
||||||
|
pagination,
|
||||||
|
position,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializePDFReader(metadata: any): Promise<PDFReader> {
|
async function initializePDFReader(metadata: any): Promise<PDFReader> {
|
||||||
const { initializePDFReader } = await import("./pdf/pdfjs-wrapper");
|
const { initializePDFReader } = await import("./formats/pdf/pdfjs-wrapper");
|
||||||
return initializePDFReader(metadata);
|
return initializePDFReader(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeComicReader(metadata: any): Promise<ComicReader> {
|
async function initializeComicReader(metadata: any): Promise<ComicReader> {
|
||||||
const { initializeComicReader } = await import("./comic/image-parser");
|
const { initializeComicReader } =
|
||||||
|
await import("./formats/comic/image-parser");
|
||||||
return initializeComicReader(metadata);
|
return initializeComicReader(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeMangaReader(metadata: any): Promise<MangaReader> {
|
async function initializeMangaReader(metadata: any): Promise<MangaReader> {
|
||||||
const { initializeMangaReader } = await import("./comic/image-parser");
|
const { initializeMangaReader } =
|
||||||
|
await import("./formats/comic/image-parser");
|
||||||
return initializeMangaReader(metadata);
|
return initializeMangaReader(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +354,149 @@ function getFileExtension(filepath: string): string {
|
|||||||
return match ? `.${match[1]}` : "";
|
return match ? `.${match[1]}` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Viewport Resize Handler
|
||||||
|
// ============================================================
|
||||||
|
function handleResize(): void {
|
||||||
|
const state = getState();
|
||||||
|
if (!state.currentReader || state.currentReader.type !== "ebook") return;
|
||||||
|
const book = state.currentReader as UniversalReader;
|
||||||
|
if (!book.pagination) return;
|
||||||
|
loadSettings()
|
||||||
|
.then((userSettings) => {
|
||||||
|
const newSettings: PaginationSettings = {
|
||||||
|
fontSize: userSettings.font_size,
|
||||||
|
lineHeight: userSettings.line_height,
|
||||||
|
viewportWidth: window.innerWidth,
|
||||||
|
viewportHeight: window.innerHeight - 120,
|
||||||
|
wordsPerPage: 250,
|
||||||
|
};
|
||||||
|
if (shouldRecalculate(book.pagination, newSettings)) {
|
||||||
|
const currentCFI = book.position.cfi;
|
||||||
|
const currentPage = book.position.currentPage;
|
||||||
|
calculatePagination(book.cif.spine, book.cif.resources, newSettings)
|
||||||
|
.then((newPagination) => {
|
||||||
|
const updatedBook: UniversalReader = {
|
||||||
|
type: "ebook",
|
||||||
|
cif: book.cif,
|
||||||
|
currentSpineIndex: book.position.spineIndex,
|
||||||
|
currentPage: book.position.currentPage,
|
||||||
|
pagination: newPagination,
|
||||||
|
position: {
|
||||||
|
currentPage: currentPage,
|
||||||
|
spineIndex: book.position.spineIndex,
|
||||||
|
localPageIndex: book.position.localPageIndex,
|
||||||
|
cfi: currentCFI,
|
||||||
|
progress: currentPage / newPagination.totalPages,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setState({ currentReader: updatedBook });
|
||||||
|
const container = document.getElementById("reader-content");
|
||||||
|
if (container) {
|
||||||
|
const reflowableNav = require("./formats/reflowable/navigation");
|
||||||
|
const { success, content: pageContent } = reflowableNav.goToPage(
|
||||||
|
updatedBook,
|
||||||
|
updatedBook.position.currentPage,
|
||||||
|
);
|
||||||
|
if (success) {
|
||||||
|
const {
|
||||||
|
renderPage,
|
||||||
|
} = require("./formats/reflowable/content-renderer");
|
||||||
|
renderPage(container, pageContent);
|
||||||
|
updatePageDisplay(
|
||||||
|
container,
|
||||||
|
updatedBook.position.currentPage,
|
||||||
|
newPagination.totalPages,
|
||||||
|
);
|
||||||
|
updateProgressBar(
|
||||||
|
container,
|
||||||
|
Math.round(updatedBook.position.progress * 100),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Pagination recalculation failed:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn(
|
||||||
|
"Failed to load user settings for resize, using defaults:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
const newSettings: PaginationSettings = {
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeight: 1.6,
|
||||||
|
viewportWidth: window.innerWidth,
|
||||||
|
viewportHeight: window.innerHeight - 120,
|
||||||
|
wordsPerPage: 250,
|
||||||
|
};
|
||||||
|
if (shouldRecalculate(book.pagination, newSettings)) {
|
||||||
|
const currentCFI = book.position.cfi;
|
||||||
|
const currentPage = book.position.currentPage;
|
||||||
|
calculatePagination(book.cif.spine, book.cif.resources, newSettings)
|
||||||
|
.then((newPagination) => {
|
||||||
|
const updatedBook: UniversalReader = {
|
||||||
|
type: "ebook",
|
||||||
|
cif: book.cif,
|
||||||
|
currentSpineIndex: book.position.spineIndex,
|
||||||
|
currentPage: book.position.currentPage,
|
||||||
|
pagination: newPagination,
|
||||||
|
position: {
|
||||||
|
currentPage: currentPage,
|
||||||
|
spineIndex: book.position.spineIndex,
|
||||||
|
localPageIndex: book.position.localPageIndex,
|
||||||
|
cfi: currentCFI,
|
||||||
|
progress: currentPage / newPagination.totalPages,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setState({ currentReader: updatedBook });
|
||||||
|
const container = document.getElementById("reader-content");
|
||||||
|
if (container) {
|
||||||
|
const reflowableNav = require("./formats/reflowable/navigation");
|
||||||
|
const { success, content: pageContent } = reflowableNav.goToPage(
|
||||||
|
updatedBook,
|
||||||
|
updatedBook.position.currentPage,
|
||||||
|
);
|
||||||
|
if (success) {
|
||||||
|
const pageData =
|
||||||
|
newPagination.pageMap.get(
|
||||||
|
updatedBook.position.currentPage - 1,
|
||||||
|
) || null;
|
||||||
|
const {
|
||||||
|
renderPage,
|
||||||
|
} = require("./formats/reflowable/content-renderer");
|
||||||
|
renderPage(container, pageContent, pageData);
|
||||||
|
updatePageDisplay(
|
||||||
|
container,
|
||||||
|
updatedBook.position.currentPage,
|
||||||
|
newPagination.totalPages,
|
||||||
|
);
|
||||||
|
updateProgressBar(
|
||||||
|
container,
|
||||||
|
Math.round(updatedBook.position.progress * 100),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Pagination recalculation failed:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function debounce(
|
||||||
|
func: (...args: any[]) => void,
|
||||||
|
wait: number,
|
||||||
|
): (...args: any[]) => void {
|
||||||
|
let timeout: any;
|
||||||
|
return function (this: any, ...args: any[]) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
window.addEventListener("resize", debounce(handleResize, 300));
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Alpine.js Integration
|
// Alpine.js Integration
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -268,11 +523,10 @@ Alpine.data("readerShell", () => ({
|
|||||||
get totalPages() {
|
get totalPages() {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
if (!state.currentReader || !state.readerMetadata) return 0;
|
if (!state.currentReader || !state.readerMetadata) return 0;
|
||||||
|
|
||||||
if (state.currentReader.type === "ebook") {
|
if (state.currentReader.type === "ebook") {
|
||||||
// Use dynamic page calculation if available
|
// Use pagination if available
|
||||||
if (state.currentReader.pageCalculationResult) {
|
if ((state.currentReader as UniversalReader).pagination) {
|
||||||
return state.currentReader.pageCalculationResult.totalPages;
|
return (state.currentReader as UniversalReader).pagination!.totalPages;
|
||||||
}
|
}
|
||||||
return state.currentReader.cif.spine.length;
|
return state.currentReader.cif.spine.length;
|
||||||
} else if (state.currentReader.type === "pdf") {
|
} else if (state.currentReader.type === "pdf") {
|
||||||
|
|||||||
Reference in New Issue
Block a user