- panel-detection.service.ts: Main orchestration with OpenCV → ML → Grid fallback chain - panel-detection.opencv.ts: Edge detection using OpenCV for 80% of comics - panel-detection.ml.ts: COCO-SSD object detection for irregular layouts - panel-detector.ts: Unified detector interface - panel-editor.ts: Manual panel editor UI for user corrections - panel-ml-detector.ts: TensorFlow.js integration for ML detection - page-cache.ts: Efficient page caching for large comics - background-color.ts: Auto-detect comic background color - chapter-markers.ts: Chapter detection and navigation - page-scrubber.ts: Fast page scrubbing/thumbnails - page-order.ts: RTL/LTR page ordering support - panel-gap.ts: Panel gap detection
152 lines
3.9 KiB
TypeScript
152 lines
3.9 KiB
TypeScript
// Lazy-loading page cache with 5-page ahead prefetch
|
|
// Shared by both comic and manga readers
|
|
|
|
// Lazy-loading page cache with 5-page ahead prefetch
|
|
// Procedural implementation (no OOP)
|
|
|
|
import { detectPanels } from "./panel-detection.service";
|
|
|
|
interface PageCacheState {
|
|
cache: Map<number, HTMLImageElement>;
|
|
loading: Set<number>;
|
|
maxAhead: number;
|
|
mediaItemId: string;
|
|
panelData: Map<number, { panels: any[]; method: string; confidence: number }>;
|
|
}
|
|
|
|
function createPageCache(mediaItemId: string): PageCacheState {
|
|
return {
|
|
cache: new Map(),
|
|
loading: new Set(),
|
|
maxAhead: 5,
|
|
mediaItemId,
|
|
panelData: new Map(),
|
|
};
|
|
}
|
|
|
|
async function getCachedPage(
|
|
state: PageCacheState,
|
|
pageNumber: number,
|
|
): Promise<PageCacheState & { page: HTMLImageElement }> {
|
|
if (state.cache.has(pageNumber)) {
|
|
return { ...state, page: state.cache.get(pageNumber)! };
|
|
}
|
|
|
|
if (state.loading.has(pageNumber)) {
|
|
return new Promise((resolve) => {
|
|
const checkInterval = setInterval(() => {
|
|
if (state.cache.has(pageNumber)) {
|
|
clearInterval(checkInterval);
|
|
resolve({ ...state, page: state.cache.get(pageNumber)! });
|
|
}
|
|
}, 100);
|
|
}) as Promise<PageCacheState & { page: HTMLImageElement }>;
|
|
}
|
|
|
|
const newLoading = new Set(state.loading);
|
|
newLoading.add(pageNumber);
|
|
|
|
const img = await loadComicPage(state, pageNumber);
|
|
|
|
const newCache = new Map(state.cache);
|
|
newCache.set(pageNumber, img);
|
|
newLoading.delete(pageNumber);
|
|
|
|
const newState = { ...state, cache: newCache, loading: newLoading };
|
|
|
|
prefetchPages(newState, pageNumber + 1);
|
|
cleanupPageCache(newState, pageNumber);
|
|
|
|
return { ...newState, page: img };
|
|
}
|
|
|
|
async function loadComicPage(
|
|
state: PageCacheState,
|
|
pageNumber: number,
|
|
): Promise<HTMLImageElement> {
|
|
const token = localStorage.getItem("token");
|
|
const response = await fetch(
|
|
`/readers/${state.mediaItemId}/pages/${pageNumber}`,
|
|
{
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
},
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load page ${pageNumber}`);
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const img = new Image();
|
|
img.src = URL.createObjectURL(blob);
|
|
await new Promise((resolve) => {
|
|
img.onload = resolve;
|
|
});
|
|
return img;
|
|
}
|
|
|
|
function prefetchPages(state: PageCacheState, startPage: number): void {
|
|
for (let i = startPage; i < startPage + state.maxAhead; i++) {
|
|
if (!state.cache.has(i) && !state.loading.has(i)) {
|
|
loadComicPage(state, i).then((img) => {
|
|
state.cache.set(i, img);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function cleanupPageCache(
|
|
state: PageCacheState,
|
|
currentPage: number,
|
|
): PageCacheState {
|
|
const keepPages = 10;
|
|
const newCache = new Map(state.cache);
|
|
|
|
for (const [page] of state.cache) {
|
|
if (page < currentPage - keepPages) {
|
|
newCache.delete(page);
|
|
}
|
|
}
|
|
|
|
return { ...state, cache: newCache };
|
|
}
|
|
|
|
// Add this function
|
|
async function detectPagePanels(
|
|
state: PageCacheState,
|
|
pageNumber: number,
|
|
): Promise<any[]> {
|
|
// Check if already detected
|
|
if (state.panelData?.has(pageNumber)) {
|
|
return state.panelData.get(pageNumber)!.panels;
|
|
}
|
|
|
|
// Get or create image
|
|
let image: HTMLImageElement;
|
|
if (state.cache.has(pageNumber)) {
|
|
image = state.cache.get(pageNumber)!;
|
|
} else {
|
|
image = await loadComicPage(state, pageNumber);
|
|
}
|
|
|
|
// Run detection on demand
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = image.width;
|
|
canvas.height = image.height;
|
|
const ctx = canvas.getContext("2d")!;
|
|
ctx.drawImage(image, 0, 0);
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
|
|
const result = await detectPanels(imageData, true);
|
|
|
|
if (!state.panelData) {
|
|
state.panelData = new Map();
|
|
}
|
|
state.panelData.set(pageNumber, result);
|
|
|
|
return result.panels;
|
|
}
|
|
|
|
// Export the new function
|
|
export { createPageCache, getCachedPage, loadComicPage, detectPagePanels };
|