From 9ab4fc503afce8ea9a669219b3ae11eb6518950d Mon Sep 17 00:00:00 2001 From: Ujwal Date: Sun, 22 Feb 2026 22:26:33 +0545 Subject: [PATCH] improvement: improved UI Elements and some optimizations --- lib/screens/main_webview_page.dart | 111 ++++++++++++------------- lib/services/injection_controller.dart | 29 +++++++ lib/services/navigation_guard.dart | 78 +++++++---------- 3 files changed, 109 insertions(+), 109 deletions(-) diff --git a/lib/screens/main_webview_page.dart b/lib/screens/main_webview_page.dart index a610c4e..25b87d3 100644 --- a/lib/screens/main_webview_page.dart +++ b/lib/screens/main_webview_page.dart @@ -9,7 +9,6 @@ import '../services/injection_controller.dart'; import '../services/navigation_guard.dart'; import 'session_modal.dart'; import 'settings_page.dart'; -import 'reel_player_overlay.dart'; class MainWebViewPage extends StatefulWidget { const MainWebViewPage({super.key}); @@ -116,8 +115,6 @@ class _MainWebViewPageState extends State { } void _initWebView() { - final sessionManager = context.read(); - _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setUserAgent(InjectionController.iOSUserAgent) @@ -134,23 +131,20 @@ class _MainWebViewPageState extends State { if (mounted) setState(() => _isLoading = false); _applyInjections(); _updateCurrentTab(url); - // Cache username whenever we finish loading any page _cacheUsername(); + // Inject swipe-blocker when on a specific reel page + if (NavigationGuard.isSpecificReel(url)) { + _controller.runJavaScript(InjectionController.reelSwipeBlockerJS); + } }, onNavigationRequest: (request) { - final isDmReel = NavigationGuard.isDmReelLink(request.url); - - final decision = NavigationGuard.evaluate( - url: request.url, - sessionActive: sessionManager.isSessionActive, - isDmReelException: isDmReel, - ); + final decision = NavigationGuard.evaluate(url: request.url); if (decision.blocked) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(decision.reason ?? 'Blocked'), + content: Text(decision.reason ?? 'Navigation blocked'), backgroundColor: Colors.red.shade900, behavior: SnackBarBehavior.floating, margin: const EdgeInsets.fromLTRB(16, 0, 16, 80), @@ -161,22 +155,6 @@ class _MainWebViewPageState extends State { return NavigationDecision.prevent; } - // Open DM reel in isolated player - if (isDmReel && !sessionManager.isSessionActive) { - final canonicalUrl = NavigationGuard.canonicalizeDmReelUrl( - request.url, - ); - if (canonicalUrl != null) { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ReelPlayerOverlay(url: canonicalUrl), - ), - ); - return NavigationDecision.prevent; - } - } - return NavigationDecision.navigate; }, ), @@ -290,39 +268,49 @@ class _MainWebViewPageState extends State { @override Widget build(BuildContext context) { + final topPad = MediaQuery.of(context).padding.top; + const barHeight = 60.0; + return Scaffold( backgroundColor: Colors.black, - body: SafeArea( - bottom: false, - child: Column( - children: [ - // Status Bar — always on top - _StatusBar(), - - // WebView - Expanded( - child: Stack( - children: [ - WebViewWidget(controller: _controller), - // Thin loading bar (not full-screen spinner) - if (_isLoading) - const LinearProgressIndicator( - backgroundColor: Colors.transparent, - color: Colors.blue, - minHeight: 2, - ), - ], - ), - ), - ], - ), - ), - bottomNavigationBar: _FocusGramNavBar( - currentIndex: _currentIndex, - onTap: _onTabTapped, - ), floatingActionButton: _SessionFAB(onTap: _openSessionModal), floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, + body: Stack( + children: [ + // ── WebView: full screen (behind everything) ──────────────── + Positioned.fill(child: WebViewWidget(controller: _controller)), + + // ── Thin loading indicator at very top ────────────────────── + if (_isLoading) + Positioned( + top: 0, + left: 0, + right: 0, + child: const LinearProgressIndicator( + backgroundColor: Colors.transparent, + color: Colors.blue, + minHeight: 2, + ), + ), + + // ── Status bar overlaid at top (below system status bar) ──── + Positioned(top: topPad, left: 0, right: 0, child: _StatusBar()), + + // ── Our bottom bar overlaid on top of Instagram's bar ─────── + // Making it taller than Instagram's native bar (~50dp) means + // theirs is fully hidden behind ours — no CSS needed as fallback. + Positioned( + bottom: 0, + left: 0, + right: 0, + child: _FocusGramNavBar( + currentIndex: _currentIndex, + onTap: _onTabTapped, + height: barHeight, + ), + ), + ], + ), ); } @@ -426,8 +414,13 @@ class _StatusBar extends StatelessWidget { class _FocusGramNavBar extends StatelessWidget { final int currentIndex; final Future Function(int) onTap; + final double height; - const _FocusGramNavBar({required this.currentIndex, required this.onTap}); + const _FocusGramNavBar({ + required this.currentIndex, + required this.onTap, + this.height = 52, + }); @override Widget build(BuildContext context) { @@ -444,7 +437,7 @@ class _FocusGramNavBar extends StatelessWidget { child: SafeArea( top: false, child: SizedBox( - height: 52, + height: height, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: items.asMap().entries.map((entry) { diff --git a/lib/services/injection_controller.dart b/lib/services/injection_controller.dart index bc9320c..c752f5c 100644 --- a/lib/services/injection_controller.dart +++ b/lib/services/injection_controller.dart @@ -71,6 +71,13 @@ class InjectionController { } '''; + /// CSS that adds bottom padding so feed content doesn't hide behind our bar. + static const String _bottomPaddingCSS = ''' + body, #react-root > div { + padding-bottom: 64px !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) */ @@ -250,6 +257,27 @@ class InjectionController { })(); '''; + /// JS to disable vertical swipe gestures that drive reel-to-reel transition. + static const String reelSwipeBlockerJS = ''' + (function() { + let _touchStartY = 0; + document.addEventListener('touchstart', function(e) { + _touchStartY = e.touches[0].clientY; + }, { passive: true }); + + document.addEventListener('touchmove', function(e) { + const deltaY = e.touches[0].clientY - _touchStartY; + // If swiping UP (negative delta), block it to prevent next reel load + if (deltaY < -10) { + if (e.cancelable) { + e.preventDefault(); + e.stopPropagation(); + } + } + }, { passive: false }); + })(); + '''; + // ── Reel scroll-lock ──────────────────────────────────────────────────────── /// JS that prevents the user from scrolling to a different reel. @@ -329,6 +357,7 @@ class InjectionController { }) { final StringBuffer css = StringBuffer(); css.write(_hideInstagramNavCSS); + css.write(_bottomPaddingCSS); // Ensure content isn't behind our bar if (!sessionActive) css.write(_hideReelsCSS); if (blurExplore) css.write(_blurExploreCSS); diff --git a/lib/services/navigation_guard.dart b/lib/services/navigation_guard.dart index 4ea4cac..89b0b4a 100644 --- a/lib/services/navigation_guard.dart +++ b/lib/services/navigation_guard.dart @@ -1,34 +1,40 @@ /// Determines whether a navigation request should be blocked. /// /// Rules: -/// - /reels/* and /reel/* are blocked unless [sessionActive] is true OR -/// [isDmReelException] is true (single DM reel open). -/// - /explore/ is allowed (but explore content is blurred via CSS). -/// - Only instagram.com domains are allowed (blocks external redirects). +/// - instagram.com/reels (and /reels/) = BLOCKED — this is the mindless feed tab +/// - instagram.com/reel/SHORTCODE/ = ALLOWED — a specific reel (e.g. from a DM) +/// - /explore/ is allowed (explore content is blurred via CSS instead) +/// - Only instagram.com domains are allowed (blocks external redirects) class NavigationGuard { static const _allowedHosts = ['instagram.com', 'www.instagram.com']; - static const _blockedPathPrefixes = ['/reels', '/reel/']; + /// Regex matching the Reels FEED root — NOT individual reels. + static final _reelsFeedRegex = RegExp( + r'instagram\.com/reels/?$', + caseSensitive: false, + ); + + /// Regex matching a specific reel (e.g. /reel/ABC123/). + static final _specificReelRegex = RegExp( + r'instagram\.com/reel/[^/?#]+', + caseSensitive: false, + ); /// Returns a [BlockDecision] for the given [url]. - static BlockDecision evaluate({ - required String url, - required bool sessionActive, - required bool isDmReelException, - }) { + static BlockDecision evaluate({required String url}) { Uri uri; try { uri = Uri.parse(url); } catch (_) { - return BlockDecision(blocked: false, reason: null); + return const BlockDecision(blocked: false, reason: null); } // Allow non-HTTP schemes (about:blank, data:, etc.) if (!uri.scheme.startsWith('http')) { - return BlockDecision(blocked: false, reason: null); + return const BlockDecision(blocked: false, reason: null); } - // Block non-Instagram domains (prevents phishing redirects) + // Block non-Instagram domains (prevents phishing/external redirects) final host = uri.host.toLowerCase(); if (!_allowedHosts.any((h) => host == h || host.endsWith('.$h'))) { return BlockDecision( @@ -37,49 +43,21 @@ class NavigationGuard { ); } - // Check reel/reels path - final path = uri.path.toLowerCase(); - final isReelUrl = _blockedPathPrefixes.any((p) => path.startsWith(p)); - - if (isReelUrl) { - if (sessionActive || isDmReelException) { - return BlockDecision(blocked: false, reason: null); - } - return BlockDecision( + // Block ONLY the Reels feed tab root (/reels, /reels/) + // but allow specific reels (/reel/SHORTCODE/) opened from DMs + if (_reelsFeedRegex.hasMatch(url)) { + return const BlockDecision( blocked: true, - reason: 'Reel navigation blocked — no active session', + reason: + 'Reels feed is disabled — open a specific reel from DMs instead', ); } - return BlockDecision(blocked: false, reason: null); + return const BlockDecision(blocked: false, reason: null); } - /// Returns true if the URL looks like a Reel link from a DM. - static bool isDmReelLink(String url) { - try { - final uri = Uri.parse(url); - final path = uri.path.toLowerCase(); - return path.startsWith('/reel/') || path.startsWith('/reels/'); - } catch (_) { - return false; - } - } - - /// Extracts a canonical single-reel URL from a DM reel link. - /// Strips query params that might trigger Reels feed. - static String? canonicalizeDmReelUrl(String url) { - try { - final uri = Uri.parse(url); - // Keep only the reel path, strip all query parameters - return Uri( - scheme: 'https', - host: 'www.instagram.com', - path: uri.path, - ).toString(); - } catch (_) { - return null; - } - } + /// True if the URL is a specific individual reel (from a DM share). + static bool isSpecificReel(String url) => _specificReelRegex.hasMatch(url); } class BlockDecision {