Compare commits
3 Commits
b7a50a053d
...
cb062589fa
| Author | SHA1 | Date | |
|---|---|---|---|
| cb062589fa | |||
| ab931e1264 | |||
| 8f1a0fe28f |
420
content.js
420
content.js
@ -4,8 +4,18 @@ 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 ---------------- */
|
||||||
|
|
||||||
@ -19,6 +29,121 @@ 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);
|
||||||
}
|
}
|
||||||
@ -84,13 +209,25 @@ function loadSettings() {
|
|||||||
}
|
}
|
||||||
updateKnownVisibility();
|
updateKnownVisibility();
|
||||||
process();
|
process();
|
||||||
|
updateHudDot();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
loadStoredIndex();
|
||||||
|
|
||||||
// Reload settings if popup changes them.
|
// Reload settings or shared HUD data if storage changes.
|
||||||
chrome.storage.onChanged.addListener(loadSettings);
|
chrome.storage.onChanged.addListener(changes => {
|
||||||
|
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 ---------------- */
|
||||||
|
|
||||||
@ -109,22 +246,40 @@ 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 channel = channelEl?.textContent?.trim() ?? "(no channel)";
|
const thumbImg = video.querySelector("ytd-thumbnail img");
|
||||||
const channelKey = normalizeKey(channel);
|
const thumb =
|
||||||
const id = getVideoId(video, url);
|
thumbImg?.getAttribute("src") ||
|
||||||
|
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) {
|
||||||
memberOnlyIndex.set(id, { channelKey, hidden: false });
|
const existing = memberOnlyIndex.get(id);
|
||||||
|
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);
|
||||||
@ -140,15 +295,22 @@ 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)) {
|
||||||
memberOnlyIndex.get(id).hidden = true;
|
const meta = memberOnlyIndex.get(id);
|
||||||
|
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)) {
|
||||||
memberOnlyIndex.get(id).hidden = false;
|
const meta = memberOnlyIndex.get(id);
|
||||||
|
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.
|
||||||
@ -166,12 +328,17 @@ 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.
|
||||||
@ -196,24 +363,58 @@ function revealAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Choose a stable identifier for indexing.
|
// Choose a stable identifier for indexing.
|
||||||
function getVideoId(video, url) {
|
function getOrCreateVideoId(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;
|
||||||
return fallbackId || "";
|
if (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;
|
||||||
|
}
|
||||||
|
|
||||||
const channelEl =
|
function getChannelKeyFromData(channelText, channelUrl) {
|
||||||
video.querySelector("ytd-channel-name a") ||
|
const handle = getChannelHandleFromUrl(channelUrl);
|
||||||
video.querySelector(".ytd-channel-name a");
|
if (handle) return normalizeKey(handle);
|
||||||
const channel = channelEl?.textContent?.trim() ?? "";
|
return channelText ? normalizeKey(channelText) : "";
|
||||||
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.
|
||||||
@ -228,6 +429,187 @@ 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();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user