// Vertical scroll mode for webtoons/manhwa // Feature Registration Pattern implementation import type { ReaderContext } from "../core/reader-context"; export function init(context: ReaderContext): void { let state: VerticalScrollState | null = null; context.events.on("reader:loaded", (detail: { container: HTMLElement; mediaItemId: string; totalPages: number }) => { state = createVerticalScroll(detail.container, detail.mediaItemId, detail.totalPages); }); context.events.on("vertical-scroll:load-page", async (detail: { pageNumber: number }) => { if (state) { await loadPage(state, detail.pageNumber); } }); context.events.on("vertical-scroll:get-current", () => { if (state) { const currentPage = getCurrentPageFromScroll(state); context.events.emit("vertical-scroll:current-page", { page: currentPage }); } }); context.events.on("reader:unload", () => { if (state) { destroyVerticalScroll(state); state = null; } }); } interface VerticalScrollState { container: HTMLElement; loadedPages: Set; loadingPages: Set; currentPage: number; totalPages: number; threshold: number; mediaItemId: string; } function createVerticalScroll( container: HTMLElement, mediaItemId: string, totalPages: number, ): VerticalScrollState { const state: VerticalScrollState = { container, loadedPages: new Set(), loadingPages: new Set(), currentPage: 1, totalPages, threshold: 500, mediaItemId, }; loadPage(state, 1); setupScrollListener(state); return state; } async function loadPage( state: VerticalScrollState, pageNumber: number, ): Promise { if (state.loadedPages.has(pageNumber) || state.loadingPages.has(pageNumber)) { return; } state.loadingPages.add(pageNumber); try { 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 imgUrl = URL.createObjectURL(blob); const pageContainer = document.createElement("div"); pageContainer.className = "vertical-page"; pageContainer.dataset.pageNumber = pageNumber.toString(); const img = document.createElement("img"); img.src = imgUrl; img.alt = `Page ${pageNumber}`; img.loading = "lazy"; pageContainer.appendChild(img); state.container.appendChild(pageContainer); state.loadedPages.add(pageNumber); state.loadingPages.delete(pageNumber); if (pageNumber < state.totalPages) { loadPage(state, pageNumber + 1); if (pageNumber + 1 < state.totalPages) { loadPage(state, pageNumber + 2); } } } catch (error) { console.error(`Failed to load page ${pageNumber}:`, error); state.loadingPages.delete(pageNumber); } } function setupScrollListener(state: VerticalScrollState): void { let scrollTimeout: number | undefined; state.container.addEventListener("scroll", () => { clearTimeout(scrollTimeout); scrollTimeout = window.setTimeout(() => { checkScrollPosition(state); }, 100); }); } function checkScrollPosition(state: VerticalScrollState): void { const scrollBottom = state.container.scrollHeight - state.container.scrollTop - state.container.clientHeight; if (scrollBottom < state.threshold) { const lastPage = Math.max(...state.loadedPages); if (lastPage < state.totalPages) { loadPage(state, lastPage + 1); } } const currentPage = getCurrentPageFromScroll(state); if (currentPage !== state.currentPage) { state.currentPage = currentPage; window.dispatchEvent( new CustomEvent("page-change", { detail: { page: currentPage }, }), ); } } function getCurrentPageFromScroll(state: VerticalScrollState): number { const pages = Array.from(state.container.querySelectorAll(".vertical-page")); for (const page of pages) { const rect = page.getBoundingClientRect(); const containerRect = state.container.getBoundingClientRect(); const pageMiddle = rect.top + rect.height / 2; const viewportMiddle = containerRect.top + containerRect.height / 2; if (Math.abs(pageMiddle - viewportMiddle) < containerRect.height / 4) { return parseInt((page as HTMLElement).dataset.pageNumber || "1"); } } return state.currentPage; } function destroyVerticalScroll(state: VerticalScrollState): void { const images = Array.from(state.container.querySelectorAll("img")); images.forEach((img) => { const url = img.src; if (url.startsWith("blob:")) { URL.revokeObjectURL(url); } }); state.container.innerHTML = ""; state.loadedPages.clear(); state.loadingPages.clear(); }