(function () { if (globalThis.__TextPhantomContentLoaded) return; globalThis.__TextPhantomContentLoaded = true;
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }; const CURRENT_LEVEL_NAME = "debug"; const CURRENT_LEVEL = LEVELS[CURRENT_LEVEL_NAME] ?? 0;
function formatTimestamp() { return new Date().toISOString(); }
function createLogger(namespace) {
function _log(level, ...args) {
if (LEVELS[level] >= CURRENT_LEVEL) {
const prefix = [${formatTimestamp()}][${namespace}][${level.toUpperCase()}];
if (level === "debug") console.debug(prefix, ...args);
else if (level === "info") console.info(prefix, ...args);
else if (level === "warn") console.warn(prefix, ...args);
else if (level === "error") console.error(prefix, ...args);
else console.log(prefix, ...args);
}
}
const logger = (...a) => _log("info", ...a);
logger.debug = (...a) => _log("debug", ...a);
logger.info = (...a) => _log("info", ...a);
logger.warn = (...a) => _log("warn", ...a);
logger.error = (...a) => _log("error", ...a);
return logger;
}
window.createLogger = window.createLogger || createLogger;
const log = window.createLogger ? window.createLogger("content_bootstrap") : console;
const KEEPALIVE_PORT_NAME = "TP_KEEPALIVE"; const KEEPALIVE_PING_MS = 20_000; let keepAlivePort = null; let keepAliveTimer = null; let keepAliveStopAt = 0;
function stopKeepAlive() { keepAliveStopAt = 0; if (keepAliveTimer) { clearInterval(keepAliveTimer); keepAliveTimer = null; } if (keepAlivePort) { try { keepAlivePort.disconnect(); } catch {} keepAlivePort = null; } }
function startKeepAlive(ms) { const dur = Number(ms) > 0 ? Number(ms) : 10 * 60 * 1000; keepAliveStopAt = Math.max(keepAliveStopAt || 0, Date.now() + dur); if (!keepAlivePort) { try { keepAlivePort = chrome.runtime.connect({ name: KEEPALIVE_PORT_NAME }); keepAlivePort.onDisconnect.addListener(() => stopKeepAlive()); } catch { stopKeepAlive(); return; } } const ping = () => { if (!keepAlivePort) return; if (keepAliveStopAt && Date.now() >= keepAliveStopAt) { stopKeepAlive(); return; } try { keepAlivePort.postMessage({ type: "TP_KEEPALIVE", ts: Date.now() }); } catch { stopKeepAlive(); } }; ping(); if (!keepAliveTimer) keepAliveTimer = setInterval(ping, KEEPALIVE_PING_MS); }
const __tpVer = (() => { try { return (chrome?.runtime?.getManifest?.() || {}).version || ""; } catch { return ""; } })(); const __tpIsTop = (() => { try { return window.top === window; } catch { return false; } })();
try { chrome.runtime.sendMessage( { type: "TP_CONTENT_READY", href: location.href, ver: __tpVer, top: __tpIsTop, }, () => void chrome.runtime.lastError, ); } catch {}
function setupLocationChangeNotifier() { if (!__tpIsTop) return; if (globalThis.__tpLocationNotifyInstalled) return; globalThis.__tpLocationNotifyInstalled = true; let lastHref = location.href; let canSend = true; const send = () => { if (!canSend) return; const href = location.href; if (href === lastHref) return; lastHref = href; try { chrome.runtime.sendMessage( { type: "TP_LOCATION_CHANGED", href, top: true, ver: __tpVer }, () => void chrome.runtime.lastError, ); } catch { canSend = false; } }; const wrap = (name) => { const orig = history[name]; if (typeof orig !== "function") return; history[name] = function (...args) { const r = orig.apply(this, args); Promise.resolve().then(send); return r; }; }; wrap("pushState"); wrap("replaceState"); addEventListener("popstate", send, { passive: true }); addEventListener("hashchange", send, { passive: true }); }
setupLocationChangeNotifier();
try { console.info("[TextPhantom][content] loaded", { href: location.href, ver: __tpVer, top: __tpIsTop, }); } catch {}
function truncate(s, len = 180) { if (!s) return s; try { s = String(s); } catch {} return s.length > len ? s.slice(0, len) + "…" : s; }
const replaceStateByOriginal = new Map();
function noteReplaceState(original, state) { const key = _normUrl(original); if (!key) return; replaceStateByOriginal.set(key, { state, ts: Date.now() }); }
function shouldShowReplaceError(original) { const key = _normUrl(original); if (!key) return true; const s = replaceStateByOriginal.get(key); if (!s) return true; if (s.state === "ok") return false; if (s.state === "pending" && Date.now() - s.ts < 12000) return false; return true; }
function markImageError(original, msg) { if (!shouldShowReplaceError(original)) { log.debug("markImageError skipped (replace ok/pending)", { original: truncate(original), }); return; } const img = findTargetImage(original); if (!img || img.dataset.lensError) { log.info("markImageError", { count: 0, original: truncate(original), message: msg, }); return; }
const alreadyReplaced =
!!img.dataset.tpBlobUrl ||
(img.currentSrc || img.src || "").startsWith("blob:") ||
(img.currentSrc || img.src || "").startsWith("data:");
if (alreadyReplaced) {
log.debug("markImageError skipped (image replaced)", {
original: truncate(original),
});
return;
}
img.style.outline = "3px solid red";
const badge = document.createElement("div");
badge.textContent = "⚠️";
badge.title = msg || "OCR error";
const r = img.getBoundingClientRect();
badge.style.position = "absolute";
badge.style.left = `${r.left + window.scrollX + 4}px`;
badge.style.top = `${r.top + window.scrollY + 4}px`;
badge.style.background = "rgba(255,255,255,0.9)";
badge.style.padding = "2px 4px";
badge.style.borderRadius = "4px";
badge.style.zIndex = 9999;
document.body.appendChild(badge);
img.dataset.lensError = "1";
log.info("markImageError", {
count: 1,
original: truncate(original),
message: msg,
});
}
async function getSettings() { return new Promise((res) => { chrome.storage.local.get( ["mode", "lang", "sources", "aiKey"], (items) => { res({ mode: typeof items.mode === "string" ? items.mode : "lens_images", lang: typeof items.lang === "string" ? items.lang : "th", sources: typeof items.sources === "string" ? items.sources : "translated", aiKey: typeof items.aiKey === "string" ? items.aiKey : "", }); }, ); }); }
function removeLazyScriptsAndForceSrc() { const lazyScripts = document.querySelectorAll('script[src*="lazy"]'); lazyScripts.forEach((script) => script.remove());
const lazyImgs = document.querySelectorAll("img[data-src]");
lazyImgs.forEach((img) => {
img.removeAttribute("loading");
img.classList.remove("lazyload", "lazy", "lazyloaded");
if (img.dataset.src) {
img.src = img.dataset.src;
}
});
log.info("lazy removed + src forced from data-src", {
lazyScripts: lazyScripts.length,
lazyImgs: lazyImgs.length,
});
}
function buildPipelineEvent(stage) { return { stage, at: new Date().toISOString() }; }
function buildPositionFromElement(img) { const rect = img.getBoundingClientRect(); return { top: rect.top, left: rect.left, width: rect.width, height: rect.height, viewport_width: window.innerWidth, viewport_height: window.innerHeight, scroll_x: window.scrollX, scroll_y: window.scrollY, }; }
function getBestImgUrl(img) { const tp = img?.dataset?.tpOriginal || (typeof img?.getAttribute === "function" ? img.getAttribute("data-tp-original") : ""); if (tp && /^https?:/i.test(tp)) return tp; return ( img.currentSrc || img.src || img.getAttribute("data-src") || img.getAttribute("data-original") || img.getAttribute("data-lazy-src") || "" ); }
function _normUrl(u) { if (!u) return ""; try { u = String(u); } catch { return ""; } u = u.trim(); if (!u) return ""; try { const x = new URL(u, location.href); x.hash = ""; return x.toString(); } catch { return u.split("#")[0].split("?")[0]; } }
let __tp_lastRightClick = { img: null, ts: 0, urls: [] }; const __tp_recentImgByUrl = new Map(); function rememberImgUrls(img, urls) { const ts = Date.now(); for (const u of urls || []) { const k = _normUrl(u); if (!k) continue; __tp_recentImgByUrl.set(k, { img, ts }); } if (__tp_recentImgByUrl.size > 200) { const cutoff = ts - 5 * 60 * 1000; for (const [k, v] of __tp_recentImgByUrl.entries()) { if (!v?.img || !v.img.isConnected || (v.ts || 0) < cutoff) __tp_recentImgByUrl.delete(k); } while (__tp_recentImgByUrl.size > 220) __tp_recentImgByUrl.delete(__tp_recentImgByUrl.keys().next().value); } } document.addEventListener( "contextmenu", (e) => { const img = e?.target?.closest ? e.target.closest("img") : null; if (!img) return; const urls = [img.currentSrc, img.src, getBestImgUrl(img)] .filter(Boolean) .map(_normUrl) .filter(Boolean); if (urls[0] && img?.dataset) img.dataset.tpOriginal = urls[0]; __tp_lastRightClick = { img, ts: Date.now(), urls, }; rememberImgUrls(img, urls); }, true, );
function findTargetImage(original) { const o = _normUrl(original);
const mdKey = mdKeyFromUrl(original);
if (mdKey) {
const byKey = Array.from(document.images || []).find(
(img) => String(img.dataset.tpOriginalKey || "") === mdKey,
);
if (byKey) return byKey;
}
if (o) {
const byData = Array.from(document.images || []).find(
(img) => _normUrl(img.dataset.tpOriginal) === o,
);
if (byData) return byData;
}
if (o) {
const rec = __tp_recentImgByUrl.get(o);
if (rec?.img && rec.img.isConnected) return rec.img;
}
const now = Date.now();
if (__tp_lastRightClick.img && now - __tp_lastRightClick.ts < 8000) {
const urls = __tp_lastRightClick.urls || [];
if (!o || urls.includes(o)) return __tp_lastRightClick.img;
}
const imgs = Array.from(document.images || []);
for (const img of imgs) {
const a = _normUrl(img.currentSrc);
const b = _normUrl(img.src);
if (o && (a === o || b === o)) return img;
}
return null;
} function isHttpish(u) { return typeof u === "string" && /^https?:/i.test(u); }
function isInlineableImageUrl(u) { return typeof u === "string" && /^(?:data:|blob:|file:|chrome-extension:)/i.test(u); }
async function blobToDataUri(blob) { return await new Promise((resolve) => { try { const reader = new FileReader(); reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : ""); reader.onerror = () => resolve(""); reader.readAsDataURL(blob); } catch { resolve(""); } }); }
async function getImageDataUriFromElement(img) { const src = _normUrl(getBestImgUrl(img)); if (!src) return ""; if (src.startsWith("data:")) return src;
try {
const w = Number(img?.naturalWidth) || Number(img?.width) || 0;
const h = Number(img?.naturalHeight) || Number(img?.height) || 0;
if (w > 0 && h > 0) {
const canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d", { willReadFrequently: false });
if (ctx) {
ctx.drawImage(img, 0, 0, w, h);
const du = canvas.toDataURL("image/png");
if (du && du.startsWith("data:image/")) return du;
}
}
} catch {}
try {
const res = await fetch(src, { cache: "force-cache" });
const blob = await res.blob();
const du = await blobToDataUri(blob);
if (du) return du;
} catch {}
return "";
}
async function buildPayloadFromImage( img, mode, lang, menuSource = "page_scan", customStage, includeDataUri = false, ) { const src = _normUrl(getBestImgUrl(img)); const imageDataUri = includeDataUri && isInlineableImageUrl(src) ? await getImageDataUriFromElement(img) : ""; if (!src && !imageDataUri) return null; return buildPayload( { original_image_url: src || null, position: buildPositionFromElement(img), imageDataUri: imageDataUri || null, }, mode, lang, menuSource, customStage, ); }
function emitViewerEvent(type, detail) { try { window.dispatchEvent( new CustomEvent(type, { detail: detail && typeof detail === "object" ? detail : {}, }), ); } catch {} }
function normalizeLazyImages() { document.querySelectorAll("img").forEach((img) => { const candSet = img.getAttribute("data-srcset") || img.getAttribute("data-lazy-srcset"); if (candSet && !img.srcset) img.srcset = candSet;
const candSrc =
img.getAttribute("data-original") || img.getAttribute("data-lazy-src");
if (candSrc && !img.src) img.src = candSrc;
img.loading = "eager";
img.decoding = "sync";
});
} async function collectImagesForScan(mode, lang, sourceTag) { const seen = new Set(); const imgs = Array.from(document.images || []); const out = []; for (const img of imgs) { const r = typeof img.getBoundingClientRect === "function" ? img.getBoundingClientRect() : null; const w = Math.max(Number(r?.width) || 0, Number(img.naturalWidth) || 0); const h = Math.max( Number(r?.height) || 0, Number(img.naturalHeight) || 0, ); if (w && h) { if (Math.min(w, h) < 120) continue; if (w * h < 60000) continue; } const payload = await buildPayloadFromImage(img, mode, lang, sourceTag); const key = _normUrl(payload?.src) || String(payload?.metadata?.image_id || ""); if (!key || seen.has(key)) continue; seen.add(key); out.push(payload); } return out.filter(Boolean); }
const OVERLAY_STYLE_ID = "textphantom_overlay_css";
const sendBg = (msg) => new Promise((resolve) => { try { chrome.runtime.sendMessage(msg, (r) => resolve(r || null)); } catch (_e) { resolve(null); } });
const mdCacheGet = (keys, includeNewImg = false, lang = "", mode = "") => sendBg({ type: "TP_MD_CACHE_GET", keys, includeNewImg: !!includeNewImg, lang, mode, });
const mdCacheGetNewImg = async (key, lang, mode) => { const resp = await mdCacheGet([key], true, lang, mode); const rec = resp?.items?.[key] || null; return ( rec?.newImg || extractNewImageSrc(rec?.result || null) || extractNewImageSrc(rec || null) || null ); };
const mdApplyCachedNewImg = ( originalUrl, key, isTextMode, imgElement, lang, mode, ) => { mdCacheGetNewImg(key, lang, mode).then((newSrc) => { if (!newSrc) return; if (isTextMode) { const mdKey = mdKeyFromUrl(originalUrl); if (!mdKey) return; const existing = mdHtmlOverlaysByKey.get(mdKey) || null; const rec = existing || (() => { const rect = imgElement.getBoundingClientRect(); const r = upsertMangaDexHtmlOverlay( mdKey, imgElement, Math.max(1, rect.width), Math.max(1, rect.height), "badge", ); r.scope.textContent = ""; return r; })(); rec.isTextMode = true; updateCleanLayer(rec, imgElement, newSrc); mdScheduleUpdate(); return; } replaceImageInDOM(originalUrl, newSrc).then((applied) => { if (!applied) mdRememberPending(originalUrl, { needNewImg: true, needMode: "replace", needLang: lang, needCacheMode: mode, }); }); }); };
function ensureOverlayStyle(cssText) { if (!cssText) cssText = ""; let styleEl = document.getElementById(OVERLAY_STYLE_ID); if (!styleEl) { styleEl = document.createElement("style"); styleEl.id = OVERLAY_STYLE_ID; styleEl.type = "text/css"; document.head.appendChild(styleEl); }
const harden =
`\n/* TextPhantom harden */\n` +
`.tp-ol-root{position:absolute!important;left:0!important;top:0!important;pointer-events:none!important;z-index:2147483647!important;` +
`display:block!important;opacity:1!important;visibility:visible!important;overflow:visible!important;transform-origin:0 0!important;}` +
`.tp-ol-scope{position:absolute!important;left:0!important;top:0!important;pointer-events:none!important;` +
`display:block!important;opacity:1!important;visibility:visible!important;overflow:visible!important;}` +
`.tp-ol-scope *{box-sizing:border-box!important;pointer-events:none!important;}` +
`.tp-ol-container{position:relative!important;display:inline-block!important;line-height:0!important;overflow:visible!important;}`;
const next = String(cssText) + harden;
if (styleEl.textContent !== next) styleEl.textContent = next;
}
function computeScale(imgElement, baseW, baseH, preferRect = false) { const rect = imgElement.getBoundingClientRect(); const cw = preferRect ? rect.width || 1 : imgElement.offsetWidth || imgElement.clientWidth || imgElement.width || rect.width || 1; const ch = preferRect ? rect.height || 1 : imgElement.offsetHeight || imgElement.clientHeight || imgElement.height || rect.height || 1; const nw = (Number.isFinite(baseW) && baseW > 0 ? baseW : imgElement.naturalWidth) || cw || 1; const nh = (Number.isFinite(baseH) && baseH > 0 ? baseH : imgElement.naturalHeight) || ch || 1; const cs = getComputedStyle(imgElement); const fit = cs.objectFit || "fill"; const transform = cs.transform || "none"; const transformOrigin = cs.transformOrigin || "0 0";
const parsePos = (v, pad) => {
if (!pad) return 0;
if (!v) return pad * 0.5;
const s = String(v).trim();
if (s.endsWith("%")) return (pad * parseFloat(s)) / 100;
if (s.endsWith("px")) return Math.min(pad, Math.max(0, parseFloat(s)));
const n = parseFloat(s);
return Number.isFinite(n) ? Math.min(pad, Math.max(0, n)) : pad * 0.5;
};
const posParts = (cs.objectPosition || "50% 50%").trim().split(/\s+/);
const posX = posParts[0];
const posY = posParts[1] || posParts[0];
let sx = cw / nw;
let sy = ch / nh;
let offX = 0;
let offY = 0;
if (fit !== "fill") {
let scale = 1;
if (fit === "contain") scale = Math.min(sx, sy);
else if (fit === "cover") scale = Math.max(sx, sy);
else if (fit === "scale-down") scale = Math.min(1, Math.min(sx, sy));
else if (fit === "none") scale = 1;
sx = scale;
sy = scale;
const drawnW = nw * scale;
const drawnH = nh * scale;
const padX = Math.max(0, cw - drawnW);
const padY = Math.max(0, ch - drawnH);
offX = parsePos(posX, padX);
offY = parsePos(posY, padY);
}
return {
rect,
cw,
ch,
nw,
nh,
sx: isFinite(sx) && sx > 0 ? sx : 1,
sy: isFinite(sy) && sy > 0 ? sy : 1,
offX,
offY,
transform,
transformOrigin,
};
}
function createOverlayBadge(label) { const badge = document.createElement("div"); badge.textContent = label || "AI…"; badge.style.position = "absolute"; badge.style.left = "6px"; badge.style.top = "6px"; badge.style.padding = "4px 6px"; badge.style.borderRadius = "6px"; badge.style.background = "rgba(255,255,255,.75)"; badge.style.color = "rgba(20,20,20,.95)"; badge.style.fontFamily = "var(--tp-font,system-ui)"; badge.style.fontSize = "12px"; badge.style.lineHeight = "1.2"; badge.style.textShadow = "0 0 2px rgba(255,255,255,.90),0 1px 1px rgba(0,0,0,.25)"; return badge; }
function extractNewImageSrc(result) { return ( result?.imageDataUri || result?.image || result?.imageUrl || result?.image_url || result?.imageURL || null ); }
function ensureCleanLayer(rec) { if (rec?.cleanImg && rec.cleanImg.isConnected) return rec.cleanImg; if (!rec?.host) return null; const img = document.createElement("img"); img.className = "tp-ol-clean-img"; img.decoding = "sync"; img.loading = "eager"; img.style.position = "absolute"; img.style.left = "0px"; img.style.top = "0px"; img.style.width = "100%"; img.style.height = "100%"; img.style.pointerEvents = "none"; img.style.maxWidth = "none"; img.style.maxHeight = "none"; img.style.objectFit = "contain"; img.style.objectPosition = "center center"; img.style.display = "none"; if (rec.host.firstChild) rec.host.insertBefore(img, rec.host.firstChild); else rec.host.appendChild(img); rec.cleanImg = img; return img; }
function syncCleanLayerFit(cleanImg, imgElement) { try { const cs = getComputedStyle(imgElement); if (cs?.objectFit) cleanImg.style.objectFit = cs.objectFit; if (cs?.objectPosition) cleanImg.style.objectPosition = cs.objectPosition; } catch {} }
function updateCleanLayer(rec, imgElement, newSrc) { if (!rec?.host || !imgElement) return; const layer = ensureCleanLayer(rec); if (!layer) return; if (!newSrc) { layer.style.display = "none"; return; } syncCleanLayerFit(layer, imgElement); if (layer.src !== newSrc) layer.src = newSrc; layer.style.display = "block"; }
function applyHtmlOverlay( imgElement, result, source, isTextMode, original = "", ) { const aiHtml = result?.Ai?.aihtml || result?.ai?.aihtml || ""; const translatedHtml = result?.translated?.translatedhtml || result?.translatedhtml || ""; const originalHtml = result?.original?.originalhtml || result?.originalhtml || "";
const req = String(source || "")
.trim()
.toLowerCase();
const chosen = req === "ai" || req === "original" ? req : "translated";
const html =
chosen === "ai"
? aiHtml
: chosen === "original"
? originalHtml
: translatedHtml;
const cssParts = [String(result?.htmlCss || "")];
if (chosen === "ai")
cssParts.push(
String(result?.Ai?.aihtmlCss || result?.ai?.aihtmlCss || ""),
);
const cssText = Array.from(
new Set(cssParts.map((s) => String(s || "").trim()).filter(Boolean)),
).join("\n");
const meta =
chosen === "ai"
? result?.Ai?.aihtmlMeta || result?.ai?.aihtmlMeta || {}
: result?.htmlMeta || {};
const baseW =
Number(meta.baseW || meta.sourceWidth) ||
imgElement.naturalWidth ||
imgElement.width ||
1;
const baseH =
Number(meta.baseH || meta.sourceHeight) ||
imgElement.naturalHeight ||
imgElement.height ||
1;
const newImgSrc = isTextMode ? extractNewImageSrc(result) : null;
log.debug("OVERLAY_HTML", {
source: req,
chosen,
aiHtmlLen: aiHtml ? String(aiHtml).length : 0,
translatedHtmlLen: translatedHtml ? String(translatedHtml).length : 0,
originalHtmlLen: originalHtml ? String(originalHtml).length : 0,
baseW,
baseH,
});
ensureOverlayStyle(cssText);
const mdKey = isMangaDexHost() ? mdGetKeyForImg(imgElement) : "";
if (mdKey) {
if (isTextMode && req === "ai" && !aiHtml) {
const rec = upsertMangaDexHtmlOverlay(
mdKey,
imgElement,
baseW,
baseH,
"badge",
);
updateCleanLayer(rec, imgElement, newImgSrc);
rec.scope.textContent = "";
const aiMeta = (result && result.Ai && result.Ai.meta) || {};
const reason = String(
aiMeta.skipped_reason || aiMeta.reason || "",
).trim();
const badgeText =
reason === "no_text"
? "No text"
: reason === "rate_limited"
? "Rate limit"
: "AI…";
rec.scope.appendChild(createOverlayBadge(badgeText));
scheduleMangaDexOverlayUpdate();
if (!imgElement.complete) {
imgElement.addEventListener("load", scheduleMangaDexOverlayUpdate, {
once: true,
passive: true,
});
}
setTimeout(scheduleMangaDexOverlayUpdate, 50);
return;
}
if (!html) {
if (isTextMode && newImgSrc) {
const rec = upsertMangaDexHtmlOverlay(
mdKey,
imgElement,
baseW,
baseH,
"badge",
);
updateCleanLayer(rec, imgElement, newImgSrc);
rec.scope.textContent = "";
scheduleMangaDexOverlayUpdate();
if (!imgElement.complete) {
imgElement.addEventListener("load", scheduleMangaDexOverlayUpdate, {
once: true,
passive: true,
});
}
setTimeout(scheduleMangaDexOverlayUpdate, 50);
return;
}
hideMangaDexHtmlOverlay(mdKey);
return;
}
const rec = upsertMangaDexHtmlOverlay(
mdKey,
imgElement,
baseW,
baseH,
"html",
);
updateCleanLayer(rec, imgElement, newImgSrc);
rec.scope.textContent = "";
const tmp = document.createElement("div");
tmp.innerHTML = String(html || "")
.replace(/<<TP_P\d+>>/g, "")
.replace(/<<TP_P/g, "");
while (tmp.firstChild) rec.scope.appendChild(tmp.firstChild);
scheduleMangaDexOverlayUpdate();
if (!imgElement.complete) {
imgElement.addEventListener("load", scheduleMangaDexOverlayUpdate, {
once: true,
passive: true,
});
}
setTimeout(scheduleMangaDexOverlayUpdate, 50);
return;
}
const key = _normUrl(original) || _normUrl(getBestImgUrl(imgElement));
if (!key) return;
if (isTextMode && req === "ai" && !aiHtml) {
const rec = upsertHtmlOverlay(key, imgElement, baseW, baseH, "badge");
updateCleanLayer(rec, imgElement, newImgSrc);
rec.scope.textContent = "";
rec.scope.appendChild(createOverlayBadge("AI…"));
scheduleHtmlOverlayUpdate();
if (!imgElement.complete) {
imgElement.addEventListener("load", scheduleHtmlOverlayUpdate, {
once: true,
passive: true,
});
}
setTimeout(scheduleHtmlOverlayUpdate, 50);
return;
}
if (!html) {
if (isTextMode && newImgSrc) {
const rec = upsertHtmlOverlay(key, imgElement, baseW, baseH, "badge");
updateCleanLayer(rec, imgElement, newImgSrc);
rec.scope.textContent = "";
scheduleHtmlOverlayUpdate();
if (!imgElement.complete) {
imgElement.addEventListener("load", scheduleHtmlOverlayUpdate, {
once: true,
passive: true,
});
}
setTimeout(scheduleHtmlOverlayUpdate, 50);
return;
}
hideHtmlOverlay(key);
return;
}
const rec = upsertHtmlOverlay(key, imgElement, baseW, baseH, "html");
updateCleanLayer(rec, imgElement, newImgSrc);
rec.scope.textContent = "";
const tmp = document.createElement("div");
tmp.innerHTML = String(html || "")
.replace(/<<TP_P\d+>>/g, "")
.replace(/<<TP_P/g, "");
while (tmp.firstChild) rec.scope.appendChild(tmp.firstChild);
scheduleHtmlOverlayUpdate();
if (!imgElement.complete) {
imgElement.addEventListener("load", scheduleHtmlOverlayUpdate, {
once: true,
passive: true,
});
}
setTimeout(scheduleHtmlOverlayUpdate, 50);
emitViewerEvent("textphantom:overlay-updated", {
original,
result,
mode: isTextMode ? "lens_text" : "lens_images",
source: req,
});
}
function buildPayload( { original_image_url, position, imageDataUri }, mode, lang, menuSource = "page_scan", customStage, ) { const pipeline = customStage ? [buildPipelineEvent(customStage)] : [buildPipelineEvent("collected")];
const payload = {
mode,
lang,
type: "image",
src: original_image_url || null,
imageDataUri: imageDataUri || null,
menu: menuSource,
context: {
page_url: location.href,
timestamp: new Date().toISOString(),
},
metadata: {
image_id: crypto.randomUUID(),
original_image_url: original_image_url || null,
position,
pipeline,
ocr_image: null,
extra: null,
},
};
log.info("Built payload", {
src: truncate(original_image_url),
hasData: Boolean(imageDataUri),
menu: menuSource,
mode,
lang,
});
return payload;
}
function mdKeyFromUrl(u) {
const s = _normUrl(u);
if (!s) return "";
try {
const x = new URL(s, location.href);
const parts = String(x.pathname || "")
.split("/")
.filter(Boolean);
for (let i = parts.length - 1; i >= 0; i--) {
const p = parts[i];
if (p === "data" || p === "data-saver") {
if (parts.length >= i + 3)
return md:${parts[i]}/${parts[i + 1]}/${parts[i + 2]};
break;
}
}
return "";
} catch {
return "";
}
}
const mdPendingByOriginal = new Map();
function mdPendingTrim(maxSize = 80) { if (mdPendingByOriginal.size <= maxSize) return; const items = Array.from(mdPendingByOriginal.entries()).sort( (a, b) => (a[1]?.ts || 0) - (b[1]?.ts || 0), ); const drop = Math.max(0, items.length - maxSize); for (let i = 0; i < drop; i++) mdPendingByOriginal.delete(items[i][0]); }
function mdRememberPending(original, patch) { const key = mdKeyFromUrl(original) || _normUrl(original); if (!key) return; const prev = mdPendingByOriginal.get(key) || {}; mdPendingByOriginal.set(key, { ...prev, ...(patch || {}), ts: Date.now() }); mdPendingTrim(); }
function mdTakePending(key) { const v = mdPendingByOriginal.get(key); if (v) mdPendingByOriginal.delete(key); return v || null; }
function isMangaDexHost() { return /(^|.)mangadex.org$/i.test(String(location.hostname || "")); }
function getMangaDexChapterId() { const m = String(location.pathname || "").match( //chapter/([a-f0-9-]{8,})/i, ); return m ? m[1] : ""; }
function getMangaDexPageIndexFromUrl() { const parts = String(location.pathname || "") .split("/") .filter(Boolean); const ci = parts.indexOf("chapter"); if (ci >= 0 && parts.length >= ci + 3) { const p = parts[ci + 2]; if (/^\d+$/.test(p)) return Math.max(0, Number(p) - 1); } try { const qs = new URLSearchParams(String(location.search || "")); const q = qs.get("page") || qs.get("p"); if (q && /^\d+$/.test(q)) return Math.max(0, Number(q) - 1); } catch {} const hm = String(location.hash || "").match(/(?:^|[?#&])page=(\d+)/i); if (hm) return Math.max(0, Number(hm[1]) - 1); return null; }
const MD_CACHE_TTL_MS = 180000; let mdCache = null;
async function fetchMangaDexChapterUrls() { if (!isMangaDexHost()) return null;
const chapterId = getMangaDexChapterId();
if (!chapterId) return null;
const now = Date.now();
if (
mdCache &&
mdCache.chapterId === chapterId &&
now - (mdCache.ts || 0) < MD_CACHE_TTL_MS &&
Array.isArray(mdCache.urls) &&
mdCache.urls.length
)
return mdCache;
const apiUrl = `https://api.mangadex.org/at-home/server/${chapterId}`;
try {
const res = await fetch(apiUrl, { credentials: "omit" });
if (!res.ok) throw new Error(`MangaDex API ${res.status}`);
const baseInfo = await res.json();
const baseUrl = baseInfo?.baseUrl;
const chapter = baseInfo?.chapter;
const hash = chapter?.hash;
const data = Array.isArray(chapter?.data) ? chapter.data : [];
const dataSaver = Array.isArray(chapter?.dataSaver)
? chapter.dataSaver
: [];
let path = "data";
let files = data;
if (!files.length && dataSaver.length) {
path = "data-saver";
files = dataSaver;
}
if (!baseUrl || !hash || !files.length)
throw new Error("Unexpected MangaDex API shape");
const urls = files.map(
(filename) => `${baseUrl}/${path}/${hash}/${filename}`,
);
mdCache = { chapterId, ts: now, urls, path };
return mdCache;
} catch (e) {
console.warn(
"[TextPhantom][content] MangaDex API error",
e?.message || e,
);
mdCache = { chapterId, ts: now, urls: [], path: "data" };
return null;
}
}
async function hydrateMangaDexFromCache() { if (!isMangaDexHost() || !__tpIsTop) return null;
const info = await fetchMangaDexChapterUrls();
const urls = info?.urls;
if (!Array.isArray(urls) || !urls.length) return null;
const pairs = urls
.map((u) => ({ url: u, key: mdKeyFromUrl(u) }))
.filter((p) => !!p.key);
const keys = Array.from(new Set(pairs.map((p) => p.key))).slice(0, 600);
if (!keys.length) return null;
const st = await getSettings();
const resp = await mdCacheGet(keys, false, st.lang, st.mode);
const items = resp?.items || null;
if (!items) return null;
const isText = String(st.mode || "lens_text") === "lens_text";
const source = isText ? String(st.sources || "translated") : "translated";
for (const p of pairs) {
const rec = items[p.key];
if (!rec) continue;
if (rec.result) {
const img = findTargetImage(p.url);
if (img) applyHtmlOverlay(img, rec.result, source, isText, p.url);
else
mdRememberPending(p.url, {
overlay: { result: rec.result, source, isTextMode: isText },
});
}
const hasNewImg =
Boolean(rec?.hasNewImg) ||
Boolean(rec?.newImg) ||
Boolean(extractNewImageSrc(rec?.result || null));
if (hasNewImg) {
const img = findTargetImage(p.url);
if (img)
mdApplyCachedNewImg(p.url, p.key, isText, img, st.lang, st.mode);
else
mdRememberPending(p.url, {
needNewImg: true,
needMode: isText ? "clean" : "replace",
needLang: st.lang,
needCacheMode: st.mode,
});
}
}
return { pairs, items };
}
function isProbableMangaDexPageImage(img) { if (!img || !img.getBoundingClientRect) return false; const u = String(getBestImgUrl(img) || ""); if (!u) return false; const rect = img.getBoundingClientRect(); const area = Math.max(0, rect.width) * Math.max(0, rect.height); if (area < 65000 && (rect.width < 260 || rect.height < 260)) return false; if (u.startsWith("blob:") || u.startsWith("data:")) return true; if (/mangadex.network|uploads.mangadex.org/i.test(u)) return true; return false; }
function getMangaDexPageImagesInDOM() { const root = document; const nodes = Array.from(root.querySelectorAll(".md--page img")); const imgs = nodes.length ? nodes : Array.from(document.images || []); return imgs.filter(isProbableMangaDexPageImage); }
function inferMangaDexPageIndexForImg(img) { const alt = String( img?.getAttribute?.("alt") || img?.getAttribute?.("aria-label") || "", ); const m0 = alt.match(/^\s*(\d+)\s*[-_]/); if (m0) return Math.max(0, Number(m0[1]) - 1); const m1 = alt.match(/(?:^|\D)(\d+)\s/\s(\d+)/); if (m1) return Math.max(0, Number(m1[1]) - 1); const m2 = alt.match(/page\s*(\d+)/i); if (m2) return Math.max(0, Number(m2[1]) - 1);
const tpPage =
img?.dataset?.tpMdPage ||
(typeof img?.getAttribute === "function"
? img.getAttribute("data-tp-md-page")
: "");
if (tpPage && /^\d+$/.test(String(tpPage)))
return Math.max(0, Number(tpPage) - 1);
const key = String(img?.dataset?.tpOriginalKey || "");
const km = key.match(/\/(\d+)\s*[-_]/);
if (km) return Math.max(0, Number(km[1]) - 1);
const cur = String(img?.dataset?.tpOriginal || "");
const cm = cur.match(/\/(\d+)\s*[-_]/);
if (cm) return Math.max(0, Number(cm[1]) - 1);
const direct =
img?.getAttribute?.("data-page") ||
img?.dataset?.page ||
img?.dataset?.pageIndex ||
"";
if (direct && /^\d+$/.test(String(direct)))
return Math.max(0, Number(direct) - 1);
const near =
img?.closest?.("[data-page]")?.getAttribute?.("data-page") || "";
if (near && /^\d+$/.test(String(near)))
return Math.max(0, Number(near) - 1);
return null;
}
function mdApplyOriginalToImg(img, idx, urls) { if (!img || !Array.isArray(urls) || idx == null) return; if (idx < 0 || idx >= urls.length) return; const url = _normUrl(urls[idx]); if (!url) return; const key = mdKeyFromUrl(url) || url; if (img.dataset.tpOriginalKey !== key) img.dataset.tpOriginalKey = key; if (_normUrl(img.dataset.tpOriginal) !== url) img.dataset.tpOriginal = url; img.dataset.tpMdPage = String(idx + 1); const pending = mdTakePending(key); if (pending) { if (pending.newSrc) replaceImageInDOM(url, pending.newSrc).catch(() => {}); if (pending.cleanSrc) { const existing = mdHtmlOverlaysByKey.get(key) || null; const rec = existing || (() => { const rect = img.getBoundingClientRect(); const r = upsertMangaDexHtmlOverlay( key, img, Math.max(1, rect.width), Math.max(1, rect.height), "badge", ); r.scope.textContent = ""; return r; })(); rec.isTextMode = true; updateCleanLayer(rec, img, pending.cleanSrc); mdScheduleUpdate(); } if (pending.overlay) { try { applyHtmlOverlay( img, pending.overlay.result, pending.overlay.source, pending.overlay.isTextMode, url, ); } catch {} } if (pending.needNewImg) mdApplyCachedNewImg( url, key, pending.needMode === "clean", img, pending.needLang, pending.needCacheMode, ); } }
async function ensureMangaDexDomMapping() { if (!isMangaDexHost()) return; const info = await fetchMangaDexChapterUrls(); const urls = info?.urls || []; if (!urls.length) return;
const imgs = getMangaDexPageImagesInDOM();
if (!imgs.length) return;
imgs.forEach((img) => {
const idx = inferMangaDexPageIndexForImg(img);
if (idx == null) return;
mdApplyOriginalToImg(img, idx, urls);
});
}
let mdMappingScheduled = false; function scheduleMangaDexMapping() { if (!isMangaDexHost()) return; if (mdMappingScheduled) return; mdMappingScheduled = true; setTimeout(() => { mdMappingScheduled = false; ensureMangaDexDomMapping() .then(() => { if (mdOverlaysByKey.size) scheduleMangaDexOverlayUpdate(); }) .catch(() => {}); }, 60); }
async function resolveMangaDexOriginalForBlob(blobUrl) { if (!isMangaDexHost()) return null; if (!blobUrl?.startsWith("blob:")) return null;
scheduleMangaDexMapping();
const info = await fetchMangaDexChapterUrls();
const urls = info?.urls || [];
if (!urls.length) return null;
const img = Array.from(document.images || []).find(
(i) => (i.currentSrc || i.src) === blobUrl,
);
if (!img) return null;
if (img.dataset.tpOriginal) return _normUrl(img.dataset.tpOriginal);
const byAlt = inferMangaDexPageIndexForImg(img);
if (byAlt != null) {
mdApplyOriginalToImg(img, byAlt, urls);
return _normUrl(img.dataset.tpOriginal);
}
const byUrl = getMangaDexPageIndexFromUrl();
if (byUrl != null) {
mdApplyOriginalToImg(img, byUrl, urls);
return _normUrl(img.dataset.tpOriginal);
}
const pageImgs = getMangaDexPageImagesInDOM();
const pos = pageImgs.indexOf(img);
if (pos >= 0) {
mdApplyOriginalToImg(img, pos, urls);
return _normUrl(img.dataset.tpOriginal);
}
return null;
}
let toastEl = null; let toastTimer = 0; function showToast(text, ms = 2000) { if (!text) return; if (!toastEl) { toastEl = document.createElement("div"); toastEl.style.position = "fixed"; toastEl.style.right = "10px"; toastEl.style.bottom = "10px"; toastEl.style.zIndex = 2147483647; toastEl.style.padding = "8px 10px"; toastEl.style.borderRadius = "10px"; toastEl.style.background = "rgba(0,0,0,.75)"; toastEl.style.color = "#fff"; toastEl.style.fontSize = "12px"; toastEl.style.lineHeight = "1.2"; toastEl.style.maxWidth = "68vw"; toastEl.style.pointerEvents = "none"; document.documentElement.appendChild(toastEl); } toastEl.textContent = String(text); toastEl.style.display = "block"; if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout( () => { if (toastEl) toastEl.style.display = "none"; }, Math.max(800, Number(ms) || 0), ); }
if (isMangaDexHost() && __tpIsTop) { scheduleMangaDexMapping(); try { const mo = new MutationObserver(() => scheduleMangaDexMapping()); mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["src", "srcset", "data-src", "alt", "aria-label"], }); window.addEventListener("popstate", () => { mdCache = null; scheduleMangaDexMapping(); }); window.addEventListener("hashchange", () => { mdCache = null; scheduleMangaDexMapping(); });
if (!history.__tpMdPatched) {
history.__tpMdPatched = true;
const ps = history.pushState;
history.pushState = function (...args) {
const r = ps.apply(this, args);
scheduleMangaDexMapping();
return r;
};
const rs = history.replaceState;
history.replaceState = function (...args) {
const r = rs.apply(this, args);
scheduleMangaDexMapping();
return r;
};
}
} catch {}
}
async function dataUriToBlobUrl(dataUri) { try { const res = await fetch(dataUri); const blob = await res.blob(); return URL.createObjectURL(blob); } catch { return null; } }
const mdOverlaysByKey = new Map(); const mdHtmlOverlaysByKey = new Map(); let mdOverlayRaf = 0;
const htmlOverlaysByKey = new Map(); let htmlOverlayRaf = 0;
function updateHtmlOverlays() { if (!htmlOverlaysByKey.size) return; for (const [key, rec] of htmlOverlaysByKey.entries()) { const host = rec?.host; const scope = rec?.scope; if (!host || !scope) { htmlOverlaysByKey.delete(key); continue; }
const prevImg = rec.img;
let img = prevImg;
if (!img || !img.isConnected) img = findTargetImage(key);
if (!img) {
host.style.display = "none";
rec.img = null;
continue;
}
const curKey = _normUrl(getBestImgUrl(img));
if (curKey && curKey !== key) {
host.style.display = "none";
rec.img = null;
continue;
}
if (img !== prevImg) {
if (img?.dataset) img.dataset.tpOriginal = key;
if (rec.ro) {
try {
rec.ro.disconnect();
} catch {}
}
rec.ro = new ResizeObserver(() => scheduleHtmlOverlayUpdate());
rec.ro.observe(img);
}
const parent = ensureOverlayHostMountedNearImage(rec, img);
if (!parent) {
host.style.display = "none";
rec.img = img;
continue;
}
const { r, left, top } = getOverlayBoxFromParent(img, parent);
if (r.width < 2 || r.height < 2) {
host.style.display = "none";
rec.img = img;
continue;
}
rec.img = img;
host.style.display = "block";
host.style.setProperty("left", `${left}px`, "important");
host.style.setProperty("top", `${top}px`, "important");
host.style.setProperty("width", `${r.width}px`, "important");
host.style.setProperty("height", `${r.height}px`, "important");
if (rec.kind === "badge") {
scope.style.width = `${r.width}px`;
scope.style.height = `${r.height}px`;
scope.style.transform = "";
scope.style.transformOrigin = "0 0";
host.style.transform = "";
host.style.transformOrigin = "";
continue;
}
const { nw, nh, sx, sy, offX, offY } = computeScale(
img,
rec.baseW,
rec.baseH,
true,
);
scope.style.width = `${nw}px`;
scope.style.height = `${nh}px`;
scope.style.transform = `translate(${offX}px, ${offY}px) scale(${sx}, ${sy})`;
scope.style.transformOrigin = "0 0";
host.style.transform = "";
host.style.transformOrigin = "";
}
}
function scheduleHtmlOverlayUpdate() { if (htmlOverlayRaf) return; htmlOverlayRaf = requestAnimationFrame(() => { htmlOverlayRaf = 0; updateHtmlOverlays(); }); }
function ensureOverlayHostMountedNearImage(rec, img) { const host = rec?.host; if (!host || !img?.isConnected) return null; const parent = img.parentElement; if (!parent) return null;
if (host.parentElement !== parent || host.nextSibling !== img) {
try {
if (host.parentElement) host.parentElement.removeChild(host);
} catch {}
try {
parent.insertBefore(host, img);
} catch {
try {
parent.appendChild(host);
} catch {}
}
}
try {
if (getComputedStyle(parent).position === "static") {
parent.style.position = "relative";
}
} catch {}
return host.parentElement === parent ? parent : null;
}
function getOverlayBoxFromParent(img, parent) { const r = img.getBoundingClientRect(); if (!parent) return { r, left: 0, top: 0 }; const pr = parent.getBoundingClientRect(); return { r, left: r.left - pr.left + (parent.scrollLeft || 0), top: r.top - pr.top + (parent.scrollTop || 0), }; }
function ensureHtmlOverlayListeners() { if (window.__tpHtmlOverlayListeners) return; window.__tpHtmlOverlayListeners = true; window.addEventListener("scroll", scheduleHtmlOverlayUpdate, { passive: true, capture: true, }); window.addEventListener("resize", scheduleHtmlOverlayUpdate, { passive: true, }); try { const mo = new MutationObserver(() => scheduleHtmlOverlayUpdate()); mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["src", "srcset", "data-src", "data-srcset", "style", "class"], }); } catch {} }
function upsertHtmlOverlay(key, img, baseW, baseH, kind) { let rec = htmlOverlaysByKey.get(key); if (!rec) { const host = document.createElement("div"); host.className = "tp-ol-root"; host.style.position = "absolute"; host.style.left = "0px"; host.style.top = "0px"; host.style.zIndex = 2147483647; host.style.pointerEvents = "none"; host.style.display = "none"; const scope = document.createElement("div"); scope.className = "tp-ol-scope"; scope.style.position = "relative"; host.appendChild(scope); rec = { host, scope, img: null, baseW: 1, baseH: 1, kind: "html", ro: null, }; htmlOverlaysByKey.set(key, rec); ensureHtmlOverlayListeners(); }
if (img) ensureOverlayHostMountedNearImage(rec, img);
rec.img = img;
rec.baseW = Number.isFinite(baseW) && baseW > 0 ? baseW : 1;
rec.baseH = Number.isFinite(baseH) && baseH > 0 ? baseH : 1;
rec.kind = kind || "html";
if (img?.dataset && !img.dataset.tpOriginal) img.dataset.tpOriginal = key;
if (rec.ro) {
try {
rec.ro.disconnect();
} catch {}
}
rec.ro = new ResizeObserver(() => scheduleHtmlOverlayUpdate());
rec.ro.observe(img);
return rec;
}
function hideHtmlOverlay(key) { const rec = htmlOverlaysByKey.get(key); if (rec?.cleanImg) rec.cleanImg.style.display = "none"; if (rec?.host) rec.host.style.display = "none"; }
function findMangaDexImgByKey(key) { if (!key) return null; return Array.from(document.images || []).find( (img) => String(img.dataset.tpOriginalKey || "") === key, ); }
function updateMangaDexOverlays() { if (!mdOverlaysByKey.size && !mdHtmlOverlaysByKey.size) return; for (const [key, rec] of mdOverlaysByKey.entries()) { const el = rec?.el; if (!el) { mdOverlaysByKey.delete(key); continue; }
let img = rec.img;
if (!img || !img.isConnected) img = findMangaDexImgByKey(key);
if (!img) {
el.style.display = "none";
rec.img = null;
continue;
}
const r = img.getBoundingClientRect();
if (r.width < 2 || r.height < 2) {
el.style.display = "none";
rec.img = img;
continue;
}
rec.img = img;
el.style.display = "block";
el.style.left = `${r.left + window.scrollX}px`;
el.style.top = `${r.top + window.scrollY}px`;
el.style.width = `${r.width}px`;
el.style.height = `${r.height}px`;
}
for (const [key, rec] of mdHtmlOverlaysByKey.entries()) {
const host = rec?.host;
const scope = rec?.scope;
if (!host || !scope) {
mdHtmlOverlaysByKey.delete(key);
continue;
}
let img = rec.img;
if (!img || !img.isConnected) img = findMangaDexImgByKey(key);
if (!img) {
host.style.display = "none";
rec.img = null;
continue;
}
const curKey = mdKeyFromUrl(getBestImgUrl(img));
if (curKey && curKey !== key) {
host.style.display = "none";
rec.img = null;
continue;
}
const parent = ensureOverlayHostMountedNearImage(rec, img);
if (!parent) {
host.style.display = "none";
rec.img = img;
continue;
}
const { r, left, top } = getOverlayBoxFromParent(img, parent);
if (r.width < 2 || r.height < 2) {
host.style.display = "none";
rec.img = img;
continue;
}
rec.img = img;
host.style.display = "block";
host.style.setProperty("left", `${left}px`, "important");
host.style.setProperty("top", `${top}px`, "important");
host.style.setProperty("width", `${r.width}px`, "important");
host.style.setProperty("height", `${r.height}px`, "important");
if (rec.kind === "badge") {
scope.style.width = `${r.width}px`;
scope.style.height = `${r.height}px`;
scope.style.transform = "";
scope.style.transformOrigin = "0 0";
host.style.transform = "";
host.style.transformOrigin = "";
continue;
}
const { nw, nh, sx, sy, offX, offY, transform, transformOrigin } =
computeScale(img, rec.baseW, rec.baseH, true);
scope.style.width = `${nw}px`;
scope.style.height = `${nh}px`;
scope.style.transform = `translate(${offX}px, ${offY}px) scale(${sx}, ${sy})`;
scope.style.transformOrigin = "0 0";
if (transform && transform !== "none") {
host.style.transform = transform;
host.style.transformOrigin = transformOrigin || "0 0";
} else {
host.style.transform = "";
host.style.transformOrigin = "";
}
}
}
function mdScheduleUpdate() { scheduleMangaDexOverlayUpdate(); }
function scheduleMangaDexOverlayUpdate() { if (mdOverlayRaf) return; mdOverlayRaf = requestAnimationFrame(() => { mdOverlayRaf = 0; updateMangaDexOverlays(); }); }
function ensureMangaDexOverlayListeners() { if (window.__tpMdOverlayListeners) return; window.__tpMdOverlayListeners = true; window.addEventListener("scroll", scheduleMangaDexOverlayUpdate, { passive: true, }); window.addEventListener("resize", scheduleMangaDexOverlayUpdate, { passive: true, }); try { const mo = new MutationObserver(() => scheduleMangaDexOverlayUpdate()); mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["src", "srcset", "data-src", "data-srcset", "style", "class"], }); } catch {} }
function mdGetKeyForImg(img) { const k = String(img?.dataset?.tpOriginalKey || ""); if (k) return k; const u = String(img?.dataset?.tpOriginal || ""); return mdKeyFromUrl(u); }
function upsertMangaDexHtmlOverlay(mdKey, img, baseW, baseH, kind) { let rec = mdHtmlOverlaysByKey.get(mdKey); if (!rec) { const host = document.createElement("div"); host.className = "tp-ol-root"; host.style.zIndex = 2147483647; host.style.pointerEvents = "none"; host.style.display = "none"; const scope = document.createElement("div"); scope.className = "tp-ol-scope"; host.appendChild(scope); rec = { host, scope, img: null, baseW: 1, baseH: 1, kind: "html" }; mdHtmlOverlaysByKey.set(mdKey, rec); ensureMangaDexOverlayListeners(); }
if (img) ensureOverlayHostMountedNearImage(rec, img);
rec.img = img;
rec.baseW = Number.isFinite(baseW) && baseW > 0 ? baseW : 1;
rec.baseH = Number.isFinite(baseH) && baseH > 0 ? baseH : 1;
rec.kind = kind || "html";
return rec;
}
function hideMangaDexHtmlOverlay(mdKey) { const rec = mdHtmlOverlaysByKey.get(mdKey); if (rec?.cleanImg) rec.cleanImg.style.display = "none"; if (rec?.host) rec.host.style.display = "none"; }
async function replaceMangaDexImageWithOverlay(original, newSrc) { const mdKey = mdKeyFromUrl(original); if (!mdKey) return 0;
let img = findMangaDexImgByKey(mdKey);
if (!img) img = findTargetImage(original);
if (!img) {
log.warn("REPLACE_IMAGE target not found", {
original: truncate(original),
});
return 0;
}
img.dataset.tpOriginalKey = mdKey;
const key = _normUrl(original);
if (key) img.dataset.tpOriginal = key;
noteReplaceState(original, "pending");
let rec = mdOverlaysByKey.get(mdKey);
if (!rec) {
const el = document.createElement("img");
el.decoding = "sync";
el.loading = "eager";
el.style.position = "absolute";
el.style.zIndex = 2147483647;
el.style.pointerEvents = "none";
el.style.objectFit = "contain";
el.style.maxWidth = "none";
el.style.maxHeight = "none";
el.style.display = "none";
document.documentElement.appendChild(el);
rec = { el, img: null, blobUrl: "", original: "" };
mdOverlaysByKey.set(mdKey, rec);
ensureMangaDexOverlayListeners();
el.addEventListener(
"load",
() => {
const okKey = rec.original;
if (okKey)
replaceStateByOriginal.set(okKey, { state: "ok", ts: Date.now() });
},
{ passive: true },
);
el.addEventListener(
"error",
() => {
const errKey = rec.original;
if (errKey) {
replaceStateByOriginal.set(errKey, {
state: "fail",
ts: Date.now(),
});
markImageError(errKey, "Failed to load replaced image");
}
},
{ passive: true },
);
}
rec.img = img;
rec.original = _normUrl(original);
let nextSrc = newSrc;
if (typeof newSrc === "string" && newSrc.startsWith("data:")) {
const blobUrl = await dataUriToBlobUrl(newSrc);
if (blobUrl) nextSrc = blobUrl;
}
if (
rec.blobUrl &&
rec.blobUrl.startsWith("blob:") &&
rec.blobUrl !== nextSrc
) {
try {
URL.revokeObjectURL(rec.blobUrl);
} catch {}
}
rec.blobUrl =
typeof nextSrc === "string" && nextSrc.startsWith("blob:") ? nextSrc : "";
rec.el.src = nextSrc;
scheduleMangaDexOverlayUpdate();
return 1;
}
async function replaceImageInDOM(original, newSrc) { if (isMangaDexHost() && mdKeyFromUrl(original)) return replaceMangaDexImageWithOverlay(original, newSrc);
const img = findTargetImage(original);
if (!img) {
log.warn("REPLACE_IMAGE target not found", {
original: truncate(original),
});
return 0;
}
const mdKey = mdKeyFromUrl(original);
if (mdKey) img.dataset.tpOriginalKey = mdKey;
const key = _normUrl(original);
if (key) img.dataset.tpOriginal = key;
noteReplaceState(original, "pending");
if (!img.dataset.tpReplaceTracked) {
img.dataset.tpReplaceTracked = "1";
img.addEventListener(
"load",
() => {
const k = _normUrl(img.dataset.tpOriginal);
if (k) replaceStateByOriginal.set(k, { state: "ok", ts: Date.now() });
},
{ passive: true },
);
img.addEventListener(
"error",
() => {
const k = _normUrl(img.dataset.tpOriginal);
if (k) {
replaceStateByOriginal.set(k, { state: "fail", ts: Date.now() });
markImageError(k, "Failed to load replaced image");
}
},
{ passive: true },
);
}
const before = img.currentSrc || img.src;
let nextSrc = newSrc;
if (typeof newSrc === "string" && newSrc.startsWith("data:")) {
const blobUrl = await dataUriToBlobUrl(newSrc);
if (blobUrl) nextSrc = blobUrl;
}
const prevBlob = img.dataset.tpBlobUrl;
if (prevBlob && prevBlob.startsWith("blob:")) {
try {
URL.revokeObjectURL(prevBlob);
} catch {}
delete img.dataset.tpBlobUrl;
}
if (typeof nextSrc === "string" && nextSrc.startsWith("blob:")) {
img.dataset.tpBlobUrl = nextSrc;
}
img.src = nextSrc;
img.removeAttribute("srcset");
img.removeAttribute("sizes");
img.removeAttribute("data-src");
img.removeAttribute("data-srcset");
img.removeAttribute("loading");
img.decoding = "sync";
img.loading = "eager";
emitViewerEvent("textphantom:image-updated", {
original,
newSrc: nextSrc,
rawNewSrc: newSrc,
});
log.info("REPLACE_IMAGE done", {
before: truncate(before),
original: truncate(original),
});
return 1;
}
chrome.runtime.onMessage.addListener((msg, _sender, sendResp) => { (async () => { if (msg?.type === "TP_PING") { sendResp({ ok: true }); return; } if (msg?.type === "TP_KEEPALIVE_START") { startKeepAlive(msg?.ms); sendResp({ ok: true }); return; } if (msg?.type === "TP_KEEPALIVE_STOP") { stopKeepAlive(); sendResp({ ok: true }); return; } if (msg?.type === "TP_TOAST") { showToast(msg?.text || msg?.message || "", msg?.ms || 1600); sendResp({ ok: true }); return; } const { mode, lang } = await getSettings(); try { log.debug("MSG", { type: msg?.type, original: truncate(msg?.original), }); } catch {} if ( msg?.type === "WS_STATUS_UPDATE" || msg?.type === "API_STATUS_UPDATE" ) { sendResp({ ok: true }); return; }
if (msg.type === "GET_IMAGES") {
removeLazyScriptsAndForceSrc();
normalizeLazyImages();
let infos = [];
const isMangaDex = isMangaDexHost();
if (isMangaDex) {
showToast("TextPhantom: loading MangaDex pages…", 2600);
scheduleMangaDexMapping();
await ensureMangaDexDomMapping();
const hyd = await hydrateMangaDexFromCache().catch(() => null);
const mdCacheItems = hyd?.items || null;
const wantsHtml = String(mode || "").includes("text");
const isCachedUrl = (src) => {
if (!mdCacheItems) return false;
const k = mdKeyFromUrl(String(src || ""));
if (!k) return false;
const rec = mdCacheItems[k];
if (!rec) return false;
return wantsHtml ? Boolean(rec.result) : Boolean(rec.hasNewImg);
};
const seen = new Set();
const posBySrc = new Map();
getMangaDexPageImagesInDOM().forEach((img) => {
const src = _normUrl(getBestImgUrl(img));
if (!isHttpish(src) || isCachedUrl(src)) return;
if (!posBySrc.has(src))
posBySrc.set(src, buildPositionFromElement(img));
});
const md = await fetchMangaDexChapterUrls();
const urls = Array.isArray(md?.urls) ? md.urls : [];
const all = [];
for (const src of urls) {
const u = _normUrl(src);
if (!isHttpish(u) || isCachedUrl(u) || seen.has(u)) continue;
seen.add(u);
all.push(
buildPayload(
{ original_image_url: u, position: posBySrc.get(u) || null },
mode,
lang,
"page_scan",
"collected_mangadex_api",
),
);
}
for (const [src, pos] of posBySrc.entries()) {
if (!isHttpish(src) || isCachedUrl(src) || seen.has(src)) continue;
seen.add(src);
all.push(
buildPayload(
{ original_image_url: src, position: pos || null },
mode,
lang,
"page_scan",
"collected_mangadex_dom",
),
);
}
infos = all.filter(Boolean);
} else {
infos = await collectImagesForScan(mode, lang, "page_scan");
}
log.info("GET_IMAGES", {
returned: infos.length,
host: location.host,
href: location.href,
});
sendResp(infos);
return;
}
if (msg.type === "GET_CONTEXT_IMAGE_PAYLOAD") {
const img =
__tp_lastRightClick.img && __tp_lastRightClick.img.isConnected
? __tp_lastRightClick.img
: null;
const payload = img
? await buildPayloadFromImage(
img,
mode,
lang,
"img_one",
"context_menu_single",
true,
)
: null;
sendResp({ ok: Boolean(payload), payload });
return;
}
if (msg.type === "REPLACE_IMAGE") {
const { original, newSrc } = msg;
log.info("REPLACE_IMAGE request", { original: truncate(original) });
const applied = await replaceImageInDOM(original, newSrc);
if (!applied && isMangaDexHost())
mdRememberPending(original, { newSrc });
sendResp({ ok: true, applied: !!applied });
return;
}
if (msg.type === "RESOLVE_AND_REPLACE_MANGADEX_BLOB") {
const real = await resolveMangaDexOriginalForBlob(msg.blobUrl);
sendResp({ resolved: real });
return;
}
if (msg.type === "IMAGE_ERROR") {
const original = msg.original;
const message = msg.message;
const isNoOverlay = /no overlay data/i.test(String(message || ""));
setTimeout(() => {
if (isNoOverlay)
log.warn("overlay.noData", { original: truncate(original) });
if (shouldShowReplaceError(original)) {
markImageError(
original,
isNoOverlay ? "No text detected" : message,
);
} else {
log.debug("IMAGE_ERROR skipped (image replaced)", {
original: truncate(original),
});
}
}, 1200);
sendResp({ ok: true });
return;
}
if (msg.type === "OVERLAY_HTML") {
try {
const mode = typeof msg?.mode === "string" ? msg.mode : "";
if (!mode) {
log.warn("OVERLAY_HTML missing mode", {
original: truncate(msg.original),
});
sendResp({ ok: true, ignored: true });
return;
}
const isText = mode === "lens_text";
const source = isText
? String(msg?.source || "")
.trim()
.toLowerCase()
: "translated";
if (isText && !source) {
log.warn("OVERLAY_HTML missing source", {
original: truncate(msg.original),
});
sendResp({ ok: true, ignored: true });
return;
}
const img = findTargetImage(msg.original);
if (img) {
applyHtmlOverlay(img, msg.result, source, isText, msg.original);
log.info("OVERLAY_HTML applied", {
original: truncate(msg.original),
});
} else if (isMangaDexHost()) {
mdRememberPending(msg.original, {
overlay: { result: msg.result, source, isTextMode: isText },
});
}
} catch (e) {
console.warn("[LensOCR content] OVERLAY_HTML failed", e);
}
sendResp({ ok: true });
return;
}
sendResp({ ok: true, ignored: true });
})();
return true;
}); })();