Progress SAve- downloader,blur,ghost mode(Partially) works

This commit is contained in:
Ujwal223
2026-05-25 18:00:57 +05:45
parent 4f63e784ac
commit 2d33dcb889
66 changed files with 6373 additions and 909 deletions
+100
View File
@@ -0,0 +1,100 @@
/**
* FocusGram DOM Ad Blocker
* Removes sponsored posts, "Suggested for you" injections, and ad elements.
* Uses structure-based selectors — NOT class names (those change weekly).
* Injected at DOCUMENT_END.
*/
(function () {
'use strict';
// ─── Sponsored text signals (Instagram localizes these) ───────────────────
// We match the STRUCTURE not just English text.
// In IG mobile web, sponsored label appears as a <span> or <div>
// that is a direct sibling/child of the article header area.
const SPONSORED_TEXTS = new Set([
'sponsored', // en
'gesponsert', // de
'patrocinado', // es/pt
'sponsorisé', // fr
'sponsorizzato', // it
'sponsrad', // sv
'sponsoreret', // da
'gesponsord', // nl
'рекламa', // ru
'विज्ञापन', // hi
'广告', // zh
'ad', // en short
]);
const isSponsoredText = (text) =>
SPONSORED_TEXTS.has(text.trim().toLowerCase());
// ─── Remove a single article element ──────────────────────────────────────
const removeArticle = (el) => {
// Walk up to find the article or main feed item container
const target = el.closest('article') ?? el.closest('div[data-media-id]') ?? el;
target.remove();
};
// ─── Core ad scanner ──────────────────────────────────────────────────────
const scanAndRemove = () => {
// Strategy 1: <a href="/ads/..."> inside feed
document.querySelectorAll('a[href*="/ads/"]').forEach((a) => {
a.closest('article')?.remove();
});
// Strategy 2: Sponsored text in article spans
document.querySelectorAll('article').forEach((article) => {
const spans = article.querySelectorAll('span, div');
for (const span of spans) {
if (
span.children.length === 0 && // leaf node
isSponsoredText(span.textContent)
) {
article.remove();
return;
}
}
});
// Strategy 3: "Suggested for you" feed injections
document.querySelectorAll('article, section').forEach((el) => {
const firstText = el.querySelector('span, div, h4')?.textContent?.trim();
if (
firstText &&
(firstText.toLowerCase().startsWith('suggested') ||
firstText.toLowerCase().startsWith('you might') ||
firstText.toLowerCase() === 'posts you might like')
) {
el.remove();
}
});
// Strategy 4: Instagram marks some ad containers with aria-label
document
.querySelectorAll('[aria-label*="Sponsored"], [aria-label*="Ad"]')
.forEach((el) => {
el.closest('article')?.remove();
});
// Strategy 5: Tracking pixel iframes / hidden images
document.querySelectorAll('iframe[width="0"], iframe[height="0"]').forEach((el) => el.remove());
document
.querySelectorAll('img[width="1"][height="1"], img[width="0"][height="0"]')
.forEach((el) => el.remove());
};
// ─── Run on load + watch for new content ──────────────────────────────────
scanAndRemove();
const observer = new MutationObserver((mutations) => {
// Only scan if nodes were added (skip attribute/text changes)
const hasAdditions = mutations.some((m) => m.addedNodes.length > 0);
if (hasAdditions) scanAndRemove();
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
})();
+83
View File
@@ -0,0 +1,83 @@
/**
* FocusGram Autoplay Blocker
* Injected at DOCUMENT_START — before Instagram's JS loads.
* Prevents video autoplay by:
* 1. Blocking play() calls on video elements
* 2. Disabling autoplay attribute
* 3. Removing preload attributes
*/
(function () {
'use strict';
window.__fgBlockAutoplay = false;
// Override HTMLMediaElement.play() to check our flag
const _play = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.play = function () {
if (window.__fgBlockAutoplay) {
// Return a resolved promise to avoid breaking Instagram's code
return Promise.resolve();
}
return _play.call(this);
};
// Override autoplay property setter
const _videoDescriptor = Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'autoplay') || {};
const _originalAutoplaySetter = _videoDescriptor.set;
Object.defineProperty(HTMLVideoElement.prototype, 'autoplay', {
set: function (value) {
if (window.__fgBlockAutoplay && value) {
// Silently ignore autoplay attempts when blocking is enabled
return;
}
if (_originalAutoplaySetter) {
_originalAutoplaySetter.call(this, value);
}
},
get: function () {
if (_videoDescriptor.get) {
return _videoDescriptor.get.call(this);
}
return this.getAttribute('autoplay') !== null;
},
enumerable: _videoDescriptor.enumerable,
configurable: true,
});
// On page load and SPA navigation, scan for video elements and remove autoplay
const removeAutoplayFromVideos = () => {
document.querySelectorAll('video, [role="video"]').forEach(el => {
if (window.__fgBlockAutoplay) {
el.autoplay = false;
el.removeAttribute('autoplay');
if (el.paused === false) {
el.pause();
}
}
});
};
// Run on load and when document changes
removeAutoplayFromVideos();
if (!window.__fgAutoplayObserver) {
let _timer = null;
window.__fgAutoplayObserver = new MutationObserver(() => {
clearTimeout(_timer);
_timer = setTimeout(removeAutoplayFromVideos, 500);
});
window.__fgAutoplayObserver.observe(document.documentElement, {
childList: true,
subtree: true,
});
}
// Allow Flutter to toggle
window.__fgSetBlockAutoplay = function (enabled) {
window.__fgBlockAutoplay = !!enabled;
if (enabled) {
removeAutoplayFromVideos();
}
};
})();
+304
View File
@@ -0,0 +1,304 @@
/**
* FocusGram Content Hider
* Toggleable visibility for: stories tray, feed posts, reels, suggested content.
* Flutter controls via window.__fgContent.*
* Injected at DOCUMENT_END.
*
* Key fixes applied:
* - Blank-feed fix: hideReels uses DOM removal (not display:none) so layout doesn't collapse
* - MutationObserver callback now re-applies CSS AND re-runs all hide functions each cycle
* - SPA-heartbeat via window event listener re-applies CSS on pushState/replaceState
* - Stories tray detection strengthened for fresh SPA navigations
* - Suggested posts detection uses multiple text-node matching strategies
*/
(function () {
'use strict';
if (window.__fgContent && window.__fgContent.__focusgramReady) {
return;
}
const STYLE_ID = 'fg-content-hider';
let hideStories = false;
let hidePosts = false;
let hideSuggested = false;
let hideReels = false;
// ─── CSS rules ─────────────────────────────────────────────────────────────
function buildCSS() {
const selectors = [];
if (hideStories) {
selectors.push(
'[role="list"]:has([aria-label*="tory"])',
'[role="listbox"]:has([aria-label*="tory"])',
'[role="menu"] > ul',
'section > div > div:first-child [style*="overflow"]',
'[role="list"] [style*="overflow"]',
);
}
if (hidePosts) {
selectors.push(
'body:not([path*="direct"]):not([data-fg-path*="direct"]) article:not([aria-label])',
'body:not([path*="direct"]):not([data-fg-path*="direct"]) [data-pressable-container] > article',
);
}
// hideReels CSS is intentionally NOT added here.
// We use DOM removal instead (see removeReels()) so that room is never left
// blank in the feed, and Instagram's infinite-scroll can prove scroll height.
return selectors.length
? selectors.join(',\n') + ' { display: none !important; visibility: hidden !important; }'
: '';
}
function applyCSS() {
if (document.body) {
document.body.setAttribute('data-fg-path', window.location.pathname || '/');
}
let style = document.getElementById(STYLE_ID);
if (!style) {
style = document.createElement('style');
style.id = STYLE_ID;
document.head.appendChild(style);
}
style.textContent = buildCSS();
}
// ─── Story tray JS ─────────────────────────────────────────────────────────
function hideStoryTray() {
if (!hideStories) return;
// Strategy 1: <ul> children of a named list or menu
document.querySelectorAll('[role="list"] ul, [role="menu"] ul').forEach(function (ul) {
try {
const items = ul.querySelectorAll('li, button, a');
if (items.length < 2) return;
ul.style.setProperty('display', 'none', 'important');
} catch (_) {}
});
// Strategy 2: horizontally scrolling container with circle items
document.querySelectorAll('[style*="overflow"], [style*="overflow-x"]').forEach(function (c) {
try {
if ('' === (window.getComputedStyle(c).overflow + '').replace(/none/g, '')) return;
const cands = c.querySelectorAll('li, div[class*="story"], [class*="story"]');
if (cands.length < 2) return;
const s0 = window.getComputedStyle(cands[0]);
if (s0.width && parseFloat(s0.width) <= 90) {
c.parentElement && (c.parentElement.style.setProperty('display', 'none', 'important'));
}
} catch (_) {}
});
}
// ─── Suggested posts ───────────────────────────────────────────────────────
function removeSuggested() {
if (!hideSuggested) return;
var SIGNALS = [
'suggested for you',
'suggested posts',
'suggested reels',
'suggested',
'because you watched',
'because you follow',
'you might like',
'posts you might like',
'accounts you might like',
'recommendations',
];
function norm(s) {
return (s || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function hasSignal(s) {
var t = norm(s);
if (!t) return false;
return SIGNALS.some(function (signal) {
if (signal === 'suggested') return t === signal;
return t.indexOf(signal) >= 0;
});
}
function hideContainer(from) {
var parent = from;
for (var depth = 0; depth < 10 && parent && parent !== document.body; depth++) {
var role = parent.getAttribute && parent.getAttribute('role');
var tag = parent.tagName;
var hasMedia = parent.querySelector && parent.querySelector('img,video,a[href*="/p/"],a[href*="/reel/"]');
if (
tag === 'ARTICLE' ||
tag === 'SECTION' ||
role === 'listitem' ||
(hasMedia && parent.getBoundingClientRect && parent.getBoundingClientRect().height > 120)
) {
parent.style.setProperty('display', 'none', 'important');
parent.setAttribute('data-fg-hidden-suggested', '1');
return true;
}
parent = parent.parentElement;
}
return false;
}
document.querySelectorAll('article, section, [role="listitem"]').forEach(function (node) {
try {
if (node.getAttribute('data-fg-hidden-suggested') === '1') return;
var ownLabel = node.getAttribute('aria-label');
if (hasSignal(ownLabel)) { hideContainer(node); return; }
var text = norm(node.innerText || node.textContent || '');
if (
text.indexOf('suggested for you') >= 0 ||
text.indexOf('suggested posts') >= 0 ||
text.indexOf('suggested reels') >= 0 ||
text.indexOf('because you watched') >= 0 ||
text.indexOf('because you follow') >= 0
) {
hideContainer(node);
}
} catch (_) {}
});
document.querySelectorAll('span, h1, h2, h3, h4, div[aria-label], a[aria-label]').forEach(function (el) {
try {
if (hasSignal(el.textContent) || hasSignal(el.getAttribute('aria-label'))) {
hideContainer(el);
}
} catch (_) {}
});
}
// ─── Reels DOM REMOVE (not display:none) ─────────────────────────────────
// display:none keeps the element in the DOM, so Instagram's virtual-scroll still
// reserves the slot → blank gaps. Removing the article from the DOM collapses the
// gap cleanly and lets the feed flow naturally.
function removeReels() {
if (!hideReels) return;
var toRemove = [];
document.querySelectorAll('article').forEach(function (el) {
try {
// Fast path: check for a reel-signal attribute first
var mt = (el.getAttribute('data-media-type') || el.dataset && el.dataset.mediaType || '').trim();
if (mt === '2') { toRemove.push(el); return; }
// Fallback: text-node scan for /reels/ markers
var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
var n;
while ((n = walker.nextNode())) {
if (n.nodeValue.indexOf('/reels/') >= 0 || n.nodeValue.indexOf('/reel/') >= 0) {
toRemove.push(el); break;
}
}
} catch (_) {}
});
toRemove.forEach(function (el) { try { el.remove(); } catch (_) {} });
}
// ─── Public API ────────────────────────────────────────────────────────────
window.__fgContent = {
__focusgramReady: true,
setHideStories: function (val) { hideStories = !!val; applyCSS(); hideStoryTray(); },
setHidePosts: function (val) { hidePosts = !!val; applyCSS(); },
setHideSuggested: function (val) {
hideSuggested = !!val;
applyCSS();
if (val) removeSuggested();
},
setHideReels: function (val) {
hideReels = !!val;
applyCSS();
if (val) removeReels();
},
applyAll: function (flags) {
hideStories = !!flags.stories;
hidePosts = !!flags.posts;
hideReels = !!flags.reels;
hideSuggested = !!flags.suggested;
applyCSS();
if (hideSuggested) removeSuggested();
if (hideStories) hideStoryTray();
if (hideReels) removeReels();
},
};
// ─── SPA heartbeat ─────────────────────────────────────────────────────────
// pushState/replaceState don't fire any DOM event we can listen for.
// Hook the methods themselves so we know a navigation happened, then debounce
// re-apply. This also catches the case where the MutationObserver was on `body`
// and that node got replaced by Instagram's SPA re-render.
function scheduleReapply() {
clearTimeout(window.__fg_applyTimer);
window.__fg_applyTimer = setTimeout(function () {
applyCSS();
if (hideStories) hideStoryTray();
if (hideSuggested) removeSuggested();
if (hideReels) removeReels();
}, 250);
}
var _origPush = history.pushState;
var _origReplace = history.replaceState;
history.pushState = function () {
_origPush.apply(this, arguments);
scheduleReapply();
};
history.replaceState = function () {
_origReplace.apply(this, arguments);
scheduleReapply();
};
// Reinforce on popstate too (user hits back/forward)
window.addEventListener('popstate', scheduleReapply, { passive: true });
// For pushState on the same URL (rare but possible) poll path briefly
window.addEventListener('pageshow', scheduleReapply, { passive: true });
window.addEventListener('focus', scheduleReapply, { passive: true });
// ─── MutationObserver ───────────────────────────────────────────────────────
// Monitors for dynamic DOM changes (new rows, lazy-loaded articles) and
// re-applies everything on each cycle. Does NOT guard on a per-element timer
// that would never re-fire after the body is replaced by SPA re-render.
if (!window.__fgContentObserver) {
window.__fgContentObserver = new MutationObserver(function () {
clearTimeout(window.__fg_moTimer);
window.__fg_moTimer = setTimeout(function () {
applyCSS();
if (hideStories) hideStoryTray();
if (hideSuggested) removeSuggested();
if (hideReels) removeReels();
}, 300);
});
// `document.documentElement` survives SPA navigations (body gets replaced
// but <html> stays). Observing it catches both subtree mutations and, via
// the SPA heartbeat above, re-applies after pushState.
window.__fgContentObserver.observe(document.documentElement, {
childList: true,
subtree: true,
});
}
// ─── Initial run ────────────────────────────────────────────────────────────
applyCSS();
if (hideStories) hideStoryTray();
if (hideSuggested) removeSuggested();
if (hideReels) removeReels();
// Signal ready — Flutter will call applyAll() with stored prefs
if (window.ContentChannel) {
window.ContentChannel.postMessage(JSON.stringify({ type: 'ready' }));
}
})();
+281
View File
@@ -0,0 +1,281 @@
/**
* FocusGram Unified Feed Filter via Fetch Interception
* Injected at DOCUMENT_START — before Instagram's JS loads.
*
* This script intercepts GraphQL fetch calls and filters feed content based on:
* - Ads (is_ad, ad_action_link, product_type, ad_id, ad_header_style)
* - Sponsored posts (ad_action_link, ad_header_style)
* - Suggested posts (is_suggested, is_suggested_for_you, __typename)
* - Videos/Reels (is_video, media_type, clips_metadata)
* - Autoplay blocking (video autoplay prevention)
*/
(function () {
'use strict';
// Configuration flags (set by Flutter via prefs)
window.__fgFilterConfig = {
blockAds: false,
blockSponsored: false,
blockSuggested: false,
blockVideos: false,
blockAutoplay: false,
blockGraphQLQueryWhenFeedPosts: false,
};
// Helper: Check if a node is an ad
const isAdNode = (node) => {
if (!node || typeof node !== 'object') return false;
return !!(
node.is_ad ||
node.ad_action_link ||
node.ad_id ||
(node.product_type && node.product_type === 'ad') ||
(node.ad_header_style && node.ad_header_style !== 'none') ||
(node.__typename && node.__typename === 'GraphAdStory')
);
};
// Helper: Check if a node is sponsored
const isSponsoredNode = (node) => {
if (!node || typeof node !== 'object') return false;
return !!(
(node.ad_action_link && node.ad_action_link.href) ||
(node.ad_header_style && node.ad_header_style !== 'none')
);
};
// Helper: Check if a node is suggested content
const isSuggestedNode = (node) => {
if (!node || typeof node !== 'object') return false;
const typename = String(node.__typename || '');
const reason = JSON.stringify({
reason: node.suggested_reason,
social_context: node.social_context,
title: node.title,
header: node.header,
label: node.label,
}).toLowerCase();
return !!(
node.is_suggested ||
node.is_suggested_for_you ||
node.is_recommendation ||
node.suggested_users ||
node.suggested_media ||
node.suggested_content ||
node.recommendation_source ||
typename.includes('Suggested') ||
typename.includes('Recommendation') ||
reason.includes('suggested') ||
reason.includes('recommend')
);
};
// Helper: Check if a node is a video/reel
const isVideoNode = (node) => {
if (!node || typeof node !== 'object') return false;
return !!(
node.is_video ||
(node.media_type === 2) ||
node.clips_metadata ||
(node.__typename && (
node.__typename.includes('Clips') ||
node.__typename.includes('Video')
))
);
};
const isFeedMediaNode = (node) => {
if (!node || typeof node !== 'object') return false;
return !!(
node.pk ||
node.id ||
node.code ||
node.media_type ||
node.image_versions2 ||
node.video_versions ||
node.carousel_media ||
node.__typename?.includes('Media') ||
node.__typename?.includes('Timeline')
);
};
// Helper: Check for media in carousel
const hasVideoInCarousel = (node) => {
if (!node || typeof node !== 'object') return false;
if (node.media_type === 8) {
const edges = node.edge_sidecar_to_children?.edges || [];
return edges.some(edge => isVideoNode(edge.node));
}
return false;
};
// Main filter function for feed nodes
const shouldFilterNode = (node) => {
const config = window.__fgFilterConfig;
if (!node || typeof node !== 'object') return false;
if (config.blockGraphQLQueryWhenFeedPosts && isFeedMediaNode(node)) {
return true;
}
// Check ads
if (config.blockAds && isAdNode(node)) {
return true;
}
// Check sponsored (separate from ads)
if (config.blockSponsored && isSponsoredNode(node) && !isAdNode(node)) {
return true;
}
// Check suggested content
if (config.blockSuggested && isSuggestedNode(node)) {
return true;
}
// Check videos/reels
if (config.blockVideos && (isVideoNode(node) || hasVideoInCarousel(node))) {
return true;
}
return false;
};
// Recursively filter GraphQL response edges
const filterEdges = (edges, path = []) => {
if (!Array.isArray(edges)) return edges;
return edges.filter(edge => {
if (!edge || !edge.node) return true;
const node = edge.node;
// Keep the edge if it doesn't match any filter
if (!shouldFilterNode(node)) return true;
// Log filtered content for debugging
if (window.__fgDebugFilter) {
const type = node.__typename || 'Unknown';
console.debug('[FocusGram Filter]', `Filtered ${type} at ${path.join('/')}`);
}
return false;
});
};
// Recursively walk GraphQL response and filter edges
const walkAndFilter = (obj, visited = new Set()) => {
if (!obj || typeof obj !== 'object' || visited.has(obj)) return;
visited.add(obj);
// Handle arrays
if (Array.isArray(obj)) {
obj.forEach(item => walkAndFilter(item, visited));
return;
}
// Check for edges array (common GraphQL pattern)
if (obj.edges && Array.isArray(obj.edges)) {
obj.edges = filterEdges(obj.edges);
}
// Recurse into children
for (const key in obj) {
if (obj.hasOwnProperty(key) && key !== '__typename') {
const val = obj[key];
if (val && typeof val === 'object') {
walkAndFilter(val, visited);
}
}
}
};
// Override fetch
const _fetch = window.fetch.bind(window);
window.fetch = async function (input, init) {
const url = typeof input === 'string'
? input
: input instanceof URL
? input.href
: input?.url ?? '';
// Call original fetch
let response = await _fetch(input, init);
// Only intercept GraphQL feed queries
if (!url.includes('/graphql/query') && !url.includes('/api/v1/feed')) {
return response;
}
// Clone response to read body
const cloned = response.clone();
try {
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
return response;
}
const data = await cloned.json();
// Filter the response data
walkAndFilter(data);
// Return modified response
return new Response(JSON.stringify(data), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} catch (e) {
// On error, return original response
return response;
}
};
// Preserve native function appearance
Object.defineProperty(window, 'fetch', {
value: window.fetch,
writable: true,
configurable: true,
});
window.fetch.toString = () => 'function fetch() { [native code] }';
const _xhrOpen = XMLHttpRequest.prototype.open;
const _xhrSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
this.__fgUrl = typeof url === 'string' ? url : String(url || '');
return _xhrOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
if (
window.__fgFilterConfig.blockVideos &&
this.__fgUrl &&
(this.__fgUrl.includes('/api/v1/clips/') ||
this.__fgUrl.includes('/api/v1/discover/'))
) {
try { this.abort(); } catch (_) {}
return;
}
return _xhrSend.apply(this, arguments);
};
// Allow Flutter to update config flags
window.__fgSetFilterConfig = function (config) {
if (typeof config === 'object') {
Object.assign(window.__fgFilterConfig, config);
if (window.__fgDebugFilter) {
console.debug('[FocusGram Filter] Config updated:', window.__fgFilterConfig);
}
}
};
// Enable debug logging
window.__fgDebugFilter = false;
})();
+207
View File
@@ -0,0 +1,207 @@
/**
* FocusGram Ghost Mode
* Injected at DOCUMENT_START — before Instagram's JS loads.
* Blocks story-seen, message-seen, and online-presence signals.
*/
(function () {
'use strict';
// ─── Seen API patterns ────────────────────────────────────────────────────
const SEEN_PATTERNS = [
/\/api\/v1\/media\/[\w-]+\/seen\//,
/\/api\/v1\/stories\/reel\/seen\//,
/\/api\/v1\/direct_v2\/threads\/[\w-]+\/seen\//,
/\/api\/v1\/direct_v2\/visual_message\/[\w-]+\/seen\//,
/\/api\/v1\/live\/[\w-]+\/comment\/seen\//,
];
// ─── Activity patterns (like, comment) — intercepted for local history ────
const ACTIVITY_PATTERNS = [
/\/api\/v1\/web\/likes\/[\w-]+\/like\//,
/\/api\/v1\/web\/comments\/add\//,
/\/api\/v1\/friendships\/[\w-]+\/follow\//,
];
const isSeen = (url) => SEEN_PATTERNS.some((p) => p.test(url));
const isActivity = (url) => ACTIVITY_PATTERNS.some((p) => p.test(url));
const fakeOkResponse = () =>
new Response(JSON.stringify({ status: 'ok' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
// ─── Fetch override ───────────────────────────────────────────────────────
const _fetch = window.fetch.bind(window);
const patchedFetch = async function (input, init) {
const url =
typeof input === 'string'
? input
: input instanceof URL
? input.href
: input?.url ?? '';
// Block seen
if (isSeen(url)) {
if (window.GhostChannel) {
window.GhostChannel.postMessage(
JSON.stringify({ type: 'seen_blocked', url })
);
}
return fakeOkResponse();
}
// Intercept activity for local history
if (isActivity(url) && window.ActivityChannel) {
const body = init?.body;
const bodyText =
body instanceof URLSearchParams
? body.toString()
: typeof body === 'string'
? body
: '';
window.ActivityChannel.postMessage(
JSON.stringify({ url, body: bodyText, timestamp: Date.now() })
);
}
return _fetch(input, init);
};
// Disguise as native
Object.defineProperty(window, 'fetch', {
value: patchedFetch,
writable: true,
configurable: true,
enumerable: true,
});
window.fetch.toString = () => 'function fetch() { [native code] }';
window.fetch[Symbol.toStringTag] = 'fetch';
// ─── XMLHttpRequest override ──────────────────────────────────────────────
const _XHROpen = XMLHttpRequest.prototype.open;
const _XHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url, ...args) {
this._fg_url = url ?? '';
this._fg_method = (method ?? '').toUpperCase();
return _XHROpen.call(this, method, url, ...args);
};
XMLHttpRequest.prototype.send = function (body) {
if (this._fg_url && isSeen(this._fg_url)) {
// Fire readyState 4 with fake success without actually sending
const self = this;
setTimeout(() => {
Object.defineProperty(self, 'readyState', { get: () => 4 });
Object.defineProperty(self, 'status', { get: () => 200 });
Object.defineProperty(self, 'responseText', {
get: () => '{"status":"ok"}',
});
Object.defineProperty(self, 'response', {
get: () => '{"status":"ok"}',
});
self.dispatchEvent(new Event('readystatechange'));
self.dispatchEvent(new Event('load'));
}, 10);
return;
}
return _XHRSend.call(this, body);
};
// ─── WebSocket intercept (message-seen via WS) ────────────────────────────
// Strict WS URL blocking (ghost mode requirement)
// sid/cid vary per user/chat; block by endpoint prefix, not exact query.
const isBlockedWssUrl = (u) => {
if (!u) return false;
const urlStr = String(u);
return (
urlStr.startsWith('wss://gateway.instagram.com/ws/streamcontroller') ||
urlStr.startsWith('wss://edge-chat.instagram.com/chat?sid=')
);
};
// Signal to other injected scripts that ghost-mode is active
window.__fgGhostModeActive = true;
const _WS = window.WebSocket;
function PatchedWebSocket(url, protocols) {
const urlStr = typeof url === 'string' ? url : url?.toString?.() ?? '';
// If the WebSocket URL is one of the blocked endpoints, return an inert WS-like object
if (isBlockedWssUrl(urlStr)) {
return {
send: () => {},
close: () => {},
readyState: 1,
addEventListener: () => {},
removeEventListener: () => {},
};
}
const ws = protocols ? new _WS(url, protocols) : new _WS(url);
const _send = ws.send.bind(ws);
ws.send = function (data) {
if (typeof data === 'string') {
// IG sends seen ops as JSON with "op":"4" or "op":"seen" depending on version
try {
const parsed = JSON.parse(data);
if (
parsed?.op === '4' ||
parsed?.op === 'seen' ||
(parsed?.payload && JSON.parse(parsed.payload)?.op === 'seen')
) {
return; // drop
}
} catch (_) {}
// Text-based seen signal check
if (data.includes('"seen"') && data.includes('"thread_id"')) {
return;
}
}
return _send(data);
};
return ws;
}
// Preserve WebSocket prototype chain so IG's ws checks pass
PatchedWebSocket.prototype = _WS.prototype;
PatchedWebSocket.CONNECTING = _WS.CONNECTING;
PatchedWebSocket.OPEN = _WS.OPEN;
PatchedWebSocket.CLOSING = _WS.CLOSING;
PatchedWebSocket.CLOSED = _WS.CLOSED;
window.WebSocket = PatchedWebSocket;
// ─── Visibility trick — hide "Active Now" ────────────────────────────────
// Only applied if user enables online-status hiding
// Wrapped in a named fn so Flutter can call it:
// controller.evaluateJavascript(source: 'window.__fgEnableOnlineHide()')
window.__fgEnableOnlineHide = function () {
Object.defineProperty(document, 'visibilityState', {
get: () => 'hidden',
configurable: true,
});
Object.defineProperty(document, 'hidden', {
get: () => true,
configurable: true,
});
document.dispatchEvent(new Event('visibilitychange'));
};
window.__fgDisableOnlineHide = function () {
// Restore by deleting the overrides (falls back to native getter)
delete document.visibilityState;
delete document.hidden;
document.dispatchEvent(new Event('visibilitychange'));
};
// Signal to Flutter that ghost mode JS is active
if (window.GhostChannel) {
window.GhostChannel.postMessage(JSON.stringify({ type: 'ready' }));
}
})();
+89
View File
@@ -0,0 +1,89 @@
/**
* FocusGram Theme Detector
* Reads Instagram's background + bottom nav color and reports to Flutter.
* Injected at DOCUMENT_END so DOM is ready.
*/
(function () {
'use strict';
const parseRgb = (str) => {
// Parses "rgb(255, 255, 255)" or "rgba(0, 0, 0, 1)" → { r, g, b, a }
const m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (!m) return null;
return {
r: parseInt(m[1]),
g: parseInt(m[2]),
b: parseInt(m[3]),
a: m[4] !== undefined ? parseFloat(m[4]) : 1,
};
};
const toHex = ({ r, g, b }) =>
'#' +
[r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('');
const detectColors = () => {
// Background — Instagram sets it on <body> or a root div
const bodyBg = getComputedStyle(document.body).backgroundColor;
// Bottom nav — IG mobile web renders a fixed bottom bar
// Target by role="navigation" or position:fixed at bottom
let navBg = bodyBg;
const navCandidates = document.querySelectorAll(
'nav, [role="navigation"], div[style*="bottom"]'
);
for (const el of navCandidates) {
const style = getComputedStyle(el);
if (
style.position === 'fixed' &&
parseInt(style.bottom) <= 10 &&
style.backgroundColor !== 'rgba(0, 0, 0, 0)'
) {
navBg = style.backgroundColor;
break;
}
}
const bodyColor = parseRgb(bodyBg);
const navColor = parseRgb(navBg);
if (!bodyColor) return;
// Determine dark/light
const luminance = (0.299 * bodyColor.r + 0.587 * bodyColor.g + 0.114 * bodyColor.b) / 255;
const isDark = luminance < 0.5;
const payload = {
bodyHex: toHex(bodyColor),
navHex: navColor ? toHex(navColor) : toHex(bodyColor),
isDark,
};
if (window.ThemeChannel) {
window.ThemeChannel.postMessage(JSON.stringify(payload));
}
};
// Run on load
detectColors();
// Watch for Instagram's dark mode toggle (adds/removes class on <html>)
const observer = new MutationObserver(detectColors);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'style', 'color-scheme'],
});
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class', 'style'],
});
// Also run after navigation (Instagram is SPA, URL changes without reload)
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(detectColors, 300); // small delay for IG to render new page
}
}).observe(document.body, { childList: true, subtree: true });
})();