Compare commits
2 Commits
88abb559d1
...
a4f2f5c60b
| Author | SHA1 | Date | |
|---|---|---|---|
| a4f2f5c60b | |||
| f708833cdf |
251
content.js
251
content.js
@ -14,7 +14,43 @@ let hudDirty = false;
|
||||
let lastHudSignature = "";
|
||||
const STORAGE_KEY = "memberOnlyHidden";
|
||||
const DEFAULT_MAX_ARCHIVE = 500;
|
||||
const MAX_SNAPSHOT_CHARS = 20000;
|
||||
let persistTimer = null;
|
||||
let contextInvalidated = false;
|
||||
let hudFlashTimer = null;
|
||||
|
||||
function safeSnapshot(video) {
|
||||
if (typeof buildSnapshot !== "function") return "";
|
||||
return buildSnapshot(video);
|
||||
}
|
||||
|
||||
function stripHiddenMarkers(html) {
|
||||
const pattern = new RegExp(
|
||||
`\\s*data-${DATA_PREFIX}-[^=\\s>]+(?:=(\"[^\"]*\"|'[^']*'|[^\\s>]*))?`,
|
||||
"gi"
|
||||
);
|
||||
return html
|
||||
.replace(pattern, "")
|
||||
.replace(new RegExp(`data-${DATA_PREFIX}-[^\\s>]+`, "gi"), "");
|
||||
}
|
||||
|
||||
function sanitizeSnapshotToFragment(html) {
|
||||
const template = document.createElement("template");
|
||||
const cleaned = stripHiddenMarkers(html);
|
||||
template.innerHTML = cleaned;
|
||||
return template.content;
|
||||
}
|
||||
|
||||
function forceStripDataMarkers(root) {
|
||||
const all = root.querySelectorAll("*");
|
||||
all.forEach(el => {
|
||||
Array.from(el.attributes).forEach(attr => {
|
||||
if (attr.name.includes(`data-${DATA_PREFIX}-`)) {
|
||||
el.removeAttribute(attr.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- CSS injection ---------------- */
|
||||
|
||||
@ -47,6 +83,13 @@ function injectStyleOnce() {
|
||||
box-shadow: 0 0 0 3px rgba(255, 140, 0, 0.4);
|
||||
animation: ${DATA_PREFIX}-pulse 1.8s ease-in-out infinite;
|
||||
}
|
||||
#${HUD_DOT_ID}[data-${DATA_PREFIX}-flash="true"] {
|
||||
background: #ffd84a;
|
||||
border-color: #e0a600;
|
||||
transform: scale(1.8);
|
||||
box-shadow: 0 0 0 10px rgba(255, 216, 74, 0.4);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
@keyframes ${DATA_PREFIX}-pulse {
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
@ -168,6 +211,26 @@ function injectStyleOnce() {
|
||||
.${DATA_PREFIX}-hud-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.${DATA_PREFIX}-snapshot {
|
||||
border: 1px solid #3a2c22;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: #232323;
|
||||
color: #f5efe6;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.${DATA_PREFIX}-snapshot a {
|
||||
color: #ffb26a;
|
||||
text-decoration: none;
|
||||
}
|
||||
.${DATA_PREFIX}-snapshot img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
@ -221,6 +284,7 @@ function normalizeWhitelist(list) {
|
||||
|
||||
// Read settings and re-apply visibility immediately.
|
||||
function loadSettings() {
|
||||
if (!chrome?.storage?.local) return;
|
||||
chrome.storage.local.get({ whitelist: [], debug: false }, data => {
|
||||
const { names, handles } = normalizeWhitelist(data.whitelist);
|
||||
whitelist = names;
|
||||
@ -269,36 +333,16 @@ function process(root = document) {
|
||||
|
||||
if (!video) return;
|
||||
|
||||
const linkEl =
|
||||
video.querySelector('a[href^="/watch"]') ||
|
||||
video.querySelector("#video-title-link") ||
|
||||
video.querySelector("#video-title");
|
||||
const title =
|
||||
linkEl?.getAttribute("title") ||
|
||||
linkEl?.textContent?.trim() ||
|
||||
"(no title)";
|
||||
const rawUrl = linkEl?.getAttribute("href") || "";
|
||||
const url = rawUrl.startsWith("http")
|
||||
? rawUrl
|
||||
: rawUrl
|
||||
? new URL(rawUrl, location.origin).toString()
|
||||
: "";
|
||||
const thumbImg =
|
||||
video.querySelector("ytd-thumbnail img") ||
|
||||
video.querySelector("yt-img-shadow img") ||
|
||||
video.querySelector("img#img");
|
||||
const thumb =
|
||||
thumbImg?.currentSrc ||
|
||||
thumbImg?.getAttribute("src") ||
|
||||
thumbImg?.getAttribute("data-thumb") ||
|
||||
thumbImg?.getAttribute("data-src") ||
|
||||
"";
|
||||
const videoMeta = getVideoMeta(video);
|
||||
const title = videoMeta.title;
|
||||
const url = videoMeta.url;
|
||||
const thumb = videoMeta.thumb;
|
||||
const channelInfo = getChannelInfo(video);
|
||||
const channel = channelInfo.label || "(no channel)";
|
||||
const channelUrl = channelInfo.url || "";
|
||||
const channelKey = channelInfo.key;
|
||||
const id = getOrCreateVideoId(video, url);
|
||||
const stableId = getStableId(video, url, id);
|
||||
const id = videoMeta.videoId || getOrCreateVideoId(video, url);
|
||||
const stableId = videoMeta.videoId || getStableId(video, url, id);
|
||||
|
||||
video.setAttribute(`data-${DATA_PREFIX}-member-only`, "true");
|
||||
if (channelKey) video.setAttribute(`data-${DATA_PREFIX}-channel`, channelKey);
|
||||
@ -325,6 +369,10 @@ function process(root = document) {
|
||||
if (!existing.thumb && thumb) existing.thumb = thumb;
|
||||
if (!existing.channelLabel && channel) existing.channelLabel = channel;
|
||||
if (!existing.channelUrl && channelUrl) existing.channelUrl = channelUrl;
|
||||
if (existing.hidden) {
|
||||
if (!existing.snapshot) existing.snapshot = safeSnapshot(video);
|
||||
updateSharedIndex(existing, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -339,13 +387,15 @@ function process(root = document) {
|
||||
debugGroupEnd();
|
||||
|
||||
if (!whitelisted) {
|
||||
video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true");
|
||||
if (id && memberOnlyIndex.has(id)) {
|
||||
const meta = memberOnlyIndex.get(id);
|
||||
if (!meta.hidden) flashHudDot();
|
||||
if (!meta.snapshot) meta.snapshot = safeSnapshot(video);
|
||||
if (!meta.hidden) hudDirty = true;
|
||||
meta.hidden = true;
|
||||
updateSharedIndex(meta, true);
|
||||
}
|
||||
video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true");
|
||||
} else {
|
||||
video.removeAttribute(`data-${DATA_PREFIX}-hidden`);
|
||||
if (id && memberOnlyIndex.has(id)) {
|
||||
@ -355,6 +405,7 @@ function process(root = document) {
|
||||
updateSharedIndex(meta, false);
|
||||
}
|
||||
}
|
||||
if (videoMeta.needsRetry) scheduleMetaRetry(video);
|
||||
});
|
||||
updateHudDot();
|
||||
}
|
||||
@ -381,6 +432,7 @@ function updateKnownVisibility() {
|
||||
video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true");
|
||||
if (!meta.hidden) hudDirty = true;
|
||||
meta.hidden = true;
|
||||
if (!meta.snapshot) meta.snapshot = safeSnapshot(video);
|
||||
updateSharedIndex(meta, true);
|
||||
}
|
||||
}
|
||||
@ -427,6 +479,102 @@ function getStableId(video, url, fallbackId) {
|
||||
return fallbackId || "";
|
||||
}
|
||||
|
||||
function getVideoMeta(video) {
|
||||
const linkEl = findWatchLink(video);
|
||||
const href = linkEl?.getAttribute("href") || "";
|
||||
const videoId =
|
||||
getVideoIdFromHref(href) ||
|
||||
video.getAttribute("data-video-id") ||
|
||||
video.querySelector("[data-video-id]")?.getAttribute("data-video-id") ||
|
||||
"";
|
||||
const url = videoId
|
||||
? `https://www.youtube.com/watch?v=${videoId}`
|
||||
: href.startsWith("http")
|
||||
? href
|
||||
: href
|
||||
? new URL(href, location.origin).toString()
|
||||
: "";
|
||||
const title =
|
||||
getLinkTitle(linkEl) || getFallbackTitle(video) || "(no title)";
|
||||
const thumb =
|
||||
(videoId ? `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg` : "") ||
|
||||
video.querySelector("img")?.currentSrc ||
|
||||
video.querySelector("img")?.getAttribute("src") ||
|
||||
"";
|
||||
const needsRetry = !videoId || !title || title === "(no title)";
|
||||
return { title, url, thumb, videoId, needsRetry };
|
||||
}
|
||||
|
||||
function findWatchLink(video) {
|
||||
const links = Array.from(video.querySelectorAll("a[href]"));
|
||||
let best = null;
|
||||
for (const link of links) {
|
||||
const href = link.getAttribute("href") || "";
|
||||
if (!href.includes("watch?v=")) continue;
|
||||
if (!best) best = link;
|
||||
const titled = getLinkTitle(link) || link.textContent?.trim();
|
||||
if (titled) return link;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function getLinkTitle(link) {
|
||||
if (!link) return "";
|
||||
const title = link.getAttribute("title") || link.getAttribute("aria-label");
|
||||
return title ? title.trim() : "";
|
||||
}
|
||||
|
||||
function getFallbackTitle(video) {
|
||||
const titled = Array.from(video.querySelectorAll("[title]"))
|
||||
.map(el => el.getAttribute("title") || "")
|
||||
.map(text => text.trim())
|
||||
.find(text => text && text.toLowerCase() !== "undefined");
|
||||
if (titled) return titled;
|
||||
const aria = Array.from(video.querySelectorAll("[aria-label]"))
|
||||
.map(el => el.getAttribute("aria-label") || "")
|
||||
.map(text => text.trim())
|
||||
.find(text => text);
|
||||
return aria || "";
|
||||
}
|
||||
|
||||
function scheduleMetaRetry(video) {
|
||||
const attr = `data-${DATA_PREFIX}-meta-retry`;
|
||||
const current = parseInt(video.getAttribute(attr) || "0", 10);
|
||||
if (current >= 3) return;
|
||||
video.setAttribute(attr, String(current + 1));
|
||||
setTimeout(() => {
|
||||
process(video);
|
||||
}, 400 + current * 400);
|
||||
}
|
||||
|
||||
function getVideoIdFromHref(href) {
|
||||
if (!href) return "";
|
||||
const match = href.match(/[?&]v=([^&]+)/);
|
||||
if (match) return match[1];
|
||||
const shortMatch = href.match(/youtu\.be\/([^?]+)/);
|
||||
if (shortMatch) return shortMatch[1];
|
||||
return "";
|
||||
}
|
||||
|
||||
function buildSnapshot(video) {
|
||||
try {
|
||||
const root =
|
||||
video.closest(
|
||||
"ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer"
|
||||
) || video;
|
||||
const clone = root.cloneNode(true);
|
||||
removeDataMarkers(clone);
|
||||
const html = stripHiddenMarkers(clone.outerHTML).trim();
|
||||
if (!html) return "";
|
||||
if (html.length > MAX_SNAPSHOT_CHARS) {
|
||||
return html.slice(0, MAX_SNAPSHOT_CHARS);
|
||||
}
|
||||
return html;
|
||||
} catch (error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Read the channel key from DOM or fallback to text.
|
||||
function getChannelKey(video) {
|
||||
const stored = video.getAttribute(`data-${DATA_PREFIX}-channel`);
|
||||
@ -535,20 +683,31 @@ function updateHudDot() {
|
||||
}
|
||||
}
|
||||
|
||||
function flashHudDot() {
|
||||
const dot = ensureHudDot();
|
||||
dot.setAttribute(`data-${DATA_PREFIX}-flash`, "true");
|
||||
if (hudFlashTimer) clearTimeout(hudFlashTimer);
|
||||
hudFlashTimer = setTimeout(() => {
|
||||
dot.removeAttribute(`data-${DATA_PREFIX}-flash`);
|
||||
}, 900);
|
||||
}
|
||||
|
||||
function updateSharedIndex(meta, hidden) {
|
||||
if (!meta.stableId) {
|
||||
meta.stableId = meta.url || meta.id || "";
|
||||
}
|
||||
if (!meta.stableId) return;
|
||||
if (hidden) {
|
||||
const existing = sharedIndex.get(meta.stableId) || {};
|
||||
sharedIndex.set(meta.stableId, {
|
||||
id: meta.stableId,
|
||||
channelKey: meta.channelKey,
|
||||
channelLabel: meta.channelLabel,
|
||||
channelUrl: meta.channelUrl,
|
||||
title: meta.title,
|
||||
url: meta.url,
|
||||
thumb: meta.thumb,
|
||||
channelKey: meta.channelKey || existing.channelKey,
|
||||
channelLabel: meta.channelLabel || existing.channelLabel,
|
||||
channelUrl: meta.channelUrl || existing.channelUrl,
|
||||
title: meta.title || existing.title,
|
||||
url: meta.url || existing.url,
|
||||
thumb: meta.thumb || existing.thumb,
|
||||
snapshot: meta.snapshot ? stripHiddenMarkers(meta.snapshot) : existing.snapshot,
|
||||
hidden: true,
|
||||
lastSeen: Date.now()
|
||||
});
|
||||
@ -567,20 +726,23 @@ function schedulePersistSharedIndex() {
|
||||
}
|
||||
|
||||
function persistSharedIndex() {
|
||||
if (!chrome?.storage?.local || contextInvalidated) return;
|
||||
const items = Array.from(sharedIndex.values());
|
||||
items.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0));
|
||||
chrome.storage.local.get({ maxArchive: DEFAULT_MAX_ARCHIVE }, data => {
|
||||
const maxArchive = Number(data.maxArchive);
|
||||
const max = Number.isFinite(maxArchive) ? maxArchive : DEFAULT_MAX_ARCHIVE;
|
||||
if (!chrome?.storage?.local || contextInvalidated) return;
|
||||
if (max < 0) {
|
||||
chrome.storage.local.set({ [STORAGE_KEY]: items });
|
||||
safeStorageSet({ [STORAGE_KEY]: items });
|
||||
return;
|
||||
}
|
||||
chrome.storage.local.set({ [STORAGE_KEY]: items.slice(0, max) });
|
||||
safeStorageSet({ [STORAGE_KEY]: items.slice(0, max) });
|
||||
});
|
||||
}
|
||||
|
||||
function loadStoredIndex() {
|
||||
if (!chrome?.storage?.local) return;
|
||||
chrome.storage.local.get({ [STORAGE_KEY]: [] }, data => {
|
||||
sharedIndex.clear();
|
||||
data[STORAGE_KEY].forEach(item => sharedIndex.set(item.id, item));
|
||||
@ -644,6 +806,15 @@ function renderHudPanel(panel) {
|
||||
list.className = `${DATA_PREFIX}-video-list`;
|
||||
|
||||
items.forEach(meta => {
|
||||
if (meta.snapshot) {
|
||||
const snap = document.createElement("div");
|
||||
snap.className = `${DATA_PREFIX}-snapshot`;
|
||||
snap.appendChild(sanitizeSnapshotToFragment(meta.snapshot));
|
||||
forceStripDataMarkers(snap);
|
||||
list.appendChild(snap);
|
||||
return;
|
||||
}
|
||||
|
||||
const item = document.createElement("div");
|
||||
item.className = `${DATA_PREFIX}-hud-item`;
|
||||
|
||||
@ -671,7 +842,15 @@ function renderHudPanel(panel) {
|
||||
|
||||
function clearArchive() {
|
||||
sharedIndex.clear();
|
||||
chrome.storage.local.set({ [STORAGE_KEY]: [] }, () => updateHudDot());
|
||||
safeStorageSet({ [STORAGE_KEY]: [] }, () => updateHudDot());
|
||||
}
|
||||
|
||||
function safeStorageSet(payload, callback) {
|
||||
try {
|
||||
chrome.storage.local.set(payload, callback);
|
||||
} catch (error) {
|
||||
contextInvalidated = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- initial + observer ---------------- */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user