import 'dart:async'; import 'package:flutter/material.dart'; import '../../../core/network/api_client.dart'; import '../../../core/theme/app_theme.dart'; import '../widgets/news_card_item.dart'; import '../widgets/advanced_news_filter_bar.dart'; /// Paginated Infinite Scroll Daily News Feed screen with deduplication and strict chronological sorting. class NewsFeedScreen extends StatefulWidget { final ApiClient apiClient; const NewsFeedScreen({super.key, required this.apiClient}); @override State createState() => _NewsFeedScreenState(); } class _NewsFeedScreenState extends State { final ScrollController _scrollController = ScrollController(); final List _newsItems = []; int _currentPage = 1; static const int _pageSize = 15; bool _isLoading = false; bool _hasMore = true; // Filter States String? _searchQuery; DateTime? _selectedDate; String? _selectedIsin; bool _hasSentimentOnly = false; Timer? _debounceTimer; @override void initState() { super.initState(); _loadNews(refresh: true); _scrollController.addListener(() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { _loadNews(); } }); } @override void dispose() { _debounceTimer?.cancel(); _scrollController.dispose(); super.dispose(); } DateTime _parseDateTime(dynamic val) { if (val == null) return DateTime.fromMillisecondsSinceEpoch(0); final str = val.toString().trim(); if (str.isEmpty) return DateTime.fromMillisecondsSinceEpoch(0); try { return DateTime.parse(str).toUtc(); } catch (_) { return DateTime.fromMillisecondsSinceEpoch(0); } } 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 queryParams = { 'page': _currentPage, 'pageSize': _pageSize, }; if (_selectedDate != null) { queryParams['date'] = _selectedDate!.toIso8601String().substring(0, 10); } if (_searchQuery != null && _searchQuery!.trim().isNotEmpty) { queryParams['query'] = _searchQuery!.trim(); } if (_selectedIsin != null && _selectedIsin!.trim().isNotEmpty) { queryParams['isin'] = _selectedIsin!.trim(); } if (_hasSentimentOnly) { queryParams['hasSentiment'] = true; } final res = await widget.apiClient.get('/api/v1/news', queryParameters: queryParams); if (res.statusCode == 200 && res.data != null && res.data is List) { final List fetched = res.data as List; setState(() { // Deduplicate by ID final existingIds = _newsItems.map((e) => e['id'] ?? e['Id']).where((id) => id != null).toSet(); for (final item in fetched) { final id = item['id'] ?? item['Id']; if (id == null || !existingIds.contains(id)) { _newsItems.add(item); if (id != null) existingIds.add(id); } } // Re-sort strictly by publication timestamp descending (newest articles at the top) _newsItems.sort((a, b) { final dtA = _parseDateTime(a['publishedAt'] ?? a['PublishedAt'] ?? a['scrapedAt'] ?? a['ScrapedAt']); final dtB = _parseDateTime(b['publishedAt'] ?? b['PublishedAt'] ?? b['scrapedAt'] ?? b['ScrapedAt']); return dtB.compareTo(dtA); }); _currentPage++; if (fetched.length < _pageSize) { _hasMore = false; } }); } } catch (_) { // Handle error visually if necessary, currently silent fallback } finally { setState(() => _isLoading = false); } } 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; }); _loadNews(refresh: true); } @override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Marktnachrichten & Feed', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 12), AdvancedNewsFilterBar( searchQuery: _searchQuery, selectedDate: _selectedDate, selectedIsin: _selectedIsin, hasSentimentOnly: _hasSentimentOnly, onSearchChanged: _onSearchChanged, onIsinChanged: _onIsinChanged, onDateChanged: (val) { setState(() => _selectedDate = val); _loadNews(refresh: true); }, onSentimentToggleChanged: (val) { setState(() => _hasSentimentOnly = val); _loadNews(refresh: true); }, onResetFilters: _resetFilters, ), const SizedBox(height: 16), Expanded( child: _newsItems.isEmpty && !_isLoading ? Center( child: Text('Keine Nachrichten für diese Filterkriterien gefunden.', style: TextStyle(color: AppTheme.textMuted)), ) : ListView.builder( controller: _scrollController, itemCount: _newsItems.length + (_hasMore ? 1 : 0), itemBuilder: (context, index) { if (index == _newsItems.length) { return Center( child: Padding( padding: const EdgeInsets.all(16), child: CircularProgressIndicator(color: AppTheme.primaryEmerald), ), ); } return NewsCardItem(item: _newsItems[index], apiClient: widget.apiClient); }, ), ), ], ), ), ); } }