diff --git a/lib/providers/news_provider.dart b/lib/providers/news_provider.dart new file mode 100644 index 0000000..18b3a66 --- /dev/null +++ b/lib/providers/news_provider.dart @@ -0,0 +1,157 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../data/models/briefing_feed.dart'; +import '../data/models/news_item.dart'; +import 'api_client_provider.dart'; + +// ─── NewsState ──────────────────────────────────────────────────────────────── + +class NewsState { + final List items; + final int offset; + final bool hasMore; + final bool loadingMore; + final int? selectedFeedId; + final Map reactions; + + const NewsState({ + required this.items, + required this.offset, + required this.hasMore, + required this.loadingMore, + required this.selectedFeedId, + required this.reactions, + }); + + NewsState copyWith({ + List? items, + int? offset, + bool? hasMore, + bool? loadingMore, + Object? selectedFeedId = _sentinel, + Map? reactions, + }) { + return NewsState( + items: items ?? this.items, + offset: offset ?? this.offset, + hasMore: hasMore ?? this.hasMore, + loadingMore: loadingMore ?? this.loadingMore, + selectedFeedId: selectedFeedId == _sentinel + ? this.selectedFeedId + : selectedFeedId as int?, + reactions: reactions ?? this.reactions, + ); + } +} + +const _sentinel = Object(); + +// ─── NewsNotifier ───────────────────────────────────────────────────────────── + +final newsProvider = + AsyncNotifierProvider(NewsNotifier.new); + +class NewsNotifier extends AsyncNotifier { + static const _limit = 40; + + @override + Future build() async { + final items = await ref.read(newsApiProvider).getNewsItems( + days: 90, + limit: _limit, + offset: 0, + ); + return NewsState( + items: items, + offset: items.length, + hasMore: items.length == _limit, + loadingMore: false, + selectedFeedId: null, + reactions: {for (final item in items) item.id: item.reaction}, + ); + } + + Future loadMore() async { + final current = state.value; + if (current == null || current.loadingMore || !current.hasMore) return; + state = AsyncData(current.copyWith(loadingMore: true)); + try { + final items = await ref.read(newsApiProvider).getNewsItems( + days: 90, + limit: _limit, + offset: current.offset, + feedId: current.selectedFeedId, + ); + final updatedReactions = Map.from(current.reactions); + for (final item in items) { + updatedReactions.putIfAbsent(item.id, () => item.reaction); + } + state = AsyncData(current.copyWith( + items: [...current.items, ...items], + offset: current.offset + items.length, + hasMore: items.length == _limit, + loadingMore: false, + reactions: updatedReactions, + )); + } catch (e) { + state = AsyncData(current.copyWith(loadingMore: false)); + rethrow; + } + } + + Future setFeed(int? feedId) async { + state = const AsyncLoading(); + try { + final items = await ref.read(newsApiProvider).getNewsItems( + days: 90, + limit: _limit, + offset: 0, + feedId: feedId, + ); + state = AsyncData(NewsState( + items: items, + offset: items.length, + hasMore: items.length == _limit, + loadingMore: false, + selectedFeedId: feedId, + reactions: {for (final item in items) item.id: item.reaction}, + )); + } catch (e, st) { + state = AsyncError(e, st); + } + } + + void toggleReaction(int itemId, String reaction) { + final current = state.value; + if (current == null) return; + final prev = current.reactions[itemId]; + final next = prev == reaction ? null : reaction; + state = AsyncData(current.copyWith( + reactions: {...current.reactions, itemId: next}, + )); + final briefingApi = ref.read(briefingApiProvider); + final future = next == null + ? briefingApi.deleteRssReaction(itemId) + : briefingApi.postRssReaction(itemId, next); + future.catchError((_) { + final s = state.value; + if (s != null) { + state = AsyncData(s.copyWith( + reactions: {...s.reactions, itemId: prev}, + )); + } + }); + } +} + +// ─── FeedsNotifier ──────────────────────────────────────────────────────────── + +final feedsProvider = + AsyncNotifierProvider>(FeedsNotifier.new); + +class FeedsNotifier extends AsyncNotifier> { + @override + Future> build() async { + return ref.read(newsApiProvider).getFeeds(); + } +}