diff --git a/lib/screens/about_page.dart b/lib/screens/about_page.dart index 71fd823..fd03287 100644 --- a/lib/screens/about_page.dart +++ b/lib/screens/about_page.dart @@ -94,9 +94,12 @@ class AboutPage extends StatelessWidget { } Future _launchURL(String url) async { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { + final uri = Uri.tryParse(url); + if (uri == null) return; + try { await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (_) { + // Ignore if cannot launch } } } diff --git a/lib/screens/guardrails_page.dart b/lib/screens/guardrails_page.dart index 2e5e95b..125120d 100644 --- a/lib/screens/guardrails_page.dart +++ b/lib/screens/guardrails_page.dart @@ -209,6 +209,12 @@ class _FrictionSliderTileState extends State<_FrictionSliderTile> { _draftValue = widget.value; } + @override + void didUpdateWidget(_FrictionSliderTile old) { + super.didUpdateWidget(old); + if (!_pendingConfirm) _draftValue = widget.value; + } + @override Widget build(BuildContext context) { final divisions = ((widget.max - widget.min) / widget.divisor).round(); diff --git a/lib/screens/main_webview_page.dart b/lib/screens/main_webview_page.dart index 9bd2ab4..37969bb 100644 --- a/lib/screens/main_webview_page.dart +++ b/lib/screens/main_webview_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../services/session_manager.dart'; import '../services/settings_service.dart'; @@ -29,6 +30,7 @@ class _MainWebViewPageState extends State // Watchdog for app-session expiry Timer? _watchdog; bool _extensionDialogShown = false; + bool _lastSessionActive = false; @override void initState() { @@ -36,12 +38,28 @@ class _MainWebViewPageState extends State WidgetsBinding.instance.addObserver(this); _initWebView(); _startWatchdog(); + + // Listen to session changes to update JS context immediately + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().addListener(_onSessionChanged); + _lastSessionActive = context.read().isSessionActive; + }); + } + + void _onSessionChanged() { + if (!mounted) return; + final sm = context.read(); + if (_lastSessionActive != sm.isSessionActive) { + _lastSessionActive = sm.isSessionActive; + _applyInjections(); + } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _watchdog?.cancel(); + context.read().removeListener(_onSessionChanged); super.dispose(); } @@ -154,6 +172,15 @@ class _MainWebViewPageState extends State ); }, onNavigationRequest: (request) { + // Handle external links (non-Instagram/Facebook) + final uri = Uri.tryParse(request.url); + if (uri != null && + !uri.host.contains('instagram.com') && + !uri.host.contains('facebook.com')) { + launchUrl(uri, mode: LaunchMode.externalApplication); + return NavigationDecision.prevent; + } + final decision = NavigationGuard.evaluate(url: request.url); if (decision.blocked) { @@ -255,7 +282,11 @@ class _MainWebViewPageState extends State await _navigateTo('/explore/'); break; case 2: - _openSessionModal(); + if (context.read().isSessionActive) { + await _navigateTo('/reels/'); + } else { + _openSessionModal(); + } break; case 3: await _navigateTo('/direct/inbox/'); @@ -393,11 +424,21 @@ class _EdgePanelState extends State<_EdgePanel> { // Hits will pass through the Stack to the WebView except on our children. return Stack( children: [ + // ── Tap-to-close Backdrop (only when expanded) ── + if (_isExpanded) + Positioned.fill( + child: GestureDetector( + onTap: _toggleExpansion, + behavior: HitTestBehavior.opaque, + child: Container(color: Colors.black.withValues(alpha: 0.15)), + ), + ), + // ── The Handle (Minimized State) ── if (!_isExpanded) Positioned( left: 0, - top: MediaQuery.of(context).size.height * 0.35, + top: MediaQuery.of(context).size.height * 0.35 + 30, // Added margin child: Material( color: Colors.transparent, child: Column( @@ -480,7 +521,7 @@ class _EdgePanelState extends State<_EdgePanel> { duration: const Duration(milliseconds: 350), curve: Curves.easeOutQuart, left: _isExpanded ? 0 : -220, - top: MediaQuery.of(context).size.height * 0.25, + top: MediaQuery.of(context).size.height * 0.25 + 30, // Added margin child: GestureDetector( onHorizontalDragUpdate: (details) { if (details.delta.dx < -10) _toggleExpansion(); diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index fd8a120..38d43ef 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -10,7 +10,7 @@ class SettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { - final settings = context.watch(); + // Watching services ensures the UI rebuilds when settings or session state change. final sm = context.watch(); return Scaffold( @@ -45,7 +45,7 @@ class SettingsPage extends StatelessWidget { title: 'Distraction Management', subtitle: 'Blur explore and reel controls', icon: Icons.visibility_off_outlined, - destination: _DistractionSettingsPage(settings: settings), + destination: const _DistractionSettingsPage(), ), _buildSettingsTile( context: context, @@ -149,11 +149,11 @@ class SettingsPage extends StatelessWidget { } class _DistractionSettingsPage extends StatelessWidget { - final SettingsService settings; - const _DistractionSettingsPage({required this.settings}); + const _DistractionSettingsPage(); @override Widget build(BuildContext context) { + final settings = context.watch(); return Scaffold( backgroundColor: Colors.black, appBar: AppBar( diff --git a/lib/services/injection_controller.dart b/lib/services/injection_controller.dart index 903a785..7cf40bb 100644 --- a/lib/services/injection_controller.dart +++ b/lib/services/injection_controller.dart @@ -278,6 +278,10 @@ class InjectionController { (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; @@ -321,6 +325,7 @@ class InjectionController { /// 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 => { @@ -330,6 +335,10 @@ class InjectionController { })(); '''; + /// 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. @@ -345,9 +354,11 @@ class InjectionController { if (blurExplore) css.write(_blurExploreCSS); return ''' + ${buildSessionStateJS(sessionActive)} ${_buildMutationObserver(css.toString())} $_periodicNavRemoverJS $_dismissAppBannerJS + $reelsMutationObserverJS '''; } } diff --git a/lib/utils/discipline_challenge.dart b/lib/utils/discipline_challenge.dart index 7a65fe8..b136db8 100644 --- a/lib/utils/discipline_challenge.dart +++ b/lib/utils/discipline_challenge.dart @@ -20,6 +20,499 @@ class DisciplineChallenge { 'patience', 'intent', 'choice', + 'calm', + 'peace', + 'truth', + 'grit', + 'work', + 'time', + 'life', + 'soul', + 'mind', + 'body', + 'heart', + 'will', + 'hope', + 'love', + 'kind', + 'help', + 'give', + 'take', + 'stay', + 'move', + 'jump', + 'run', + 'walk', + 'talk', + 'sing', + 'play', + 'read', + 'write', + 'draw', + 'cook', + 'bake', + 'farm', + 'fish', + 'hunt', + 'grow', + 'learn', + 'teach', + 'lead', + 'follow', + 'build', + 'fix', + 'save', + 'spend', + 'win', + 'lose', + 'try', + 'fail', + 'rise', + 'fall', + 'start', + 'stop', + 'wait', + 'now', + 'here', + 'slow', + 'fast', + 'high', + 'low', + 'deep', + 'wide', + 'long', + 'short', + 'big', + 'small', + 'old', + 'new', + 'good', + 'bad', + 'real', + 'fake', + 'pure', + 'dark', + 'light', + 'fire', + 'water', + 'earth', + 'air', + 'wind', + 'storm', + 'sun', + 'moon', + 'star', + 'sky', + 'road', + 'path', + 'gate', + 'door', + 'room', + 'house', + 'home', + 'city', + 'town', + 'land', + 'sea', + 'ocean', + 'lake', + 'river', + 'wood', + 'tree', + 'leaf', + 'root', + 'seed', + 'fruit', + 'food', + 'bread', + 'cake', + 'milk', + 'wine', + 'beer', + 'salt', + 'gold', + 'iron', + 'rock', + 'sand', + 'dust', + 'bone', + 'blood', + 'skin', + 'hair', + 'eyes', + 'ears', + 'nose', + 'mouth', + 'hand', + 'foot', + 'wing', + 'tail', + 'bird', + 'cat', + 'dog', + 'bear', + 'wolf', + 'lion', + 'deer', + 'horse', + 'cow', + 'pig', + 'sheep', + 'goat', + 'bee', + 'ant', + 'fly', + 'worm', + 'snake', + 'frog', + 'turtle', + 'crab', + 'whale', + 'shark', + 'dolphin', + 'eagle', + 'hawk', + 'owl', + 'swan', + 'duck', + 'goose', + 'rose', + 'lily', + 'pine', + 'oak', + 'fern', + 'moss', + 'weed', + 'grass', + 'corn', + 'rice', + 'bean', + 'pea', + 'nut', + 'oil', + 'honey', + 'wax', + 'silk', + 'wool', + 'flax', + 'hemp', + 'paper', + 'ink', + 'pen', + 'book', + 'lamp', + 'bed', + 'chair', + 'desk', + 'wall', + 'roof', + 'step', + 'mile', + 'inch', + 'yard', + 'hour', + 'day', + 'week', + 'month', + 'year', + 'age', + 'past', + 'next', + 'death', + 'birth', + 'name', + 'word', + 'song', + 'poem', + 'story', + 'myth', + 'fact', + 'law', + 'rule', + 'king', + 'queen', + 'lord', + 'lady', + 'man', + 'woman', + 'child', + 'youth', + 'elder', + 'friend', + 'foe', + 'guest', + 'host', + 'war', + 'fight', + 'deal', + 'buy', + 'sell', + 'pay', + 'debt', + 'cost', + 'coin', + 'cash', + 'bank', + 'shop', + 'mall', + 'mill', + 'port', + 'ship', + 'boat', + 'car', + 'bus', + 'bike', + 'train', + 'plane', + 'street', + 'hill', + 'peak', + 'cave', + 'well', + 'bridge', + 'tower', + 'fort', + 'camp', + 'tent', + 'ash', + 'smoke', + 'coal', + 'log', + 'branch', + 'stick', + 'tool', + 'hammer', + 'nail', + 'saw', + 'knife', + 'fork', + 'spoon', + 'bowl', + 'cup', + 'plate', + 'pot', + 'pan', + 'stove', + 'oven', + 'sink', + 'bath', + 'soap', + 'towel', + 'comb', + 'brush', + 'mirror', + 'clock', + 'watch', + 'ring', + 'belt', + 'boot', + 'shoe', + 'sock', + 'hat', + 'coat', + 'shirt', + 'pant', + 'dress', + 'skirt', + 'bag', + 'box', + 'case', + 'lock', + 'key', + 'bell', + 'horn', + 'drum', + 'pipe', + 'lute', + 'harp', + 'flute', + 'reed', + 'cord', + 'rope', + 'knot', + 'net', + 'hook', + 'line', + 'bait', + 'trap', + 'plow', + 'hoe', + 'rake', + 'spade', + 'crop', + 'wheat', + 'oats', + 'rye', + 'zinc', + 'lead', + 'copper', + 'tin', + 'silver', + 'stone', + 'clay', + 'mud', + 'rain', + 'snow', + 'hail', + 'mist', + 'fog', + 'cloud', + 'dawn', + 'dusk', + 'noon', + 'night', + 'ghost', + 'angel', + 'devil', + 'god', + 'fate', + 'doom', + 'fear', + 'joy', + 'woe', + 'pain', + 'care', + 'hate', + 'rage', + 'lust', + 'greed', + 'pride', + 'envy', + 'sloth', + 'wrath', + 'holy', + 'sin', + 'hell', + 'heaven', + 'void', + 'space', + 'form', + 'idea', + 'thought', + 'dot', + 'circle', + 'square', + 'point', + 'edge', + 'flow', + 'wave', + 'tide', + 'shore', + 'bank', + 'cliff', + 'vale', + 'meadow', + 'field', + 'plain', + 'desert', + 'forest', + 'jungle', + 'swamp', + 'marsh', + 'glade', + 'grove', + 'peak', + 'ridge', + 'slope', + 'track', + 'trail', + 'lane', + 'path', + 'alley', + 'court', + 'plaza', + 'park', + 'bridge', + 'tunnel', + 'arch', + 'dome', + 'spire', + 'tower', + 'wall', + 'fence', + 'gate', + 'stair', + 'floor', + 'beam', + 'pole', + 'mast', + 'sail', + 'deck', + 'hull', + 'keel', + 'oar', + 'helm', + 'anchor', + 'net', + 'rope', + 'knot', + 'line', + 'hook', + 'bait', + 'trap', + 'net', + 'spear', + 'bow', + 'arrow', + 'sword', + 'shield', + 'armor', + 'helm', + 'boot', + 'glove', + 'cloak', + 'scarf', + 'belt', + 'ujwal', + 'ring', + 'gem', + 'jewel', + 'pearl', + 'gold', + 'silver', + 'bronze', + 'iron', + 'steel', + 'brass', + 'glass', + 'stone', + 'brick', + 'tile', + 'wood', + 'clay', + 'wax', + 'ink', + 'paint', + 'dye', + 'glue', + 'oil', + 'coal', + 'gas', + 'steam', + 'heat', + 'cold', + 'ice', + 'frost', + 'thaw', + 'melt', + 'burn', + 'glow', + 'shine', + 'beam', + 'ray', + 'spark', + 'flash', + 'flare', + 'blast', + 'shock', + 'wave', + 'pulse', + 'beat', + 'rhythm', + 'tone', + 'note', + 'tune', + 'song', ]; /// Shows the word challenge dialog. Returns true if successful.