improvement: improved UI Elements and some optimizations

This commit is contained in:
Ujwal
2026-02-22 22:26:33 +05:45
parent a848b9222d
commit 9ab4fc503a
3 changed files with 109 additions and 109 deletions
+52 -59
View File
@@ -9,7 +9,6 @@ import '../services/injection_controller.dart';
import '../services/navigation_guard.dart'; import '../services/navigation_guard.dart';
import 'session_modal.dart'; import 'session_modal.dart';
import 'settings_page.dart'; import 'settings_page.dart';
import 'reel_player_overlay.dart';
class MainWebViewPage extends StatefulWidget { class MainWebViewPage extends StatefulWidget {
const MainWebViewPage({super.key}); const MainWebViewPage({super.key});
@@ -116,8 +115,6 @@ class _MainWebViewPageState extends State<MainWebViewPage> {
} }
void _initWebView() { void _initWebView() {
final sessionManager = context.read<SessionManager>();
_controller = WebViewController() _controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted) ..setJavaScriptMode(JavaScriptMode.unrestricted)
..setUserAgent(InjectionController.iOSUserAgent) ..setUserAgent(InjectionController.iOSUserAgent)
@@ -134,23 +131,20 @@ class _MainWebViewPageState extends State<MainWebViewPage> {
if (mounted) setState(() => _isLoading = false); if (mounted) setState(() => _isLoading = false);
_applyInjections(); _applyInjections();
_updateCurrentTab(url); _updateCurrentTab(url);
// Cache username whenever we finish loading any page
_cacheUsername(); _cacheUsername();
// Inject swipe-blocker when on a specific reel page
if (NavigationGuard.isSpecificReel(url)) {
_controller.runJavaScript(InjectionController.reelSwipeBlockerJS);
}
}, },
onNavigationRequest: (request) { onNavigationRequest: (request) {
final isDmReel = NavigationGuard.isDmReelLink(request.url); final decision = NavigationGuard.evaluate(url: request.url);
final decision = NavigationGuard.evaluate(
url: request.url,
sessionActive: sessionManager.isSessionActive,
isDmReelException: isDmReel,
);
if (decision.blocked) { if (decision.blocked) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(decision.reason ?? 'Blocked'), content: Text(decision.reason ?? 'Navigation blocked'),
backgroundColor: Colors.red.shade900, backgroundColor: Colors.red.shade900,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.fromLTRB(16, 0, 16, 80), margin: const EdgeInsets.fromLTRB(16, 0, 16, 80),
@@ -161,22 +155,6 @@ class _MainWebViewPageState extends State<MainWebViewPage> {
return NavigationDecision.prevent; 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; return NavigationDecision.navigate;
}, },
), ),
@@ -290,39 +268,49 @@ class _MainWebViewPageState extends State<MainWebViewPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final topPad = MediaQuery.of(context).padding.top;
const barHeight = 60.0;
return Scaffold( return Scaffold(
backgroundColor: Colors.black, 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), floatingActionButton: _SessionFAB(onTap: _openSessionModal),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, 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 { class _FocusGramNavBar extends StatelessWidget {
final int currentIndex; final int currentIndex;
final Future<void> Function(int) onTap; final Future<void> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -444,7 +437,7 @@ class _FocusGramNavBar extends StatelessWidget {
child: SafeArea( child: SafeArea(
top: false, top: false,
child: SizedBox( child: SizedBox(
height: 52, height: height,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: items.asMap().entries.map((entry) { children: items.asMap().entries.map((entry) {
+29
View File
@@ -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). /// CSS to blur Explore feed posts/reels (keeps stories visible).
static const String _blurExploreCSS = ''' static const String _blurExploreCSS = '''
/* Blur Explore grid posts and reel cards (not stories row) */ /* 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 ──────────────────────────────────────────────────────── // ── Reel scroll-lock ────────────────────────────────────────────────────────
/// JS that prevents the user from scrolling to a different reel. /// JS that prevents the user from scrolling to a different reel.
@@ -329,6 +357,7 @@ class InjectionController {
}) { }) {
final StringBuffer css = StringBuffer(); final StringBuffer css = StringBuffer();
css.write(_hideInstagramNavCSS); css.write(_hideInstagramNavCSS);
css.write(_bottomPaddingCSS); // Ensure content isn't behind our bar
if (!sessionActive) css.write(_hideReelsCSS); if (!sessionActive) css.write(_hideReelsCSS);
if (blurExplore) css.write(_blurExploreCSS); if (blurExplore) css.write(_blurExploreCSS);
+28 -50
View File
@@ -1,34 +1,40 @@
/// Determines whether a navigation request should be blocked. /// Determines whether a navigation request should be blocked.
/// ///
/// Rules: /// Rules:
/// - /reels/* and /reel/* are blocked unless [sessionActive] is true OR /// - instagram.com/reels (and /reels/) = BLOCKED — this is the mindless feed tab
/// [isDmReelException] is true (single DM reel open). /// - instagram.com/reel/SHORTCODE/ = ALLOWED — a specific reel (e.g. from a DM)
/// - /explore/ is allowed (but explore content is blurred via CSS). /// - /explore/ is allowed (explore content is blurred via CSS instead)
/// - Only instagram.com domains are allowed (blocks external redirects). /// - Only instagram.com domains are allowed (blocks external redirects)
class NavigationGuard { class NavigationGuard {
static const _allowedHosts = ['instagram.com', 'www.instagram.com']; 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]. /// Returns a [BlockDecision] for the given [url].
static BlockDecision evaluate({ static BlockDecision evaluate({required String url}) {
required String url,
required bool sessionActive,
required bool isDmReelException,
}) {
Uri uri; Uri uri;
try { try {
uri = Uri.parse(url); uri = Uri.parse(url);
} catch (_) { } catch (_) {
return BlockDecision(blocked: false, reason: null); return const BlockDecision(blocked: false, reason: null);
} }
// Allow non-HTTP schemes (about:blank, data:, etc.) // Allow non-HTTP schemes (about:blank, data:, etc.)
if (!uri.scheme.startsWith('http')) { 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(); final host = uri.host.toLowerCase();
if (!_allowedHosts.any((h) => host == h || host.endsWith('.$h'))) { if (!_allowedHosts.any((h) => host == h || host.endsWith('.$h'))) {
return BlockDecision( return BlockDecision(
@@ -37,49 +43,21 @@ class NavigationGuard {
); );
} }
// Check reel/reels path // Block ONLY the Reels feed tab root (/reels, /reels/)
final path = uri.path.toLowerCase(); // but allow specific reels (/reel/SHORTCODE/) opened from DMs
final isReelUrl = _blockedPathPrefixes.any((p) => path.startsWith(p)); if (_reelsFeedRegex.hasMatch(url)) {
return const BlockDecision(
if (isReelUrl) {
if (sessionActive || isDmReelException) {
return BlockDecision(blocked: false, reason: null);
}
return BlockDecision(
blocked: true, 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. /// True if the URL is a specific individual reel (from a DM share).
static bool isDmReelLink(String url) { static bool isSpecificReel(String url) => _specificReelRegex.hasMatch(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;
}
}
} }
class BlockDecision { class BlockDecision {