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