mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-06-07 06:03:53 +02:00
fe2d793b93
bugfixes added word libraby (~500 words)
365 lines
13 KiB
Dart
365 lines
13 KiB
Dart
/// Generates all CSS and JavaScript injection strings for the WebView.
|
|
///
|
|
/// Strategy:
|
|
/// - Instagram's own bottom nav bar is hidden via both CSS and a periodic JS
|
|
/// removal loop, since SPA re-renders can outpace MutationObserver.
|
|
/// - Reel elements are hidden/blurred based on settings/session state.
|
|
/// - A MutationObserver keeps re-applying the rules after SPA re-renders.
|
|
/// - App-install banners are auto-dismissed.
|
|
class InjectionController {
|
|
/// iOS Safari user-agent — reduces login friction with Instagram.
|
|
static const String iOSUserAgent =
|
|
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) '
|
|
'AppleWebKit/605.1.15 (KHTML, like Gecko) '
|
|
'Version/17.0 Mobile/15E148 Safari/604.1';
|
|
|
|
// ── CSS injection ───────────────────────────────────────────────────────────
|
|
|
|
/// Robust CSS that hides Instagram's native bottom nav bar.
|
|
/// Covers all known selector patterns including dynamic class names.
|
|
static const String _hideInstagramNavCSS = '''
|
|
/* ── Instagram bottom navigation bar — hide completely ── */
|
|
/* Role-based selectors */
|
|
div[role="tablist"],
|
|
nav[role="navigation"],
|
|
/* Fixed-position bottom bar */
|
|
div[style*="position: fixed"][style*="bottom"],
|
|
div[style*="position:fixed"][style*="bottom"],
|
|
/* Instagram legacy class names */
|
|
._acbl, ._aa4b, ._aahi, ._ab8s,
|
|
/* Section nav elements */
|
|
section nav,
|
|
/* Any nav inside the main app shell */
|
|
#react-root nav,
|
|
/* The outer wrapper of the bottom bar (PWA/mobile web) */
|
|
[class*="x1n2onr6"][class*="x1vjfegm"] > nav,
|
|
/* Catch-all: any fixed bottom element containing nav links */
|
|
footer nav,
|
|
div[class*="bottom"] nav {
|
|
display: none !important;
|
|
visibility: hidden !important;
|
|
height: 0 !important;
|
|
overflow: hidden !important;
|
|
pointer-events: none !important;
|
|
}
|
|
/* Ensure the body doesn't add bottom padding for the nav */
|
|
body, #react-root, main {
|
|
padding-bottom: 0 !important;
|
|
margin-bottom: 0 !important;
|
|
}
|
|
''';
|
|
|
|
/// CSS to hide Reel-related elements everywhere (feed, profile, search).
|
|
/// Used when session is NOT active.
|
|
static const String _hideReelsCSS = '''
|
|
/* Hide reel thumbnails and links */
|
|
a[href*="/reel/"],
|
|
a[href*="/reels"],
|
|
[aria-label*="Reel"],
|
|
[aria-label*="Reels"],
|
|
div[data-media-type="2"],
|
|
/* Profile grid reel filter tabs */
|
|
[aria-label="Reels"],
|
|
/* Reel indicators on feed thumbnails */
|
|
svg[aria-label="Reels"],
|
|
/* Video/reel chips in feed */
|
|
[class*="reel"],
|
|
[class*="Reel"] {
|
|
display: none !important;
|
|
visibility: hidden !important;
|
|
pointer-events: none !important;
|
|
}
|
|
''';
|
|
|
|
/// CSS that adds bottom padding so feed content doesn't hide behind our bar.
|
|
/// Added more selectors to cover dynamic drawers like Notes and Reactions.
|
|
static const String _bottomPaddingCSS = '''
|
|
body, #react-root > div, [role="presentation"] > div {
|
|
padding-bottom: 72px !important;
|
|
}
|
|
/* Special handling for dynamic bottom drawers */
|
|
div[style*="bottom: 0px"], div[style*="bottom: 0"] {
|
|
padding-bottom: 72px !important;
|
|
}
|
|
''';
|
|
|
|
/// CSS to push IG content down so it doesn't hide behind our status bar.
|
|
static const String _topPaddingCSS = '''
|
|
header, #react-root > div > div > div:first-child {
|
|
margin-top: 44px !important;
|
|
}
|
|
/* Shift fixed headers down */
|
|
div[style*="position: fixed"][style*="top: 0"] {
|
|
top: 44px !important;
|
|
}
|
|
''';
|
|
|
|
/// CSS to blur Explore feed posts/reels (keeps stories visible).
|
|
static const String _blurExploreCSS = '''
|
|
/* Blur Explore grid posts and reel cards (not stories row) */
|
|
main[role="main"] section > div > div:not(:first-child) a img,
|
|
main[role="main"] section > div > div:not(:first-child) video,
|
|
main[role="main"] section > div > div:not(:first-child) [class*="x6s0dn4"],
|
|
main[role="main"] article img,
|
|
main[role="main"] article video,
|
|
/* Explore page grid */
|
|
._aagv img,
|
|
._aagv video {
|
|
filter: blur(12px) !important;
|
|
pointer-events: none !important;
|
|
}
|
|
/* Overlay to block tapping blurred content */
|
|
._aagv::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 99;
|
|
cursor: not-allowed;
|
|
}
|
|
._aagv {
|
|
position: relative !important;
|
|
overflow: hidden !important;
|
|
}
|
|
''';
|
|
|
|
/// Auto-dismiss "Open in App" banner that Instagram shows in mobile browsers.
|
|
static const String _dismissAppBannerJS = '''
|
|
(function dismissBanners() {
|
|
const selectors = [
|
|
'[id*="app-banner"]',
|
|
'[class*="app-banner"]',
|
|
'[data-testid*="app-banner"]',
|
|
'div[role="dialog"][aria-label*="app"]',
|
|
'div[role="dialog"][aria-label*="App"]',
|
|
];
|
|
selectors.forEach(sel => {
|
|
document.querySelectorAll(sel).forEach(el => el.remove());
|
|
});
|
|
})();
|
|
''';
|
|
|
|
/// Periodic remover: every 500ms force-removes the bottom nav.
|
|
/// Complements the MutationObserver for sites that rebuild DOM faster.
|
|
static const String _periodicNavRemoverJS = '''
|
|
(function periodicNavRemove() {
|
|
function removeNav() {
|
|
// Target all fixed-bottom elements that could be the nav bar
|
|
document.querySelectorAll([
|
|
'div[role="tablist"]',
|
|
'nav[role="navigation"]',
|
|
'._acbl', '._aa4b', '._aahi', '._ab8s',
|
|
'section nav',
|
|
'footer nav'
|
|
].join(',')).forEach(function(el) {
|
|
el.style.cssText += ';display:none!important;height:0!important;overflow:hidden!important;';
|
|
});
|
|
// Also hide any element that is fixed at the bottom and contains nav links
|
|
document.querySelectorAll('div[style]').forEach(function(el) {
|
|
const s = el.style;
|
|
if ((s.position === 'fixed' || s.position === 'sticky') &&
|
|
(s.bottom === '0px' || s.bottom === '0') &&
|
|
el.querySelector('a,button')) {
|
|
el.style.cssText += ';display:none!important;';
|
|
}
|
|
});
|
|
}
|
|
removeNav();
|
|
setInterval(removeNav, 500);
|
|
})();
|
|
''';
|
|
|
|
/// MutationObserver that continuously re-applies CSS after SPA re-renders.
|
|
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(function() {
|
|
if (!document.getElementById(STYLE_ID)) {
|
|
injectCSS();
|
|
}
|
|
});
|
|
|
|
observer.observe(document.documentElement, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
})();
|
|
''';
|
|
|
|
static String _escapeJsString(String s) {
|
|
// Wrap in JS template literal backticks; escape any internal backticks.
|
|
final escaped = s.replaceAll(r'\', r'\\').replaceAll('`', r'\`');
|
|
return '`$escaped`';
|
|
}
|
|
|
|
// ── Navigation helpers ──────────────────────────────────────────────────────
|
|
|
|
/// JS that soft-navigates Instagram's SPA without a full page reload.
|
|
/// [path] should start with / e.g. '/direct/inbox/'.
|
|
static String softNavigateJS(String path) =>
|
|
'''
|
|
(function() {
|
|
const target = ${_escapeJsString(path)};
|
|
// Try React Router / Instagram SPA navigation first (pushState trick)
|
|
if (window.location.pathname !== target) {
|
|
window.location.href = target;
|
|
}
|
|
})();
|
|
''';
|
|
|
|
/// JS to click Instagram's native "create post" button.
|
|
static const String clickCreateButtonJS = '''
|
|
(function() {
|
|
const btn = document.querySelector(
|
|
'[aria-label="New post"], [aria-label="Create"], svg[aria-label="New post"]'
|
|
);
|
|
if (btn) {
|
|
btn.closest('a, button') ? btn.closest('a, button').click() : btn.click();
|
|
} else {
|
|
// Fallback: navigate to home first, create will open as modal
|
|
window.location.href = '/';
|
|
}
|
|
})();
|
|
''';
|
|
|
|
/// JS to get the currently logged-in user's username.
|
|
static const String getLoggedInUsernameJS = '''
|
|
(function() {
|
|
try {
|
|
// Try shared data approach
|
|
const scripts = Array.from(document.querySelectorAll('script[type="application/json"]'));
|
|
for (const s of scripts) {
|
|
try {
|
|
const d = JSON.parse(s.textContent);
|
|
if (d && d.config && d.config.viewer && d.config.viewer.username) {
|
|
return d.config.viewer.username;
|
|
}
|
|
} catch(e){}
|
|
}
|
|
// Try window additionalDataLoaded
|
|
if (window.__additionalDataLoaded) {
|
|
const keys = Object.keys(window.__additionalDataLoaded || {});
|
|
for (const k of keys) {
|
|
const v = window.__additionalDataLoaded[k];
|
|
if (v && v.data && v.data.user && v.data.user.username) {
|
|
return v.data.user.username;
|
|
}
|
|
}
|
|
}
|
|
// Fallback: try profile anchor in nav
|
|
const profileLink = document.querySelector('a[href][aria-label*="rofile"]');
|
|
if (profileLink) {
|
|
const href = profileLink.getAttribute('href');
|
|
if (href) {
|
|
const parts = href.replace(/^[/]/, "").split("/");
|
|
if (parts[0] && parts[0].length > 0) return parts[0];
|
|
}
|
|
}
|
|
return null;
|
|
} catch(e) { return null; }
|
|
})();
|
|
''';
|
|
|
|
/// MutationObserver to watch for Reel players and lock their scrolling.
|
|
static const String reelsMutationObserverJS = '''
|
|
(function() {
|
|
function lockReelScroll(reelContainer) {
|
|
if (reelContainer.dataset.scrollLocked) return;
|
|
|
|
// If session is active, don't lock
|
|
if (window.__focusgramSessionActive === true) return;
|
|
|
|
reelContainer.dataset.scrollLocked = 'true';
|
|
|
|
let startY = 0;
|
|
|
|
reelContainer.addEventListener('touchstart', (e) => {
|
|
startY = e.touches[0].clientY;
|
|
}, { passive: true });
|
|
|
|
reelContainer.addEventListener('touchmove', (e) => {
|
|
const deltaY = e.touches[0].clientY - startY;
|
|
// Block upward swipe (next reel), allow downward (go back)
|
|
if (deltaY < -10) {
|
|
if (e.cancelable) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
}
|
|
}, { passive: false });
|
|
}
|
|
|
|
// Watch for reel player being injected into DOM
|
|
const observer = new MutationObserver(() => {
|
|
// Instagram's reel player containers — multiple selectors for resilience
|
|
const reelContainers = document.querySelectorAll(
|
|
'[class*="reel"], [class*="Reel"], video'
|
|
);
|
|
reelContainers.forEach((el) => {
|
|
// If it's a video or a reel container, wrap it
|
|
lockReelScroll(el);
|
|
// Also try parent if it's a video
|
|
if (el.tagName === 'VIDEO' && el.parentElement) {
|
|
lockReelScroll(el.parentElement);
|
|
}
|
|
});
|
|
});
|
|
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
})();
|
|
''';
|
|
|
|
/// JS to disable swipe-to-next behavior inside the isolated Reel player.
|
|
static const String disableReelSwipeJS = '''
|
|
(function disableSwipeNavigation() {
|
|
if (window.__focusgramSessionActive === true) return;
|
|
let startX = 0;
|
|
document.addEventListener('touchstart', e => { startX = e.touches[0].clientX; }, {passive: true});
|
|
document.addEventListener('touchmove', e => {
|
|
const dx = Math.abs(e.touches[0].clientX - startX);
|
|
if (dx > 30) e.preventDefault();
|
|
}, {passive: false});
|
|
})();
|
|
''';
|
|
|
|
/// JS to update the session state variable in the page context.
|
|
static String buildSessionStateJS(bool active) =>
|
|
'window.__focusgramSessionActive = $active;';
|
|
|
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
|
|
/// Full injection JS to run on every page load.
|
|
static String buildInjectionJS({
|
|
required bool sessionActive,
|
|
required bool blurExplore,
|
|
}) {
|
|
final StringBuffer css = StringBuffer();
|
|
css.write(_hideInstagramNavCSS);
|
|
css.write(_bottomPaddingCSS);
|
|
css.write(_topPaddingCSS);
|
|
if (!sessionActive) css.write(_hideReelsCSS);
|
|
if (blurExplore) css.write(_blurExploreCSS);
|
|
|
|
return '''
|
|
${buildSessionStateJS(sessionActive)}
|
|
${_buildMutationObserver(css.toString())}
|
|
$_periodicNavRemoverJS
|
|
$_dismissAppBannerJS
|
|
$reelsMutationObserverJS
|
|
''';
|
|
}
|
|
}
|