Files
FocusGram-Android/lib/scripts/content_disabling.dart
Ujwal 7bb472d212 What's new
- Reordered Settings Page.
- Added "Click to Unblur" for posts.
- Added Persistent Notification
- Improved Grayscale Scheduling.
and more.
2026-03-04 10:48:14 +05:45

612 lines
20 KiB
Dart

// UI element hiding for Instagram web.
//
// SCROLL LOCK WARNING:
// MutationObserver callbacks with {childList:true, subtree:true} fire hundreds
// of times/sec on Instagram's infinite scroll feed. Expensive querySelectorAll
// calls inside these callbacks block the main thread = scroll jank.
// The JS hiders below use requestIdleCallback + a 300ms debounce so they run
// only during idle time and never on every single mutation.
// ─── CSS-based (reliable, zero perf cost) ────────────────────────────────────
const String kHideLikeCountsCSS =
"""
[role="button"][aria-label${r"$"}=" like"],
[role="button"][aria-label${r"$"}=" likes"],
[role="button"][aria-label${r"$"}=" view"],
[role="button"][aria-label${r"$"}=" views"],
a[href*="/liked_by/"] {
display: none !important;
}
""";
const String kHideFollowerCountsCSS = """
a[href*="/followers/"] span,
a[href*="/following/"] span {
opacity: 0 !important;
pointer-events: none !important;
}
""";
// Stories bar — broad selector covering multiple Instagram DOM layouts
const String kHideStoriesBarCSS = """
[aria-label*="Stories"],
[aria-label*="stories"],
[role="list"][aria-label*="tories"],
[role="listbox"][aria-label*="tories"],
div[style*="overflow"][style*="scroll"]:has(canvas),
section > div > div[style*="overflow-x"] {
display: none !important;
}
""";
// Also do a JS sweep for stories — CSS alone isn't reliable across Instagram versions
const String kHideStoriesBarJS = r'''
(function() {
function hideStories() {
try {
// Target the horizontal scrollable stories container
document.querySelectorAll('[role="list"], [role="listbox"]').forEach(function(el) {
try {
const label = (el.getAttribute('aria-label') || '').toLowerCase();
if (label.includes('stori')) {
el.style.setProperty('display', 'none', 'important');
}
} catch(_) {}
});
// Fallback: find story bubbles (circular avatar containers at top of feed)
document.querySelectorAll('section > div > div').forEach(function(el) {
try {
const style = window.getComputedStyle(el);
if (style.overflowX === 'scroll' || style.overflowX === 'auto') {
const circles = el.querySelectorAll('canvas, [style*="border-radius: 50%"]');
if (circles.length > 2) {
el.style.setProperty('display', 'none', 'important');
}
}
} catch(_) {}
});
} catch(_) {}
}
hideStories();
if (!window.__fgStoriesObserver) {
let _storiesTimer = null;
window.__fgStoriesObserver = new MutationObserver(() => {
// Debounce — only run after mutations settle, not on every single one
clearTimeout(_storiesTimer);
_storiesTimer = setTimeout(hideStories, 300);
});
window.__fgStoriesObserver.observe(
document.documentElement,
{ childList: true, subtree: true }
);
}
})();
''';
/// Robust stories overlay - blocks clicking and applies blur when hide stories is enabled.
/// This is a more aggressive approach that places an overlay with blur on top of stories area.
const String kStoriesOverlayJS = r'''
(function() {
if (window.__fgStoriesOverlayRunning) return;
window.__fgStoriesOverlayRunning = true;
const BLOCKED_ATTR = 'data-fg-stories-blocked';
function buildOverlay(container) {
const div = document.createElement('div');
div.setAttribute(BLOCKED_ATTR, '1');
div.style.cssText = [
'position: absolute',
'inset: 0',
'z-index: 99998',
'display: flex',
'align-items: center',
'justify-content: center',
'background: rgba(0, 0, 0, 0.6)',
'backdrop-filter: blur(10px)',
'-webkit-backdrop-filter: blur(10px)',
'border-radius: 8px',
'pointer-events: all',
'cursor: not-allowed',
].join(';');
const label = document.createElement('span');
label.textContent = 'Stories blocked';
label.style.cssText = [
'color: rgba(255, 255, 255, 0.8)',
'font-size: 12px',
'font-weight: 600',
'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
'text-align: center',
'padding: 8px 16px',
'background: rgba(0, 0, 0, 0.5)',
'border-radius: 20px',
].join(';');
div.appendChild(label);
// Swallow all interaction
['click', 'touchstart', 'touchend', 'touchmove', 'pointerdown', 'mouseenter'].forEach(function(evt) {
div.addEventListener(evt, function(e) {
e.preventDefault();
e.stopImmediatePropagation();
}, { capture: true });
});
return div;
}
function overlayStoriesContainer(container) {
if (!container) return;
if (container.querySelector('[' + BLOCKED_ATTR + ']')) return;
// Check if this looks like a stories container
const hasStories = container.querySelector('canvas, [style*="border-radius: 50%"], [aria-label*="story"], [role="list"]');
if (!hasStories) return;
container.style.position = 'relative';
container.style.overflow = 'hidden';
container.appendChild(buildOverlay(container));
}
function findAndOverlayStories() {
try {
// Method 1: Find by role="list" with story-related aria-label
document.querySelectorAll('[role="list"], [role="listbox"]').forEach(function(el) {
try {
const label = (el.getAttribute('aria-label') || '').toLowerCase();
if (label.includes('stori')) {
overlayStoriesContainer(el.parentElement);
}
} catch(_) {}
});
// Method 2: Find horizontal scroll containers at top of feed
document.querySelectorAll('header + div > div, main > div > div > div').forEach(function(el) {
try {
const style = window.getComputedStyle(el);
if ((style.overflowX === 'scroll' || style.overflowX === 'auto') &&
(style.display === 'flex' || style.display === '')) {
const children = el.children;
let hasAvatar = false;
for (let i = 0; i < Math.min(children.length, 10); i++) {
const child = children[i];
const childStyle = window.getComputedStyle(child);
if (childStyle.width === '60px' || childStyle.width === '66px' ||
child.querySelector('canvas, [style*="border-radius: 50%"]')) {
hasAvatar = true;
break;
}
}
if (hasAvatar) {
overlayStoriesContainer(el);
}
}
} catch(_) {}
});
// Method 3: Find story avatars directly
document.querySelectorAll('[href*="/stories/"], [aria-label*="Your Story"]').forEach(function(el) {
try {
let container = el.parentElement;
for (let i = 0; i < 5 && container; i++) {
const style = window.getComputedStyle(container);
if (style.position !== 'static' && container.children.length < 20) {
overlayStoriesContainer(container);
break;
}
container = container.parentElement;
}
} catch(_) {}
});
} catch(_) {}
}
// Initial run
findAndOverlayStories();
// Watch for dynamic changes
let _overlayTimer = null;
new MutationObserver(function() {
clearTimeout(_overlayTimer);
_overlayTimer = setTimeout(findAndOverlayStories, 500);
}).observe(document.documentElement, { childList: true, subtree: true });
// Also run on scroll
let _scrollTimer = null;
window.addEventListener('scroll', function() {
clearTimeout(_scrollTimer);
_scrollTimer = setTimeout(findAndOverlayStories, 300);
}, { passive: true });
})();
''';
const String kHideExploreTabCSS = """
a[href="/explore/"],
a[href="/explore"] {
display: none !important;
}
""";
const String kHideReelsTabCSS = """
a[href="/reels/"],
a[href="/reels"] {
display: none !important;
}
""";
const String kHideShopTabCSS = """
a[href*="/shop"],
a[href*="/shopping"] {
display: none !important;
}
""";
// ─── Complete Section Disabling (CSS-based) ─────────────────────────────────
// Minimal mode - disables Reels and Explore entirely
const String kMinimalModeCssScript = r'''
(function() {
const css = `
/* Hide Reels tab */
a[href="/reels/"], a[href="/reels"] { display: none !important; }
/* Hide Explore tab */
a[href="/explore/"], a[href="/explore"] { display: none !important; }
/* Hide Create tab */
a[href="/create/"], a[href="/create"] { display: none !important; }
/* Hide Reels in feed */
article a[href*="/reel/"] { display: none !important; }
/* Hide Explore entry points */
svg[aria-label="Explore"], [aria-label="Explore"] { display: none !important; }
`;
const style = document.createElement('style');
style.id = 'fg-minimal-mode';
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
})();
''';
// Disable Reels entirely
const String kDisableReelsEntirelyCssScript = r'''
(function() {
const css = `
a[href="/reels/"], a[href="/reels"] { display: none !important; }
article a[href*="/reel/"] { display: none !important; }
`;
const style = document.createElement('style');
style.id = 'fg-disable-reels';
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
})();
''';
// Disable Explore entirely
const String kDisableExploreEntirelyCssScript = r'''
(function() {
const css = `
a[href="/explore/"], a[href="/explore"] { display: none !important; }
svg[aria-label="Explore"], [aria-label="Explore"] { display: none !important; }
`;
const style = document.createElement('style');
style.id = 'fg-disable-explore';
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
})();
''';
// ─── DM-embedded Reels Scroll Control ────────────────────────────────────────
// Disables vertical scroll on reels opened from DM unless comment box or share modal is open
const String kDmReelScrollLockScript = r'''
(function() {
// Track scroll lock state
window.__fgDmReelScrollLocked = true;
window.__fgDmReelCommentOpen = false;
window.__fgDmReelShareOpen = false;
function lockScroll() {
if (window.__fgDmReelScrollLocked) {
document.body.style.overflow = 'hidden';
document.documentElement.style.overflow = 'hidden';
}
}
function unlockScroll() {
document.body.style.overflow = '';
document.documentElement.style.overflow = '';
}
function updateScrollState() {
// Only unlock if comment or share modal is open
if (window.__fgDmReelCommentOpen || window.__fgDmReelShareOpen) {
unlockScroll();
} else if (window.__fgDmReelScrollLocked) {
lockScroll();
}
}
// Listen for comment box opening/closing
function setupCommentObserver() {
const commentBox = document.querySelector('div[aria-label="Comment"], section[aria-label*="Comment"]');
if (commentBox) {
window.__fgDmReelCommentOpen = true;
updateScrollState();
}
}
// Listen for share modal
function setupShareObserver() {
const shareModal = document.querySelector('div[role="dialog"][aria-label*="Share"], section[aria-label*="Share"]');
if (shareModal) {
window.__fgDmReelShareOpen = true;
updateScrollState();
}
}
// Set up MutationObserver to detect comment/share modals
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) {
const ariaLabel = node.getAttribute('aria-label') || '';
const role = node.getAttribute('role') || '';
// Check for comment box
if (ariaLabel.toLowerCase().includes('comment') ||
(role === 'dialog' && ariaLabel === '')) {
// Check if it's a comment dialog
setTimeout(function() {
window.__fgDmReelCommentOpen = !!document.querySelector('div[aria-label="Comment"], section[aria-label*="Comment"]');
updateScrollState();
}, 100);
}
// Check for share modal
if (ariaLabel.toLowerCase().includes('share')) {
window.__fgDmReelShareOpen = true;
updateScrollState();
}
}
});
mutation.removedNodes.forEach(function(node) {
if (node.nodeType === 1) {
const ariaLabel = node.getAttribute('aria-label') || '';
if (ariaLabel.toLowerCase().includes('comment')) {
setTimeout(function() {
window.__fgDmReelCommentOpen = !!document.querySelector('div[aria-label="Comment"], section[aria-label*="Comment"]');
updateScrollState();
}, 100);
}
if (ariaLabel.toLowerCase().includes('share')) {
window.__fgDmReelShareOpen = false;
updateScrollState();
}
}
});
});
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
// Initial lock
lockScroll();
// Expose functions for external control
window.__fgSetDmReelScrollLock = function(locked) {
window.__fgDmReelScrollLocked = locked;
updateScrollState();
};
})();
''';
// ─── JS-based (text-content detection, debounced) ─────────────────────────────
// Sponsored posts — scans for "Sponsored" text, debounced so it doesn't
// cause scroll jank on Instagram's constantly-mutating feed DOM.
const String kHideSponsoredPostsJS = r'''
(function() {
function hideSponsoredPosts() {
try {
document.querySelectorAll('article, li[role="listitem"]').forEach(function(el) {
try {
if (el.__fgSponsoredChecked) return; // skip already-processed elements
const spans = el.querySelectorAll('span');
for (let i = 0; i < spans.length; i++) {
const text = spans[i].textContent.trim();
if (text === 'Sponsored' || text === 'Paid partnership') {
el.style.setProperty('display', 'none', 'important');
return;
}
}
el.__fgSponsoredChecked = true; // mark as checked (non-sponsored)
} catch(_) {}
});
} catch(_) {}
}
hideSponsoredPosts();
if (!window.__fgSponsoredObserver) {
let _timer = null;
window.__fgSponsoredObserver = new MutationObserver(() => {
clearTimeout(_timer);
_timer = setTimeout(hideSponsoredPosts, 300);
});
window.__fgSponsoredObserver.observe(
document.documentElement,
{ childList: true, subtree: true }
);
}
})();
''';
// Suggested posts — debounced same way.
const String kHideSuggestedPostsJS = r'''
(function() {
function hideSuggestedPosts() {
try {
document.querySelectorAll('span, h3, h4').forEach(function(el) {
try {
const text = el.textContent.trim();
if (
text === 'Suggested for you' ||
text === 'Suggested posts' ||
text === "You're all caught up"
) {
let parent = el.parentElement;
for (let i = 0; i < 8 && parent; i++) {
const tag = parent.tagName.toLowerCase();
if (tag === 'article' || tag === 'section' || tag === 'li') {
parent.style.setProperty('display', 'none', 'important');
break;
}
parent = parent.parentElement;
}
}
} catch(_) {}
});
} catch(_) {}
}
hideSuggestedPosts();
if (!window.__fgSuggestedObserver) {
let _timer = null;
window.__fgSuggestedObserver = new MutationObserver(() => {
clearTimeout(_timer);
_timer = setTimeout(hideSuggestedPosts, 300);
});
window.__fgSuggestedObserver.observe(
document.documentElement,
{ childList: true, subtree: true }
);
}
})();
''';
// ─── DM Reel Blocker ─────────────────────────────────────────────────────────
/// Overlays a "Reels are disabled" card on reel preview cards inside DMs.
///
/// DM reel previews use pushState (SPA) not <a href> navigation, so the CSS
/// display:none in kDisableReelsEntirelyCssScript doesn't remove the preview
/// card from the thread. This script finds them structurally and covers them
/// with a blocking overlay that also swallows all touch/click events.
///
/// Inject when disableReelsEntirely OR minimalMode is on.
const String kDmReelBlockerJS = r'''
(function() {
if (window.__fgDmReelBlockerRunning) return;
window.__fgDmReelBlockerRunning = true;
const BLOCKED_ATTR = 'data-fg-blocked';
function buildOverlay() {
const div = document.createElement('div');
div.setAttribute(BLOCKED_ATTR, '1');
div.style.cssText = [
'position:absolute',
'inset:0',
'z-index:99999',
'display:flex',
'flex-direction:column',
'align-items:center',
'justify-content:center',
'background:rgba(0,0,0,0.85)',
'border-radius:inherit',
'pointer-events:all',
'gap:8px',
'cursor:default',
].join(';');
const icon = document.createElement('span');
icon.textContent = '🚫';
icon.style.cssText = 'font-size:28px;line-height:1';
const label = document.createElement('span');
label.textContent = 'Reels are disabled';
label.style.cssText = [
'color:#fff',
'font-size:13px',
'font-weight:600',
'font-family:-apple-system,sans-serif',
'text-align:center',
'padding:0 12px',
].join(';');
const sub = document.createElement('span');
sub.textContent = 'Disable "Block Reels" in FocusGram settings';
sub.style.cssText = [
'color:rgba(255,255,255,0.5)',
'font-size:11px',
'font-family:-apple-system,sans-serif',
'text-align:center',
'padding:0 16px',
].join(';');
div.appendChild(icon);
div.appendChild(label);
div.appendChild(sub);
// Swallow all interaction so the reel beneath cannot be triggered
['click','touchstart','touchend','touchmove','pointerdown'].forEach(function(evt) {
div.addEventListener(evt, function(e) {
e.preventDefault();
e.stopImmediatePropagation();
}, { capture: true });
});
return div;
}
function overlayContainer(container) {
if (!container) return;
if (container.querySelector('[' + BLOCKED_ATTR + ']')) return; // already overlaid
container.style.position = 'relative';
container.style.overflow = 'hidden';
container.appendChild(buildOverlay());
}
function blockDmReels() {
try {
// Strategy 1: <a href*="/reel/"> links inside the DM thread
document.querySelectorAll('a[href*="/reel/"]').forEach(function(link) {
try {
link.style.setProperty('pointer-events', 'none', 'important');
overlayContainer(link.closest('div') || link.parentElement);
} catch(_) {}
});
// Strategy 2: <video> inside DMs (reel cards without <a> wrapper)
// Only targets videos inside the Direct thread or on /direct/ path
document.querySelectorAll('video').forEach(function(video) {
try {
const inDm = !!video.closest('[aria-label="Direct"], [aria-label*="Direct"]');
const isDmPath = window.location.pathname.includes('/direct/');
if (!inDm && !isDmPath) return;
const container = video.closest('div[class]') || video.parentElement;
if (!container) return;
video.style.setProperty('pointer-events', 'none', 'important');
overlayContainer(container);
} catch(_) {}
});
} catch(_) {}
}
blockDmReels();
let _t = null;
new MutationObserver(function() {
clearTimeout(_t);
_t = setTimeout(blockDmReels, 200);
}).observe(document.documentElement, { childList: true, subtree: true });
})();
''';