- 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
148 lines
3.6 KiB
TypeScript
148 lines
3.6 KiB
TypeScript
// Chapter markers for manga/comics
|
|
// Visual indicators for chapter boundaries
|
|
// Procedural implementation (no OOP)
|
|
|
|
interface ChapterInfo {
|
|
chapterNumber: number;
|
|
pageStart: number;
|
|
pageEnd: number;
|
|
title?: string;
|
|
}
|
|
|
|
interface ChapterMarkerState {
|
|
chapters: ChapterInfo[];
|
|
currentChapter: number;
|
|
showMarkers: boolean;
|
|
}
|
|
|
|
function createChapterMarkerState(
|
|
chapters: ChapterInfo[],
|
|
currentPage: number,
|
|
): ChapterMarkerState {
|
|
const currentChapter =
|
|
chapters.find((c) => currentPage >= c.pageStart && currentPage <= c.pageEnd)
|
|
?.chapterNumber || 1;
|
|
|
|
return {
|
|
chapters,
|
|
currentChapter,
|
|
showMarkers: true,
|
|
};
|
|
}
|
|
|
|
function renderChapterMarkers(
|
|
container: HTMLElement,
|
|
state: ChapterMarkerState,
|
|
): void {
|
|
if (!state.showMarkers) return;
|
|
|
|
const markersContainer = document.createElement("div");
|
|
markersContainer.className =
|
|
"chapter-markers absolute left-0 right-0 pointer-events-none z-10";
|
|
|
|
state.chapters.forEach((chapter) => {
|
|
const marker = document.createElement("div");
|
|
marker.className =
|
|
"chapter-marker flex items-center gap-2 text-sm text-gray-400";
|
|
|
|
const isCurrentChapter = chapter.chapterNumber === state.currentChapter;
|
|
|
|
marker.style.position = "absolute";
|
|
marker.style.top = `${((chapter.pageStart - 1) / 100) * 100}%`;
|
|
marker.style.left = "10px";
|
|
|
|
marker.innerHTML = `
|
|
<span class="chapter-number ${isCurrentChapter ? "text-blue-400 font-bold" : ""}">
|
|
${chapter.title || `Chapter ${chapter.chapterNumber}`}
|
|
</span>
|
|
<span class="page-number text-xs">p.${chapter.pageStart}</span>
|
|
${isCurrentChapter ? '<span class="current-indicator">←</span>' : ""}
|
|
`;
|
|
|
|
markersContainer.appendChild(marker);
|
|
});
|
|
|
|
const existing = container.querySelector(".chapter-markers");
|
|
existing?.remove();
|
|
container.appendChild(markersContainer);
|
|
}
|
|
|
|
function updateCurrentChapter(
|
|
state: ChapterMarkerState,
|
|
currentPage: number,
|
|
): ChapterMarkerState {
|
|
const currentChapter =
|
|
state.chapters.find(
|
|
(c) => currentPage >= c.pageStart && currentPage <= c.pageEnd,
|
|
)?.chapterNumber || state.currentChapter;
|
|
|
|
if (currentChapter !== state.currentChapter) {
|
|
const newState = { ...state, currentChapter };
|
|
|
|
const markers = document.querySelector(".chapter-markers");
|
|
if (markers) {
|
|
renderChapterMarkers(markers.parentElement!, newState);
|
|
}
|
|
|
|
return newState;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
function toggleChapterMarkers(state: ChapterMarkerState): ChapterMarkerState {
|
|
const newState = { ...state, showMarkers: !state.showMarkers };
|
|
|
|
const markers = document.querySelector(".chapter-markers");
|
|
if (markers) {
|
|
markers.classList.toggle("hidden", !newState.showMarkers);
|
|
}
|
|
|
|
return newState;
|
|
}
|
|
|
|
function scrollToChapter(
|
|
state: ChapterMarkerState,
|
|
chapterNumber: number,
|
|
): void {
|
|
const chapter = state.chapters.find((c) => c.chapterNumber === chapterNumber);
|
|
if (chapter) {
|
|
window.dispatchEvent(
|
|
new CustomEvent("navigate-to-page", {
|
|
detail: { page: chapter.pageStart },
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
const chapterMarkerCSS = `
|
|
.chapter-marker {
|
|
padding: 4px 8px;
|
|
margin-left: -18px;
|
|
opacity: 0.7;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.chapter-marker:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
.chapter-marker .current-indicator {
|
|
color: #3b82f6;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
|
|
.chapter-marker-line {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
height: 1px;
|
|
background: linear-gradient(to right, rgba(255,255,255,0.1), transparent);
|
|
}
|
|
`;
|