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' ;
7
6
import 'package:ht_countries_client/ht_countries_client.dart' ;
8
- import 'package:ht_countries_repository/ht_countries_repository.dart' ;
7
+ // Removed repository import: import 'package:ht_countries_repository/ht_countries_repository.dart';
8
+ import 'package:ht_main/headlines-feed/bloc/countries_filter_bloc.dart' ; // Import the BLoC
9
9
import 'package:ht_main/l10n/l10n.dart' ;
10
10
import 'package:ht_main/shared/constants/constants.dart' ;
11
11
import 'package:ht_main/shared/widgets/widgets.dart' ; // For loading/error widgets
12
12
13
13
/// {@template country_filter_page}
14
14
/// A page dedicated to selecting event countries for filtering headlines.
15
15
///
16
- /// Fetches countries paginatively, allows multiple selections, and returns
17
- /// the selected list via `context.pop` when the user applies the changes.
16
+ /// Uses [CountriesFilterBloc] to fetch countries 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 CountryFilterPage extends StatefulWidget {
20
21
/// {@macro country_filter_page}
@@ -26,32 +27,13 @@ class CountryFilterPage extends StatefulWidget {
26
27
27
28
/// State for the [CountryFilterPage] .
28
29
///
29
- /// Manages the local selection state ([_pageSelectedCountries] ), fetches
30
- /// countries from the [HtCountriesRepository] , handles pagination using a
31
- /// [ScrollController] , and displays loading/error/empty/loaded states.
30
+ /// Manages the local selection state ([_pageSelectedCountries] ) and interacts
31
+ /// with [CountriesFilterBloc] for data fetching and pagination.
32
32
class _CountryFilterPageState extends State <CountryFilterPage > {
33
33
/// Stores the countries selected by the user on this page.
34
34
/// Initialized from the `extra` parameter passed during navigation.
35
35
late Set <Country > _pageSelectedCountries;
36
36
37
- /// List of all countries fetched from the repository.
38
- List <Country > _allCountries = [];
39
-
40
- /// Flag indicating if the initial country list is being loaded.
41
- bool _isLoading = true ;
42
-
43
- /// Flag indicating if more countries are being loaded for pagination.
44
- bool _isLoadingMore = false ;
45
-
46
- /// Flag indicating if more countries are available to fetch.
47
- bool _hasMore = true ;
48
-
49
- /// Cursor for fetching the next page of countries.
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 ();
@@ -64,7 +46,8 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
64
46
final initialSelection =
65
47
GoRouterState .of (context).extra as List <Country >? ;
66
48
_pageSelectedCountries = Set .from (initialSelection ?? []);
67
- _fetchCountries (); // Initial fetch
49
+ // Request initial countries from the BLoC
50
+ context.read <CountriesFilterBloc >().add (CountriesFilterRequested ());
68
51
});
69
52
_scrollController.addListener (_onScroll);
70
53
}
@@ -77,64 +60,20 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
77
60
super .dispose ();
78
61
}
79
62
80
- /// Fetches countries from the [HtCountriesRepository] .
81
- ///
82
- /// Handles both initial fetch and pagination (`loadMore = true` ). Updates
83
- /// loading states, fetched data ([_allCountries] ), pagination info
84
- /// ([_cursor] , [_hasMore] ), and error state ([_error] ).
85
- Future <void > _fetchCountries ({bool loadMore = false }) async {
86
- // Prevent unnecessary fetches
87
- if (! loadMore && _isLoading) return ;
88
- if (loadMore && (_isLoadingMore || ! _hasMore)) return ;
89
-
90
- setState (() {
91
- if (loadMore) {
92
- _isLoadingMore = true ;
93
- } else {
94
- _isLoading = true ;
95
- _error = null ;
96
- }
97
- });
98
-
99
- try {
100
- // Ensure HtCountriesRepository is provided higher up
101
- final repo = context.read <HtCountriesRepository >();
102
- // Note: fetchCountries uses 'cursor' which corresponds to 'startAfterId'
103
- final response = await repo.fetchCountries (
104
- limit: 20 , // Adjust limit as needed
105
- cursor: loadMore ? _cursor : null ,
106
- );
107
-
108
- setState (() {
109
- if (loadMore) {
110
- _allCountries.addAll (response.items);
111
- } else {
112
- _allCountries = response.items;
113
- }
114
- _cursor = response.cursor;
115
- _hasMore = response.hasMore;
116
- _isLoading = false ;
117
- _isLoadingMore = false ;
118
- });
119
- } catch (e) {
120
- setState (() {
121
- _isLoading = false ;
122
- _isLoadingMore = false ;
123
- _error = e.toString ();
124
- });
125
- }
126
- }
127
-
128
63
/// Callback function for scroll events.
129
64
///
130
65
/// Checks if the user has scrolled near the bottom of the list and triggers
131
- /// fetching more countries if available.
66
+ /// fetching more countries via the BLoC if available.
132
67
void _onScroll () {
133
68
if (! _scrollController.hasClients) return ;
134
69
final maxScroll = _scrollController.position.maxScrollExtent;
135
70
final currentScroll = _scrollController.offset;
136
- if (currentScroll >= (maxScroll * 0.9 ) && _hasMore && ! _isLoadingMore) {
137
- _fetchCountries (loadMore: true );
71
+ final bloc = context.read <CountriesFilterBloc >();
72
+ // Fetch more when nearing the bottom, if BLoC has more and isn't already loading more
73
+ if (currentScroll >= (maxScroll * 0.9 ) &&
74
+ bloc.state.hasMore &&
75
+ bloc.state.status != CountriesFilterStatus .loadingMore) {
76
+ bloc.add (CountriesFilterLoadMoreRequested ());
138
77
}
139
78
}
140
79
@@ -155,65 +94,98 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
155
94
),
156
95
],
157
96
),
158
- body: _buildBody (context),
97
+ // Use BlocBuilder to react to state changes from CountriesFilterBloc
98
+ body: BlocBuilder <CountriesFilterBloc , CountriesFilterState >(
99
+ builder: (context, state) {
100
+ return _buildBody (context, state);
101
+ },
102
+ ),
159
103
);
160
104
}
161
105
162
- /// Builds the main content body based on the current loading/error/data state .
163
- Widget _buildBody (BuildContext context) {
106
+ /// Builds the main content body based on the current [CountriesFilterState] .
107
+ Widget _buildBody (BuildContext context, CountriesFilterState state ) {
164
108
final l10n = context.l10n;
165
- if (_isLoading) {
166
- // Show initial loading indicator
109
+
110
+ // Handle initial loading state
111
+ if (state.status == CountriesFilterStatus .loading) {
167
112
return LoadingStateWidget (
168
- icon: Icons .public_outlined,
169
- headline: l10n.countryFilterLoadingHeadline,
170
- subheadline: l10n.countryFilterLoadingSubheadline,
113
+ icon: Icons .public_outlined, // Changed icon
114
+ headline: l10n.countryFilterLoadingHeadline, // Assumes this exists
115
+ subheadline: l10n.countryFilterLoadingSubheadline, // Assumes this exists
171
116
);
172
117
}
173
118
174
- if (_error != null ) {
175
- return FailureStateWidget (message: _error! , onRetry: _fetchCountries);
119
+ // Handle failure state (show error and retry button)
120
+ if (state.status == CountriesFilterStatus .failure &&
121
+ state.countries.isEmpty) {
122
+ return FailureStateWidget (
123
+ message: state.error? .toString () ?? l10n.unknownError, // Assumes this exists
124
+ onRetry: () =>
125
+ context.read <CountriesFilterBloc >().add (CountriesFilterRequested ()),
126
+ );
176
127
}
177
128
178
- if (_allCountries.isEmpty) {
129
+ // Handle empty state (after successful load but no countries found)
130
+ if (state.status == CountriesFilterStatus .success &&
131
+ state.countries.isEmpty) {
179
132
return InitialStateWidget (
180
133
icon: Icons .search_off,
181
- headline: l10n.countryFilterEmptyHeadline,
182
- subheadline: l10n.countryFilterEmptySubheadline,
134
+ headline: l10n.countryFilterEmptyHeadline, // Assumes this exists
135
+ subheadline: l10n.countryFilterEmptySubheadline, // Assumes this exists
183
136
);
184
137
}
185
138
139
+ // Handle loaded state (success or loading more)
186
140
return ListView .builder (
187
141
controller: _scrollController,
188
142
padding: const EdgeInsets .only (bottom: AppSpacing .xxl),
189
- itemCount: _allCountries.length + (_hasMore ? 1 : 0 ),
143
+ itemCount: state.countries.length +
144
+ ((state.status == CountriesFilterStatus .loadingMore ||
145
+ (state.status == CountriesFilterStatus .failure &&
146
+ state.countries.isNotEmpty))
147
+ ? 1
148
+ : 0 ),
190
149
itemBuilder: (context, index) {
191
- if (index >= _allCountries.length) {
192
- return _isLoadingMore
193
- ? const Padding (
194
- padding: EdgeInsets .symmetric (vertical: AppSpacing .lg),
195
- child: Center (child: CircularProgressIndicator ()),
196
- )
197
- : const SizedBox .shrink ();
150
+ if (index >= state.countries.length) {
151
+ if (state.status == CountriesFilterStatus .loadingMore) {
152
+ return const Padding (
153
+ padding: EdgeInsets .symmetric (vertical: AppSpacing .lg),
154
+ child: Center (child: CircularProgressIndicator ()),
155
+ );
156
+ } else if (state.status == CountriesFilterStatus .failure) {
157
+ return Padding (
158
+ padding: const EdgeInsets .symmetric (
159
+ vertical: AppSpacing .md,
160
+ horizontal: AppSpacing .lg,
161
+ ),
162
+ child: Center (
163
+ child: Text (
164
+ l10n.loadMoreError, // Assumes this exists
165
+ style: Theme .of (context).textTheme.bodySmall? .copyWith (
166
+ color: Theme .of (context).colorScheme.error,
167
+ ),
168
+ ),
169
+ ),
170
+ );
171
+ } else {
172
+ return const SizedBox .shrink ();
173
+ }
198
174
}
199
175
200
- final country = _allCountries [index];
176
+ final country = state.countries [index];
201
177
final isSelected = _pageSelectedCountries.contains (country);
202
178
203
- // Show country flag
204
179
return CheckboxListTile (
205
180
title: Text (country.name),
206
- secondary: SizedBox (
207
- width: 40 , // Consistent size
208
- height: 40 ,
181
+ secondary: SizedBox ( // Use SizedBox for consistent flag size
182
+ width: 40 ,
183
+ height: 30 , // Adjust height for flag aspect ratio if needed
209
184
child: Image .network (
210
185
country.flagUrl,
211
186
fit: BoxFit .contain,
212
- // Add error builder for network images
213
- errorBuilder:
214
- (context, error, stackTrace) => const Icon (
215
- Icons .flag_circle_outlined,
216
- ), // Placeholder icon
187
+ errorBuilder: (context, error, stackTrace) =>
188
+ const Icon (Icons .flag_outlined), // Placeholder icon
217
189
),
218
190
),
219
191
value: isSelected,
0 commit comments