1
- //
2
1
// ignore_for_file: lines_longer_than_80_chars
3
2
4
3
import 'package:flutter/material.dart' ;
5
4
import 'package:flutter_bloc/flutter_bloc.dart' ;
6
5
import 'package:go_router/go_router.dart' ;
6
+ import 'package:ht_main/headlines-feed/bloc/sources_filter_bloc.dart' ; // Import the BLoC
7
7
import 'package:ht_main/l10n/l10n.dart' ;
8
8
import 'package:ht_main/shared/constants/constants.dart' ;
9
9
import 'package:ht_main/shared/widgets/widgets.dart' ; // For loading/error widgets
10
10
import 'package:ht_sources_client/ht_sources_client.dart' ;
11
- import 'package:ht_sources_repository/ht_sources_repository.dart' ;
11
+ // Removed repository import: import 'package:ht_sources_repository/ht_sources_repository.dart';
12
12
13
13
/// {@template source_filter_page}
14
14
/// A page dedicated to selecting news sources for filtering headlines.
15
15
///
16
- /// Fetches sources paginatively, allows multiple selections, and returns
17
- /// the selected list via `context.pop` when the user applies the changes.
16
+ /// Uses [SourcesFilterBloc] to fetch sources paginatively, allows multiple
17
+ /// selections, and returns the selected list via `context.pop` when the user
18
+ /// applies the changes.
18
19
/// {@endtemplate}
19
20
class SourceFilterPage extends StatefulWidget {
20
21
/// {@macro source_filter_page}
@@ -26,32 +27,13 @@ class SourceFilterPage extends StatefulWidget {
26
27
27
28
/// State for the [SourceFilterPage] .
28
29
///
29
- /// Manages the local selection state ([_pageSelectedSources] ), fetches
30
- /// sources from the [HtSourcesRepository] , handles pagination using a
31
- /// [ScrollController] , and displays loading/error/empty/loaded states.
30
+ /// Manages the local selection state ([_pageSelectedSources] ) and interacts
31
+ /// with [SourcesFilterBloc] for data fetching and pagination.
32
32
class _SourceFilterPageState extends State <SourceFilterPage > {
33
33
/// Stores the sources selected by the user on this page.
34
34
/// Initialized from the `extra` parameter passed during navigation.
35
35
late Set <Source > _pageSelectedSources;
36
36
37
- /// List of all sources fetched from the repository.
38
- List <Source > _allSources = [];
39
-
40
- /// Flag indicating if the initial source list is being loaded.
41
- bool _isLoading = true ;
42
-
43
- /// Flag indicating if more sources are being loaded for pagination.
44
- bool _isLoadingMore = false ;
45
-
46
- /// Flag indicating if more sources are available to fetch.
47
- bool _hasMore = true ;
48
-
49
- /// Cursor for fetching the next page of sources.
50
- String ? _cursor;
51
-
52
- /// Stores any error message that occurred during fetching.
53
- String ? _error;
54
-
55
37
/// Scroll controller to detect when the user reaches the end of the list
56
38
/// for pagination.
57
39
final _scrollController = ScrollController ();
@@ -63,7 +45,8 @@ class _SourceFilterPageState extends State<SourceFilterPage> {
63
45
WidgetsBinding .instance.addPostFrameCallback ((_) {
64
46
final initialSelection = GoRouterState .of (context).extra as List <Source >? ;
65
47
_pageSelectedSources = Set .from (initialSelection ?? []);
66
- _fetchSources (); // Initial fetch
48
+ // Request initial sources from the BLoC
49
+ context.read <SourcesFilterBloc >().add (SourcesFilterRequested ());
67
50
});
68
51
_scrollController.addListener (_onScroll);
69
52
}
@@ -76,63 +59,20 @@ class _SourceFilterPageState extends State<SourceFilterPage> {
76
59
super .dispose ();
77
60
}
78
61
79
- /// Fetches sources from the [HtSourcesRepository] .
80
- ///
81
- /// Handles both initial fetch and pagination (`loadMore = true` ). Updates
82
- /// loading states, fetched data ([_allSources] ), pagination info
83
- /// ([_cursor] , [_hasMore] ), and error state ([_error] ).
84
- Future <void > _fetchSources ({bool loadMore = false }) async {
85
- // Prevent unnecessary fetches
86
- if (! loadMore && _isLoading) return ;
87
- if (loadMore && (_isLoadingMore || ! _hasMore)) return ;
88
-
89
- setState (() {
90
- if (loadMore) {
91
- _isLoadingMore = true ;
92
- } else {
93
- _isLoading = true ;
94
- _error = null ;
95
- }
96
- });
97
-
98
- try {
99
- // Ensure HtSourcesRepository is provided higher up in the widget tree
100
- final repo = context.read <HtSourcesRepository >();
101
- final response = await repo.getSources (
102
- limit: 20 , // Adjust limit as needed
103
- startAfterId: loadMore ? _cursor : null ,
104
- );
105
-
106
- setState (() {
107
- if (loadMore) {
108
- _allSources.addAll (response.items);
109
- } else {
110
- _allSources = response.items;
111
- }
112
- _cursor = response.cursor;
113
- _hasMore = response.hasMore;
114
- _isLoading = false ;
115
- _isLoadingMore = false ;
116
- });
117
- } catch (e) {
118
- setState (() {
119
- _isLoading = false ;
120
- _isLoadingMore = false ;
121
- _error = e.toString ();
122
- });
123
- }
124
- }
125
-
126
62
/// Callback function for scroll events.
127
63
///
128
64
/// Checks if the user has scrolled near the bottom of the list and triggers
129
- /// fetching more sources if available.
65
+ /// fetching more sources via the BLoC if available.
130
66
void _onScroll () {
131
67
if (! _scrollController.hasClients) return ;
132
68
final maxScroll = _scrollController.position.maxScrollExtent;
133
69
final currentScroll = _scrollController.offset;
134
- if (currentScroll >= (maxScroll * 0.9 ) && _hasMore && ! _isLoadingMore) {
135
- _fetchSources (loadMore: true );
70
+ final bloc = context.read <SourcesFilterBloc >();
71
+ // Fetch more when nearing the bottom, if BLoC has more and isn't already loading more
72
+ if (currentScroll >= (maxScroll * 0.9 ) &&
73
+ bloc.state.hasMore &&
74
+ bloc.state.status != SourcesFilterStatus .loadingMore) {
75
+ bloc.add (SourcesFilterLoadMoreRequested ());
136
76
}
137
77
}
138
78
@@ -153,61 +93,95 @@ class _SourceFilterPageState extends State<SourceFilterPage> {
153
93
),
154
94
],
155
95
),
156
- body: _buildBody (context),
96
+ // Use BlocBuilder to react to state changes from SourcesFilterBloc
97
+ body: BlocBuilder <SourcesFilterBloc , SourcesFilterState >(
98
+ builder: (context, state) {
99
+ return _buildBody (context, state);
100
+ },
101
+ ),
157
102
);
158
103
}
159
104
160
- /// Builds the main content body based on the current loading/error/data state .
161
- Widget _buildBody (BuildContext context) {
105
+ /// Builds the main content body based on the current [SourcesFilterState] .
106
+ Widget _buildBody (BuildContext context, SourcesFilterState state ) {
162
107
final l10n = context.l10n;
163
- if (_isLoading) {
164
- // Show initial loading indicator
108
+
109
+ // Handle initial loading state
110
+ if (state.status == SourcesFilterStatus .loading) {
165
111
return LoadingStateWidget (
166
112
icon: Icons .source_outlined,
167
113
headline: l10n.sourceFilterLoadingHeadline,
168
114
subheadline: l10n.sourceFilterLoadingSubheadline,
169
115
);
170
116
}
171
117
172
- if (_error != null ) {
173
- return FailureStateWidget (message: _error! , onRetry: _fetchSources);
118
+ // Handle failure state (show error and retry button)
119
+ if (state.status == SourcesFilterStatus .failure && state.sources.isEmpty) {
120
+ return FailureStateWidget (
121
+ message: state.error? .toString () ?? l10n.unknownError, // Assumes unknownError exists
122
+ onRetry: () =>
123
+ context.read <SourcesFilterBloc >().add (SourcesFilterRequested ()),
124
+ );
174
125
}
175
126
176
- if (_allSources.isEmpty) {
127
+ // Handle empty state (after successful load but no sources found)
128
+ if (state.status == SourcesFilterStatus .success && state.sources.isEmpty) {
177
129
return InitialStateWidget (
178
130
icon: Icons .search_off,
179
131
headline: l10n.sourceFilterEmptyHeadline,
180
132
subheadline: l10n.sourceFilterEmptySubheadline,
181
133
);
182
134
}
183
135
136
+ // Handle loaded state (success or loading more)
184
137
return ListView .builder (
185
138
controller: _scrollController,
186
139
padding: const EdgeInsets .only (bottom: AppSpacing .xxl),
187
- itemCount: _allSources.length + (_hasMore ? 1 : 0 ),
140
+ itemCount: state.sources.length +
141
+ ((state.status == SourcesFilterStatus .loadingMore ||
142
+ (state.status == SourcesFilterStatus .failure &&
143
+ state.sources.isNotEmpty))
144
+ ? 1
145
+ : 0 ),
188
146
itemBuilder: (context, index) {
189
- if (index >= _allSources.length) {
190
- return _isLoadingMore
191
- ? const Padding (
192
- padding: EdgeInsets .symmetric (vertical: AppSpacing .lg),
193
- child: Center (child: CircularProgressIndicator ()),
194
- )
195
- : const SizedBox .shrink ();
147
+ if (index >= state.sources.length) {
148
+ if (state.status == SourcesFilterStatus .loadingMore) {
149
+ return const Padding (
150
+ padding: EdgeInsets .symmetric (vertical: AppSpacing .lg),
151
+ child: Center (child: CircularProgressIndicator ()),
152
+ );
153
+ } else if (state.status == SourcesFilterStatus .failure) {
154
+ return Padding (
155
+ padding: const EdgeInsets .symmetric (
156
+ vertical: AppSpacing .md,
157
+ horizontal: AppSpacing .lg,
158
+ ),
159
+ child: Center (
160
+ child: Text (
161
+ l10n.loadMoreError, // Assumes loadMoreError exists
162
+ style: Theme .of (context).textTheme.bodySmall? .copyWith (
163
+ color: Theme .of (context).colorScheme.error,
164
+ ),
165
+ ),
166
+ ),
167
+ );
168
+ } else {
169
+ return const SizedBox .shrink ();
170
+ }
196
171
}
197
172
198
- final source = _allSources [index];
173
+ final source = state.sources [index];
199
174
final isSelected = _pageSelectedSources.contains (source);
200
175
201
- // Sources don't have icons in the model, so just use text
202
176
return CheckboxListTile (
203
177
title: Text (source.name),
204
178
subtitle:
205
179
source.description != null && source.description! .isNotEmpty
206
180
? Text (
207
- source.description! ,
208
- maxLines: 1 ,
209
- overflow: TextOverflow .ellipsis,
210
- )
181
+ source.description! ,
182
+ maxLines: 1 ,
183
+ overflow: TextOverflow .ellipsis,
184
+ )
211
185
: null ,
212
186
value: isSelected,
213
187
onChanged: (bool ? value) {
0 commit comments