diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index 1daa28cd..4324d97c 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -77,6 +77,11 @@ class _DiscoverScreenState extends State if (_onDeck.isEmpty || !_heroController.hasClients || _isAutoScrollPaused) return; + // Validate current index is within bounds before calculating next page + if (_currentHeroIndex >= _onDeck.length) { + _currentHeroIndex = 0; + } + final nextPage = (_currentHeroIndex + 1) % _onDeck.length; _heroController.animateToPage( nextPage, @@ -112,6 +117,41 @@ class _DiscoverScreenState extends State _startAutoScroll(); } + // Helper method to calculate visible dot range (max 5 dots) + ({int start, int end}) _getVisibleDotRange() { + final totalDots = _onDeck.length; + if (totalDots <= 5) { + return (start: 0, end: totalDots - 1); + } + + // Center the active dot when possible + final center = _currentHeroIndex; + int start = (center - 2).clamp(0, totalDots - 5); + int end = start + 4; // 5 dots total (0-4 inclusive) + + return (start: start, end: end); + } + + // Helper method to determine dot size based on position + double _getDotSize(int dotIndex, int start, int end) { + final totalDots = _onDeck.length; + + // If we have 5 or fewer dots, all are full size (8px) + if (totalDots <= 5) { + return 8.0; + } + + // First and last visible dots are smaller if there are more items beyond them + final isFirstVisible = dotIndex == start && start > 0; + final isLastVisible = dotIndex == end && end < totalDots - 1; + + if (isFirstVisible || isLastVisible) { + return 5.0; // Smaller edge dots + } + + return 8.0; // Normal size + } + Future _loadContent() async { appLogger.d('Loading discover content'); setState(() { @@ -137,7 +177,16 @@ class _DiscoverScreenState extends State _onDeck = onDeck; _recentlyAdded = recentlyAdded; _isLoading = false; + + // Reset hero index to avoid sync issues + _currentHeroIndex = 0; }); + + // Sync PageController to first page after data loads + if (_heroController.hasClients && onDeck.isNotEmpty) { + _heroController.jumpToPage(0); + } + appLogger.d('Discover content loaded successfully'); } catch (e) { appLogger.e('Failed to load discover content', error: e); @@ -452,10 +501,13 @@ class _DiscoverScreenState extends State controller: _heroController, itemCount: _onDeck.length, onPageChanged: (index) { - setState(() { - _currentHeroIndex = index; - }); - _resetAutoScrollTimer(); + // Validate index is within bounds before updating + if (index >= 0 && index < _onDeck.length) { + setState(() { + _currentHeroIndex = index; + }); + _resetAutoScrollTimer(); + } }, itemBuilder: (context, index) { return _buildHeroItem(_onDeck[index]); @@ -488,57 +540,67 @@ class _DiscoverScreenState extends State ), // Spacer to separate indicators from button const SizedBox(width: 8), - // Page indicators - ...List.generate(_onDeck.length, (index) { - final isActive = _currentHeroIndex == index; - if (isActive) { - // Animated progress indicator for active page - return AnimatedBuilder( - animation: _indicatorAnimationController, - builder: (context, child) { - // Fill width animates from 8px to 24px - final fillWidth = - 8.0 + - (16.0 * _indicatorAnimationController.value); + // Page indicators (limited to 5 dots) + ...() { + final range = _getVisibleDotRange(); + return List.generate( + range.end - range.start + 1, + (i) { + final index = range.start + i; + final isActive = _currentHeroIndex == index; + final dotSize = _getDotSize(index, range.start, range.end); + + if (isActive) { + // Animated progress indicator for active page + return AnimatedBuilder( + animation: _indicatorAnimationController, + builder: (context, child) { + // Fill width animates based on dot size + final maxWidth = dotSize * 3; // 24px for normal, 15px for small + final fillWidth = + dotSize + + ((maxWidth - dotSize) * _indicatorAnimationController.value); + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: const EdgeInsets.symmetric(horizontal: 4), + width: maxWidth, + height: dotSize, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(dotSize / 2), + ), + child: Align( + alignment: Alignment.centerLeft, + child: Container( + width: fillWidth, + height: dotSize, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(dotSize / 2), + ), + ), + ), + ); + }, + ); + } else { + // Static indicator for inactive pages return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, margin: const EdgeInsets.symmetric(horizontal: 4), - width: 24, - height: 8, + width: dotSize, + height: dotSize, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.4), - borderRadius: BorderRadius.circular(4), - ), - child: Align( - alignment: Alignment.centerLeft, - child: Container( - width: fillWidth, - height: 8, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), - ), - ), + borderRadius: BorderRadius.circular(dotSize / 2), ), ); - }, - ); - } else { - // Static indicator for inactive pages - return AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - margin: const EdgeInsets.symmetric(horizontal: 4), - width: 8, - height: 8, - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.4), - borderRadius: BorderRadius.circular(4), - ), - ); - } - }), + } + }, + ); + }(), ], ), ),