1
0

Compare commits

..

5 Commits

11 changed files with 105 additions and 23 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist/

22
build.sh Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT_DIR="${ROOT_DIR}/dist"
OUT_FILE="${OUT_DIR}/anti-bs-extension.zip"
mkdir -p "${OUT_DIR}"
(
cd "${ROOT_DIR}"
zip -r "${OUT_FILE}" \
manifest.json \
content.js \
popup.html \
popup.js \
popup.css \
icons \
README.md
)
echo "Built: ${OUT_FILE}"

View File

@ -1,11 +1,15 @@
// Current settings snapshot.
let whitelist = []; let whitelist = [];
let whitelistHandles = []; let whitelistHandles = [];
let debugEnabled = false; let debugEnabled = false;
const memberOnlyIndex = new Map(); // In-memory index of known member-only videos.
const DATA_PREFIX = "chipperfluff-nobs"; const memberOnlyIndex = new Map(); // id -> { channelKey, hidden }
// Data-* attribute prefix for DOM tags.
const DATA_PREFIX = "chipperfluff-nobs"; // nobs = "no-bs" (member-only videos)
/* ---------------- CSS injection ---------------- */ /* ---------------- CSS injection ---------------- */
// Inject a single style tag for hiding matched videos.
function injectStyleOnce() { function injectStyleOnce() {
if (document.getElementById("member-filter-style")) return; if (document.getElementById("member-filter-style")) return;
@ -23,11 +27,23 @@ injectStyleOnce();
/* ---------------- load whitelist ---------------- */ /* ---------------- load whitelist ---------------- */
// Debug helpers (no output unless debug is enabled).
function debugLog(...args) { function debugLog(...args) {
if (!debugEnabled) return; if (!debugEnabled) return;
console.log("[MemberFilter]", ...args); console.log("[MemberFilter]", ...args);
} }
function debugGroup(...args) {
if (!debugEnabled) return;
console.group("[MemberFilter]", ...args);
}
function debugGroupEnd() {
if (!debugEnabled) return;
console.groupEnd();
}
// Normalize strings so matching is consistent.
function normalizeKey(value) { function normalizeKey(value) {
return value return value
.toLowerCase() .toLowerCase()
@ -37,6 +53,7 @@ function normalizeKey(value) {
.trim(); .trim();
} }
// Support legacy strings and new { name, handle } entries.
function normalizeWhitelist(list) { function normalizeWhitelist(list) {
const names = []; const names = [];
const handles = []; const handles = [];
@ -53,6 +70,7 @@ function normalizeWhitelist(list) {
return { names, handles }; return { names, handles };
} }
// Read settings and re-apply visibility immediately.
function loadSettings() { function loadSettings() {
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);
@ -71,11 +89,12 @@ function loadSettings() {
loadSettings(); loadSettings();
// Reload settings if popup changes them // Reload settings if popup changes them.
chrome.storage.onChanged.addListener(loadSettings); chrome.storage.onChanged.addListener(loadSettings);
/* ---------------- detection logic ---------------- */ /* ---------------- detection logic ---------------- */
// Detect member-only videos and tag them for future updates.
function process(root = document) { function process(root = document) {
if (isWhitelistedChannelPage()) return; if (isWhitelistedChannelPage()) return;
const badges = root.querySelectorAll("badge-shape"); const badges = root.querySelectorAll("badge-shape");
@ -110,15 +129,13 @@ function process(root = document) {
const whitelisted = whitelist.includes(channelKey); const whitelisted = whitelist.includes(channelKey);
if (debugEnabled) { debugGroup();
console.group("[MemberFilter]"); debugLog("Title :", title);
console.log("Title :", title); debugLog("Channel:", channel);
console.log("Channel:", channel); debugLog("URL :", url);
console.log("URL :", url); debugLog("ID :", id || "(no id)");
console.log("ID :", id || "(no id)"); debugLog("Whitelisted:", whitelisted);
console.log("Whitelisted:", whitelisted); debugGroupEnd();
console.groupEnd();
}
if (!whitelisted) { if (!whitelisted) {
video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true"); video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true");
@ -134,6 +151,7 @@ function process(root = document) {
}); });
} }
// Apply whitelist to already-tagged member-only videos.
function updateKnownVisibility() { function updateKnownVisibility() {
if (isWhitelistedChannelPage()) { if (isWhitelistedChannelPage()) {
revealAll(); revealAll();
@ -156,17 +174,20 @@ function updateKnownVisibility() {
} }
} }
// Allow full visibility on whitelisted channel pages.
function isWhitelistedChannelPage() { function isWhitelistedChannelPage() {
const handle = getChannelHandleFromUrl(location.href); const handle = getChannelHandleFromUrl(location.href);
if (!handle) return false; if (!handle) return false;
return whitelistHandles.includes(normalizeKey(handle)); return whitelistHandles.includes(normalizeKey(handle));
} }
// Extract @handle from a channel URL.
function getChannelHandleFromUrl(url) { function getChannelHandleFromUrl(url) {
const match = url.match(/youtube\.com\/@([^/]+)/i); const match = url.match(/youtube\.com\/@([^/]+)/i);
return match ? match[1] : ""; return match ? match[1] : "";
} }
// Remove hidden flags from any tagged videos.
function revealAll() { function revealAll() {
const videos = document.querySelectorAll( const videos = document.querySelectorAll(
`[data-${DATA_PREFIX}-hidden="true"]` `[data-${DATA_PREFIX}-hidden="true"]`
@ -174,6 +195,7 @@ function revealAll() {
videos.forEach(video => video.removeAttribute(`data-${DATA_PREFIX}-hidden`)); videos.forEach(video => video.removeAttribute(`data-${DATA_PREFIX}-hidden`));
} }
// Choose a stable identifier for indexing.
function getVideoId(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;
@ -182,6 +204,7 @@ function getVideoId(video, url) {
return fallbackId || ""; return fallbackId || "";
} }
// 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;
@ -193,11 +216,13 @@ function getChannelKey(video) {
return channel ? normalizeKey(channel) : ""; return channel ? normalizeKey(channel) : "";
} }
// Find a video element by stored id.
function findVideoById(id) { function findVideoById(id) {
const escaped = cssEscape(id); const escaped = cssEscape(id);
return document.querySelector(`[data-${DATA_PREFIX}-id="${escaped}"]`); return document.querySelector(`[data-${DATA_PREFIX}-id="${escaped}"]`);
} }
// Safe attribute selector escaping.
function cssEscape(value) { function cssEscape(value) {
if (window.CSS && CSS.escape) return CSS.escape(value); if (window.CSS && CSS.escape) return CSS.escape(value);
return String(value).replace(/["\\]/g, "\\$&"); return String(value).replace(/["\\]/g, "\\$&");
@ -207,6 +232,7 @@ function cssEscape(value) {
process(); process();
// Watch for new items so member-only tags are applied as they load.
const observer = new MutationObserver(mutations => { const observer = new MutationObserver(mutations => {
for (const m of mutations) { for (const m of mutations) {
for (const node of m.addedNodes) { for (const node of m.addedNodes) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,11 +1,8 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "ANTI-BS", "name": "ANTI-BS",
"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", "version": "0.1.0",
"author": "Chipperfluff (Jack)", "description": "Hide YouTube member-only videos by default, with support for a configurable channel whitelist. Runs locally with optional debug logging. Created by a squirrel-themed independent developer. :3",
"repo": "https://git.chipperfluff.at/projects/ANTI-BS",
"org": "Chipperfluff",
"permissions": ["storage"], "permissions": ["storage"],
"host_permissions": ["https://www.youtube.com/*"], "host_permissions": ["https://www.youtube.com/*"],
@ -23,18 +20,14 @@
"16": "icons/anti-bs-icon-16.png", "16": "icons/anti-bs-icon-16.png",
"32": "icons/anti-bs-icon-32.png", "32": "icons/anti-bs-icon-32.png",
"48": "icons/anti-bs-icon-48.png", "48": "icons/anti-bs-icon-48.png",
"64": "icons/anti-bs-icon-64.png",
"128": "icons/anti-bs-icon-128.png" "128": "icons/anti-bs-icon-128.png"
} }
}, },
"icons": { "icons": {
"16": "icons/anti-bs-icon-16.png", "16": "icons/anti-bs-icon-16.png",
"32": "icons/anti-bs-icon-32.png", "32": "icons/anti-bs-icon-32.png",
"48": "icons/anti-bs-icon-48.png", "48": "icons/anti-bs-icon-48.png",
"64": "icons/anti-bs-icon-64.png", "128": "icons/anti-bs-icon-128.png"
"128": "icons/anti-bs-icon-128.png",
"256": "icons/anti-bs-icon-256.png",
"512": "icons/anti-bs-icon-512.png",
"1024": "icons/anti-bs-icon-1024.png"
} }
} }

View File

@ -1,3 +1,4 @@
/* Base layout */
body { body {
font-family: "Trebuchet MS", "Verdana", sans-serif; font-family: "Trebuchet MS", "Verdana", sans-serif;
min-width: 260px; min-width: 260px;
@ -7,6 +8,7 @@ body {
color: #2b2014; color: #2b2014;
} }
/* Title */
h3 { h3 {
margin: 0 0 10px; margin: 0 0 10px;
letter-spacing: 0.4px; letter-spacing: 0.4px;
@ -14,6 +16,18 @@ h3 {
font-size: 13px; font-size: 13px;
} }
.subtext {
margin: 0 0 10px;
font-size: 11px;
line-height: 1.3;
color: #5b4a33;
}
.subtext.subtext-debug {
margin: 6px 0 8px;
}
/* Form rows */
.row { .row {
display: flex; display: flex;
gap: 6px; gap: 6px;
@ -25,6 +39,7 @@ h3 {
justify-content: space-between; justify-content: space-between;
} }
/* Inputs */
input { input {
flex: 1; flex: 1;
padding: 6px 8px; padding: 6px 8px;
@ -34,6 +49,7 @@ input {
font-size: 12px; font-size: 12px;
} }
/* Buttons */
button { button {
padding: 6px 10px; padding: 6px 10px;
border: 1px solid #b2985b; border: 1px solid #b2985b;
@ -53,12 +69,14 @@ button:hover {
filter: brightness(0.95); filter: brightness(0.95);
} }
/* List */
ul { ul {
padding-left: 0; padding-left: 0;
margin: 0; margin: 0;
list-style: none; list-style: none;
} }
/* List rows */
li { li {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -77,6 +95,7 @@ li button {
background: #f2a285; background: #f2a285;
} }
/* Debug toggle row */
.toggle { .toggle {
display: flex; display: flex;
gap: 6px; gap: 6px;

View File

@ -6,8 +6,14 @@
<link rel="stylesheet" href="popup.css" /> <link rel="stylesheet" href="popup.css" />
</head> </head>
<body> <body>
<!-- Title + short description -->
<h3>Channel Whitelist</h3> <h3>Channel Whitelist</h3>
<p class="subtext">
Hide member-only videos by default. Whitelisted creators stay visible.
Add the channel name and its @handle or link.
</p>
<!-- Inputs -->
<div class="row"> <div class="row">
<input id="channelInput" placeholder="Channel name" /> <input id="channelInput" placeholder="Channel name" />
</div> </div>
@ -16,14 +22,19 @@
<button id="addBtn">Add</button> <button id="addBtn">Add</button>
</div> </div>
<!-- Settings -->
<div class="row settings"> <div class="row settings">
<label class="toggle"> <label class="toggle">
<input id="debugToggle" type="checkbox" /> <input id="debugToggle" type="checkbox" />
<span>Debug</span> <span>Debug logging</span>
</label> </label>
<button id="resetBtn" class="ghost">Reset defaults</button> <button id="resetBtn" class="ghost">Reset defaults</button>
</div> </div>
<p class="subtext subtext-debug">
Debug logs will appear in the console only when enabled.
</p>
<!-- Whitelist list -->
<ul id="channelList"></ul> <ul id="channelList"></ul>
<script src="popup.js"></script> <script src="popup.js"></script>

View File

@ -1,3 +1,4 @@
// UI elements.
const input = document.getElementById("channelInput"); const input = document.getElementById("channelInput");
const linkInput = document.getElementById("channelLinkInput"); const linkInput = document.getElementById("channelLinkInput");
const addBtn = document.getElementById("addBtn"); const addBtn = document.getElementById("addBtn");
@ -5,11 +6,13 @@ const list = document.getElementById("channelList");
const debugToggle = document.getElementById("debugToggle"); const debugToggle = document.getElementById("debugToggle");
const resetBtn = document.getElementById("resetBtn"); const resetBtn = document.getElementById("resetBtn");
// Default settings payload.
const DEFAULTS = { const DEFAULTS = {
whitelist: [], whitelist: [],
debug: false debug: false
}; };
// Render whitelist and debug state.
function loadChannels() { function loadChannels() {
chrome.storage.local.get(DEFAULTS, ({ whitelist, debug }) => { chrome.storage.local.get(DEFAULTS, ({ whitelist, debug }) => {
list.innerHTML = ""; list.innerHTML = "";
@ -32,6 +35,7 @@ function loadChannels() {
}); });
} }
// Add a new whitelist entry with name + handle.
function addChannel() { function addChannel() {
const name = input.value.trim(); const name = input.value.trim();
const link = linkInput.value.trim(); const link = linkInput.value.trim();
@ -49,6 +53,7 @@ function addChannel() {
linkInput.value = ""; linkInput.value = "";
} }
// Normalize storage entries to { name, handle } objects.
function normalizeWhitelist(whitelist) { function normalizeWhitelist(whitelist) {
return (whitelist || []).map(item => { return (whitelist || []).map(item => {
if (typeof item === "string") { if (typeof item === "string") {
@ -58,6 +63,7 @@ function normalizeWhitelist(whitelist) {
}); });
} }
// Remove a whitelist entry by index.
function removeChannel(index) { function removeChannel(index) {
chrome.storage.local.get(DEFAULTS, ({ whitelist }) => { chrome.storage.local.get(DEFAULTS, ({ whitelist }) => {
const entries = normalizeWhitelist(whitelist); const entries = normalizeWhitelist(whitelist);
@ -66,14 +72,17 @@ function removeChannel(index) {
}); });
} }
// Persist debug toggle.
function setDebug(enabled) { function setDebug(enabled) {
chrome.storage.local.set({ debug: Boolean(enabled) }); chrome.storage.local.set({ debug: Boolean(enabled) });
} }
// Reset all settings to defaults.
function resetDefaults() { function resetDefaults() {
chrome.storage.local.set(DEFAULTS, loadChannels); chrome.storage.local.set(DEFAULTS, loadChannels);
} }
// Accept @handle or full channel URL.
function normalizeHandle(value) { function normalizeHandle(value) {
const atMatch = value.match(/@([^/?#]+)/); const atMatch = value.match(/@([^/?#]+)/);
if (atMatch) return atMatch[1].trim(); if (atMatch) return atMatch[1].trim();
@ -83,6 +92,7 @@ function normalizeHandle(value) {
return ""; return "";
} }
// Wire UI events.
addBtn.onclick = addChannel; addBtn.onclick = addChannel;
debugToggle.onchange = e => setDebug(e.target.checked); debugToggle.onchange = e => setDebug(e.target.checked);
resetBtn.onclick = resetDefaults; resetBtn.onclick = resetDefaults;