diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..a8088aa Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..0bc9fdc Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..907db7f Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..3978d81 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..fcef524 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..5f349f7 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..4891768 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..2332984 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..64205f4 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..9fc60ef 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..6ffbd9f 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..beab31f --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #000000 + \ No newline at end of file diff --git a/assets/images/focusgram.ico b/assets/images/focusgram.ico new file mode 100644 index 0000000..fc0fb57 Binary files /dev/null and b/assets/images/focusgram.ico differ diff --git a/assets/images/focusgram.png b/assets/images/focusgram.png new file mode 100644 index 0000000..8c4d9b7 Binary files /dev/null and b/assets/images/focusgram.png differ diff --git a/lib/screens/about_page.dart b/lib/screens/about_page.dart index fd03287..a7e10f5 100644 --- a/lib/screens/about_page.dart +++ b/lib/screens/about_page.dart @@ -1,9 +1,87 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:http/http.dart' as http; -class AboutPage extends StatelessWidget { +class AboutPage extends StatefulWidget { const AboutPage({super.key}); + @override + State createState() => _AboutPageState(); +} + +class _AboutPageState extends State { + final String _currentVersion = '0.8.5'; + bool _isChecking = false; + + Future _checkUpdate() async { + setState(() => _isChecking = true); + try { + final response = await http + .get( + Uri.parse( + 'https://api.github.com/repos/Ujwal223/FocusGram/releases/latest', + ), + ) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + final latestVersion = data['tag_name'].toString().replaceAll('v', ''); + final downloadUrl = data['html_url']; + + if (latestVersion != _currentVersion) { + _showUpdateDialog(latestVersion, downloadUrl); + } else { + _showSnackBar('You are up to date! 🎉'); + } + } else { + _showSnackBar('Could not check for updates.'); + } + } catch (_) { + _showSnackBar('Connectivity issue. Try again later.'); + } finally { + if (mounted) setState(() => _isChecking = false); + } + } + + void _showUpdateDialog(String version, String url) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: const Color(0xFF1A1A1A), + title: const Text( + 'Update Available!', + style: TextStyle(color: Colors.white), + ), + content: Text( + 'A new version ($version) is available on GitHub.', + style: const TextStyle(color: Colors.white70), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Later', style: TextStyle(color: Colors.white38)), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(ctx); + _launchURL(url); + }, + style: ElevatedButton.styleFrom(backgroundColor: Colors.blue), + child: const Text('Download'), + ), + ], + ), + ); + } + + void _showSnackBar(String msg) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(msg), duration: const Duration(seconds: 2)), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -48,9 +126,9 @@ class AboutPage extends StatelessWidget { ), ), const SizedBox(height: 8), - const Text( - 'Version 1.1.0', - style: TextStyle(color: Colors.white38, fontSize: 13), + Text( + 'Version $_currentVersion', + style: const TextStyle(color: Colors.white38, fontSize: 13), ), const SizedBox(height: 40), const Text( @@ -67,7 +145,30 @@ class AboutPage extends StatelessWidget { fontWeight: FontWeight.w600, ), ), - const SizedBox(height: 60), + const SizedBox(height: 40), + ElevatedButton.icon( + onPressed: _isChecking ? null : _checkUpdate, + icon: _isChecking + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.update), + label: Text(_isChecking ? 'Checking...' : 'Check for Update'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent.withValues(alpha: 0.2), + foregroundColor: Colors.white, + minimumSize: const Size(200, 45), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + const SizedBox(height: 16), ElevatedButton.icon( onPressed: () => _launchURL('https://github.com/Ujwal223/FocusGram'), @@ -76,6 +177,7 @@ class AboutPage extends StatelessWidget { style: ElevatedButton.styleFrom( backgroundColor: Colors.white10, foregroundColor: Colors.white, + minimumSize: const Size(200, 45), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -84,7 +186,10 @@ class AboutPage extends StatelessWidget { const SizedBox(height: 20), const Text( 'FocusGram is not affiliated with Instagram.', - style: TextStyle(color: Colors.white12, fontSize: 10), + style: TextStyle( + color: Color.fromARGB(48, 255, 255, 255), + fontSize: 10, + ), ), ], ), @@ -98,8 +203,6 @@ class AboutPage extends StatelessWidget { if (uri == null) return; try { await launchUrl(uri, mode: LaunchMode.externalApplication); - } catch (_) { - // Ignore if cannot launch - } + } catch (_) {} } } diff --git a/lib/screens/main_webview_page.dart b/lib/screens/main_webview_page.dart index 37969bb..d843070 100644 --- a/lib/screens/main_webview_page.dart +++ b/lib/screens/main_webview_page.dart @@ -8,6 +8,7 @@ import '../services/session_manager.dart'; import '../services/settings_service.dart'; import '../services/injection_controller.dart'; import '../services/navigation_guard.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'session_modal.dart'; import 'settings_page.dart'; @@ -23,14 +24,25 @@ class _MainWebViewPageState extends State late final WebViewController _controller; int _currentIndex = 0; bool _isLoading = true; - - // Cached username for profile navigation - String? _cachedUsername; - // Watchdog for app-session expiry Timer? _watchdog; bool _extensionDialogShown = false; bool _lastSessionActive = false; + String? _cachedUsername; + String _currentUrl = 'https://www.instagram.com/'; + bool _hasError = false; + + /// Helper to determine if we are on a login/onboarding page. + bool get _isOnOnboardingPage { + final path = Uri.tryParse(_currentUrl)?.path ?? ''; + final lowerPath = path.toLowerCase(); + return lowerPath.contains('/accounts/login') || + lowerPath.contains('/accounts/emailsignup') || + lowerPath.contains('/accounts/signup') || + lowerPath.contains('/legal/') || + lowerPath.contains('/help/') || + _currentUrl.contains('instagram.com/accounts/login'); + } @override void initState() { @@ -39,9 +51,10 @@ class _MainWebViewPageState extends State _initWebView(); _startWatchdog(); - // Listen to session changes to update JS context immediately + // Listen to session & settings changes WidgetsBinding.instance.addPostFrameCallback((_) { context.read().addListener(_onSessionChanged); + context.read().addListener(_onSettingsChanged); _lastSessionActive = context.read().isSessionActive; }); } @@ -53,6 +66,14 @@ class _MainWebViewPageState extends State _lastSessionActive = sm.isSessionActive; _applyInjections(); } + // Force rebuild for timer updates + setState(() {}); + } + + void _onSettingsChanged() { + if (!mounted) return; + _applyInjections(); + _controller.reload(); } @override @@ -60,6 +81,7 @@ class _MainWebViewPageState extends State WidgetsBinding.instance.removeObserver(this); _watchdog?.cancel(); context.read().removeListener(_onSessionChanged); + context.read().removeListener(_onSettingsChanged); super.dispose(); } @@ -156,13 +178,20 @@ class _MainWebViewPageState extends State ..setNavigationDelegate( NavigationDelegate( onPageStarted: (url) { - // Only show loading if it's a real page load (not SPA nav) - if (!url.contains('#')) { - if (mounted) setState(() => _isLoading = true); + if (mounted) { + setState(() { + _isLoading = !url.contains('#'); + _currentUrl = url; // Update immediately to hide/show UI + }); } }, onPageFinished: (url) { - if (mounted) setState(() => _isLoading = false); + if (mounted) { + setState(() { + _isLoading = false; + _currentUrl = url; + }); + } _applyInjections(); _updateCurrentTab(url); _cacheUsername(); @@ -181,6 +210,16 @@ class _MainWebViewPageState extends State return NavigationDecision.prevent; } + // Facebook Login Warning + if (uri != null && + uri.host.contains('facebook.com') && + _isOnOnboardingPage) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Sorry, Please use Email login')), + ); + return NavigationDecision.prevent; + } + final decision = NavigationGuard.evaluate(url: request.url); if (decision.blocked) { @@ -202,24 +241,49 @@ class _MainWebViewPageState extends State }, ), ) - ..loadRequest(Uri.parse('https://www.instagram.com/')); + ..loadRequest(Uri.parse('https://www.instagram.com/accounts/login/')); } void _applyInjections() { + if (!mounted) return; + if (_isOnOnboardingPage) return; // Restore native login/signup behavior + final sessionManager = context.read(); final settings = context.read(); final js = InjectionController.buildInjectionJS( sessionActive: sessionManager.isSessionActive, blurExplore: settings.blurExplore, + blurReels: settings.blurReels, + ghostMode: settings.ghostMode, + enableTextSelection: settings.enableTextSelection, ); _controller.runJavaScript(js); } + Future _signOut() async { + final manager = WebViewCookieManager(); + await manager.clearCookies(); + await _controller.clearCache(); + // Force immediate state update and navigation + if (mounted) { + setState(() { + _currentIndex = 0; + _cachedUsername = null; + _isLoading = true; // Show indicator during reload + }); + await _controller.loadRequest( + Uri.parse('https://www.instagram.com/accounts/login/'), + ); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Signed out successfully'))); + } + } + Future _cacheUsername() async { - if (_cachedUsername != null) return; // Already known try { final result = await _controller.runJavaScriptReturningResult( - InjectionController.getLoggedInUsernameJS, + "document.querySelector('header h2')?.innerText || ''", ); final raw = result.toString().replaceAll('"', '').replaceAll("'", ''); if (raw.isNotEmpty && raw != 'null' && raw != 'undefined') { @@ -268,8 +332,10 @@ class _MainWebViewPageState extends State } Future _onTabTapped(int index) async { - // Don't re-navigate if already on this tab - if (index == _currentIndex) return; + if (index == _currentIndex) { + await _controller.reload(); + return; + } setState(() => _currentIndex = index); switch (index) { @@ -284,9 +350,8 @@ class _MainWebViewPageState extends State case 2: if (context.read().isSessionActive) { await _navigateTo('/reels/'); - } else { - _openSessionModal(); } + // If not active, do nothing (disabled as requested) break; case 3: await _navigateTo('/direct/inbox/'); @@ -326,13 +391,28 @@ class _MainWebViewPageState extends State backgroundColor: Colors.black, body: Stack( children: [ - // ── WebView: full screen (behind everything) ──────────────── - Positioned.fill(child: WebViewWidget(controller: _controller)), + // ── Main Content Layout ──────────────────────────────────── + SafeArea( + child: Column( + children: [ + if (!_isOnOnboardingPage) _BrandedTopBar(), + Expanded(child: WebViewWidget(controller: _controller)), + ], + ), + ), - // ── Thin loading indicator at very top ────────────────────── + if (_hasError) + _NoInternetScreen( + onRetry: () { + setState(() => _hasError = false); + _controller.reload(); + }, + ), + + // ── Thin loading indicator (Placed below Top Bar) ────────── if (_isLoading) Positioned( - top: 0, + top: 60 + MediaQuery.of(context).padding.top, left: 0, right: 0, child: const LinearProgressIndicator( @@ -342,36 +422,26 @@ class _MainWebViewPageState extends State ), ), - // ── The Edge Panel (replaced _StatusBar) ──────────────────── - // No Positioned wrapper here: _EdgePanel returns its own Positioned siblings + // ── The Edge Panel ────────────────────────────────────────── _EdgePanel(controller: _controller), - // ── 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, + // ── Our bottom bar ────────────────────────────────────────── + if (!_isOnOnboardingPage) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: _FocusGramNavBar( + currentIndex: _currentIndex, + onTap: _onTabTapped, + height: barHeight * 0.99, // 1% reduction + ), ), - ), ], ), ), ); } - - void _openSessionModal() { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (_) => const SessionModal(), - ); - } } // ────────────────────────────────────────────────────────────────────────────── @@ -424,7 +494,6 @@ 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( @@ -584,7 +653,13 @@ class _EdgePanelState extends State<_EdgePanel> { ), const SizedBox(height: 8), Text( - _formatTime(sm.remainingSessionSeconds), + context.read().isSessionActive + ? _formatTime( + context + .read() + .remainingSessionSeconds, + ) + : 'Off', style: TextStyle( color: barColor, fontSize: 40, @@ -592,67 +667,58 @@ class _EdgePanelState extends State<_EdgePanel> { letterSpacing: 2, ), ), - const SizedBox(height: 24), - // Daily Remaining - const Text( - 'DAILY QUOTA', - style: TextStyle( - color: Colors.white30, - fontSize: 11, - letterSpacing: 1, - ), - ), - const SizedBox(height: 8), - Text( + const SizedBox(height: 20), + _buildStatRow( + 'REEL QUOTA', '${sm.dailyRemainingSeconds ~/ 60}m Left', - style: const TextStyle( - color: Colors.white70, - fontSize: 18, - fontWeight: FontWeight.w500, - ), + Icons.timer_outlined, + ), + _buildStatRow( + 'AUTO-CLOSE', + _formatTime(sm.appSessionRemainingSeconds), + Icons.hourglass_empty_rounded, + ), + _buildStatRow( + 'COOLDOWN', + sm.isCooldownActive + ? _formatTime(sm.cooldownRemainingSeconds) + : 'Off', + Icons.coffee_rounded, + isWarning: sm.isCooldownActive, ), const SizedBox(height: 32), - const Divider(color: Colors.white10, height: 1), - const SizedBox(height: 20), - // Settings Link - InkWell( - onTap: () { - _toggleExpansion(); - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const SettingsPage()), - ); - }, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: Colors.blue.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.tune_rounded, - color: Colors.blueAccent, - size: 18, - ), - ), - const SizedBox(width: 14), - const Text( - 'Preferences', - style: TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.w500, - ), - ), - ], + if (!context + .findAncestorStateOfType<_MainWebViewPageState>()! + ._isOnOnboardingPage) ...[ + const Divider(color: Colors.white10), + const SizedBox(height: 8), + ListTile( + onTap: () async { + _toggleExpansion(); + final state = context + .findAncestorStateOfType<_MainWebViewPageState>(); + if (state != null) { + await state._signOut(); + } + }, + leading: const Icon( + Icons.logout_rounded, + color: Colors.redAccent, + size: 20, ), + title: const Text( + 'Switch Account', + style: TextStyle( + color: Colors.redAccent, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + dense: true, + contentPadding: EdgeInsets.zero, ), - ), + const SizedBox(height: 8), + ], ], ), ), @@ -662,6 +728,60 @@ class _EdgePanelState extends State<_EdgePanel> { ); } + Widget _buildStatRow( + String label, + String value, + IconData icon, { + bool isWarning = false, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isWarning + ? Colors.redAccent.withValues(alpha: 0.1) + : Colors.white.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + icon, + color: isWarning ? Colors.redAccent : Colors.white70, + size: 16, + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + color: Colors.white38, + fontSize: 9, + fontWeight: FontWeight.bold, + letterSpacing: 1, + ), + ), + const SizedBox(height: 2), + Text( + value, + style: TextStyle( + color: isWarning ? Colors.redAccent : Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ], + ), + ); + } + String _formatTime(int seconds) { final m = seconds ~/ 60; final s = seconds % 60; @@ -670,9 +790,35 @@ class _EdgePanelState extends State<_EdgePanel> { } // ────────────────────────────────────────────────────────────────────────────── -// Custom Bottom Nav Bar — minimal, Instagram-like +// Branded Top Bar — minimal, Instagram-like font // ────────────────────────────────────────────────────────────────────────────── +class _BrandedTopBar extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + height: 60, + decoration: const BoxDecoration( + color: Colors.black, + border: Border(bottom: BorderSide(color: Colors.white12, width: 0.5)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'FocusGram', + style: GoogleFonts.grandHotel( + color: Colors.white, + fontSize: 32, + letterSpacing: 0.5, + ), + ), + ], + ), + ); + } +} + class _FocusGramNavBar extends StatelessWidget { final int currentIndex; final Future Function(int) onTap; @@ -711,6 +857,7 @@ class _FocusGramNavBar extends StatelessWidget { behavior: HitTestBehavior.opaque, child: SizedBox( width: 60, + height: double.infinity, child: Center( child: Icon( isSelected ? filledIcon : outlinedIcon, @@ -727,3 +874,54 @@ class _FocusGramNavBar extends StatelessWidget { ); } } + +// ────────────────────────────────────────────────────────────────────────────── +// No Internet Screen — minimal, branded +// ────────────────────────────────────────────────────────────────────────────── + +class _NoInternetScreen extends StatelessWidget { + final VoidCallback onRetry; + const _NoInternetScreen({required this.onRetry}); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.black, + width: double.infinity, + height: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.wifi_off_rounded, color: Colors.white24, size: 80), + const SizedBox(height: 24), + const Text( + 'No Connection', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + const Text( + 'Please check your internet settings.', + style: TextStyle(color: Colors.white38, fontSize: 14), + ), + const SizedBox(height: 40), + ElevatedButton( + onPressed: onRetry, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: const Text('Retry'), + ), + ], + ), + ); + } +} diff --git a/lib/screens/reel_player_overlay.dart b/lib/screens/reel_player_overlay.dart index 5a83964..87e9049 100644 --- a/lib/screens/reel_player_overlay.dart +++ b/lib/screens/reel_player_overlay.dart @@ -41,6 +41,9 @@ class _ReelPlayerOverlayState extends State { InjectionController.buildInjectionJS( sessionActive: true, blurExplore: false, + blurReels: false, + ghostMode: false, + enableTextSelection: true, ), ); }, diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 38d43ef..4166b40 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -47,6 +47,13 @@ class SettingsPage extends StatelessWidget { icon: Icons.visibility_off_outlined, destination: const _DistractionSettingsPage(), ), + _buildSettingsTile( + context: context, + title: 'Extras', + subtitle: 'Ghost mode, text selection and experimental features', + icon: Icons.extension_outlined, + destination: const _ExtrasSettingsPage(), + ), _buildSettingsTile( context: context, title: 'About', @@ -365,3 +372,53 @@ class _FrictionSliderTileState extends State<_FrictionSliderTile> { ); } } + +class _ExtrasSettingsPage extends StatelessWidget { + const _ExtrasSettingsPage(); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + title: const Text('Extras', style: TextStyle(fontSize: 17)), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new, size: 18), + onPressed: () => Navigator.pop(context), + ), + ), + body: ListView( + children: [ + SwitchListTile( + title: const Text( + 'Ghost Mode', + style: TextStyle(color: Colors.white), + ), + subtitle: const Text( + 'Hides "typing..." and "seen" status in DMs', + style: TextStyle(color: Colors.white54, fontSize: 13), + ), + value: settings.ghostMode, + onChanged: (v) => settings.setGhostMode(v), + activeThumbColor: Colors.blue, + ), + SwitchListTile( + title: const Text( + 'Enable Text Selection', + style: TextStyle(color: Colors.white), + ), + subtitle: const Text( + 'Allows copying text from posts and captions', + style: TextStyle(color: Colors.white54, fontSize: 13), + ), + value: settings.enableTextSelection, + onChanged: (v) => settings.setEnableTextSelection(v), + activeThumbColor: Colors.blue, + ), + ], + ), + ); + } +} diff --git a/lib/services/injection_controller.dart b/lib/services/injection_controller.dart index 7cf40bb..7aa9157 100644 --- a/lib/services/injection_controller.dart +++ b/lib/services/injection_controller.dart @@ -1,70 +1,116 @@ -/// 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. +/// 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 { - /// iOS Safari user-agent — reduces login friction with Instagram. + /// The requested iOS 18.6 User Agent for Instagram App feel. 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'; + '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 injection ─────────────────────────────────────────────────────────── + // ── 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. - /// 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 { + 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; } - /* 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. + /// CSS to hide Reel-related elements everywhere. 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"] { + 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; @@ -72,95 +118,49 @@ class InjectionController { '''; /// 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"] { + div[style*="bottom: 0px"], div[style*="bottom: 0"], form[method="POST"] { 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; + div[role="main"] div[style*="position: fixed"] { + bottom: 72px !important; } '''; - /// CSS to blur Explore feed posts/reels (keeps stories visible). + /// CSS to blur Explore feed posts/reels. 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 { + main[role="main"] article img, main[role="main"] article video, + ._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; + '''; + + static const String _blurReelsCSS = ''' + a[href*="/reel/"] img, a[href*="/reels"] img { + filter: blur(12px) !important; } '''; - /// Auto-dismiss "Open in App" banner that Instagram shows in mobile browsers. + /// Auto-dismiss "Open in App" banner. 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()); - }); + 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: every 500ms force-removes the bottom nav. - /// Complements the MutationObserver for sites that rebuild DOM faster. + /// Periodic remover for bottom nav. 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;'; - } + document.querySelectorAll('div[role="tablist"], nav[role="navigation"], footer nav').forEach(el => { + el.style.cssText += ';display:none!important;height:0!important;'; }); } removeNav(); @@ -168,12 +168,11 @@ class InjectionController { })(); '''; - /// MutationObserver that continuously re-applies CSS after SPA re-renders. + /// 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) { @@ -183,175 +182,88 @@ class InjectionController { } el.textContent = ${_escapeJsString(cssContent)}; } - injectCSS(); - - const observer = new MutationObserver(function() { - if (!document.getElementById(STYLE_ID)) { - injectCSS(); - } - }); - - observer.observe(document.documentElement, { - childList: true, - subtree: true, + const observer = new MutationObserver(() => { + 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 = '/'; - } + const btn = document.querySelector('[aria-label="New post"], [aria-label="Create"]'); + if (btn) btn.closest('a, button') ? btn.closest('a, button').click() : btn.click(); })(); '''; - /// 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. + /// MutationObserver for Reel scroll locking. 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('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; - // Block upward swipe (next reel), allow downward (go back) - if (deltaY < -10) { - if (e.cancelable) { - e.preventDefault(); - e.stopPropagation(); - } + if (deltaY < -10 && 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 + document.querySelectorAll('[class*="ReelsVideoPlayer"], video').forEach((el) => { + if (el.tagName === 'VIDEO' && el.closest('article')) return; lockReelScroll(el); - // Also try parent if it's a video - if (el.tagName === 'VIDEO' && el.parentElement) { - lockReelScroll(el.parentElement); - } + 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. + /// 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); - css.write(_topPaddingCSS); - if (!sessionActive) css.write(_hideReelsCSS); - if (blurExplore) css.write(_blurExploreCSS); + + if (!sessionActive) { + css.write(_hideReelsCSS); + if (blurExplore) css.write(_blurExploreCSS); + if (blurReels) css.write(_blurReelsCSS); + } return ''' ${buildSessionStateJS(sessionActive)} @@ -359,6 +271,8 @@ class InjectionController { $_periodicNavRemoverJS $_dismissAppBannerJS $reelsMutationObserverJS + $_brandingJS + ${ghostMode ? _ghostModeJS : ''} '''; } } diff --git a/lib/services/session_manager.dart b/lib/services/session_manager.dart index e6d9526..425ba62 100644 --- a/lib/services/session_manager.dart +++ b/lib/services/session_manager.dart @@ -101,6 +101,7 @@ class SessionManager extends ChangeNotifier { int get perSessionSeconds => _perSessionSeconds; int get cooldownSeconds => _cooldownSeconds; + DateTime? get lastSessionEnd => _lastSessionEnd; // ── Public getters — App session ────────────────────────── diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index b2ab6d8..a946b6f 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -8,6 +8,8 @@ class SettingsService extends ChangeNotifier { static const _keyRequireLongPress = 'set_require_long_press'; static const _keyShowBreathGate = 'set_show_breath_gate'; static const _keyRequireWordChallenge = 'set_require_word_challenge'; + static const _keyGhostMode = 'set_ghost_mode'; + static const _keyEnableTextSelection = 'set_enable_text_selection'; SharedPreferences? _prefs; @@ -15,14 +17,17 @@ class SettingsService extends ChangeNotifier { bool _blurReels = false; // If false: hide reels in feed (after session ends) bool _requireLongPress = true; // Long-press FAB to start session bool _showBreathGate = true; // Show breathing gate on every open - bool _requireWordChallenge = - true; // Random word sequence challenge before changes + bool _requireWordChallenge = true; + bool _ghostMode = true; // Default: hide seen/typing + bool _enableTextSelection = false; // Default: disabled bool get blurExplore => _blurExplore; bool get blurReels => _blurReels; bool get requireLongPress => _requireLongPress; bool get showBreathGate => _showBreathGate; bool get requireWordChallenge => _requireWordChallenge; + bool get ghostMode => _ghostMode; + bool get enableTextSelection => _enableTextSelection; Future init() async { _prefs = await SharedPreferences.getInstance(); @@ -31,6 +36,8 @@ class SettingsService extends ChangeNotifier { _requireLongPress = _prefs!.getBool(_keyRequireLongPress) ?? true; _showBreathGate = _prefs!.getBool(_keyShowBreathGate) ?? true; _requireWordChallenge = _prefs!.getBool(_keyRequireWordChallenge) ?? true; + _ghostMode = _prefs!.getBool(_keyGhostMode) ?? true; + _enableTextSelection = _prefs!.getBool(_keyEnableTextSelection) ?? false; notifyListeners(); } @@ -63,4 +70,16 @@ class SettingsService extends ChangeNotifier { await _prefs?.setBool(_keyRequireWordChallenge, v); notifyListeners(); } + + Future setGhostMode(bool v) async { + _ghostMode = v; + await _prefs?.setBool(_keyGhostMode, v); + notifyListeners(); + } + + Future setEnableTextSelection(bool v) async { + _enableTextSelection = v; + await _prefs?.setBool(_keyEnableTextSelection, v); + notifyListeners(); + } } diff --git a/lib/utils/discipline_challenge.dart b/lib/utils/discipline_challenge.dart index b136db8..5bdaf88 100644 --- a/lib/utils/discipline_challenge.dart +++ b/lib/utils/discipline_challenge.dart @@ -543,7 +543,7 @@ class DisciplineChallenge { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - 'Type the following sequence exactly to proceed:', + 'FocusGram is currently blocked to help you stay focused. Type the following sequence exactly to proceed:', style: TextStyle(color: Colors.white70, fontSize: 14), ), const SizedBox(height: 16), diff --git a/pubspec.lock b/pubspec.lock index e03fe53..8c39466 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" args: dependency: transitive description: @@ -33,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -41,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -49,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" dbus: dependency: transitive description: @@ -86,6 +126,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" flutter_lints: dependency: "direct dev" description: @@ -136,8 +184,32 @@ packages: description: flutter source: sdk version: "0.0.0" - http: + glob: dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: db9df7a5898d894eeda4c78143f35c30a243558be439518972366880b80bf88e + url: "https://pub.dev" + source: hosted + version: "8.0.2" + hooks: + dependency: transitive + description: + name: hooks + sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + http: + dependency: "direct main" description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" @@ -152,6 +224,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" intl: dependency: "direct main" description: @@ -160,6 +240,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" leak_tracker: dependency: transitive description: @@ -192,6 +280,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -216,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + url: "https://pub.dev" + source: hosted + version: "0.17.4" nested: dependency: transitive description: @@ -224,6 +328,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" path: dependency: transitive description: @@ -232,6 +344,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -280,6 +416,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" provider: dependency: "direct main" description: @@ -288,6 +432,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" shared_preferences: dependency: "direct main" description: @@ -549,6 +701,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index bf79a66..dcac008 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,11 +28,28 @@ dependencies: # URL launcher for About page links — latest stable url_launcher: ^6.3.2 + google_fonts: ^8.0.2 + http: ^1.3.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 + flutter_launcher_icons: ^0.13.1 flutter: uses-material-design: true + + assets: + - assets/images/focusgram.png + - assets/images/focusgram.ico + +flutter_launcher_icons: + android: true + ios: false + image_path: "assets/images/focusgram.png" + adaptive_icon_background: "#000000" + adaptive_icon_foreground: "assets/images/focusgram.png" + min_sdk_android: 21 + + diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 32a6ecb..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:focusgram/main.dart'; - -void main() { - // Widget tests for FocusGram are not yet implemented. - // The app requires SharedPreferences and WebView which need mocking. - test('placeholder', () { - expect(FocusGramApp, isNotNull); - }); -}