Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 1.3.0 (2026-11-11)

- Twitch: GTA streams are now marked red instead of hidden, and opening one shows a warning before the stream plays ("Go back" or "Watch anyway"). After "Watch anyway" the stream keeps a red frame.
- New setting "Twitch": mark red and warn (default) or remove GTA streams completely (previous behaviour).

## 1.2.0 (2026-11-11)

- The extension's own GitHub repository is never blocked (hardcoded): pages, issues, raw files, zip and release downloads.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Everything runs locally in your browser. No tracking, no analytics, no remote co
| **Before they load** | Any GTA search (`gta 6`, `grand theft auto v`, `vice city`, `#gtaonline`...) on Google, Bing, DuckDuckGo, YouTube, Instagram, X etc., GTA hashtag pages, `rockstargames.com/VI` and other GTA pages on Rockstar's site, GTA fan sites (GTAForums, GTABase, the GTA wiki...) and embedded GTA iframes are stopped at the network level. |
| **Search results** | Google, Bing, DuckDuckGo, Brave, Ecosia, Yahoo result cards. |
| **Videos** | YouTube home, search, sidebar, Shorts shelves, comments and community posts; TikTok cards. |
| **Twitch** | Streams in any Grand Theft Auto category (GTA VI, V, IV, San Andreas, Vice City...) are hidden from the sidebar, front page and directory, and a channel page streaming GTA is blocked. If a streamer switches to GTA mid-stream, the stream is blocked right away. |
| **Twitch** | Streams in any Grand Theft Auto category (GTA VI, V, IV, San Andreas, Vice City...) are marked red in the sidebar, front page and directory, and opening one shows a warning before the stream plays (go back or watch anyway). Also when a streamer switches to GTA mid-stream. In the settings you can instead remove GTA streams completely. |
| **Swipe feeds** | In the YouTube Shorts player, Instagram Reels, TikTok and Facebook Reels, a GTA video is covered with a block screen and muted, and you can keep scrolling to the next one. |
| **Social posts** | Reddit, X/Twitter, Bluesky, Mastodon, Facebook and Instagram posts, captions, hashtags, grid tiles and comments. |
| **Everything else** | Any element on any site whose text, link, image `alt`, `title` or `aria-label` matches a keyword, including content loaded later by infinite scroll. |
Expand All @@ -80,6 +80,7 @@ Default keywords: `GTA`, `GTA 6`, `GTA VI`, `GTA 5`, `GTA V`, `GTA Online`, `Gra
- "Block on this site" switch to allow a site
- Badge counter showing how many items were hidden on the current tab
- Editable keyword list and allowlist in the settings page
- Twitch setting: mark GTA streams red and warn (default), or remove them completely
- This project's own GitHub repository (`github.com/Backware/gta6-blocker`, raw files, zip and release downloads) is never blocked, so you can always read the docs, report issues and update
- No tracking, no network calls, no remote code. Settings stay in your browser.

Expand Down
Binary file added docs/screenshots/twitch-red.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/twitch-warning.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gta6-blocker",
"version": "1.2.0",
"version": "1.3.0",
"private": true,
"description": "Browser extension that blocks GTA 6 / Grand Theft Auto VI content in Chrome and Safari.",
"license": "MIT",
Expand Down
34 changes: 34 additions & 0 deletions src/content.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,37 @@ html[data-gta6-page-blocked] body {
font: 600 16px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
visibility: visible !important;
}

/* Twitch, "mark red and warn" mode: GTA streams stay visible but are
clearly marked, and opening one shows a warning first. */
[data-gta6-flagged] {
position: relative !important;
outline: 3px solid #e5484d !important;
outline-offset: -3px;
background-color: rgba(229, 72, 77, 0.16) !important;
border-radius: 6px;
}
[data-gta6-flagged]::before {
content: "GTA";
position: absolute;
top: 6px;
right: 6px;
z-index: 10;
padding: 2px 6px;
border-radius: 4px;
background: #e5484d;
color: #fff;
font: 700 11px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
letter-spacing: .04em;
pointer-events: none;
}

/* A GTA stream the user chose to watch anyway: red frame around the page. */
html[data-gta6-gta-stream]::after {
content: "";
position: fixed;
inset: 0;
z-index: 2147483645;
border: 5px solid #e5484d;
pointer-events: none;
}
162 changes: 157 additions & 5 deletions src/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
'div[data-e2e="recommend-list-item-container"]', 'article[data-e2e="recommend-list-item-container"]',
'div[data-e2e="feed-video"]', 'section[data-e2e="feed-video"]'
].join(',');
const BLOCKED_SEL = '[data-gta6-blocked],[data-gta6-covered]';
const BLOCKED_SEL = '[data-gta6-blocked],[data-gta6-covered],[data-gta6-flagged]';

function isSnapFeed() {
const h = location.hostname;
Expand Down Expand Up @@ -122,6 +122,11 @@
// True on the extension's own GitHub repo, which is never blocked (see
// isProjectUrl in shared.js). Re-checked on every SPA navigation.
let exempt = false;
// Twitch: 'warn' marks GTA streams red and warns before one is opened,
// 'hide' removes them completely (settings page).
let twitchMode = 'warn';
let warnHost = null;
let warnTimer = null;
let blockedCount = 0;
let pageBlocked = false;
let overlayHost = null;
Expand Down Expand Up @@ -228,6 +233,14 @@

function hide(el) {
if (!el || el.nodeType !== 1 || el.closest(BLOCKED_SEL) || isEditable(el)) return;
if (isTwitch() && twitchMode === 'warn') {
const target = containerFor(el);
if (!target || target.contains(warnHost)) return;
target.setAttribute('data-gta6-flagged', '');
blockedCount++;
reportCount();
return;
}
if (isSnapFeed()) {
const player = playerFor(el);
if (player) {
Expand Down Expand Up @@ -313,7 +326,7 @@
// Open shadow roots (Reddit, some YouTube parts).
if (root.querySelectorAll) {
root.querySelectorAll('*').forEach((el) => {
if (el.shadowRoot && !el.shadowRoot.__gta6Observed) {
if (el.shadowRoot && el !== warnHost && !el.shadowRoot.__gta6Observed) {
el.shadowRoot.__gta6Observed = true;
scanText(el.shadowRoot);
scanAttributes(el.shadowRoot);
Expand All @@ -329,7 +342,7 @@
if (urlMatches(location.href)) return true;
// Swipe feeds change the title per video; items are covered one by one instead.
if (isSnapFeed()) return false;
if (twitchStreamIsGta()) return true;
if (twitchMode === 'hide' && twitchStreamIsGta()) return true;
if (matches(document.title)) return true;
const og = document.querySelector('meta[property="og:title"], meta[name="twitter:title"]');
if (og && matches(og.getAttribute('content'))) return true;
Expand Down Expand Up @@ -408,12 +421,147 @@
reportCount();
}

// ---------- Twitch warning (twitchMode 'warn') ----------

const ALLOWED_KEY = 'gta6-twitch-allowed';

/** "shroud" for twitch.tv/shroud, "videos/123" for a VOD. */
function twitchChannelKey(pathname) {
const parts = String(pathname || '').toLowerCase().split('/').filter(Boolean);
return parts[0] === 'videos' ? parts.slice(0, 2).join('/') : (parts[0] || '');
}

function allowedChannels() {
try { return JSON.parse(sessionStorage.getItem(ALLOWED_KEY) || '[]'); } catch (e) { return []; }
}

function allowChannel(key) {
try {
const list = allowedChannels();
if (!list.includes(key)) sessionStorage.setItem(ALLOWED_KEY, JSON.stringify(list.concat(key)));
} catch (e) { /* storage blocked: warn again next time */ }
}

function isChannelAllowed(key) {
return allowedChannels().includes(key);
}

/** Is this link a Twitch channel or video page (not directory, search, ...)? */
function twitchStreamLink(a) {
let u;
try { u = new URL(a.getAttribute('href'), location.href); } catch (e) { return null; }
if (!/(^|\.)twitch\.tv$/.test(u.hostname)) return null;
if (/^\/(directory|search|settings|downloads|subscriptions|drops|inventory|wallet|prime|turbo|jobs|p)?(\/|$)/i.test(u.pathname)) return null;
return u;
}

/**
* Full-screen warning in front of a GTA stream. The player is paused and
* muted while it is shown. "Watch anyway" remembers the channel for this
* tab, then runs onContinue (or simply reveals the stream).
*/
function showTwitchWarning(key, onContinue) {
if (warnHost && warnHost.isConnected) return;
warnHost = document.createElement('gta6-twitch-warning');
const shadow = warnHost.attachShadow({ mode: 'open' });
const iconUrl = ext.runtime.getURL('icons/icon-128.png');
shadow.innerHTML = `
<style>
:host { all: initial; }
.wrap { position: fixed; inset: 0; z-index: 2147483647; display: flex; align-items: center;
justify-content: center; background: rgba(15, 15, 20, .97); color: #f2f2f5;
box-shadow: inset 0 0 0 6px #e5484d;
font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
.card { max-width: 460px; padding: 32px; text-align: center; }
img { width: 80px; height: 80px; }
h1 { font-size: 26px; margin: 16px 0 8px; color: #ff6b6f; }
p { color: #c4c4d0; margin: 0 0 24px; }
.buttons { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
button { font: inherit; padding: 10px 20px; border-radius: 10px; border: 0; cursor: pointer; font-weight: 600; }
#gta6-back { background: #e5484d; color: white; }
#gta6-back:hover { background: #f2555a; }
#gta6-continue { background: transparent; color: #c4c4d0; box-shadow: inset 0 0 0 1px #4a4a55; }
#gta6-continue:hover { color: #fff; }
</style>
<div class="wrap" role="alertdialog" aria-labelledby="t">
<div class="card">
<img src="${iconUrl}" alt="">
<h1 id="t">Warning: GTA stream</h1>
<p>This stream is playing Grand Theft Auto and may contain GTA 6 spoilers.</p>
<div class="buttons">
<button type="button" id="gta6-back">Go back</button>
<button type="button" id="gta6-continue">Watch anyway</button>
</div>
</div>
</div>`;
shadow.getElementById('gta6-back').addEventListener('click', () => {
if (onContinue) { hideTwitchWarning(); return; } // warned before navigating: just stay
if (history.length > 1) history.back();
else location.href = 'https://www.twitch.tv/';
});
shadow.getElementById('gta6-continue').addEventListener('click', () => {
allowChannel(key);
hideTwitchWarning();
if (onContinue) onContinue();
});
(document.body || document.documentElement).appendChild(warnHost);
if (!onContinue) {
// In front of the stream: keep the player paused until the user decides.
pauseAllMedia();
warnTimer = setInterval(() => {
pauseAllMedia();
if (warnHost && !warnHost.isConnected) (document.body || document.documentElement).appendChild(warnHost);
}, 400);
}
}

function hideTwitchWarning() {
clearInterval(warnTimer);
warnTimer = null;
if (warnHost) warnHost.remove();
warnHost = null;
}

/** Channel page in warn mode: red frame, plus the warning until the user continues. */
function updateTwitchStream() {
const html = document.documentElement;
const gta = enabled && !exempt && twitchMode === 'warn' && twitchStreamIsGta();
if (!gta) {
html.removeAttribute('data-gta6-gta-stream');
if (warnHost && !warnHost.__gta6Link) hideTwitchWarning();
return;
}
html.setAttribute('data-gta6-gta-stream', '');
const key = twitchChannelKey(location.pathname);
if (!isChannelAllowed(key)) showTwitchWarning(key, null);
}

// Warn before a flagged (red) stream is even opened.
document.addEventListener('click', (e) => {
if (!enabled || exempt || twitchMode !== 'warn' || !isTwitch()) return;
if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return; // new tab: warned there
const t = e.target;
const a = t && t.closest && t.closest('a[href]');
if (!a || !a.closest('[data-gta6-flagged]')) return;
const u = twitchStreamLink(a);
if (!u) return;
const key = twitchChannelKey(u.pathname);
if (isChannelAllowed(key)) return;
e.preventDefault();
e.stopImmediatePropagation();
hideTwitchWarning();
showTwitchWarning(key, () => location.assign(u.href));
warnHost.__gta6Link = true;
}, true);

function checkPage() {
if (!enabled) return;
if (exempt) {
if (pageBlocked) unblockPage();
updateTwitchStream();
return;
}
if (isTwitch()) updateTwitchStream();
if (pageShouldBlock()) blockPage();
else if (pageBlocked) {
unblockPage();
Expand All @@ -438,6 +586,9 @@

function clearMarks() {
document.querySelectorAll('[data-gta6-blocked]').forEach((el) => el.removeAttribute('data-gta6-blocked'));
document.querySelectorAll('[data-gta6-flagged]').forEach((el) => el.removeAttribute('data-gta6-flagged'));
document.documentElement.removeAttribute('data-gta6-gta-stream');
hideTwitchWarning();
document.querySelectorAll('[data-gta6-covered]').forEach((el) => el.removeAttribute('data-gta6-covered'));
}

Expand Down Expand Up @@ -483,7 +634,7 @@
for (const m of mutations) {
if (m.type === 'childList') {
m.addedNodes.forEach((n) => {
if (n.nodeType === 1 && n !== overlayHost) schedule(n);
if (n.nodeType === 1 && n !== overlayHost && n !== warnHost) schedule(n);
else if (n.nodeType === 3 && n.parentElement) schedule(n.parentElement);
});
if (m.target.nodeName === 'TITLE' || m.target.nodeName === 'HEAD') schedule(document.documentElement);
Expand Down Expand Up @@ -520,6 +671,7 @@
enabled = !!s.enabled && !allowed;
exempt = isProjectUrl(location.href);
matcher = buildMatcher(s.keywords);
twitchMode = s.twitchMode === 'hide' ? 'hide' : 'warn';

if (!enabled) {
stopObserver();
Expand All @@ -541,7 +693,7 @@

ext.storage.onChanged.addListener((changes, area) => {
if (area !== 'local') return;
if (changes.enabled || changes.keywords || changes.allowlist) apply();
if (changes.enabled || changes.keywords || changes.allowlist || changes.twitchMode) apply();
});

ext.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
Expand Down
2 changes: 1 addition & 1 deletion src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "GTA 6 Blocker",
"short_name": "GTA6 Blocker",
"version": "1.2.0",
"version": "1.3.0",
"description": "Blocks pages, searches, videos, Shorts, Reels and posts about GTA 6 and Grand Theft Auto. Spoiler- and hype-free browsing.",
"icons": {
"16": "icons/icon-16.png",
Expand Down
2 changes: 2 additions & 0 deletions src/options/options.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ small, .hint { color: var(--muted); }
.hint { margin: 0 0 10px; font-size: 13px; }
.actions { display: flex; align-items: center; gap: 12px; }
#status { flex: 1; text-align: right; color: var(--muted); }
.choice { display: flex; align-items: flex-start; gap: 10px; padding: 8px 0; cursor: pointer; }
.choice input { margin: 4px 0 0; accent-color: var(--accent); flex: none; }
9 changes: 9 additions & 0 deletions src/options/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ <h1>GTA 6 Blocker settings</h1>
<input type="checkbox" id="networkBlocking" class="switch"></label>
</section>

<section>
<h2>Twitch</h2>
<p class="hint">What to do with streams playing Grand Theft Auto (any GTA category).</p>
<label class="choice"><input type="radio" name="twitchMode" value="warn">
<span><b>Mark red and warn</b><br><small>GTA streams stay in lists but are marked red. Opening one shows a warning first; you can go back or watch anyway.</small></span></label>
<label class="choice"><input type="radio" name="twitchMode" value="hide">
<span><b>Remove completely</b><br><small>GTA streams are removed from the sidebar, front page and directory, and GTA stream pages are blocked.</small></span></label>
</section>

<section>
<h2>Keywords</h2>
<p class="hint">One per line, not case sensitive. Spaces also match dashes, dots and no space at all, so <code>GTA 6</code> catches "GTA-6" and "gta6". Hashtags match by prefix, so <code>GTA</code> also catches #gtaonline.</p>
Expand Down
5 changes: 5 additions & 0 deletions src/options/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
$('networkBlocking').checked = s.networkBlocking;
$('keywords').value = (s.keywords || []).join('\n');
$('allowlist').value = (s.allowlist || []).join('\n');
const mode = s.twitchMode === 'hide' ? 'hide' : 'warn';
document.querySelector('input[name="twitchMode"][value="' + mode + '"]').checked = true;
}

function flash(msg) {
Expand All @@ -21,6 +23,9 @@
$('enabled').addEventListener('change', (e) => setSettings({ enabled: e.target.checked }).then(() => flash('Saved')));
$('networkBlocking').addEventListener('change', (e) => setSettings({ networkBlocking: e.target.checked }).then(() => flash('Saved')));

document.querySelectorAll('input[name="twitchMode"]').forEach((r) =>
r.addEventListener('change', (e) => setSettings({ twitchMode: e.target.value }).then(() => flash('Saved'))));

$('save').addEventListener('click', async () => {
const keywords = lines($('keywords').value);
if (!keywords.length) return flash('Add at least one keyword.');
Expand Down
3 changes: 3 additions & 0 deletions src/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
networkBlocking: true,
keywords: DEFAULT_KEYWORDS,
allowlist: [],
// Twitch GTA streams: 'warn' = mark red + warning before opening,
// 'hide' = remove them completely.
twitchMode: 'warn',
blockedTotal: 0
};

Expand Down
Loading
Loading