mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-07-02 17:35:46 +02:00
Feature Pack with bug fixes for V2
This commit is contained in:
@@ -223,3 +223,36 @@ const String kAutoplayBlockerJS = r'''
|
||||
}, true);
|
||||
})();
|
||||
''';
|
||||
|
||||
// Reinforcement observer — catches videos that Instagram creates after the
|
||||
// prototype override (e.g. React re-renders). Runs a MutationObserver that
|
||||
// pauses any <video> that tries to autoplay.
|
||||
const String kAutoplayObserverJS = r'''
|
||||
(function fgAutoplayObserver() {
|
||||
if (window.__fgAutoplayObserverRunning) return;
|
||||
window.__fgAutoplayObserverRunning = true;
|
||||
|
||||
function pauseIfBlocked(v) {
|
||||
try {
|
||||
if (window.__fgBlockAutoplay === false) return;
|
||||
if (window.__focusgramSessionActive) return;
|
||||
const url = window.location.href || '';
|
||||
if (url.includes('/reels/') || url.includes('/reel/')) return;
|
||||
if (v.paused) return;
|
||||
if (v.getAttribute('data-fg-user-played') === '1') return;
|
||||
v.pause();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Check all existing videos periodically
|
||||
setInterval(function() {
|
||||
document.querySelectorAll('video').forEach(pauseIfBlocked);
|
||||
}, 500);
|
||||
|
||||
// Mark video as user-played on click
|
||||
document.addEventListener('click', function(e) {
|
||||
var v = e.target && e.target.closest ? e.target.closest('video') : null;
|
||||
if (v) v.setAttribute('data-fg-user-played', '1');
|
||||
}, true);
|
||||
})();
|
||||
''';
|
||||
|
||||
@@ -40,8 +40,11 @@ const String kBlurHomeFeedAndExploreCSS = '''
|
||||
transition: filter 0.15s ease !important;
|
||||
}
|
||||
/* Per-post unblur override (set by kTapToUnblurJS) */
|
||||
[data-fg-unblurred="1"] img,
|
||||
[data-fg-unblurred="1"] video {
|
||||
/* Must match the blur selector's specificity (body[path="/"] article img = 0,0,1,3) */
|
||||
body[path="/"] [data-fg-unblurred="1"] img,
|
||||
body[path="/"] [data-fg-unblurred="1"] video,
|
||||
body[path^="/explore"] [data-fg-unblurred="1"] img,
|
||||
body[path^="/explore"] [data-fg-unblurred="1"] video {
|
||||
filter: none !important;
|
||||
-webkit-filter: none !important;
|
||||
}
|
||||
@@ -149,6 +152,15 @@ const String kTapToUnblurJS = r'''
|
||||
}
|
||||
}
|
||||
|
||||
function unblurAllMediaInHost(host) {
|
||||
try {
|
||||
host.querySelectorAll('img,video').forEach(function(el) {
|
||||
el.style.setProperty('filter', 'none', 'important');
|
||||
el.style.setProperty('-webkit-filter', 'none', 'important');
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function unblurMedia(media) {
|
||||
try {
|
||||
media.style.setProperty('filter', 'none', 'important');
|
||||
@@ -164,11 +176,15 @@ const String kTapToUnblurJS = r'''
|
||||
if (!media) return;
|
||||
const host = getHost(media);
|
||||
if (!host) return;
|
||||
if (isUnblurred(host)) return; // allow normal Instagram behaviour
|
||||
|
||||
// ALWAYS re-unblur media — Instagram swaps DOM elements in carousels,
|
||||
// so the inline style applied on first tap is lost on subsequent pages.
|
||||
unblurMedia(media);
|
||||
|
||||
if (isUnblurred(host)) return; // allow normal Instagram click-through
|
||||
|
||||
// First tap: unblur and swallow click so it doesn't open the post.
|
||||
markUnblurred(host);
|
||||
unblurMedia(media);
|
||||
if (e.cancelable) e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} catch (_) {}
|
||||
|
||||
+410
-54
@@ -1,77 +1,413 @@
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import '../focus_settings.dart';
|
||||
|
||||
// Ghost Mode
|
||||
const String ghostModeJS = '''
|
||||
const _WS = window.WebSocket;
|
||||
window.WebSocket = function(url, protocols) {
|
||||
if (url.includes('edge-chat.instagram.com') ||
|
||||
url.includes('gateway.instagram.com')) {
|
||||
return {
|
||||
send: ()=>{}, close: ()=>{},
|
||||
readyState: 1,
|
||||
addEventListener: ()=>{},
|
||||
removeEventListener: ()=>{},
|
||||
};
|
||||
/// Flutter sets these flags after settings load to enable ghost modes.
|
||||
/// Must be called from onWebViewCreated or on settings change.
|
||||
const String kSetGhostFlagsJS = '''
|
||||
(function(){
|
||||
// Placeholder — Flutter replaces these with actual setting values:
|
||||
// window.__fgPartialGhost = true/false;
|
||||
// window.__fgFullDmGhost = true/false;
|
||||
// window.__fgStoryGhost = true/false;
|
||||
// window.__fgGhostReady = true; // signals scripts can proceed
|
||||
})();
|
||||
''';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// PARTIAL GHOST MODE — existing behavior
|
||||
// Blocks seen API patterns, WebSocket chat gateways, and uses
|
||||
// first-click gate for api/graphql on /direct/* (inbox loads, then block).
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
const String kPartialGhostJS = r'''
|
||||
(function() {
|
||||
if (window.__fgPartialGhostPatched) return;
|
||||
window.__fgPartialGhostPatched = true;
|
||||
|
||||
// ── Seen API patterns ──────────────────────────────────────
|
||||
var SEEN = [/\/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\//];
|
||||
function isSeen(u) { for(var i=0;i<SEEN.length;i++){if(SEEN[i].test(u))return true;}return false; }
|
||||
|
||||
// ── First-click gate for api/graphql on /direct/* ──────────
|
||||
window.__fgDirectApiBlocked = false;
|
||||
document.addEventListener('click',function(){
|
||||
if(window.location.pathname.indexOf('/direct/')===0) window.__fgDirectApiBlocked=true;
|
||||
},true);
|
||||
document.addEventListener('touchstart',function(){
|
||||
if(window.location.pathname.indexOf('/direct/')===0) window.__fgDirectApiBlocked=true;
|
||||
},true);
|
||||
var _prevD=window.location.pathname.indexOf('/direct/')===0;
|
||||
setInterval(function(){
|
||||
var n=window.location.pathname.indexOf('/direct/')===0;
|
||||
if(n!==_prevD){_prevD=n;window.__fgDirectApiBlocked=false;}
|
||||
},300);
|
||||
|
||||
function partialEnabled() { return window.__fgPartialGhost===true; }
|
||||
function shouldBlock(u) {
|
||||
if (!partialEnabled()) return false;
|
||||
return window.location.pathname.indexOf('/direct/')===0 &&
|
||||
window.__fgDirectApiBlocked &&
|
||||
u.indexOf('/api/graphql')!==-1;
|
||||
}
|
||||
return new _WS(url, protocols);
|
||||
};
|
||||
window.WebSocket.prototype = _WS.prototype;
|
||||
|
||||
// ── Fetch override (chain with previous fetch) ─────────────
|
||||
var _prevFetch = window.fetch;
|
||||
window.fetch=function(i,init){
|
||||
var u=(typeof i==='string')?i:(i&&i.url)?i.url:'';
|
||||
if(partialEnabled()&&(isSeen(u)||shouldBlock(u))) return Promise.resolve(new Response(JSON.stringify({status:'ok'}),{status:200,headers:{'Content-Type':'application/json'}}));
|
||||
return _prevFetch.call(window,i,init);
|
||||
};
|
||||
|
||||
// ── XHR override (chain) ───────────────────────────────────
|
||||
var _prevOpen=XMLHttpRequest.prototype.open,_prevSend=XMLHttpRequest.prototype.send;
|
||||
XMLHttpRequest.prototype.open=function(m,u){this.__fgU=u||'';return _prevOpen.apply(this,arguments);};
|
||||
XMLHttpRequest.prototype.send=function(b){
|
||||
if(partialEnabled()&&(isSeen(this.__fgU||'')||shouldBlock(this.__fgU||''))){
|
||||
var self=this;setTimeout(function(){
|
||||
Object.defineProperty(self,'readyState',{get:function(){return 4}});
|
||||
Object.defineProperty(self,'status',{get:function(){return 200}});
|
||||
Object.defineProperty(self,'responseText',{get:function(){return '{"status":"ok"}'}});
|
||||
Object.defineProperty(self,'response',{get:function(){return '{"status":"ok"}'}});
|
||||
try{self.onreadystatechange&&self.onreadystatechange();}catch(e){}
|
||||
try{self.onload&&self.onload();}catch(e){}
|
||||
['readystatechange','load'].forEach(function(t){try{self.dispatchEvent(new Event(t));}catch(e){}});
|
||||
},5);return;
|
||||
}
|
||||
return _prevSend.apply(this,arguments);
|
||||
};
|
||||
|
||||
// ── Selective WS seen-message filter (no gouger) ───────────
|
||||
(function() {
|
||||
var _WS = window.WebSocket;
|
||||
function PartialWS(url, protocols) {
|
||||
var ws = protocols ? new _WS(url, protocols) : new _WS(url);
|
||||
var _send = ws.send.bind(ws);
|
||||
ws.send = function(data) {
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
var parsed = JSON.parse(data);
|
||||
if (parsed && (parsed.op === '4' || parsed.op === 'seen')) return;
|
||||
} catch(e) {}
|
||||
if (data.indexOf('"seen"') !== -1 && data.indexOf('"thread_id"') !== -1) return;
|
||||
}
|
||||
return _send(data);
|
||||
};
|
||||
return ws;
|
||||
}
|
||||
PartialWS.prototype = _WS.prototype;
|
||||
PartialWS.CONNECTING = _WS.CONNECTING;
|
||||
PartialWS.OPEN = _WS.OPEN;
|
||||
PartialWS.CLOSING = _WS.CLOSING;
|
||||
PartialWS.CLOSED = _WS.CLOSED;
|
||||
window.WebSocket = PartialWS;
|
||||
})();
|
||||
})();
|
||||
''';
|
||||
|
||||
// No Story Tray
|
||||
const String hideStoryTrayJS = '''
|
||||
const style = document.createElement('style');
|
||||
style.textContent = '[data-pagelet="story_tray"] { display: none !important; }';
|
||||
document.head.appendChild(style);
|
||||
''';
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// FULL DM GHOST — blocks ALL api/graphql on /direct/* immediately
|
||||
// (inbox won't load, messages can't be sent)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
const String kFullDmGhostJS = r'''
|
||||
(function() {
|
||||
if (window.__fgFullDmGhostPatched) return;
|
||||
window.__fgFullDmGhostPatched = true;
|
||||
|
||||
// No Autoplay
|
||||
const String noAutoplayJS = '''
|
||||
document.addEventListener('play', function(e) {
|
||||
if (e.target.tagName === 'VIDEO') {
|
||||
e.target.pause();
|
||||
// ── Smart path-based blocking ──────────────────────────────
|
||||
// /direct/inbox/ → allow (inbox loads)
|
||||
// /direct/t/* → block ALL api/graphql immediately
|
||||
// any /direct/* → block except /direct/inbox/
|
||||
function shouldBlockDmPath() {
|
||||
if (window.__fgFullDmGhost !== true) return false;
|
||||
var p = window.location.pathname;
|
||||
if (p.indexOf('/direct/') !== 0) return false;
|
||||
if (p === '/direct/inbox/' || p === '/direct/inbox') return false;
|
||||
return true;
|
||||
}
|
||||
}, true);
|
||||
|
||||
// ── DM URL blocklist ───────────────────────────────────────
|
||||
var DM_URLS = [
|
||||
/\\/api\\/v1\\/direct_v2\\/threads\\/[^/]+\\/mark_item_seen\\//,
|
||||
/\\/api\\/v1\\/direct_v2\\/mark_item_seen\\//,
|
||||
/\\/api\\/v1\\/direct_v2\\/threads\\/[^/]+\\/items\\/[^/]+\\/mark_visual_item_seen\\//,
|
||||
/\\/api\\/v1\\/direct_v2\\/visual_thread\\/[^/]+\\/seen\\//,
|
||||
/\\/api\\/v1\\/direct_v2\\/threads\\/[^/]+\\/items\\/[^/]+\\/mark_audio_seen\\//,
|
||||
/\\/api\\/v1\\/live\\/[^/]+\\/join\\//,
|
||||
/\\/api\\/v1\\/live\\/[^/]+\\/get_join_requests\\//,
|
||||
/\\/api\\/v1\\/media\\/seen\\//,
|
||||
/\\/api\\/v1\\/feed\\/viewed_story\\//,
|
||||
/\\/api\\/v1\\/feed\\/reels_tray\\/seen\\//,
|
||||
/\\/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\\//,
|
||||
/\\/api\\/v1\\/qe\\//,
|
||||
/\\/api\\/v1\\/launcher\\/sync\\//,
|
||||
/\\/api\\/v1\\/logging\\//,
|
||||
/\\/api\\/v1\\/fb_onetap_logging\\//,
|
||||
/\\/ajax\\/bz/,
|
||||
/\\/ajax\\/logging\\//,
|
||||
/\\/api\\/v1\\/stats\\//,
|
||||
/\\/api\\/v1\\/fbanalytics\\//,
|
||||
];
|
||||
|
||||
function matchUrl(url) {
|
||||
if (!url) return false;
|
||||
for (var i = 0; i < DM_URLS.length; i++) { if (DM_URLS[i].test(url)) return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── DM GraphQL operations ──────────────────────────────────
|
||||
var DM_OPS = [
|
||||
'MarkDirectThreadItemSeen','markDirectThreadItemSeen',
|
||||
'DirectMarkItemSeen','DirectThreadMarkSeen',
|
||||
'MarkVisualMessageSeen','DirectMarkVisualItemSeen',
|
||||
'MarkAudioMessageSeen','AudioSeenMutation',
|
||||
'LiveJoinBroadcast','JoinLiveBroadcast','MarkLiveViewer',
|
||||
'MarkStorySeen','markStorySeen','ReelSeenMutation','reel_seen','IgFeedSeen',
|
||||
'LogImpression','LogClick','FeedbackSeenMutation',
|
||||
];
|
||||
|
||||
function matchGraphQL(body) {
|
||||
if (!body) return false;
|
||||
var str = typeof body === 'string' ? body : String(body);
|
||||
for (var i = 0; i < DM_OPS.length; i++) { if (str.indexOf(DM_OPS[i]) !== -1) return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
function isGraphql(url) {
|
||||
return url.indexOf('/api/graphql') !== -1 || url.indexOf('/graphql') !== -1;
|
||||
}
|
||||
|
||||
function shouldBlock(url, init) {
|
||||
// 1. Path-based: on /direct/t/* block ALL graphql
|
||||
if (shouldBlockDmPath() && isGraphql(url)) return true;
|
||||
// 2. URL blocklist match
|
||||
if (matchUrl(url)) return true;
|
||||
// 3. GraphQL body op-name match
|
||||
if (isGraphql(url) && init) {
|
||||
var bs = '';
|
||||
if (typeof init.body === 'string') bs = init.body;
|
||||
else if (init.body && init.body.toString) bs = init.body.toString();
|
||||
if (matchGraphQL(bs)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function fakeOk() { return new Response(JSON.stringify({status:'ok'}),{status:200,headers:{'Content-Type':'application/json'}}); }
|
||||
|
||||
// ── Fetch override (chain) ─────────────────────────────────
|
||||
var _prevFetch = window.fetch;
|
||||
window.fetch = function(i, init) {
|
||||
var u = (typeof i === 'string') ? i : (i && i.url) ? i.url : String(i);
|
||||
if (shouldBlock(u, init)) return Promise.resolve(fakeOk());
|
||||
return _prevFetch.apply(this, arguments);
|
||||
};
|
||||
|
||||
// ── XHR override (chain) ───────────────────────────────────
|
||||
var _prevOpen = XMLHttpRequest.prototype.open;
|
||||
var _prevSend = XMLHttpRequest.prototype.send;
|
||||
XMLHttpRequest.prototype.open = function(m, u) { this.__fgDU = u || ''; return _prevOpen.apply(this, arguments); };
|
||||
XMLHttpRequest.prototype.send = function(b) {
|
||||
var u = this.__fgDU || '';
|
||||
if (shouldBlock(u, {body: b}) || (isGraphql(u) && shouldBlockDmPath())) {
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
Object.defineProperty(self,'readyState',{get:function(){return 4}});
|
||||
Object.defineProperty(self,'status',{get:function(){return 200}});
|
||||
Object.defineProperty(self,'responseText',{get:function(){return '{"status":"ok"}'}});
|
||||
Object.defineProperty(self,'response',{get:function(){return '{"status":"ok"}'}});
|
||||
try{self.onreadystatechange&&self.onreadystatechange();}catch(e){}
|
||||
try{self.onload&&self.onload();}catch(e){}
|
||||
['readystatechange','load'].forEach(function(t){try{self.dispatchEvent(new Event(t));}catch(e){}});
|
||||
}, 5);
|
||||
return;
|
||||
}
|
||||
return _prevSend.apply(this, arguments);
|
||||
};
|
||||
|
||||
// ── SW killer ──────────────────────────────────────────────
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register = function() { return Promise.reject(new Error('blocked')); };
|
||||
navigator.serviceWorker.getRegistrations().then(function(regs) { regs.forEach(function(r) { r.unregister(); }); }).catch(function(){});
|
||||
}
|
||||
|
||||
// ── Beacon blocker ─────────────────────────────────────────
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon = function(url) { return true; };
|
||||
}
|
||||
|
||||
// ── MQTT WS intercept (typing / live viewer) ───────────────
|
||||
(function() {
|
||||
var _WS = window.WebSocket;
|
||||
function DmGhostWS(url, protocols) {
|
||||
var ws = protocols ? new _WS(url, protocols) : new _WS(url);
|
||||
var _send = ws.send.bind(ws);
|
||||
ws.send = function(data) {
|
||||
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
||||
var bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
||||
var packetType = bytes[0] & 0xF0;
|
||||
if (packetType === 0x30) {
|
||||
try {
|
||||
var decoded = new TextDecoder('utf-8').decode(bytes);
|
||||
if (decoded.indexOf('/t_fs') !== -1 || decoded.indexOf('activity_indicator') !== -1 ||
|
||||
decoded.indexOf('is_typing') !== -1 || decoded.indexOf('direct_typing') !== -1 ||
|
||||
decoded.indexOf('/live/viewer') !== -1 || decoded.indexOf('live_viewer_list') !== -1) {
|
||||
return;
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
} else if (typeof data === 'string') {
|
||||
if (data.indexOf('typing') !== -1 || data.indexOf('live_viewer') !== -1 || data.indexOf('is_typing') !== -1) return;
|
||||
}
|
||||
return _send(data);
|
||||
};
|
||||
return ws;
|
||||
}
|
||||
DmGhostWS.prototype = _WS.prototype;
|
||||
DmGhostWS.CONNECTING = _WS.CONNECTING;
|
||||
DmGhostWS.OPEN = _WS.OPEN;
|
||||
DmGhostWS.CLOSING = _WS.CLOSING;
|
||||
DmGhostWS.CLOSED = _WS.CLOSED;
|
||||
window.WebSocket = DmGhostWS;
|
||||
})();
|
||||
})();
|
||||
''';
|
||||
|
||||
// No Reels / Explore
|
||||
const String hideReelsJS = '''
|
||||
const hideReels = () => {
|
||||
// nav bar reels icon
|
||||
document.querySelectorAll('a[href="/reels/"]').forEach(el => {
|
||||
el.closest('div')?.style.setProperty('display', 'none', 'important');
|
||||
});
|
||||
// explore page
|
||||
document.querySelectorAll('a[href="/explore/"]').forEach(el => {
|
||||
el.closest('div')?.style.setProperty('display', 'none', 'important');
|
||||
});
|
||||
};
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// STORY GHOST — blocks api/graphql on homepage (/) and /stories/*
|
||||
// Allows viewing stories without sending seen indicators.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
const String kStoryGhostJS = r'''
|
||||
(function() {
|
||||
if (window.__fgStoryGhostPatched) return;
|
||||
window.__fgStoryGhostPatched = true;
|
||||
|
||||
new MutationObserver(hideReels).observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
// ── Smart path-based blocking ──────────────────────────────
|
||||
// On /, /stories/*, /story/* → block ALL api/graphql
|
||||
// On /direct/inbox/ → allow (DMs need graphql to load messages)
|
||||
function shouldBlockByPath() {
|
||||
if (window.__fgStoryGhost !== true) return false;
|
||||
var p = window.location.pathname;
|
||||
// Don't block on DM pages
|
||||
if (p.indexOf('/direct/') === 0) return false;
|
||||
var isStory = p.indexOf('/stories/') === 0 || p.indexOf('/story/') === 0;
|
||||
var isHome = p === '/' || p === '';
|
||||
return isHome || isStory;
|
||||
}
|
||||
|
||||
hideReels();
|
||||
''';
|
||||
|
||||
// No DMs
|
||||
const String hideDMsJS = '''
|
||||
const style = document.createElement('style');
|
||||
style.textContent = 'a[href="/direct/inbox/"] { display: none !important; }';
|
||||
document.head.appendChild(style);
|
||||
// ── Story URL blocklist ────────────────────────────────────
|
||||
var STORY_URLS = [
|
||||
/\\/api\\/v1\\/media\\/[\\w-]+\\/seen\\//,
|
||||
/\\/api\\/v1\\/stories\\/reel\\/seen\\//,
|
||||
/\\/api\\/v1\\/feed\\/viewed_story\\//,
|
||||
/\\/api\\/v1\\/feed\\/reels_tray\\/seen\\//,
|
||||
/\\/api\\/v1\\/media\\/seen\\//,
|
||||
];
|
||||
|
||||
function matchUrl(url) {
|
||||
if (!url) return false;
|
||||
for (var i = 0; i < STORY_URLS.length; i++) { if (STORY_URLS[i].test(url)) return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Story GraphQL operations ───────────────────────────────
|
||||
var STORY_OPS = [
|
||||
'MarkStorySeen','markStorySeen','ReelSeenMutation','reel_seen','IgFeedSeen',
|
||||
'FeedbackSeenMutation',
|
||||
];
|
||||
|
||||
function matchGraphQL(body) {
|
||||
if (!body) return false;
|
||||
var str = typeof body === 'string' ? body : String(body);
|
||||
for (var i = 0; i < STORY_OPS.length; i++) { if (str.indexOf(STORY_OPS[i]) !== -1) return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
function isGraphql(url) {
|
||||
return url.indexOf('/api/graphql') !== -1 || url.indexOf('/graphql') !== -1;
|
||||
}
|
||||
|
||||
function shouldBlock(url, init) {
|
||||
// 1. Path-based: on story pages block ALL graphql
|
||||
if (shouldBlockByPath() && isGraphql(url)) return true;
|
||||
// 2. URL blocklist match
|
||||
if (matchUrl(url)) return true;
|
||||
// 3. GraphQL body op-name match
|
||||
if (isGraphql(url) && init) {
|
||||
var bs = '';
|
||||
if (typeof init.body === 'string') bs = init.body;
|
||||
else if (init.body && init.body.toString) bs = init.body.toString();
|
||||
if (matchGraphQL(bs)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function fakeOk() { return new Response(JSON.stringify({status:'ok'}),{status:200,headers:{'Content-Type':'application/json'}}); }
|
||||
|
||||
// ── Fetch override (chain) ─────────────────────────────────
|
||||
var _prevFetch = window.fetch;
|
||||
window.fetch = function(i, init) {
|
||||
var u = (typeof i === 'string') ? i : (i && i.url) ? i.url : String(i);
|
||||
if (shouldBlock(u, init)) return Promise.resolve(fakeOk());
|
||||
return _prevFetch.apply(this, arguments);
|
||||
};
|
||||
|
||||
// ── XHR override (chain) ───────────────────────────────────
|
||||
var _prevOpen = XMLHttpRequest.prototype.open;
|
||||
var _prevSend = XMLHttpRequest.prototype.send;
|
||||
XMLHttpRequest.prototype.open = function(m, u) { this.__fgSU = u || ''; return _prevOpen.apply(this, arguments); };
|
||||
XMLHttpRequest.prototype.send = function(b) {
|
||||
var u = this.__fgSU || '';
|
||||
if (shouldBlock(u, {body: b}) || (isGraphql(u) && shouldBlockByPath())) {
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
Object.defineProperty(self,'readyState',{get:function(){return 4}});
|
||||
Object.defineProperty(self,'status',{get:function(){return 200}});
|
||||
Object.defineProperty(self,'responseText',{get:function(){return '{"status":"ok"}'}});
|
||||
Object.defineProperty(self,'response',{get:function(){return '{"status":"ok"}'}});
|
||||
try{self.onreadystatechange&&self.onreadystatechange();}catch(e){}
|
||||
try{self.onload&&self.onload();}catch(e){}
|
||||
['readystatechange','load'].forEach(function(t){try{self.dispatchEvent(new Event(t));}catch(e){}});
|
||||
}, 5);
|
||||
return;
|
||||
}
|
||||
return _prevSend.apply(this, arguments);
|
||||
};
|
||||
|
||||
// ── SW killer ──────────────────────────────────────────────
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register = function() { return Promise.reject(new Error('blocked')); };
|
||||
navigator.serviceWorker.getRegistrations().then(function(regs) { regs.forEach(function(r) { r.unregister(); }); }).catch(function(){});
|
||||
}
|
||||
|
||||
// ── Beacon blocker ─────────────────────────────────────────
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon = function(url) { return true; };
|
||||
}
|
||||
})();
|
||||
''';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Builder — injects the right scripts based on settings
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
List<UserScript> buildUserScripts(FocusSettings settings) {
|
||||
final startScripts = <String>[];
|
||||
final endScripts = <String>[];
|
||||
|
||||
// AT_DOCUMENT_START scripts
|
||||
if (settings.ghostMode) startScripts.add(ghostModeJS);
|
||||
// Prepend flag values directly into the script so they survive page navigation.
|
||||
// (evaluateJavascript-set flags are destroyed when the JS context resets on load.)
|
||||
// DM Ghost uses the comprehensive Full DM approach (URL blocklist, GraphQL ops, SW killer, beacon, WS).
|
||||
// it should have worked, but sadly it didnt
|
||||
if (settings.ghostMode)
|
||||
startScripts.add('window.__fgFullDmGhost=true;' + kFullDmGhostJS);
|
||||
if (settings.noAutoplay) startScripts.add(noAutoplayJS);
|
||||
|
||||
// AT_DOCUMENT_END scripts
|
||||
// AT_DOCUMENT_END
|
||||
if (settings.noStories) endScripts.add(hideStoryTrayJS);
|
||||
if (settings.noReels) endScripts.add(hideReelsJS);
|
||||
if (settings.noDMs) endScripts.add(hideDMsJS);
|
||||
@@ -97,3 +433,23 @@ List<UserScript> buildUserScripts(FocusSettings settings) {
|
||||
}
|
||||
return scripts;
|
||||
}
|
||||
|
||||
// ── Existing non-ghost helpers (unchanged) ───────────────────
|
||||
|
||||
const String noAutoplayJS = '''
|
||||
document.addEventListener('play', function(e) {
|
||||
if (e.target.tagName === 'VIDEO') e.target.pause();
|
||||
}, true);
|
||||
''';
|
||||
|
||||
const String hideStoryTrayJS = '''
|
||||
(function(){var s=document.createElement('style');s.textContent='[data-pagelet="story_tray"]{display:none!important}';document.head.appendChild(s);})();
|
||||
''';
|
||||
|
||||
const String hideReelsJS = '''
|
||||
(function(){new MutationObserver(function(){document.querySelectorAll('a[href="/reels/"]').forEach(function(e){var p=e.closest('div');if(p)p.style.setProperty('display','none','important')});document.querySelectorAll('a[href="/explore/"]').forEach(function(e){var p=e.closest('div');if(p)p.style.setProperty('display','none','important')})}).observe(document.body,{childList:true,subtree:true});})();
|
||||
''';
|
||||
|
||||
const String hideDMsJS = '''
|
||||
(function(){var s=document.createElement('style');s.textContent='a[href="/direct/inbox/"]{display:none!important}';document.head.appendChild(s);})();
|
||||
''';
|
||||
|
||||
@@ -15,8 +15,8 @@ const String kReelMetadataExtractorScript = r'''
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a reel page
|
||||
if (!currentUrl.includes('/reel/')) {
|
||||
// Check if this is a reel page (Instagram uses /reels/ not /reel/)
|
||||
if (!currentUrl.includes('/reels/') && !currentUrl.includes('/reel/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user