-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.js
More file actions
393 lines (328 loc) · 16.1 KB
/
content.js
File metadata and controls
393 lines (328 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
// ==UserScript==
// @name Channel Blocker v1.2 (Bugs Fixed)
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Blocks YouTube Shorts channels based on a simple user-managed list. Fixes gray overlay and double skip issues.
// @author UserScript Author
// @match https://www.youtube.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
const CONFIG = {
SAVE_DEBOUNCE_MS: 5000,
STORAGE_KEY: 'ChannelBlockerList'
};
// --- DATA STRUCTURES ---
let BlockedChannelsWithTime = []; // Array of { name: string, blockedAt: number }
let BlockedChannelNamesFastLookup = new Set();
let saveTimer = null;
// --- STATE ---
let activeChannelNameRaw = "";
let userHasInteracted = false;
let isCurrentChannelBlocked = false;
// --- UTILS ---
function triggerSkipToNextVideo() {
const nextBtn = document.querySelector('[aria-label="Next video"]');
if (nextBtn) {
nextBtn.click();
} else {
const event = new KeyboardEvent('keydown', {
key: 'ArrowDown', code: 'ArrowDown', keyCode: 40, bubbles: true, cancelable: true
});
document.dispatchEvent(event);
}
}
// --- PERSISTENCE ---
function loadBlockList() {
try {
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
chrome.storage.local.get([CONFIG.STORAGE_KEY], (result) => {
if (result[CONFIG.STORAGE_KEY] && Array.isArray(result[CONFIG.STORAGE_KEY])) {
BlockedChannelsWithTime = result[CONFIG.STORAGE_KEY];
BlockedChannelNamesFastLookup = new Set(BlockedChannelsWithTime.map(item => item.name));
}
});
}
} catch (e) { /* Storage access failed silently */ }
}
function scheduleSave() {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
try {
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
chrome.storage.local.set({ [CONFIG.STORAGE_KEY]: BlockedChannelsWithTime });
}
} catch (e) { /* Save failed silently */ }
}, CONFIG.SAVE_DEBOUNCE_MS);
}
// --- FEEDBACK & UI ACTIONS ---
function flashFeedback(text) {
const el = document.getElementById('blocker-feedback');
if (el) {
el.innerText = text;
el.style.opacity = '1';
setTimeout(() => {
el.style.opacity = '0';
setTimeout(() => updateOverlay(activeChannelNameRaw, isCurrentChannelBlocked), 100);
}, 1200);
}
}
// --- BLOCK LIST CORE LOGIC ---
function isChannelNameBlocked(channelName) {
if (!channelName) return false;
return BlockedChannelNamesFastLookup.has(channelName.toLowerCase());
}
function handleBlockCurrentChannel() {
if (!activeChannelNameRaw) {
flashFeedback("Error: No channel detected.");
return;
}
const channelKeyLowercase = activeChannelNameRaw.toLowerCase();
let feedbackMessage = "Already Blocked.";
if (!BlockedChannelNamesFastLookup.has(channelKeyLowercase)) {
const newItem = { name: channelKeyLowercase, blockedAt: Date.now() };
BlockedChannelsWithTime.push(newItem);
BlockedChannelNamesFastLookup.add(channelKeyLowercase);
feedbackMessage = `Blocked: ${activeChannelNameRaw}`;
isCurrentChannelBlocked = true;
// ⭐️ FIX: Only trigger skip here, immediately after blocking.
setTimeout(triggerSkipToNextVideo, 400);
}
scheduleSave();
flashFeedback(feedbackMessage);
checkActiveVideoAndFilter(); // Re-check to update the UI button status
}
function unblockChannel(channelName) {
const channelKeyLowercase = channelName.toLowerCase();
if (BlockedChannelNamesFastLookup.delete(channelKeyLowercase)) {
BlockedChannelsWithTime = BlockedChannelsWithTime.filter(item => item.name !== channelKeyLowercase);
scheduleSave();
if (activeChannelNameRaw.toLowerCase() === channelKeyLowercase) {
isCurrentChannelBlocked = false;
checkActiveVideoAndFilter();
}
return true;
}
return false;
}
// --- MANAGEMENT MODAL ---
function showManagementModal() {
const existing = document.getElementById('blocker-list-modal');
if (existing) {
existing.remove();
return;
}
const sortedList = [...BlockedChannelsWithTime].sort((a, b) => b.blockedAt - a.blockedAt);
const modalElement = document.createElement('div');
modalElement.id = 'blocker-list-modal';
const baseStyle = `
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 350px; max-height: 80vh; background: rgba(10, 10, 10, 0.98);
border: 1px solid #444; border-radius: 12px; z-index: 100000;
color: #fff; font-family: sans-serif; display: flex; flex-direction: column;
box-shadow: 0 0 50px rgba(0,0,0,0.8); backdrop-filter: blur(10px);
`;
modalElement.style.cssText = baseStyle;
const listContent = sortedList.map(item => {
const dateStr = new Date(item.blockedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
return `
<div class="list-item" data-channel="${item.name}" style="display:flex; justify-content:space-between; align-items:center; padding:6px 12px; border-bottom:1px solid #222;">
<span style="font-size: 13px; color: #a855f7;">@${item.name}</span>
<div style="display:flex; align-items:center;">
<span style="font-size: 10px; color: #6b7280; margin-right: 10px;">${dateStr}</span>
<button class="btn-unblock" data-channel="${item.name}" style="background:#060; border:none; color:#fff; cursor:pointer; padding: 4px 8px; border-radius: 4px; font-size: 10px; font-weight: bold;">
Re-Allow
</button>
</div>
</div>
`;
}).join('');
const emptyState = '<div style="padding:20px; text-align:center; color:#555;">No channels currently blocked.</div>';
modalElement.innerHTML = `
<div style="padding: 12px; border-bottom: 1px solid #333; display: flex; justify-content: space-between; align-items: center; background: #111; border-radius: 12px 12px 0 0;">
<span style="font-weight: bold; color: #4ade80; font-size: 14px;">Blocked Channel List (${BlockedChannelsWithTime.length})</span>
<button id="btn-close-list" style="background:none; border:none; color:#fff; cursor:pointer; font-size:16px;">✕</button>
</div>
<div id="list-content" style="overflow-y: auto; padding: 0; font-size: 12px; scrollbar-width: thin; scrollbar-color: #444 #111;">
${sortedList.length > 0 ? listContent : emptyState}
</div>
`;
document.body.appendChild(modalElement);
document.getElementById('btn-close-list').onclick = () => modalElement.remove();
modalElement.querySelectorAll('.btn-unblock').forEach(button => {
button.onclick = (e) => {
const channelToUnblock = e.target.getAttribute('data-channel');
if (unblockChannel(channelToUnblock)) {
e.target.closest('.list-item').remove();
const header = modalElement.querySelector('span');
if (header) header.textContent = `Blocked Channel List (${BlockedChannelsWithTime.length})`;
if (BlockedChannelsWithTime.length === 0) {
document.getElementById('list-content').innerHTML = emptyState;
}
}
};
});
}
// --- UI INJECTION ---
function createOverlay() {
const OVERLAY_ID = 'channel-blocker-overlay';
const existing = document.getElementById(OVERLAY_ID);
if (existing) return existing;
const overlayElement = document.createElement('div');
overlayElement.id = OVERLAY_ID;
const overlayStyle = `
position: fixed; top: 70px; right: 20px; z-index: 99999;
background: rgba(10, 10, 10, 0.95); border: 1px solid #333;
color: #fff; padding: 12px; border-radius: 12px;
font-family: 'Roboto', sans-serif; width: 220px;
backdrop-filter: blur(4px); box-shadow: 0 10px 25px rgba(0,0,0,0.5);
`;
overlayElement.style.cssText = overlayStyle;
const CSS_STYLES = `
.block-btn {
color: #fff;
border: 1px solid;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: transform 0.1s, background 0.2s, border-color 0.2s, opacity 0.2s;
flex-shrink: 0;
width: 28px;
height: 28px;
padding: 0;
font-size: 14px;
margin-right: 6px;
}
.block-btn:active { transform: scale(0.95); }
`;
// Inject styles
if (!document.getElementById('blocker-styles')) {
const style = document.createElement('style');
style.id = 'blocker-styles';
style.innerHTML = CSS_STYLES;
document.head.appendChild(style);
}
overlayElement.innerHTML = `
<div id="blocker-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<div style="font-size: 10px; color: #888; letter-spacing: 1px; font-weight:700;">CHANNEL FILTER</div>
<button id="btn-list-manage" title="Manage Block List" style="background:none; border:none; color:#a855f7; cursor:pointer; font-size:14px; padding:0;">
📋
</button>
</div>
<div id="blocker-body">
<div style="display: flex; align-items: center; justify-content: flex-start;">
<button id="btn-block-action" class="block-btn">
⛔
</button>
<div id="channel-display" style="font-size: 14px; color: #a855f7; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: calc(100% - 34px);">
Fetching Channel...
</div>
</div>
</div>
<div id="blocker-feedback" style="position: absolute; inset:0; background: rgba(0,0,0,0.9); display: flex; align-items: center; justify-content: center; font-weight: bold; border-radius: 12px; opacity: 0; pointer-events: none; transition: opacity 0.2s; text-align: center; padding: 10px; font-size: 12px; z-index:2;"></div>
`;
document.body.appendChild(overlayElement);
// Handlers
document.getElementById('btn-block-action').onclick = (e) => {
e.stopPropagation();
handleBlockCurrentChannel();
};
document.getElementById('btn-list-manage').onclick = (e) => {
e.stopPropagation();
showManagementModal();
};
return overlayElement;
}
// --- UPDATE OVERLAY (Visual State Changes) ---
function updateOverlay(channelName, isBlocked) {
const overlayElement = createOverlay();
const channelDisplayElement = document.getElementById('channel-display');
if (channelDisplayElement) channelDisplayElement.textContent = channelName ? `@${channelName}` : "Unknown Channel";
const blockButton = document.getElementById('btn-block-action');
if (isBlocked) {
// Blocked State: Button muted, Channel name red
blockButton.style.opacity = '0.7';
blockButton.style.pointerEvents = 'none';
blockButton.style.backgroundColor = '#600';
blockButton.style.borderColor = '#f87171';
channelDisplayElement.style.color = '#f87171';
} else {
// Allowed State: Button active, Channel name original color
blockButton.style.opacity = '1';
blockButton.style.pointerEvents = 'auto';
blockButton.style.backgroundColor = '#222';
blockButton.style.borderColor = '#444';
channelDisplayElement.style.color = '#a855f7';
}
}
// --- YOUTUBE CHANNEL NAME FETCHING ---
function getActiveChannelName() {
const activeRenderer = document.querySelector('ytd-reel-video-renderer[is-active]') || document.querySelector('ytd-shorts-player-page') || document;
if (!activeRenderer) return null;
let channel = "";
// Selector chain for robustness across different layouts
let channelElement = activeRenderer.querySelector('a.yt-core-attributed-string__link[href*="/shorts"]')
|| activeRenderer.querySelector('#channel-info .yt-core-attributed-string')
|| activeRenderer.querySelector('.ytd-channel-name yt-formatted-string')
|| activeRenderer.querySelector('ytd-video-owner-renderer a#owner-text');
if (channelElement) channel = channelElement.innerText.trim().replace(/^@/, '');
// Check for sponsored/ad content
const badges = Array.from(activeRenderer.querySelectorAll('.yt-badge-shape__text'));
if (badges.some(el => el.innerText.trim() === 'Sponsored')) return { isSponsored: true };
return { channel: channel || "Unknown", isSponsored: false, el: activeRenderer };
}
// --- ENGINE ---
function checkActiveVideoAndFilter() {
const videoData = getActiveChannelName();
if (!videoData) return;
// Skip ads and reset interaction status
if (videoData.isSponsored) {
updateOverlay("AD", false);
if (!userHasInteracted) {
userHasInteracted = true;
setTimeout(triggerSkipToNextVideo, 400);
}
return;
}
const newChannelName = videoData.channel;
if (newChannelName !== activeChannelNameRaw) {
activeChannelNameRaw = newChannelName;
userHasInteracted = false;
isCurrentChannelBlocked = isChannelNameBlocked(activeChannelNameRaw);
}
updateOverlay(activeChannelNameRaw, isCurrentChannelBlocked);
if (isCurrentChannelBlocked && !userHasInteracted) {
userHasInteracted = true;
triggerSkipToNextVideo();
}
}
function startEngine() {
loadBlockList();
createOverlay();
const observer = new MutationObserver((mutations) => {
let shouldCheck = false;
for (const m of mutations) {
if (m.type === 'attributes' && m.attributeName === 'is-active') {
shouldCheck = true;
break;
}
if (m.type === 'childList' && m.addedNodes.length > 0) {
shouldCheck = true;
}
}
if (shouldCheck) checkActiveVideoAndFilter();
});
const targetNode = document.getElementById('shorts-inner-container') || document.body;
observer.observe(targetNode, {
attributes: true, subtree: true, childList: true, attributeFilter: ['is-active']
});
setInterval(checkActiveVideoAndFilter, 300);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startEngine);
} else {
startEngine();
}
})();