diff --git a/content.js b/content.js index b0a74af..6820307 100644 --- a/content.js +++ b/content.js @@ -1,5 +1,8 @@ let whitelist = []; +let whitelistHandles = []; let debugEnabled = false; +const memberOnlyIndex = new Map(); +const DATA_PREFIX = "chipperfluff-nobs"; /* ---------------- CSS injection ---------------- */ @@ -9,7 +12,7 @@ function injectStyleOnce() { const style = document.createElement("style"); style.id = "member-filter-style"; style.textContent = ` - [data-member-filter-hidden="true"] { + [data-${DATA_PREFIX}-hidden="true"] { display: none !important; } `; @@ -25,11 +28,43 @@ function debugLog(...args) { console.log("[MemberFilter]", ...args); } +function normalizeKey(value) { + return value + .toLowerCase() + .replace(/\u00a0/g, " ") + .replace(/\s+/g, " ") + .replace(/\u2022/g, "") + .trim(); +} + +function normalizeWhitelist(list) { + const names = []; + const handles = []; + (list || []).forEach(item => { + if (typeof item === "string") { + if (item) names.push(normalizeKey(item)); + return; + } + const name = normalizeKey(item.name || ""); + const handle = normalizeKey(item.handle || ""); + if (name) names.push(name); + if (handle) handles.push(handle); + }); + return { names, handles }; +} + function loadSettings() { chrome.storage.local.get({ whitelist: [], debug: false }, data => { - whitelist = data.whitelist.map(n => n.toLowerCase()); + const { names, handles } = normalizeWhitelist(data.whitelist); + whitelist = names; + whitelistHandles = handles; debugEnabled = Boolean(data.debug); - debugLog("settings loaded:", { whitelist, debugEnabled }); + debugLog("settings loaded:", { whitelist, whitelistHandles, debugEnabled }); + if (isWhitelistedChannelPage()) { + revealAll(); + return; + } + updateKnownVisibility(); process(); }); } @@ -41,56 +76,133 @@ chrome.storage.onChanged.addListener(loadSettings); /* ---------------- detection logic ---------------- */ -function isMemberOnly(video) { - const badges = video.querySelectorAll("badge-shape"); - for (const badge of badges) { - if (badge.textContent?.includes("Nur für Kanalmitglieder")) { - return true; - } - } - return false; -} - function process(root = document) { - const selector = - "ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer"; - const videos = root.querySelectorAll(selector); - const rootIsVideo = - root instanceof HTMLElement && root.matches && root.matches(selector); + if (isWhitelistedChannelPage()) return; + const badges = root.querySelectorAll("badge-shape"); - const allVideos = rootIsVideo ? [root, ...videos] : Array.from(videos); + badges.forEach(badge => { + if (!badge.textContent?.includes("Nur für Kanalmitglieder")) return; + + const video = badge.closest( + "ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer" + ); + + if (!video) return; - allVideos.forEach(video => { const titleEl = video.querySelector("#video-title"); const channelEl = video.querySelector("ytd-channel-name a") || video.querySelector(".ytd-channel-name a"); - const channel = channelEl?.textContent?.trim() ?? ""; - const channelKey = channel.toLowerCase(); + const title = titleEl?.textContent?.trim() ?? "(no title)"; + const url = titleEl?.href ?? ""; + const channel = channelEl?.textContent?.trim() ?? "(no channel)"; + const channelKey = normalizeKey(channel); + const id = getVideoId(video, url); + + video.setAttribute(`data-${DATA_PREFIX}-member-only`, "true"); + if (channelKey) video.setAttribute(`data-${DATA_PREFIX}-channel`, channelKey); + if (id) video.setAttribute(`data-${DATA_PREFIX}-id`, id); + + if (id) { + memberOnlyIndex.set(id, { channelKey, hidden: false }); + } + const whitelisted = whitelist.includes(channelKey); - const memberOnly = isMemberOnly(video); if (debugEnabled) { - const title = titleEl?.textContent?.trim() ?? "(no title)"; - const url = titleEl?.href ?? "(no url)"; console.group("[MemberFilter]"); console.log("Title :", title); - console.log("Channel:", channel || "(no channel)"); + console.log("Channel:", channel); console.log("URL :", url); - console.log("MemberOnly:", memberOnly); + console.log("ID :", id || "(no id)"); console.log("Whitelisted:", whitelisted); console.groupEnd(); } - if (memberOnly && !whitelisted) { - video.setAttribute("data-member-filter-hidden", "true"); + if (!whitelisted) { + video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true"); + if (id && memberOnlyIndex.has(id)) { + memberOnlyIndex.get(id).hidden = true; + } } else { - video.removeAttribute("data-member-filter-hidden"); + video.removeAttribute(`data-${DATA_PREFIX}-hidden`); + if (id && memberOnlyIndex.has(id)) { + memberOnlyIndex.get(id).hidden = false; + } } }); } +function updateKnownVisibility() { + if (isWhitelistedChannelPage()) { + revealAll(); + return; + } + for (const [id, meta] of memberOnlyIndex.entries()) { + if (!meta.channelKey) continue; + const video = findVideoById(id); + if (!video) { + memberOnlyIndex.delete(id); + continue; + } + if (whitelist.includes(meta.channelKey)) { + video.removeAttribute(`data-${DATA_PREFIX}-hidden`); + meta.hidden = false; + } else { + video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true"); + meta.hidden = true; + } + } +} + +function isWhitelistedChannelPage() { + const handle = getChannelHandleFromUrl(location.href); + if (!handle) return false; + return whitelistHandles.includes(normalizeKey(handle)); +} + +function getChannelHandleFromUrl(url) { + const match = url.match(/youtube\.com\/@([^/]+)/i); + return match ? match[1] : ""; +} + +function revealAll() { + const videos = document.querySelectorAll( + `[data-${DATA_PREFIX}-hidden="true"]` + ); + videos.forEach(video => video.removeAttribute(`data-${DATA_PREFIX}-hidden`)); +} + +function getVideoId(video, url) { + const dataId = video.getAttribute("data-video-id"); + if (dataId) return dataId; + if (url) return url; + const fallbackId = video.id; + return fallbackId || ""; +} + +function getChannelKey(video) { + const stored = video.getAttribute(`data-${DATA_PREFIX}-channel`); + if (stored) return stored; + + const channelEl = + video.querySelector("ytd-channel-name a") || + video.querySelector(".ytd-channel-name a"); + const channel = channelEl?.textContent?.trim() ?? ""; + return channel ? normalizeKey(channel) : ""; +} + +function findVideoById(id) { + const escaped = cssEscape(id); + return document.querySelector(`[data-${DATA_PREFIX}-id="${escaped}"]`); +} + +function cssEscape(value) { + if (window.CSS && CSS.escape) return CSS.escape(value); + return String(value).replace(/["\\]/g, "\\$&"); +} + /* ---------------- initial + observer ---------------- */ process(); diff --git a/manifest.json b/manifest.json index bad29fd..58ad90b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "manifest_version": 3, "name": "ANTI-BS", - "description": "for to long youtubers thought we are stupid, now we just ignore them. let them know we are not.", + "description": "for too long, lo hark, the youtube prophets spake unto us like we had no braincells, amen. now we simply ignore them, sip our metrics, and keep shipping. we were never stupid, just allergic to bad takes. from a squirrel, soft, smug, and feral, for other squirrels. the rest can keep yelling at thumbnails. pft. uwu.", "version": "0.1.0", "author": "Chipperfluff (Jack)", - "repo": "", + "repo": "", "org": "Chipperfluff", "permissions": ["storage"], diff --git a/popup.html b/popup.html index 038308b..64b3a69 100644 --- a/popup.html +++ b/popup.html @@ -10,6 +10,9 @@
+
+
+
diff --git a/popup.js b/popup.js index effdd58..dccebf4 100644 --- a/popup.js +++ b/popup.js @@ -1,4 +1,5 @@ const input = document.getElementById("channelInput"); +const linkInput = document.getElementById("channelLinkInput"); const addBtn = document.getElementById("addBtn"); const list = document.getElementById("channelList"); const debugToggle = document.getElementById("debugToggle"); @@ -13,9 +14,13 @@ function loadChannels() { chrome.storage.local.get(DEFAULTS, ({ whitelist, debug }) => { list.innerHTML = ""; debugToggle.checked = Boolean(debug); - whitelist.forEach((name, index) => { + const entries = normalizeWhitelist(whitelist); + entries.forEach((entry, index) => { const li = document.createElement("li"); - li.textContent = name; + const text = entry.handle + ? `${entry.name} (@${entry.handle})` + : entry.name; + li.textContent = text; const remove = document.createElement("button"); remove.textContent = "x"; @@ -29,20 +34,35 @@ function loadChannels() { function addChannel() { const name = input.value.trim(); - if (!name) return; + const link = linkInput.value.trim(); + if (!name || !link) return; + const handle = normalizeHandle(link); + if (!handle) return; chrome.storage.local.get(DEFAULTS, ({ whitelist }) => { - whitelist.push(name); - chrome.storage.local.set({ whitelist }, loadChannels); + const entries = normalizeWhitelist(whitelist); + entries.push({ name, handle }); + chrome.storage.local.set({ whitelist: entries }, loadChannels); }); input.value = ""; + linkInput.value = ""; +} + +function normalizeWhitelist(whitelist) { + return (whitelist || []).map(item => { + if (typeof item === "string") { + return { name: item, handle: "" }; + } + return { name: item.name || "", handle: item.handle || "" }; + }); } function removeChannel(index) { chrome.storage.local.get(DEFAULTS, ({ whitelist }) => { - whitelist.splice(index, 1); - chrome.storage.local.set({ whitelist }, loadChannels); + const entries = normalizeWhitelist(whitelist); + entries.splice(index, 1); + chrome.storage.local.set({ whitelist: entries }, loadChannels); }); } @@ -54,6 +74,15 @@ function resetDefaults() { chrome.storage.local.set(DEFAULTS, loadChannels); } +function normalizeHandle(value) { + const atMatch = value.match(/@([^/?#]+)/); + if (atMatch) return atMatch[1].trim(); + const clean = value.replace(/^https?:\/\//i, "").trim(); + const urlMatch = clean.match(/youtube\.com\/@([^/?#]+)/i); + if (urlMatch) return urlMatch[1].trim(); + return ""; +} + addBtn.onclick = addChannel; debugToggle.onchange = e => setDebug(e.target.checked); resetBtn.onclick = resetDefaults;