mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-04-01 17:10:23 +02:00
- Reordered Settings Page. - Added "Click to Unblur" for posts. - Added Persistent Notification - Improved Grayscale Scheduling. and more.
612 lines
20 KiB
Dart
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 });
|
|
})();
|
|
''';
|