Files
FocusGram-Android/lib/services/injection_controller.dart
T
Ujwal e23731d9e8 feat:
Added Extra features like "Ghost Mode"
Added Logout Functionality
Chagned UA to latest version of iOS.
Added app icons.
Improved reels player and fixed bugs.
Discipline challenge is now compulsory for watching reels.
Now the UI doesnt feel like it is in a cheap browser.
2026-02-23 10:53:38 +05:45

279 lines
9.9 KiB
Dart

/// Controller for injecting custom JS and CSS into the WebView.
/// Uses a combination of static strings and dynamic builders to:
/// - Hide native navigation elements.
/// - Inject FocusGram branding into the native header.
/// - Implement "Ghost Mode" (stealth features).
/// - Manage Reels/Explore distractions.
class InjectionController {
/// The requested iOS 18.6 User Agent for Instagram App feel.
static const String iOSUserAgent =
'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/22G86 [FBAN/FBIOS;FBAV/531.0.0.35.77;FBBV/792629356;FBDV/iPhone17,2;FBMD/iPhone;FBSN/iOS;FBSV/18.6;FBSS/3;FBID/phone;FBLC/en_US;FBOP/5;FBRV/0;IABMV/1]';
// ── CSS & JS injection ──────────────────────────────────────────────────────
/// CSS to fix UI nuances like tap highlights.
static const String _globalUIFixesCSS = '''
* {
-webkit-tap-highlight-color: transparent !important;
outline: none !important;
}
''';
/// CSS to disable text selection globally.
static const String _disableSelectionCSS = '''
* {
-webkit-user-select: none !important;
user-select: none !important;
}
''';
/// Ghost Mode JS: Intercepts network calls to block seen/typing receipts.
static const String _ghostModeJS = '''
(function() {
const blockedUrls = [
'/api/v1/direct_v2/set_reel_seen/',
'/api/v1/direct_v2/threads/set_typing_status/',
'/api/v1/stories/reel/seen/',
'/api/v1/direct_v2/mark_visual_item_seen/'
];
// Proxy fetch
const originalFetch = window.fetch;
window.fetch = function(url, options) {
if (typeof url === 'string') {
if (blockedUrls.some(u => url.includes(u))) {
return Promise.resolve(new Response(null, { status: 204 }));
}
}
return originalFetch.apply(this, arguments);
};
// Proxy XHR
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
this._blocked = blockedUrls.some(u => url.includes(u));
return originalOpen.apply(this, arguments);
};
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
if (this._blocked) return;
return originalSend.apply(this, arguments);
};
})();
''';
/// Branding JS: Replaces Instagram logo with FocusGram while keeping icons.
static const String _brandingJS = '''
(function() {
function applyBranding() {
const igLogo = document.querySelector('svg[aria-label="Instagram"], svg[aria-label="Direct"]');
if (igLogo && !igLogo.dataset.focusgrammed) {
const container = igLogo.parentElement;
if (container) {
igLogo.style.display = 'none';
igLogo.dataset.focusgrammed = 'true';
const brandText = document.createElement('span');
brandText.innerText = 'FocusGram';
brandText.style.fontFamily = '"Grand Hotel", cursive';
brandText.style.fontSize = '24px';
brandText.style.color = 'white';
brandText.style.marginLeft = '8px';
brandText.style.verticalAlign = 'middle';
container.appendChild(brandText);
}
}
}
applyBranding();
const observer = new MutationObserver(applyBranding);
observer.observe(document.body, { childList: true, subtree: true });
})();
''';
/// Robust CSS that hides Instagram's native bottom nav bar.
static const String _hideInstagramNavCSS = '''
div[role="tablist"], nav[role="navigation"],
._acbl, ._aa4b, ._aahi, ._ab8s, section nav, footer nav {
display: none !important;
visibility: hidden !important;
height: 0 !important;
overflow: hidden !important;
pointer-events: none !important;
}
body, #react-root, main {
padding-bottom: 0 !important;
margin-bottom: 0 !important;
}
''';
/// CSS to hide Reel-related elements everywhere.
static const String _hideReelsCSS = '''
a[href*="/reel/"], a[href*="/reels"], [aria-label*="Reel"], [aria-label*="Reels"],
div[data-media-type="2"], [aria-label="Reels"], svg[aria-label="Reels"] {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
}
''';
/// CSS that adds bottom padding so feed content doesn't hide behind our bar.
static const String _bottomPaddingCSS = '''
body, #react-root > div, [role="presentation"] > div {
padding-bottom: 72px !important;
}
div[style*="bottom: 0px"], div[style*="bottom: 0"], form[method="POST"] {
padding-bottom: 72px !important;
}
div[role="main"] div[style*="position: fixed"] {
bottom: 72px !important;
}
''';
/// CSS to blur Explore feed posts/reels.
static const String _blurExploreCSS = '''
main[role="main"] section > div > div:not(:first-child) a img,
main[role="main"] section > div > div:not(:first-child) video,
main[role="main"] article img, main[role="main"] article video,
._aagv img, ._aagv video {
filter: blur(12px) !important;
pointer-events: none !important;
}
''';
static const String _blurReelsCSS = '''
a[href*="/reel/"] img, a[href*="/reels"] img {
filter: blur(12px) !important;
}
''';
/// Auto-dismiss "Open in App" banner.
static const String _dismissAppBannerJS = '''
(function dismissBanners() {
const selectors = ['[id*="app-banner"]', '[class*="app-banner"]', 'div[role="dialog"][aria-label*="app"]'];
selectors.forEach(sel => document.querySelectorAll(sel).forEach(el => el.remove()));
})();
''';
/// Periodic remover for bottom nav.
static const String _periodicNavRemoverJS = '''
(function periodicNavRemove() {
function removeNav() {
document.querySelectorAll('div[role="tablist"], nav[role="navigation"], footer nav').forEach(el => {
el.style.cssText += ';display:none!important;height:0!important;';
});
}
removeNav();
setInterval(removeNav, 500);
})();
''';
/// MutationObserver that continuously re-applies CSS.
static String _buildMutationObserver(String cssContent) =>
'''
(function applyFocusGramStyles() {
const STYLE_ID = 'focusgram-injected-style';
function injectCSS() {
let el = document.getElementById(STYLE_ID);
if (!el) {
el = document.createElement('style');
el.id = STYLE_ID;
document.head.appendChild(el);
}
el.textContent = ${_escapeJsString(cssContent)};
}
injectCSS();
const observer = new MutationObserver(() => {
if (!document.getElementById(STYLE_ID)) injectCSS();
});
observer.observe(document.documentElement, { childList: true, subtree: true });
})();
''';
static String _escapeJsString(String s) {
final escaped = s.replaceAll(r'\', r'\\').replaceAll('`', r'\`');
return '`$escaped`';
}
// ── Navigation helpers ──────────────────────────────────────────────────────
static String softNavigateJS(String path) =>
'''
(function() {
const target = ${_escapeJsString(path)};
if (window.location.pathname !== target) {
window.location.href = target;
}
})();
''';
static const String clickCreateButtonJS = '''
(function() {
const btn = document.querySelector('[aria-label="New post"], [aria-label="Create"]');
if (btn) btn.closest('a, button') ? btn.closest('a, button').click() : btn.click();
})();
''';
/// MutationObserver for Reel scroll locking.
static const String reelsMutationObserverJS = '''
(function() {
function lockReelScroll(reelContainer) {
if (reelContainer.dataset.scrollLocked) return;
reelContainer.dataset.scrollLocked = 'true';
let startY = 0;
reelContainer.addEventListener('touchstart', (e) => startY = e.touches[0].clientY, { passive: true });
reelContainer.addEventListener('touchmove', (e) => {
if (window.__focusgramSessionActive === true) return;
const deltaY = e.touches[0].clientY - startY;
if (deltaY < -10 && e.cancelable) {
e.preventDefault();
e.stopPropagation();
}
}, { passive: false });
}
const observer = new MutationObserver(() => {
document.querySelectorAll('[class*="ReelsVideoPlayer"], video').forEach((el) => {
if (el.tagName === 'VIDEO' && el.closest('article')) return;
lockReelScroll(el);
if (el.tagName === 'VIDEO' && el.parentElement) lockReelScroll(el.parentElement);
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
''';
static String buildSessionStateJS(bool active) =>
'window.__focusgramSessionActive = $active;';
/// Full injection JS to run on page load.
static String buildInjectionJS({
required bool sessionActive,
required bool blurExplore,
required bool blurReels,
required bool ghostMode,
required bool enableTextSelection,
}) {
final StringBuffer css = StringBuffer();
css.writeln(_globalUIFixesCSS);
if (!enableTextSelection) css.writeln(_disableSelectionCSS);
css.write(_hideInstagramNavCSS);
css.write(_bottomPaddingCSS);
if (!sessionActive) {
css.write(_hideReelsCSS);
if (blurExplore) css.write(_blurExploreCSS);
if (blurReels) css.write(_blurReelsCSS);
}
return '''
${buildSessionStateJS(sessionActive)}
${_buildMutationObserver(css.toString())}
$_periodicNavRemoverJS
$_dismissAppBannerJS
$reelsMutationObserverJS
$_brandingJS
${ghostMode ? _ghostModeJS : ''}
''';
}
}