1
0

Compare commits

..

No commits in common. "cb062589fac8f2ade5b1e6be580b1d8660dc0183" and "b7a50a053ddb6eb7614cfd21f558916c04f01e29" have entirely different histories.

View File

@ -4,18 +4,8 @@ let whitelistHandles = [];
let debugEnabled = false; let debugEnabled = false;
// In-memory index of known member-only videos. // In-memory index of known member-only videos.
const memberOnlyIndex = new Map(); // id -> { channelKey, hidden } const memberOnlyIndex = new Map(); // id -> { channelKey, hidden }
const sharedIndex = new Map(); // stableId -> meta for cross-tab HUD
// Data-* attribute prefix for DOM tags. // Data-* attribute prefix for DOM tags.
const DATA_PREFIX = "chipperfluff-nobs"; // nobs = "no-bs" (member-only videos) const DATA_PREFIX = "chipperfluff-nobs"; // nobs = "no-bs" (member-only videos)
const HUD_DOT_ID = `${DATA_PREFIX}-hud-dot`;
const HUD_PANEL_ID = `${DATA_PREFIX}-hud-panel`;
let generatedIdCounter = 0;
let hudDirty = false;
let lastHudSignature = "";
const STORAGE_KEY = "memberOnlyHidden";
const MAX_GLOBAL_ITEMS = 200;
const MAX_PER_CHANNEL = 20;
let persistTimer = null;
/* ---------------- CSS injection ---------------- */ /* ---------------- CSS injection ---------------- */
@ -29,121 +19,6 @@ function injectStyleOnce() {
[data-${DATA_PREFIX}-hidden="true"] { [data-${DATA_PREFIX}-hidden="true"] {
display: none !important; display: none !important;
} }
#${HUD_DOT_ID} {
position: fixed;
right: 18px;
bottom: 18px;
width: 12px;
height: 12px;
border-radius: 999px;
background: #9b9b9b;
border: 2px solid #6f6f6f;
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.12);
z-index: 2147483647;
cursor: pointer;
}
#${HUD_DOT_ID}[data-${DATA_PREFIX}-active="true"] {
background: #ff9c1a;
border-color: #b04a00;
box-shadow: 0 0 0 3px rgba(255, 140, 0, 0.4);
animation: ${DATA_PREFIX}-pulse 1.8s ease-in-out infinite;
}
@keyframes ${DATA_PREFIX}-pulse {
0% {
transform: scale(0.9);
box-shadow: 0 0 0 0 rgba(255, 140, 0, 0.6);
}
70% {
transform: scale(1.1);
box-shadow: 0 0 0 6px rgba(255, 140, 0, 0);
}
100% {
transform: scale(0.9);
box-shadow: 0 0 0 0 rgba(255, 140, 0, 0);
}
}
#${HUD_PANEL_ID} {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 520px;
height: 520px;
overflow: auto;
padding: 10px;
border-radius: 10px;
border: 1px solid #b04a00;
background: #fff6e8;
color: #2b2014;
font-size: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
z-index: 2147483647;
}
#${HUD_PANEL_ID}[data-${DATA_PREFIX}-hidden="true"] {
display: none;
}
.${DATA_PREFIX}-hud-title {
font-weight: 700;
margin: 0 0 8px;
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.4px;
}
.${DATA_PREFIX}-channel {
margin: 6px 0;
padding: 6px 8px;
border-radius: 8px;
background: #fffaf0;
border: 1px solid #e2d7be;
}
.${DATA_PREFIX}-channel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
cursor: pointer;
font-weight: 700;
}
.${DATA_PREFIX}-channel-header::before {
content: "▸";
margin-right: 6px;
color: #8b5a2a;
font-weight: 700;
}
details[open] > .${DATA_PREFIX}-channel-header::before {
content: "▾";
}
.${DATA_PREFIX}-video-list {
margin: 6px 0 0;
padding-left: 18px;
border-left: 2px solid #f0d8b4;
}
.${DATA_PREFIX}-hud-item {
display: grid;
grid-template-columns: 64px 1fr;
gap: 8px;
align-items: center;
margin: 6px 0;
padding: 6px;
border-radius: 8px;
background: #fffaf0;
border: 1px solid #e2d7be;
}
.${DATA_PREFIX}-hud-thumb {
width: 64px;
height: 36px;
object-fit: cover;
border-radius: 4px;
background: #eee2cc;
}
.${DATA_PREFIX}-hud-link {
color: #5a2b00;
text-decoration: none;
font-weight: 700;
}
.${DATA_PREFIX}-hud-link:hover {
text-decoration: underline;
}
`; `;
document.head.appendChild(style); document.head.appendChild(style);
} }
@ -209,25 +84,13 @@ function loadSettings() {
} }
updateKnownVisibility(); updateKnownVisibility();
process(); process();
updateHudDot();
}); });
} }
loadSettings(); loadSettings();
loadStoredIndex();
// Reload settings or shared HUD data if storage changes. // Reload settings if popup changes them.
chrome.storage.onChanged.addListener(changes => { chrome.storage.onChanged.addListener(loadSettings);
if (changes[STORAGE_KEY]) {
const items = changes[STORAGE_KEY].newValue || [];
sharedIndex.clear();
items.forEach(item => sharedIndex.set(item.id, item));
updateHudDot();
}
if (changes.whitelist || changes.debug) {
loadSettings();
}
});
/* ---------------- detection logic ---------------- */ /* ---------------- detection logic ---------------- */
@ -246,40 +109,22 @@ function process(root = document) {
if (!video) return; if (!video) return;
const titleEl = video.querySelector("#video-title"); const titleEl = video.querySelector("#video-title");
const channelEl =
video.querySelector("ytd-channel-name a") ||
video.querySelector(".ytd-channel-name a");
const title = titleEl?.textContent?.trim() ?? "(no title)"; const title = titleEl?.textContent?.trim() ?? "(no title)";
const url = titleEl?.href ?? ""; const url = titleEl?.href ?? "";
const thumbImg = video.querySelector("ytd-thumbnail img"); const channel = channelEl?.textContent?.trim() ?? "(no channel)";
const thumb = const channelKey = normalizeKey(channel);
thumbImg?.getAttribute("src") || const id = getVideoId(video, url);
thumbImg?.getAttribute("data-thumb") ||
thumbImg?.getAttribute("data-src") ||
"";
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);
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);
if (id) video.setAttribute(`data-${DATA_PREFIX}-id`, id); if (id) video.setAttribute(`data-${DATA_PREFIX}-id`, id);
if (id) { if (id) {
const existing = memberOnlyIndex.get(id); memberOnlyIndex.set(id, { channelKey, hidden: false });
if (!existing) {
memberOnlyIndex.set(id, {
channelKey,
channelLabel: channel,
channelUrl,
stableId,
hidden: false,
title,
url,
thumb
});
hudDirty = true;
}
} }
const whitelisted = whitelist.includes(channelKey); const whitelisted = whitelist.includes(channelKey);
@ -295,22 +140,15 @@ function process(root = document) {
if (!whitelisted) { if (!whitelisted) {
video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true"); video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true");
if (id && memberOnlyIndex.has(id)) { if (id && memberOnlyIndex.has(id)) {
const meta = memberOnlyIndex.get(id); memberOnlyIndex.get(id).hidden = true;
if (!meta.hidden) hudDirty = true;
meta.hidden = true;
updateSharedIndex(meta, 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)) {
const meta = memberOnlyIndex.get(id); memberOnlyIndex.get(id).hidden = false;
if (meta.hidden) hudDirty = true;
meta.hidden = false;
updateSharedIndex(meta, false);
} }
} }
}); });
updateHudDot();
} }
// Apply whitelist to already-tagged member-only videos. // Apply whitelist to already-tagged member-only videos.
@ -328,17 +166,12 @@ function updateKnownVisibility() {
} }
if (whitelist.includes(meta.channelKey)) { if (whitelist.includes(meta.channelKey)) {
video.removeAttribute(`data-${DATA_PREFIX}-hidden`); video.removeAttribute(`data-${DATA_PREFIX}-hidden`);
if (meta.hidden) hudDirty = true;
meta.hidden = false; meta.hidden = false;
updateSharedIndex(meta, false);
} else { } else {
video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true"); video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true");
if (!meta.hidden) hudDirty = true;
meta.hidden = true; meta.hidden = true;
updateSharedIndex(meta, true);
} }
} }
updateHudDot();
} }
// Allow full visibility on whitelisted channel pages. // Allow full visibility on whitelisted channel pages.
@ -363,58 +196,24 @@ function revealAll() {
} }
// Choose a stable identifier for indexing. // Choose a stable identifier for indexing.
function getOrCreateVideoId(video, url) { function getVideoId(video, url) {
const dataId = video.getAttribute("data-video-id"); const dataId = video.getAttribute("data-video-id");
if (dataId) return dataId; if (dataId) return dataId;
if (url) return url; if (url) return url;
const fallbackId = video.id; const fallbackId = video.id;
if (fallbackId) return fallbackId; return fallbackId || "";
const generated = `mf-${generatedIdCounter++}`;
video.setAttribute(`data-${DATA_PREFIX}-id`, generated);
return generated;
}
function getStableId(video, url) {
const dataId = video.getAttribute("data-video-id");
if (dataId) return dataId;
if (url) return url;
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`);
if (stored) return stored; if (stored) return stored;
return getChannelInfo(video).key;
}
function getChannelKeyFromData(channelText, channelUrl) { const channelEl =
const handle = getChannelHandleFromUrl(channelUrl); video.querySelector("ytd-channel-name a") ||
if (handle) return normalizeKey(handle); video.querySelector(".ytd-channel-name a");
return channelText ? normalizeKey(channelText) : ""; const channel = channelEl?.textContent?.trim() ?? "";
} return channel ? normalizeKey(channel) : "";
function getChannelInfo(video) {
const channelBlock =
video.querySelector("ytd-channel-name") ||
video.querySelector(".ytd-channel-name");
const linkEl =
channelBlock?.querySelector("a") ||
video.querySelector('a[href^="/@"]') ||
video.querySelector('a[href^="/channel/"]') ||
video.querySelector('a[href^="/c/"]');
const labelEl =
channelBlock?.querySelector("#text") ||
channelBlock?.querySelector("yt-formatted-string") ||
channelBlock;
const label = labelEl?.textContent?.trim() ?? "";
const url = linkEl?.href ?? "";
const handleFromUrl = getChannelHandleFromUrl(url);
const handleFromPage = getChannelHandleFromUrl(location.href);
const fallbackLabel = handleFromUrl || handleFromPage || "";
const finalLabel = label || fallbackLabel;
const key = getChannelKeyFromData(finalLabel, url || handleFromPage);
return { label: finalLabel, url: url || handleFromPage, key };
} }
// Find a video element by stored id. // Find a video element by stored id.
@ -429,187 +228,6 @@ function cssEscape(value) {
return String(value).replace(/["\\]/g, "\\$&"); return String(value).replace(/["\\]/g, "\\$&");
} }
function ensureHudDot() {
let dot = document.getElementById(HUD_DOT_ID);
if (!dot) {
dot = document.createElement("div");
dot.id = HUD_DOT_ID;
dot.setAttribute(`data-${DATA_PREFIX}-active`, "false");
dot.title = "Member-only videos: 0 (0 hidden)";
document.body.appendChild(dot);
}
let panel = document.getElementById(HUD_PANEL_ID);
if (!panel) {
panel = document.createElement("div");
panel.id = HUD_PANEL_ID;
panel.setAttribute(`data-${DATA_PREFIX}-hidden`, "true");
document.body.appendChild(panel);
}
if (!dot.dataset.bound) {
dot.dataset.bound = "true";
dot.addEventListener("click", event => {
event.stopPropagation();
const isHidden =
panel.getAttribute(`data-${DATA_PREFIX}-hidden`) === "true";
panel.setAttribute(
`data-${DATA_PREFIX}-hidden`,
isHidden ? "false" : "true"
);
if (isHidden) renderHudPanel(panel);
});
panel.addEventListener("click", event => event.stopPropagation());
document.addEventListener("click", event => {
if (panel.getAttribute(`data-${DATA_PREFIX}-hidden`) !== "false") return;
if (panel.contains(event.target) || dot.contains(event.target)) return;
panel.setAttribute(`data-${DATA_PREFIX}-hidden`, "true");
});
}
return dot;
}
function updateHudDot() {
const dot = ensureHudDot();
const panel = document.getElementById(HUD_PANEL_ID);
const total = sharedIndex.size;
const hidden = total;
const signature = `${total}:${hidden}`;
dot.title = `Member-only videos: ${total} (${hidden} hidden)`;
dot.setAttribute(
`data-${DATA_PREFIX}-active`,
total > 0 ? "true" : "false"
);
if (
panel &&
panel.getAttribute(`data-${DATA_PREFIX}-hidden`) === "false" &&
(hudDirty || signature !== lastHudSignature)
) {
renderHudPanel(panel);
hudDirty = false;
lastHudSignature = signature;
}
}
function updateSharedIndex(meta, hidden) {
if (!meta.stableId) return;
if (hidden) {
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,
hidden: true
});
} else {
sharedIndex.delete(meta.stableId);
}
schedulePersistSharedIndex();
}
function schedulePersistSharedIndex() {
if (persistTimer) return;
persistTimer = setTimeout(() => {
persistTimer = null;
persistSharedIndex();
}, 200);
}
function persistSharedIndex() {
const items = Array.from(sharedIndex.values());
items.sort((a, b) => a.id.localeCompare(b.id));
const perChannelCounts = new Map();
const pruned = [];
for (const item of items) {
const key = item.channelKey || item.channelLabel || "(unknown)";
const count = perChannelCounts.get(key) || 0;
if (count >= MAX_PER_CHANNEL) continue;
if (pruned.length >= MAX_GLOBAL_ITEMS) break;
perChannelCounts.set(key, count + 1);
pruned.push(item);
}
chrome.storage.local.set({ [STORAGE_KEY]: pruned });
}
function loadStoredIndex() {
chrome.storage.local.get({ [STORAGE_KEY]: [] }, data => {
sharedIndex.clear();
data[STORAGE_KEY].forEach(item => sharedIndex.set(item.id, item));
updateHudDot();
});
}
function renderHudPanel(panel) {
panel.innerHTML = "";
const title = document.createElement("div");
title.className = `${DATA_PREFIX}-hud-title`;
title.textContent = "Hidden member-only videos";
panel.appendChild(title);
const groups = new Map();
for (const meta of sharedIndex.values()) {
if (!meta.hidden) continue;
const key =
meta.channelKey ||
getChannelKeyFromData(meta.channelLabel, meta.channelUrl) ||
"(unknown)";
const list = groups.get(key) || [];
list.push(meta);
groups.set(key, list);
}
if (groups.size === 0) {
const empty = document.createElement("div");
empty.textContent = "No hidden member-only videos.";
panel.appendChild(empty);
return;
}
for (const [creator, items] of groups.entries()) {
const details = document.createElement("details");
details.className = `${DATA_PREFIX}-channel`;
details.open = false;
const summary = document.createElement("summary");
summary.className = `${DATA_PREFIX}-channel-header`;
const label = items[0]?.channelLabel && items[0].channelLabel !== "(no channel)"
? items[0].channelLabel
: creator;
summary.textContent = `${label} (${items.length})`;
details.appendChild(summary);
const list = document.createElement("div");
list.className = `${DATA_PREFIX}-video-list`;
items.forEach(meta => {
const item = document.createElement("div");
item.className = `${DATA_PREFIX}-hud-item`;
const img = document.createElement("img");
img.className = `${DATA_PREFIX}-hud-thumb`;
img.alt = meta.title || "thumbnail";
if (meta.thumb) img.src = meta.thumb;
item.appendChild(img);
const link = document.createElement("a");
link.className = `${DATA_PREFIX}-hud-link`;
link.textContent = meta.title || meta.url || "(no title)";
link.href = meta.url || "#";
link.target = "_blank";
link.rel = "noopener noreferrer";
item.appendChild(link);
list.appendChild(item);
});
panel.appendChild(details);
details.appendChild(list);
}
}
/* ---------------- initial + observer ---------------- */ /* ---------------- initial + observer ---------------- */
process(); process();