import 'dart:async'; import 'package:flutter/material.dart'; import '../../../core/network/api_client.dart'; import '../../../core/theme/app_theme.dart'; import '../models/news_article_model.dart'; import '../repositories/news_repository.dart'; import '../widgets/news_card_item.dart'; import '../widgets/advanced_news_filter_bar.dart'; /// Paginated Infinite Scroll Daily News Feed screen with sentiment toggle filters, real-time update handling, and clean navigation. class NewsFeedScreen extends StatefulWidget { final ApiClient apiClient; final NewsRepository? repository; const NewsFeedScreen({super.key, required this.apiClient, this.repository}); @override State createState() => _NewsFeedScreenState(); } class _NewsFeedScreenState extends State { late final NewsRepository _repository; final ScrollController _scrollController = ScrollController(); final List _newsItems = []; int _currentPage = 1; static const int _pageSize = 20; bool _isLoading = false; bool _hasMore = true; // Filter States String? _searchQuery; DateTime? _selectedDate; String? _selectedIsin; bool _hasSentimentOnly = false; String? _selectedSentimentFilter; // null, 'POSITIVE', 'NEUTRAL', 'NEGATIVE' Timer? _debounceTimer; @override void initState() { super.initState(); _repository = widget.repository ?? NewsRepository(apiClient: widget.apiClient, backendUrl: ApiClient.baseUrl); _loadNews(refresh: true); _scrollController.addListener(() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { _loadNews(); } }); } @override void dispose() { _debounceTimer?.cancel(); _scrollController.dispose(); super.dispose(); } Future _loadNews({bool refresh = false}) async { if (_isLoading) return; if (refresh) { _currentPage = 1; _hasMore = true; _newsItems.clear(); } if (!_hasMore) return; setState(() => _isLoading = true); try { final fetched = await _repository.fetchNews( page: _currentPage, pageSize: _pageSize, date: _selectedDate?.toIso8601String().substring(0, 10), query: _searchQuery?.trim(), isin: _selectedIsin?.trim(), hasSentiment: _hasSentimentOnly || _selectedSentimentFilter != null, ); setState(() { final existingIds = _newsItems.map((e) => e.id).where((id) => id.isNotEmpty).toSet(); for (final item in fetched) { if (item.id.isEmpty || !existingIds.contains(item.id)) { _newsItems.add(item); if (item.id.isNotEmpty) existingIds.add(item.id); } } _newsItems.sort((a, b) => b.publishedAt.compareTo(a.publishedAt)); _currentPage++; if (fetched.length < _pageSize) { _hasMore = false; } }); // If a sentiment filter is active and no items matched yet on this page, automatically load the next page if (_filteredNewsItems.isEmpty && _hasMore && _selectedSentimentFilter != null) { _loadNews(); } } catch (_) { } finally { setState(() => _isLoading = false); } } List get _filteredNewsItems { if (_selectedSentimentFilter == null) return _newsItems; final target = _selectedSentimentFilter!.toUpperCase(); return _newsItems.where((item) { final s = item.sentiment.toUpperCase(); if (target == 'POSITIVE') return s.contains('POS'); if (target == 'NEGATIVE') return s.contains('NEG'); if (target == 'NEUTRAL') return s.contains('NEU'); return s == target; }).toList(); } void _onArticleUpdated(NewsArticleModel updated) { setState(() { final idx = _newsItems.indexWhere((e) => e.id == updated.id); if (idx != -1) { _newsItems[idx] = updated; } }); } void _onSearchChanged(String? val) { _searchQuery = val; _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(milliseconds: 600), () { _loadNews(refresh: true); }); } void _onIsinChanged(String? val) { _selectedIsin = val; _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(milliseconds: 600), () { _loadNews(refresh: true); }); } void _resetFilters() { _debounceTimer?.cancel(); setState(() { _searchQuery = null; _selectedDate = null; _selectedIsin = null; _hasSentimentOnly = false; _selectedSentimentFilter = null; }); _loadNews(refresh: true); } @override Widget build(BuildContext context) { final displayedItems = _filteredNewsItems; return Scaffold( body: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Marktnachrichten & Feed', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), if (_newsItems.isNotEmpty) Text( '${displayedItems.length} Artikel angezeigt', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), ), ], ), const SizedBox(height: 12), AdvancedNewsFilterBar( searchQuery: _searchQuery, selectedDate: _selectedDate, selectedIsin: _selectedIsin, hasSentimentOnly: _hasSentimentOnly, selectedSentimentFilter: _selectedSentimentFilter, onSearchChanged: _onSearchChanged, onIsinChanged: _onIsinChanged, onDateChanged: (val) { setState(() => _selectedDate = val); _loadNews(refresh: true); }, onSentimentToggleChanged: (val) { setState(() { _hasSentimentOnly = val; if (!val) { _selectedSentimentFilter = null; } }); _loadNews(refresh: true); }, onSentimentFilterChanged: (val) { setState(() { _selectedSentimentFilter = val; if (val != null) { _hasSentimentOnly = true; } }); _loadNews(refresh: true); }, onResetFilters: _resetFilters, ), const SizedBox(height: 16), Expanded( child: displayedItems.isEmpty && !_isLoading ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.feed_outlined, size: 40, color: AppTheme.textMuted), const SizedBox(height: 8), Text('Keine Nachrichten für diese Filterkriterien gefunden.', style: TextStyle(color: AppTheme.textMuted, fontSize: 13)), const SizedBox(height: 10), TextButton( onPressed: _resetFilters, child: Text('Filter zurücksetzen', style: TextStyle(color: AppTheme.accentCyan)), ), ], ), ) : ListView.builder( controller: _scrollController, itemCount: displayedItems.length + (_hasMore ? 1 : 0), itemBuilder: (context, index) { if (index == displayedItems.length) { return Center( child: Padding( padding: const EdgeInsets.all(16), child: CircularProgressIndicator(color: AppTheme.primaryEmerald, strokeWidth: 2), ), ); } return NewsCardItem( item: displayedItems[index], apiClient: widget.apiClient, onArticleUpdated: _onArticleUpdated, ); }, ), ), ], ), ), ); } }