import { deleteLocalSession, filterImageFiles, loadLocalSession, saveLocalSession, sortLocalPages, toLocalPageRecord, } from "../shared/local_gallery.js"; const readerEl = document.getElementById("reader"); const pageListEl = document.getElementById("page-list"); const pageCountEl = document.getElementById("page-count"); const viewerStatusEl = document.getElementById("viewer-status"); const selectionSummaryEl = document.getElementById("selection-summary"); const zoomIndicatorEl = document.getElementById("zoom-indicator"); const openImagesBtn = document.getElementById("viewer-open-images"); const openFolderBtn = document.getElementById("viewer-open-folder"); const toggleSelectBtn = document.getElementById("toggle-select-pages"); const reverseOrderBtn = document.getElementById("reverse-page-order"); const clearViewerBtn = document.getElementById("clear-viewer"); const zoomOutBtn = document.getElementById("zoom-out"); const zoomInBtn = document.getElementById("zoom-in"); const fitWidthBtn = document.getElementById("fit-width"); const zoomRange = document.getElementById("zoom-range"); const downloadSelectedImagesBtn = document.getElementById("download-selected-images"); const downloadSelectedHtmlBtn = document.getElementById("download-selected-html"); const imagesInput = document.getElementById("viewer-images-input"); const folderInput = document.getElementById("viewer-folder-input"); const params = new URLSearchParams(location.search); const sessionId = String(params.get("sid") || "").trim(); const READER_WIDTH_KEY = "textphantom.viewer.width"; const pagesById = new Map(); const originalToPageId = new Map(); const selectedPageIds = new Set(); const objectUrls = new Map(); let pageOrder = []; let currentSession = null; let dragPageId = ""; let readerWidth = Number(localStorage.getItem(READER_WIDTH_KEY) || 980); function setStatus(text) { if (viewerStatusEl) viewerStatusEl.textContent = String(text || ""); } function resetInput(input) { if (input) input.value = ""; } function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); } function sanitizeFilenamePart(text) { return String(text || "") .replace(/\.[^.]+$/, "") .replace(/[\\/:*?"<>|]+/g, "_") .replace(/\s+/g, " ") .trim() || "page"; } function getPageLabel(page) { return String(page?.relativePath || page?.name || page?.id || "page"); } function escapeHtml(text) { return String(text || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function extFromMime(mime) { const m = String(mime || "").toLowerCase(); if (m.includes("png")) return "png"; if (m.includes("webp")) return "webp"; if (m.includes("gif")) return "gif"; if (m.includes("jpeg") || m.includes("jpg")) return "jpg"; return "png"; } function parseDataUriMeta(dataUri) { const m = String(dataUri || "").match(/^data:([^;,]+)?/i); const mime = m?.[1] || "image/png"; return { mime, ext: extFromMime(mime) }; } async function blobToDataUri(blob) { return await new Promise((resolve) => { const reader = new FileReader(); reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : ""); reader.onerror = () => resolve(""); reader.readAsDataURL(blob); }); } function articleForPage(pageId) { return document.querySelector(`.page-strip[data-page-id="${CSS.escape(pageId)}"]`); } function imageForPage(pageId) { return articleForPage(pageId)?.querySelector("img") || null; } function overlayRootForPage(pageId) { return articleForPage(pageId)?.querySelector(".tp-ol-root") || null; } function badgeText(page) { if (page?.overlayApplied) return "Overlay ready"; if (page?.translatedImageDataUri) return "Image ready"; return "Original"; } function badgeClass(page) { return page?.overlayApplied || page?.translatedImageDataUri ? "badge ok" : "badge"; } function revokeObjectUrls() { for (const url of objectUrls.values()) { try { URL.revokeObjectURL(url); } catch {} } objectUrls.clear(); } function getOrderedPages() { return pageOrder.map((id) => pagesById.get(id)).filter(Boolean); } function getSelectedPages() { return getOrderedPages().filter((page) => selectedPageIds.has(page.id)); } function updateSelectionSummary() { const selected = selectedPageIds.size; if (selectionSummaryEl) selectionSummaryEl.textContent = `${selected} selected`; if (toggleSelectBtn) { const total = pageOrder.length; toggleSelectBtn.textContent = total && selected === total ? "Deselect all" : "Select all"; } } function applyReaderWidth(px) { readerWidth = clamp(Number(px) || 980, Number(zoomRange.min), Number(zoomRange.max)); document.documentElement.style.setProperty("--reader-width", `${readerWidth}px`); localStorage.setItem(READER_WIDTH_KEY, String(readerWidth)); if (zoomRange) zoomRange.value = String(readerWidth); if (zoomIndicatorEl) zoomIndicatorEl.textContent = `${readerWidth}px`; } function pageCountText() { const total = pageOrder.length; return `${total} page${total === 1 ? "" : "s"}`; } function syncPageUi(pageId) { const page = pagesById.get(pageId); if (!page) return; const checked = selectedPageIds.has(pageId); document .querySelectorAll(`[data-page-id="${CSS.escape(pageId)}"] input[data-role="select-page"]`) .forEach((el) => { el.checked = checked; }); document .querySelectorAll(`[data-page-id="${CSS.escape(pageId)}"] .page-state-badge`) .forEach((badge) => { badge.className = `page-state-badge ${badgeClass(page)}`; badge.textContent = badgeText(page); }); articleForPage(pageId)?.classList.toggle("active", checked); pageListEl .querySelector(`.page-list-item[data-page-id="${CSS.escape(pageId)}"]`) ?.classList.toggle("active", checked); } function syncUi() { if (pageCountEl) pageCountEl.textContent = pageCountText(); updateSelectionSummary(); for (const id of pageOrder) syncPageUi(id); } function setPageSelection(pageId, checked) { if (checked) selectedPageIds.add(pageId); else selectedPageIds.delete(pageId); syncPageUi(pageId); updateSelectionSummary(); } function movePage(beforeId, afterId) { const from = pageOrder.indexOf(beforeId); const to = pageOrder.indexOf(afterId); if (from < 0 || to < 0 || from === to) return; const next = [...pageOrder]; const [item] = next.splice(from, 1); next.splice(to, 0, item); pageOrder = next; } async function persistCurrentOrder() { if (!currentSession?.id) return; const pages = getOrderedPages().map((page) => ({ id: page.id, name: page.name, relativePath: page.relativePath, type: page.type, size: page.size, lastModified: page.lastModified, blob: page.blob, })); currentSession = await saveLocalSession({ id: currentSession.id, createdAt: currentSession.createdAt, title: currentSession.title, pages, }); } function scrollToPage(pageId) { articleForPage(pageId)?.scrollIntoView({ behavior: "smooth", block: "start" }); } function findPageIdByOriginal(original) { const key = String(original || ""); if (!key) return ""; const direct = originalToPageId.get(key); if (direct) return direct; for (const id of pageOrder) { const page = pagesById.get(id); if (page && (page.originalKey === key || page.currentSrc === key)) return id; } return ""; } async function getCurrentSettings() { const stored = await chrome.storage.local.get(["mode", "lang"]); return { mode: String(stored.mode || "lens_text") || "lens_text", lang: String(stored.lang || "en") || "en", }; } async function makeExportFilename(page, ext) { const { mode, lang } = await getCurrentSettings(); const stem = sanitizeFilenamePart(page?.name || page?.relativePath || page?.id || "page"); const safeMode = sanitizeFilenamePart(mode || "mode").replace(/\s+/g, "-"); const safeLang = sanitizeFilenamePart(lang || "en").replace(/\s+/g, "-"); return `${stem}.${safeMode}.${safeLang}.${ext}`; } function isLocalDownloadUrl(url) { const value = String(url || ""); return value.startsWith("data:") || value.startsWith("blob:") || value.startsWith("chrome-extension:"); } function triggerAnchorDownload(url, filename) { const link = document.createElement("a"); link.href = String(url || ""); link.download = String(filename || "download"); document.body.appendChild(link); link.click(); link.remove(); } async function dataUriToBlob(dataUri) { const res = await fetch(String(dataUri || "")); return await res.blob(); } async function downloadUrl(url, filename) { const targetUrl = String(url || ""); const targetName = String(filename || "download"); if (!targetUrl) return; if (isLocalDownloadUrl(targetUrl) || !chrome?.downloads?.download) { triggerAnchorDownload(targetUrl, targetName); return; } await chrome.downloads.download({ url: targetUrl, filename: targetName, saveAs: false, }); } async function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); try { triggerAnchorDownload(url, filename); } finally { setTimeout(() => { try { URL.revokeObjectURL(url); } catch {} }, 1500); } } function currentImageUrlForPage(pageId) { const page = pagesById.get(pageId); const img = imageForPage(pageId); if (!page || !img) return ""; return String(page.translatedImageDataUri || img.currentSrc || img.src || ""); } async function downloadImageForPage(pageId) { const page = pagesById.get(pageId); if (!page) return; const src = currentImageUrlForPage(pageId); if (!src) return; if (src.startsWith("data:")) { const meta = parseDataUriMeta(src); const blob = await dataUriToBlob(src); await downloadBlob(blob, await makeExportFilename(page, meta.ext)); return; } const res = await fetch(src); const blob = await res.blob(); const ext = extFromMime(blob.type || page.type); await downloadBlob(blob, await makeExportFilename(page, ext)); } async function buildStandaloneHtml(pageId) { const page = pagesById.get(pageId); const img = imageForPage(pageId); if (!page || !img) return ""; const overlayRoot = overlayRootForPage(pageId); const overlayScope = overlayRoot?.querySelector(".tp-ol-scope") || null; const styleText = document.getElementById("textphantom_overlay_css")?.textContent || ""; const cleanImg = overlayRoot?.querySelector(".tp-ol-clean-img") || null; const width = Number(img.naturalWidth) || 1; const height = Number(img.naturalHeight) || 1; const overlayHtml = overlayScope ? overlayScope.innerHTML : ""; let bgSrc = String(page.translatedImageDataUri || ""); if (!bgSrc) { const src = String(cleanImg?.currentSrc || cleanImg?.src || img.currentSrc || img.src || ""); if (src.startsWith("data:")) { bgSrc = src; } else if (src) { const res = await fetch(src); const blob = await res.blob(); bgSrc = await blobToDataUri(blob); } } if (!bgSrc) return ""; return `