2d6a95682f
- File reorg: popup/options/bulk-check moved to src/ subdirs - Shared modules: src/shared/id-extract.js, src/options/options-shared.js - Host updates: rcjav-host.py + register/install scripts - .gitignore expanded
510 lines
21 KiB
JavaScript
510 lines
21 KiB
JavaScript
// rclonex content script.
|
||
// Responsibilities:
|
||
// 1. On request, extract a JAV ID from the page via site adapters → URL → title.
|
||
// 2. Implement an interactive element picker so the user can fill in a selector
|
||
// by clicking on the page from the options UI.
|
||
//
|
||
// Wrapped in an IIFE + global flag so re-injection (e.g. via chrome.scripting.executeScript
|
||
// when manifest content_scripts already loaded it) is a no-op instead of a SyntaxError.
|
||
|
||
if (!window.__rclonex_loaded__) {
|
||
window.__rclonex_loaded__ = true;
|
||
(() => {
|
||
|
||
// ID-extraction primitives live in src/shared/id-extract.js (loaded by the
|
||
// manifest content_scripts[] entry before this file).
|
||
const { normalizeId: _normalizeId } = self.RCJAV_IDS;
|
||
|
||
const BUILTIN_SITE_ADAPTERS = [
|
||
{ host: "clearjav.com", selector: "div.meta-chip > h3.meta-chip__value" },
|
||
];
|
||
|
||
let _userNormalizers = [];
|
||
|
||
// Thin wrapper so callers without an explicit list pick up the live settings.
|
||
function normalizeId(text, userNormalizers = _userNormalizers) {
|
||
return _normalizeId(text, userNormalizers);
|
||
}
|
||
async function loadUserNormalizers() {
|
||
try {
|
||
const { settings = {} } = await chrome.storage.sync.get("settings");
|
||
_userNormalizers = Array.isArray(settings.idNormalizers) ? settings.idNormalizers : [];
|
||
} catch { _userNormalizers = []; }
|
||
}
|
||
chrome.storage.onChanged?.addListener?.((changes, area) => {
|
||
if (area === "sync" && changes.settings) {
|
||
_userNormalizers = Array.isArray(changes.settings.newValue?.idNormalizers)
|
||
? changes.settings.newValue.idNormalizers : [];
|
||
}
|
||
});
|
||
loadUserNormalizers();
|
||
|
||
function hostMatches(pattern, host) {
|
||
// Glob: '*' = any chars. Case-insensitive.
|
||
// Convenience: a bare domain (no '*.') ALSO matches any subdomain — and vice versa.
|
||
// "clearjav.com" matches both "clearjav.com" and "www.clearjav.com"
|
||
// "*.clearjav.com" matches both "www.clearjav.com" and bare "clearjav.com"
|
||
const patterns = [pattern];
|
||
if (pattern.startsWith("*.")) patterns.push(pattern.slice(2));
|
||
else if (!pattern.includes("*")) patterns.push("*." + pattern);
|
||
for (const p of patterns) {
|
||
const re = new RegExp("^" + p.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$", "i");
|
||
if (re.test(host)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function tryAdapters(adapters, userNormalizers = _userNormalizers) {
|
||
const host = location.hostname;
|
||
for (const a of adapters) {
|
||
if (!a.host || !a.selector) continue;
|
||
if (!hostMatches(a.host, host)) continue;
|
||
const selectors = a.selector.split(",").map((s) => s.trim()).filter(Boolean);
|
||
for (const sel of selectors) {
|
||
let el;
|
||
try { el = document.querySelector(sel); } catch { continue; }
|
||
if (!el) continue;
|
||
const text = (el.textContent || el.innerText || "").trim();
|
||
const id = normalizeId(text, userNormalizers);
|
||
if (id) return { id, source: "adapter", adapter: a.host, selector: sel, raw: text.slice(0, 200) };
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function tryUrl(userNormalizers = _userNormalizers) {
|
||
const raw = decodeURIComponent(location.pathname + " " + location.search);
|
||
const id = normalizeId(raw, userNormalizers);
|
||
return id ? { id, source: "url", raw: raw.slice(0, 200) } : null;
|
||
}
|
||
|
||
function tryTitle(userNormalizers = _userNormalizers) {
|
||
const id = normalizeId(document.title, userNormalizers);
|
||
return id ? { id, source: "title", raw: document.title.slice(0, 200) } : null;
|
||
}
|
||
|
||
async function getAdapters() {
|
||
const { settings = {} } = await chrome.storage.sync.get("settings");
|
||
const user = Array.isArray(settings.siteAdapters) ? settings.siteAdapters : [];
|
||
const builtin = [...BUILTIN_SITE_ADAPTERS, ...(window.__RCLONEX_BUILTIN_ADAPTERS__ || [])];
|
||
// User entries override built-ins for the same host (user list iterated first).
|
||
return [...user, ...builtin];
|
||
}
|
||
|
||
async function tracePageExtraction(overrides = {}) {
|
||
const savedAdapters = await getAdapters();
|
||
const adapters = Array.isArray(overrides.adapters)
|
||
? [...overrides.adapters, ...BUILTIN_SITE_ADAPTERS, ...(window.__RCLONEX_BUILTIN_ADAPTERS__ || [])]
|
||
: savedAdapters;
|
||
const normalizers = Array.isArray(overrides.normalizers) ? overrides.normalizers : _userNormalizers;
|
||
const adapter = tryAdapters(adapters, normalizers);
|
||
const title = tryTitle(normalizers);
|
||
const url = tryUrl(normalizers);
|
||
return {
|
||
id: (adapter || title || url || {}).id || null,
|
||
source: (adapter || title || url || {}).source || "none",
|
||
selected: adapter || title || url || null,
|
||
stages: { adapter, title, url },
|
||
};
|
||
}
|
||
|
||
// ---------- picker ----------
|
||
|
||
let pickerActive = false;
|
||
let pickerOverlay = null;
|
||
let pickerHighlight = null;
|
||
|
||
function buildSelector(el) {
|
||
if (!el || el.nodeType !== 1) return null;
|
||
|
||
// Helper: does selector hit an element with the same JAV-ID-bearing text?
|
||
// We don't require strict uniqueness — adapter uses first match anyway, so as long
|
||
// as the FIRST match has the right textContent (or contains the same ID), we win.
|
||
const wantedText = (el.textContent || "").trim();
|
||
const wantedId = normalizeId(wantedText);
|
||
const matchesTarget = (sel) => {
|
||
let first;
|
||
try { first = document.querySelector(sel); } catch { return false; }
|
||
if (!first) return false;
|
||
if (first === el) return true;
|
||
const t = (first.textContent || "").trim();
|
||
if (wantedId && normalizeId(t) === wantedId) return true;
|
||
return t === wantedText;
|
||
};
|
||
|
||
const tag = el.tagName.toLowerCase();
|
||
const candidates = [];
|
||
|
||
// 1. #id if any
|
||
if (el.id) candidates.push("#" + CSS.escape(el.id));
|
||
|
||
// 2. tag + single class (each class on its own — pick shortest that works)
|
||
if (el.classList.length) {
|
||
for (const cls of el.classList) candidates.push(`${tag}.${CSS.escape(cls)}`);
|
||
// 3. tag + all classes combined
|
||
candidates.push(tag + Array.from(el.classList).map((c) => "." + CSS.escape(c)).join(""));
|
||
// 4. just .class (no tag) for each class
|
||
for (const cls of el.classList) candidates.push("." + CSS.escape(cls));
|
||
}
|
||
|
||
// 5. just tag (rarely unique but cheap)
|
||
candidates.push(tag);
|
||
|
||
// Try simplest first.
|
||
candidates.sort((a, b) => a.length - b.length);
|
||
for (const sel of candidates) {
|
||
if (matchesTarget(sel)) return sel;
|
||
}
|
||
|
||
// Fall back: build qualified ancestor path.
|
||
const parts = [];
|
||
let cur = el;
|
||
while (cur && cur.nodeType === 1 && cur.tagName.toLowerCase() !== "html") {
|
||
let part = cur.tagName.toLowerCase();
|
||
if (cur.id) { part += "#" + CSS.escape(cur.id); parts.unshift(part); break; }
|
||
if (cur.classList.length) {
|
||
part += Array.from(cur.classList).slice(0, 3).map((c) => "." + CSS.escape(c)).join("");
|
||
}
|
||
const siblings = cur.parentNode ? Array.from(cur.parentNode.children).filter((s) => s.tagName === cur.tagName) : [];
|
||
if (siblings.length > 1) {
|
||
part += `:nth-of-type(${siblings.indexOf(cur) + 1})`;
|
||
}
|
||
parts.unshift(part);
|
||
const candidate = parts.join(" > ");
|
||
if (matchesTarget(candidate)) return candidate;
|
||
cur = cur.parentElement;
|
||
}
|
||
return parts.join(" > ");
|
||
}
|
||
|
||
function startPicker() {
|
||
if (pickerActive) return;
|
||
pickerActive = true;
|
||
pickerOverlay = document.createElement("div");
|
||
pickerOverlay.style.cssText = "position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#ff8800;color:#000;padding:10px 16px;font:bold 14px/1.3 -apple-system,Segoe UI,sans-serif;border-radius:4px;box-shadow:0 4px 16px rgba(0,0,0,.5);pointer-events:none;max-width:90vw;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
|
||
pickerOverlay.textContent = "rclonex picker: hover an element, click to pick — Esc to cancel";
|
||
document.body.appendChild(pickerOverlay);
|
||
pickerHighlight = document.createElement("div");
|
||
pickerHighlight.style.cssText = "position:fixed;pointer-events:none;z-index:2147483646;border:3px dashed #ff8800;background:rgba(255,136,0,.25);box-shadow:0 0 0 9999px rgba(0,0,0,.35);box-sizing:border-box;transition:all 0.05s;";
|
||
document.body.appendChild(pickerHighlight);
|
||
document.addEventListener("mousemove", onPickerMove, true);
|
||
document.addEventListener("click", onPickerClick, true);
|
||
document.addEventListener("keydown", onPickerKey, true);
|
||
}
|
||
|
||
function stopPicker() {
|
||
if (!pickerActive) return;
|
||
pickerActive = false;
|
||
document.removeEventListener("mousemove", onPickerMove, true);
|
||
document.removeEventListener("click", onPickerClick, true);
|
||
document.removeEventListener("keydown", onPickerKey, true);
|
||
if (pickerOverlay) pickerOverlay.remove();
|
||
if (pickerHighlight) pickerHighlight.remove();
|
||
pickerOverlay = null;
|
||
pickerHighlight = null;
|
||
}
|
||
|
||
function onPickerMove(e) {
|
||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||
if (!el || el === pickerHighlight || el === pickerOverlay) return;
|
||
const r = el.getBoundingClientRect();
|
||
pickerHighlight.style.top = r.top + "px";
|
||
pickerHighlight.style.left = r.left + "px";
|
||
pickerHighlight.style.width = r.width + "px";
|
||
pickerHighlight.style.height = r.height + "px";
|
||
pickerOverlay.textContent = `rclonex: ${el.tagName.toLowerCase()}${el.id ? "#" + el.id : ""}${el.className && typeof el.className === "string" ? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".") : ""} — click to pick, Esc to cancel`;
|
||
}
|
||
|
||
function onPickerClick(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||
if (!el || el === pickerHighlight || el === pickerOverlay) { stopPicker(); return; }
|
||
const selector = buildSelector(el);
|
||
const text = (el.textContent || "").trim().slice(0, 200);
|
||
const id = normalizeId(text);
|
||
chrome.runtime.sendMessage({
|
||
type: "picker-result",
|
||
host: location.hostname,
|
||
selector,
|
||
sample: text,
|
||
detectedId: id,
|
||
});
|
||
stopPicker();
|
||
}
|
||
|
||
function onPickerKey(e) {
|
||
if (e.key === "Escape") { stopPicker(); chrome.runtime.sendMessage({ type: "picker-cancelled" }); }
|
||
}
|
||
|
||
// ---------- message dispatch ----------
|
||
|
||
// ---------- overlay (auto-trigger hit toast) ----------
|
||
|
||
const OVERLAY_ID = "rclonex-overlay";
|
||
|
||
function showOverlay(result, opts = {}) {
|
||
if (!result) return;
|
||
const kind = opts.kind || "match";
|
||
if (kind === "match" && (!result.structured || result.structured.length === 0)) return;
|
||
document.getElementById(OVERLAY_ID)?.remove();
|
||
document.getElementById("rclonex-overlay-style")?.remove();
|
||
|
||
const position = opts.position || "top-right";
|
||
const durationMs = Math.max(500, Math.round((opts.duration || 5) * 1000));
|
||
const glow = !!opts.glow;
|
||
const glowColor = opts.glowColor || "#6ec1ff";
|
||
const glowBlur = Number.isFinite(opts.glowBlur) ? opts.glowBlur : 10;
|
||
const glowSpread = Number.isFinite(opts.glowSpread) ? opts.glowSpread : 0;
|
||
const glowOpacity = Number.isFinite(opts.glowOpacity) ? opts.glowOpacity : 0.35;
|
||
|
||
const hexToRgba = (hex, a) => {
|
||
const m = String(hex).match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
||
if (!m) return `rgba(110,193,255,${a})`;
|
||
return `rgba(${parseInt(m[1], 16)},${parseInt(m[2], 16)},${parseInt(m[3], 16)},${a})`;
|
||
};
|
||
|
||
const positions = {
|
||
"top-right": { top: "16px", right: "16px", bottom: "auto", left: "auto", enter: "translateY(-10px)" },
|
||
"top-left": { top: "16px", left: "16px", bottom: "auto", right: "auto", enter: "translateY(-10px)" },
|
||
"bottom-right": { bottom: "16px", right: "16px", top: "auto", left: "auto", enter: "translateY(10px)" },
|
||
"bottom-left": { bottom: "16px", left: "16px", top: "auto", right: "auto", enter: "translateY(10px)" },
|
||
};
|
||
const p = positions[position] || positions["top-right"];
|
||
|
||
const glowCss = glow
|
||
? `box-shadow: 0 6px 20px rgba(0,0,0,0.55), 0 0 ${glowBlur}px ${glowSpread}px ${hexToRgba(glowColor, glowOpacity)};`
|
||
: "box-shadow: 0 8px 30px rgba(0,0,0,0.6);";
|
||
|
||
const css = `
|
||
#${OVERLAY_ID} {
|
||
position: fixed;
|
||
top: ${p.top}; right: ${p.right}; bottom: ${p.bottom}; left: ${p.left};
|
||
width: 380px;
|
||
z-index: 2147483647; background: #1a1a1a; color: #ddd;
|
||
border: 1px solid #2a2a2a; border-radius: 6px;
|
||
font-family: -apple-system, Segoe UI, sans-serif; font-size: 13px;
|
||
${glowCss}
|
||
overflow: hidden;
|
||
opacity: 0; transform: ${p.enter};
|
||
transition: opacity 0.25s, transform 0.25s;
|
||
}
|
||
#${OVERLAY_ID}.show { opacity: 1; transform: translateY(0); }
|
||
#${OVERLAY_ID} .rx-header {
|
||
padding: 8px 12px; background: #1e3a1e; color: #afa;
|
||
font-size: 13px; font-weight: 600;
|
||
display: flex; align-items: center; gap: 8px;
|
||
}
|
||
#${OVERLAY_ID} .rx-title {
|
||
min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||
}
|
||
#${OVERLAY_ID} .rx-mode {
|
||
margin-left: auto;
|
||
background: rgba(0,0,0,0.28);
|
||
border: 1px solid rgba(255,255,255,0.18);
|
||
color: currentColor;
|
||
padding: 1px 6px;
|
||
border-radius: 10px;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.4px;
|
||
flex-shrink: 0;
|
||
}
|
||
#${OVERLAY_ID} .rx-header.rx-no-match { background: #3a1e1e; color: #faa; }
|
||
#${OVERLAY_ID} .rx-subline {
|
||
padding: 8px 12px; background: #0d0d0d; color: #aaa;
|
||
font-size: 12px; font-family: Consolas, monospace;
|
||
word-break: break-all;
|
||
}
|
||
#${OVERLAY_ID} .rx-miss-detail {
|
||
padding: 8px 12px; background: #15110d; color: #ccb98f;
|
||
font-size: 11px; line-height: 1.45;
|
||
border-top: 1px solid #30251a;
|
||
}
|
||
#${OVERLAY_ID} .rx-close {
|
||
cursor: pointer; color: #8a8a8a; font-size: 16px;
|
||
width: 18px; height: 18px; line-height: 16px; text-align: center;
|
||
border-radius: 3px;
|
||
flex-shrink: 0;
|
||
}
|
||
#${OVERLAY_ID} .rx-close:hover { background: rgba(255,255,255,0.1); color: #fff; }
|
||
#${OVERLAY_ID} .rx-body { padding: 6px; background: #0d0d0d; max-height: 360px; overflow-y: auto; }
|
||
#${OVERLAY_ID} .rx-hit {
|
||
background: #161616; border: 1px solid #2a2a2a; border-radius: 4px;
|
||
padding: 10px; margin-bottom: 6px;
|
||
}
|
||
#${OVERLAY_ID} .rx-hit:last-child { margin-bottom: 0; }
|
||
#${OVERLAY_ID} .rx-file { color: #fff; font-weight: 600; font-size: 13px; word-break: break-all; margin: 0 0 4px; }
|
||
#${OVERLAY_ID} .rx-path { color: #aaa; font-size: 12px; font-family: Consolas, monospace; word-break: break-all; margin: 0 0 6px; line-height: 1.45; }
|
||
#${OVERLAY_ID} .rx-plabel { color: #555; font-weight: 600; }
|
||
#${OVERLAY_ID} .rx-meta { display: flex; align-items: center; gap: 8px; font-size: 11px; line-height: 1; }
|
||
#${OVERLAY_ID} .rx-size { color: #6ec1ff; font-weight: 600; }
|
||
#${OVERLAY_ID} .rx-src { background: #2a2a1a; color: #ffcc44; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; letter-spacing: 0.3px; }
|
||
#${OVERLAY_ID} .rx-src.source { background: #1a3a1a; color: #66dd66; }
|
||
#${OVERLAY_ID} .rx-src.catalog { background: #1a2a3a; color: #66bbff; }
|
||
#${OVERLAY_ID} .rx-reason { background: #202a32; border: 1px solid #314453; color: #9dccff; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; }
|
||
#${OVERLAY_ID} .rx-bar-wrap { height: 3px; background: #222; }
|
||
#${OVERLAY_ID} .rx-bar {
|
||
height: 100%; background: linear-gradient(90deg, #6ec1ff, #66dd66);
|
||
transform-origin: left; transform: scaleX(1);
|
||
animation: rxshrink ${durationMs}ms linear forwards;
|
||
}
|
||
@keyframes rxshrink { from { transform: scaleX(1); } to { transform: scaleX(0); } }
|
||
`;
|
||
let styleEl = document.getElementById("rclonex-overlay-style");
|
||
if (!styleEl) {
|
||
styleEl = document.createElement("style");
|
||
styleEl.id = "rclonex-overlay-style";
|
||
styleEl.textContent = css;
|
||
document.head.appendChild(styleEl);
|
||
}
|
||
|
||
const root = document.createElement("div");
|
||
root.id = OVERLAY_ID;
|
||
|
||
const idLabel = result.id || result.structured?.[0]?.jav_id || "?";
|
||
const cacheTag = result.cached && result.ts ? ` [session ${fmtOverlayAge(result.ts)}]` : "";
|
||
const modeLabel = result.cached ? "SESSION" : result.search_mode === "quick" ? "LIVE" : result.search_mode === "cached" ? "CACHE" : "";
|
||
const header = document.createElement("div");
|
||
header.className = "rx-header" + (kind === "no-match" ? " rx-no-match" : "");
|
||
const titleText = kind === "match"
|
||
? `✓ ${idLabel} — ${result.hits} hit(s)${cacheTag}`
|
||
: `✗ ${idLabel} — NOT IN LIBRARY${cacheTag}`;
|
||
if (kind === "match") {
|
||
header.innerHTML = `<span class="rx-title">${escapeOverlay(titleText)}</span>${modeLabel ? `<span class="rx-mode">${escapeOverlay(modeLabel)}</span>` : ""}<span class="rx-close" title="dismiss">×</span>`;
|
||
} else {
|
||
header.innerHTML = `<span class="rx-title">${escapeOverlay(titleText)}</span>${modeLabel ? `<span class="rx-mode">${escapeOverlay(modeLabel)}</span>` : ""}<span class="rx-close" title="dismiss">×</span>`;
|
||
}
|
||
root.appendChild(header);
|
||
|
||
if (kind === "match") {
|
||
const body = document.createElement("div");
|
||
body.className = "rx-body";
|
||
for (const h of result.structured) {
|
||
const filename = h.path.split("/").pop();
|
||
const idx = h.full_path.lastIndexOf("/");
|
||
const dir = idx >= 0 ? h.full_path.slice(0, idx) : h.full_path;
|
||
const srcCls = h.source === "Source" ? "rx-src source"
|
||
: h.source === "Catalog" ? "rx-src catalog" : "rx-src";
|
||
const confidence = h.match_confidence ? ` · ${h.match_confidence}` : "";
|
||
const reason = h.match_reason
|
||
? `<span class="rx-reason" title="Matched ${escapeOverlay(h.matched_query || h.jav_id || "")}${escapeOverlay(confidence)}">${escapeOverlay(h.match_reason)}</span>`
|
||
: "";
|
||
const card = document.createElement("div");
|
||
card.className = "rx-hit";
|
||
card.innerHTML = `
|
||
<div class="rx-file">${escapeOverlay(filename)}</div>
|
||
<div class="rx-path"><span class="rx-plabel">Path:</span> ${escapeOverlay(dir)}</div>
|
||
<div class="rx-meta">
|
||
<span class="${srcCls}">${escapeOverlay(h.source.toUpperCase())}</span>
|
||
<span class="rx-size">${escapeOverlay(h.size_human)}</span>
|
||
${reason}
|
||
</div>
|
||
`;
|
||
body.appendChild(card);
|
||
}
|
||
root.appendChild(body);
|
||
} else {
|
||
// No-match: a single sub-line saying what was scanned.
|
||
const sub = document.createElement("div");
|
||
sub.className = "rx-subline";
|
||
const remotes = (result.scanned_remotes && result.scanned_remotes.length)
|
||
? result.scanned_remotes.join(", ")
|
||
: (Object.keys(result.cache_meta || {})[0] || "library");
|
||
sub.innerHTML = `<span class="rx-plabel">Scanned:</span> ${escapeOverlay(remotes)}`;
|
||
root.appendChild(sub);
|
||
if (result.no_match_title || result.no_match_detail) {
|
||
const detail = document.createElement("div");
|
||
detail.className = "rx-miss-detail";
|
||
detail.innerHTML = `<strong>${escapeOverlay(result.no_match_title || "No match")}</strong>${result.no_match_detail ? `<div>${escapeOverlay(result.no_match_detail)}</div>` : ""}`;
|
||
root.appendChild(detail);
|
||
}
|
||
}
|
||
|
||
const barWrap = document.createElement("div");
|
||
barWrap.className = "rx-bar-wrap";
|
||
const bar = document.createElement("div");
|
||
bar.className = "rx-bar";
|
||
barWrap.appendChild(bar);
|
||
root.appendChild(barWrap);
|
||
|
||
document.body.appendChild(root);
|
||
requestAnimationFrame(() => root.classList.add("show"));
|
||
|
||
// Hover pauses the countdown
|
||
root.addEventListener("mouseenter", () => { bar.style.animationPlayState = "paused"; });
|
||
root.addEventListener("mouseleave", () => { bar.style.animationPlayState = "running"; });
|
||
// Close button
|
||
header.querySelector(".rx-close").addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
dismissOverlay(root);
|
||
});
|
||
// Animation end → fade out
|
||
bar.addEventListener("animationend", () => dismissOverlay(root));
|
||
}
|
||
|
||
function dismissOverlay(root) {
|
||
root.classList.remove("show");
|
||
setTimeout(() => root.remove(), 300);
|
||
}
|
||
|
||
function escapeOverlay(s) {
|
||
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||
}
|
||
|
||
function fmtOverlayAge(ts) {
|
||
if (!ts) return "";
|
||
const s = Math.round((Date.now() - ts) / 1000);
|
||
if (s < 60) return `${s}s ago`;
|
||
if (s < 3600) return `${Math.round(s / 60)}m ago`;
|
||
return `${Math.round(s / 3600)}h ago`;
|
||
}
|
||
|
||
// ---------- message dispatch ----------
|
||
|
||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||
if (msg.type === "show-overlay") {
|
||
showOverlay(msg.result, {
|
||
kind: msg.kind || "match",
|
||
position: msg.position,
|
||
duration: msg.duration,
|
||
glow: msg.glow,
|
||
glowColor: msg.glowColor,
|
||
glowBlur: msg.glowBlur,
|
||
glowSpread: msg.glowSpread,
|
||
glowOpacity: msg.glowOpacity,
|
||
});
|
||
sendResponse({ ok: true });
|
||
return false;
|
||
}
|
||
if (msg.type === "extract-id") {
|
||
(async () => {
|
||
const trace = await tracePageExtraction();
|
||
sendResponse(trace.selected || { id: null, source: "none" });
|
||
})();
|
||
return true;
|
||
}
|
||
if (msg.type === "trace-extract-id") {
|
||
(async () => {
|
||
sendResponse(await tracePageExtraction({
|
||
adapters: msg.adapters,
|
||
normalizers: msg.normalizers,
|
||
}));
|
||
})();
|
||
return true;
|
||
}
|
||
if (msg.type === "start-picker") {
|
||
startPicker();
|
||
sendResponse({ ok: true });
|
||
return false;
|
||
}
|
||
if (msg.type === "stop-picker") {
|
||
stopPicker();
|
||
sendResponse({ ok: true });
|
||
return false;
|
||
}
|
||
});
|
||
|
||
})();
|
||
} // end if !__rclonex_loaded__
|