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 = "";
|
let lastHudSignature = "";
|
||||||
const STORAGE_KEY = "memberOnlyHidden";
|
const STORAGE_KEY = "memberOnlyHidden";
|
||||||
const DEFAULT_MAX_ARCHIVE = 500;
|
const DEFAULT_MAX_ARCHIVE = 500;
|
||||||
|
const MAX_SNAPSHOT_CHARS = 20000;
|
||||||
let persistTimer = null;
|
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 ---------------- */
|
/* ---------------- CSS injection ---------------- */
|
||||||
|
|
||||||
@ -47,6 +83,13 @@ function injectStyleOnce() {
|
|||||||
box-shadow: 0 0 0 3px rgba(255, 140, 0, 0.4);
|
box-shadow: 0 0 0 3px rgba(255, 140, 0, 0.4);
|
||||||
animation: ${DATA_PREFIX}-pulse 1.8s ease-in-out infinite;
|
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 {
|
@keyframes ${DATA_PREFIX}-pulse {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
@ -168,6 +211,26 @@ function injectStyleOnce() {
|
|||||||
.${DATA_PREFIX}-hud-link:hover {
|
.${DATA_PREFIX}-hud-link:hover {
|
||||||
text-decoration: underline;
|
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);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
@ -221,6 +284,7 @@ function normalizeWhitelist(list) {
|
|||||||
|
|
||||||
// Read settings and re-apply visibility immediately.
|
// Read settings and re-apply visibility immediately.
|
||||||
function loadSettings() {
|
function loadSettings() {
|
||||||
|
if (!chrome?.storage?.local) return;
|
||||||
chrome.storage.local.get({ whitelist: [], debug: false }, data => {
|
chrome.storage.local.get({ whitelist: [], debug: false }, data => {
|
||||||
const { names, handles } = normalizeWhitelist(data.whitelist);
|
const { names, handles } = normalizeWhitelist(data.whitelist);
|
||||||
whitelist = names;
|
whitelist = names;
|
||||||
@ -269,36 +333,16 @@ function process(root = document) {
|
|||||||
|
|
||||||
if (!video) return;
|
if (!video) return;
|
||||||
|
|
||||||
const linkEl =
|
const videoMeta = getVideoMeta(video);
|
||||||
video.querySelector('a[href^="/watch"]') ||
|
const title = videoMeta.title;
|
||||||
video.querySelector("#video-title-link") ||
|
const url = videoMeta.url;
|
||||||
video.querySelector("#video-title");
|
const thumb = videoMeta.thumb;
|
||||||
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 channelInfo = getChannelInfo(video);
|
const channelInfo = getChannelInfo(video);
|
||||||
const channel = channelInfo.label || "(no channel)";
|
const channel = channelInfo.label || "(no channel)";
|
||||||
const channelUrl = channelInfo.url || "";
|
const channelUrl = channelInfo.url || "";
|
||||||
const channelKey = channelInfo.key;
|
const channelKey = channelInfo.key;
|
||||||
const id = getOrCreateVideoId(video, url);
|
const id = videoMeta.videoId || getOrCreateVideoId(video, url);
|
||||||
const stableId = getStableId(video, url, id);
|
const stableId = videoMeta.videoId || getStableId(video, url, id);
|
||||||
|
|
||||||
video.setAttribute(`data-${DATA_PREFIX}-member-only`, "true");
|
video.setAttribute(`data-${DATA_PREFIX}-member-only`, "true");
|
||||||
if (channelKey) video.setAttribute(`data-${DATA_PREFIX}-channel`, channelKey);
|
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.thumb && thumb) existing.thumb = thumb;
|
||||||
if (!existing.channelLabel && channel) existing.channelLabel = channel;
|
if (!existing.channelLabel && channel) existing.channelLabel = channel;
|
||||||
if (!existing.channelUrl && channelUrl) existing.channelUrl = channelUrl;
|
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();
|
debugGroupEnd();
|
||||||
|
|
||||||
if (!whitelisted) {
|
if (!whitelisted) {
|
||||||
video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true");
|
|
||||||
if (id && memberOnlyIndex.has(id)) {
|
if (id && memberOnlyIndex.has(id)) {
|
||||||
const meta = memberOnlyIndex.get(id);
|
const meta = memberOnlyIndex.get(id);
|
||||||
|
if (!meta.hidden) flashHudDot();
|
||||||
|
if (!meta.snapshot) meta.snapshot = safeSnapshot(video);
|
||||||
if (!meta.hidden) hudDirty = true;
|
if (!meta.hidden) hudDirty = true;
|
||||||
meta.hidden = true;
|
meta.hidden = true;
|
||||||
updateSharedIndex(meta, true);
|
updateSharedIndex(meta, true);
|
||||||
}
|
}
|
||||||
|
video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true");
|
||||||
} else {
|
} else {
|
||||||
video.removeAttribute(`data-${DATA_PREFIX}-hidden`);
|
video.removeAttribute(`data-${DATA_PREFIX}-hidden`);
|
||||||
if (id && memberOnlyIndex.has(id)) {
|
if (id && memberOnlyIndex.has(id)) {
|
||||||
@ -355,6 +405,7 @@ function process(root = document) {
|
|||||||
updateSharedIndex(meta, false);
|
updateSharedIndex(meta, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (videoMeta.needsRetry) scheduleMetaRetry(video);
|
||||||
});
|
});
|
||||||
updateHudDot();
|
updateHudDot();
|
||||||
}
|
}
|
||||||
@ -381,6 +432,7 @@ function updateKnownVisibility() {
|
|||||||
video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true");
|
video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true");
|
||||||
if (!meta.hidden) hudDirty = true;
|
if (!meta.hidden) hudDirty = true;
|
||||||
meta.hidden = true;
|
meta.hidden = true;
|
||||||
|
if (!meta.snapshot) meta.snapshot = safeSnapshot(video);
|
||||||
updateSharedIndex(meta, true);
|
updateSharedIndex(meta, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -427,6 +479,102 @@ function getStableId(video, url, fallbackId) {
|
|||||||
return 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.
|
// Read the channel key from DOM or fallback to text.
|
||||||
function getChannelKey(video) {
|
function getChannelKey(video) {
|
||||||
const stored = video.getAttribute(`data-${DATA_PREFIX}-channel`);
|
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) {
|
function updateSharedIndex(meta, hidden) {
|
||||||
if (!meta.stableId) {
|
if (!meta.stableId) {
|
||||||
meta.stableId = meta.url || meta.id || "";
|
meta.stableId = meta.url || meta.id || "";
|
||||||
}
|
}
|
||||||
if (!meta.stableId) return;
|
if (!meta.stableId) return;
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
|
const existing = sharedIndex.get(meta.stableId) || {};
|
||||||
sharedIndex.set(meta.stableId, {
|
sharedIndex.set(meta.stableId, {
|
||||||
id: meta.stableId,
|
id: meta.stableId,
|
||||||
channelKey: meta.channelKey,
|
channelKey: meta.channelKey || existing.channelKey,
|
||||||
channelLabel: meta.channelLabel,
|
channelLabel: meta.channelLabel || existing.channelLabel,
|
||||||
channelUrl: meta.channelUrl,
|
channelUrl: meta.channelUrl || existing.channelUrl,
|
||||||
title: meta.title,
|
title: meta.title || existing.title,
|
||||||
url: meta.url,
|
url: meta.url || existing.url,
|
||||||
thumb: meta.thumb,
|
thumb: meta.thumb || existing.thumb,
|
||||||
|
snapshot: meta.snapshot ? stripHiddenMarkers(meta.snapshot) : existing.snapshot,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
lastSeen: Date.now()
|
lastSeen: Date.now()
|
||||||
});
|
});
|
||||||
@ -567,20 +726,23 @@ function schedulePersistSharedIndex() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function persistSharedIndex() {
|
function persistSharedIndex() {
|
||||||
|
if (!chrome?.storage?.local || contextInvalidated) return;
|
||||||
const items = Array.from(sharedIndex.values());
|
const items = Array.from(sharedIndex.values());
|
||||||
items.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0));
|
items.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0));
|
||||||
chrome.storage.local.get({ maxArchive: DEFAULT_MAX_ARCHIVE }, data => {
|
chrome.storage.local.get({ maxArchive: DEFAULT_MAX_ARCHIVE }, data => {
|
||||||
const maxArchive = Number(data.maxArchive);
|
const maxArchive = Number(data.maxArchive);
|
||||||
const max = Number.isFinite(maxArchive) ? maxArchive : DEFAULT_MAX_ARCHIVE;
|
const max = Number.isFinite(maxArchive) ? maxArchive : DEFAULT_MAX_ARCHIVE;
|
||||||
|
if (!chrome?.storage?.local || contextInvalidated) return;
|
||||||
if (max < 0) {
|
if (max < 0) {
|
||||||
chrome.storage.local.set({ [STORAGE_KEY]: items });
|
safeStorageSet({ [STORAGE_KEY]: items });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
chrome.storage.local.set({ [STORAGE_KEY]: items.slice(0, max) });
|
safeStorageSet({ [STORAGE_KEY]: items.slice(0, max) });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadStoredIndex() {
|
function loadStoredIndex() {
|
||||||
|
if (!chrome?.storage?.local) return;
|
||||||
chrome.storage.local.get({ [STORAGE_KEY]: [] }, data => {
|
chrome.storage.local.get({ [STORAGE_KEY]: [] }, data => {
|
||||||
sharedIndex.clear();
|
sharedIndex.clear();
|
||||||
data[STORAGE_KEY].forEach(item => sharedIndex.set(item.id, item));
|
data[STORAGE_KEY].forEach(item => sharedIndex.set(item.id, item));
|
||||||
@ -644,6 +806,15 @@ function renderHudPanel(panel) {
|
|||||||
list.className = `${DATA_PREFIX}-video-list`;
|
list.className = `${DATA_PREFIX}-video-list`;
|
||||||
|
|
||||||
items.forEach(meta => {
|
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");
|
const item = document.createElement("div");
|
||||||
item.className = `${DATA_PREFIX}-hud-item`;
|
item.className = `${DATA_PREFIX}-hud-item`;
|
||||||
|
|
||||||
@ -671,7 +842,15 @@ function renderHudPanel(panel) {
|
|||||||
|
|
||||||
function clearArchive() {
|
function clearArchive() {
|
||||||
sharedIndex.clear();
|
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 ---------------- */
|
/* ---------------- initial + observer ---------------- */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user