import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/store_provider.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; import 'package:spotiflac_android/screens/home_tab.dart'; import 'package:spotiflac_android/screens/store_tab.dart'; import 'package:spotiflac_android/screens/queue_tab.dart'; import 'package:spotiflac_android/screens/settings/settings_tab.dart'; import 'package:spotiflac_android/services/share_intent_service.dart'; import 'package:spotiflac_android/services/update_checker.dart'; import 'package:spotiflac_android/widgets/update_dialog.dart'; import 'package:spotiflac_android/utils/logger.dart'; final _log = AppLogger('MainShell'); class MainShell extends ConsumerStatefulWidget { const MainShell({super.key}); @override ConsumerState createState() => _MainShellState(); } class _MainShellState extends ConsumerState { int _currentIndex = 0; late PageController _pageController; bool _hasCheckedUpdate = false; StreamSubscription? _shareSubscription; DateTime? _lastBackPress; @override void initState() { super.initState(); _pageController = PageController(initialPage: _currentIndex); WidgetsBinding.instance.addPostFrameCallback((_) { _checkForUpdates(); _setupShareListener(); }); } void _setupShareListener() { final pendingUrl = ShareIntentService().consumePendingUrl(); if (pendingUrl != null) { _log.d('Processing pending shared URL: $pendingUrl'); _handleSharedUrl(pendingUrl); } _shareSubscription = ShareIntentService().sharedUrlStream.listen( (url) { _log.d('Received shared URL from stream: $url'); _handleSharedUrl(url); }, onError: (error) { _log.e('Share stream error: $error'); }, cancelOnError: false, ); } void _handleSharedUrl(String url) { Navigator.of(context).popUntil((route) => route.isFirst); if (_currentIndex != 0) { _onNavTap(0); } ref.read(trackProvider.notifier).fetchFromUrl(url); ref.read(settingsProvider.notifier).setHasSearchedBefore(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.loadingSharedLink)), ); } } Future _checkForUpdates() async { if (_hasCheckedUpdate) return; _hasCheckedUpdate = true; final settings = ref.read(settingsProvider); if (!settings.checkForUpdates) return; final updateInfo = await UpdateChecker.checkForUpdate(channel: settings.updateChannel); if (updateInfo != null && mounted) { showUpdateDialog( context, updateInfo: updateInfo, onDisableUpdates: () { ref.read(settingsProvider.notifier).setCheckForUpdates(false); }, ); } } @override void dispose() { _shareSubscription?.cancel(); _pageController.dispose(); super.dispose(); } void _onNavTap(int index) { if (_currentIndex != index) { setState(() => _currentIndex = index); _pageController.animateToPage( index, duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, ); } } void _onPageChanged(int index) { if (_currentIndex != index) { setState(() => _currentIndex = index); FocusManager.instance.primaryFocus?.unfocus(); } } /// Handle back press with double-tap to exit void _handleBackPress() { final trackState = ref.read(trackProvider); final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0; if (isKeyboardVisible) { FocusManager.instance.primaryFocus?.unfocus(); return; } if (_currentIndex == 0 && trackState.isShowingRecentAccess) { ref.read(trackProvider.notifier).setShowingRecentAccess(false); FocusManager.instance.primaryFocus?.unfocus(); return; } if (_currentIndex == 0 && !trackState.isLoading && (trackState.hasSearchText || trackState.hasContent)) { ref.read(trackProvider.notifier).clear(); return; } if (_currentIndex != 0) { _onNavTap(0); return; } if (trackState.isLoading) { return; } final now = DateTime.now(); if (_lastBackPress != null && now.difference(_lastBackPress!) < const Duration(seconds: 2)) { SystemNavigator.pop(); } else { _lastBackPress = now; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(context.l10n.pressBackAgainToExit), duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, ), ); } } @override Widget build(BuildContext context) { final queueState = ref.watch(downloadQueueProvider.select((s) => s.queuedCount)); final trackState = ref.watch(trackProvider); final showStore = ref.watch(settingsProvider.select((s) => s.showExtensionStore)); final storeUpdatesCount = ref.watch(storeProvider.select((s) => s.updatesAvailableCount)); final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0; // Determine if we can pop (for predictive back animation) // canPop is true when we're at root with no content - enables predictive back gesture // IMPORTANT: Never allow pop when keyboard is visible to prevent accidental navigation final canPop = _currentIndex == 0 && !trackState.hasSearchText && !trackState.hasContent && !trackState.isLoading && !trackState.isShowingRecentAccess && !isKeyboardVisible; final tabs = [ const HomeTab(), QueueTab( parentPageController: _pageController, parentPageIndex: 1, nextPageIndex: showStore ? 2 : 3, ), if (showStore) const StoreTab(), const SettingsTab(), ]; final l10n = context.l10n; final destinations = [ NavigationDestination( icon: const Icon(Icons.home_outlined), selectedIcon: const Icon(Icons.home), label: l10n.navHome, ), NavigationDestination( icon: Badge( isLabelVisible: queueState > 0, label: Text('$queueState'), child: const Icon(Icons.history_outlined), ), selectedIcon: Badge( isLabelVisible: queueState > 0, label: Text('$queueState'), child: const Icon(Icons.history), ), label: l10n.navHistory, ), if (showStore) NavigationDestination( icon: Badge( isLabelVisible: storeUpdatesCount > 0, label: Text('$storeUpdatesCount'), child: const Icon(Icons.store_outlined), ), selectedIcon: Badge( isLabelVisible: storeUpdatesCount > 0, label: Text('$storeUpdatesCount'), child: const Icon(Icons.store), ), label: l10n.navStore, ), NavigationDestination( icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings), label: l10n.navSettings, ), ]; final maxIndex = tabs.length - 1; if (_currentIndex > maxIndex) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() => _currentIndex = maxIndex); _pageController.jumpToPage(maxIndex); } }); } return PopScope( canPop: canPop, onPopInvokedWithResult: (didPop, result) async { if (didPop) { // System handled the pop - this means predictive back completed // We need to handle double-tap to exit here return; } _handleBackPress(); }, child: Scaffold( body: PageView( controller: _pageController, onPageChanged: _onPageChanged, physics: const ClampingScrollPhysics(), children: tabs, ), bottomNavigationBar: NavigationBar( selectedIndex: _currentIndex.clamp(0, maxIndex), onDestinationSelected: _onNavTap, animationDuration: const Duration(milliseconds: 200), backgroundColor: Theme.of(context).brightness == Brightness.dark ? Color.alphaBlend(Colors.white.withValues(alpha: 0.05), Theme.of(context).colorScheme.surface) : Color.alphaBlend(Colors.black.withValues(alpha: 0.03), Theme.of(context).colorScheme.surface), destinations: destinations, ), ), ); } }