maxza258's picture
Upload 23 files
ebf054f verified
|
Raw
History Blame
67.2 kB

(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;

}); })();