11import 'package:core/core.dart' ;
22import 'package:flutter/material.dart' ;
33import 'package:flutter_bloc/flutter_bloc.dart' ;
4+ import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart' ;
45import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/bloc/countries_filter_bloc.dart' ;
56import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart' ;
67import 'package:go_router/go_router.dart' ;
@@ -96,34 +97,35 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
9697 ),
9798 actions: [
9899 // Apply My Followed Countries Button
99- BlocBuilder <CountriesFilterBloc , CountriesFilterState >(
100- builder: (context, state) {
101- // Determine if the "Apply My Followed" icon should be filled
102- final followedCountriesSet = state.followedCountries.toSet ();
103- final isFollowedFilterActive =
104- followedCountriesSet.isNotEmpty &&
105- _pageSelectedCountries.length ==
106- followedCountriesSet.length &&
107- _pageSelectedCountries.containsAll (followedCountriesSet);
100+ BlocBuilder <AppBloc , AppState >(
101+ builder: (context, appState) {
102+ final followedCountries =
103+ appState.userContentPreferences? .followedCountries ?? [];
104+ final isFollowedFilterActive = followedCountries.isNotEmpty &&
105+ _pageSelectedCountries.length == followedCountries.length &&
106+ _pageSelectedCountries.containsAll (followedCountries);
108107
109108 return IconButton (
110109 icon: isFollowedFilterActive
111110 ? const Icon (Icons .favorite)
112111 : const Icon (Icons .favorite_border),
113- color: isFollowedFilterActive
114- ? theme.colorScheme.primary
115- : null ,
112+ color: isFollowedFilterActive ? theme.colorScheme.primary : null ,
116113 tooltip: l10n.headlinesFeedFilterApplyFollowedLabel,
117- onPressed:
118- state.followedCountriesStatus ==
119- CountriesFilterStatus .loading
120- ? null // Disable while loading
121- : () {
122- // Dispatch event to BLoC to fetch and apply followed countries
123- _countriesFilterBloc.add (
124- CountriesFilterApplyFollowedRequested (),
125- );
126- },
114+ onPressed: () {
115+ setState (() {
116+ _pageSelectedCountries = Set .from (followedCountries);
117+ });
118+ if (followedCountries.isEmpty) {
119+ ScaffoldMessenger .of (context)
120+ ..hideCurrentSnackBar ()
121+ ..showSnackBar (
122+ SnackBar (
123+ content: Text (l10n.noFollowedItemsForFilterSnackbar),
124+ duration: const Duration (seconds: 3 ),
125+ ),
126+ );
127+ }
128+ },
127129 );
128130 },
129131 ),
@@ -140,171 +142,105 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
140142 ),
141143 ],
142144 ),
143- // Use BlocListener to react to state changes from CountriesFilterBloc
144- body: BlocListener <CountriesFilterBloc , CountriesFilterState >(
145- listenWhen: (previous, current) =>
146- previous.followedCountriesStatus !=
147- current.followedCountriesStatus ||
148- previous.followedCountries != current.followedCountries,
149- listener: (context, state) {
150- if (state.followedCountriesStatus == CountriesFilterStatus .success) {
151- // Update local state with followed countries from BLoC
152- setState (() {
153- _pageSelectedCountries = Set .from (state.followedCountries);
154- });
155- if (state.followedCountries.isEmpty) {
156- ScaffoldMessenger .of (context)
157- ..hideCurrentSnackBar ()
158- ..showSnackBar (
159- SnackBar (
160- content: Text (l10n.noFollowedItemsForFilterSnackbar),
161- duration: const Duration (seconds: 3 ),
162- ),
163- );
164- }
165- } else if (state.followedCountriesStatus ==
166- CountriesFilterStatus .failure) {
167- // Show error message if fetching followed countries failed
168- ScaffoldMessenger .of (context)
169- ..hideCurrentSnackBar ()
170- ..showSnackBar (
171- SnackBar (
172- content: Text (state.error? .message ?? l10n.unknownError),
173- duration: const Duration (seconds: 3 ),
174- ),
175- );
145+ body: BlocBuilder <CountriesFilterBloc , CountriesFilterState >(
146+ builder: (context, state) {
147+ // Determine overall loading status for the main list
148+ final isLoadingMainList =
149+ state.status == CountriesFilterStatus .loading;
150+
151+ // Handle initial loading state
152+ if (isLoadingMainList) {
153+ return LoadingStateWidget (
154+ icon: Icons .public_outlined,
155+ headline: l10n.countryFilterLoadingHeadline,
156+ subheadline: l10n.countryFilterLoadingSubheadline,
157+ );
176158 }
177- },
178- child: BlocBuilder <CountriesFilterBloc , CountriesFilterState >(
179- builder: (context, state) {
180- // Determine overall loading status for the main list
181- final isLoadingMainList =
182- state.status == CountriesFilterStatus .loading;
183-
184- // Determine if followed countries are currently loading
185- final isLoadingFollowedCountries =
186- state.followedCountriesStatus == CountriesFilterStatus .loading;
187159
188- // Handle initial loading state
189- if (isLoadingMainList) {
190- return LoadingStateWidget (
191- icon: Icons .public_outlined,
192- headline: l10n.countryFilterLoadingHeadline,
193- subheadline: l10n.countryFilterLoadingSubheadline,
194- );
195- }
196-
197- // Handle failure state (show error and retry button)
198- if (state.status == CountriesFilterStatus .failure &&
199- state.countries.isEmpty) {
200- return FailureStateWidget (
201- exception:
202- state.error ?? const UnknownException ('Unknown error' ),
203- onRetry: () => _countriesFilterBloc.add (
204- CountriesFilterRequested (usage: widget.filter),
205- ),
206- );
207- }
208-
209- // Handle empty state (after successful load but no countries found)
210- if (state.status == CountriesFilterStatus .success &&
211- state.countries.isEmpty) {
212- return InitialStateWidget (
213- icon: Icons .flag_circle_outlined,
214- headline: l10n.countryFilterEmptyHeadline,
215- subheadline: l10n.countryFilterEmptySubheadline,
216- );
217- }
160+ // Handle failure state (show error and retry button)
161+ if (state.status == CountriesFilterStatus .failure &&
162+ state.countries.isEmpty) {
163+ return FailureStateWidget (
164+ exception:
165+ state.error ?? const UnknownException ('Unknown error' ),
166+ onRetry: () => _countriesFilterBloc.add (
167+ CountriesFilterRequested (usage: widget.filter),
168+ ),
169+ );
170+ }
218171
219- // Handle loaded state (success)
220- return Stack (
221- children: [
222- ListView .builder (
223- padding: const EdgeInsets .symmetric (
224- vertical: AppSpacing .paddingSmall,
225- ).copyWith (bottom: AppSpacing .xxl),
226- itemCount: state.countries.length,
227- itemBuilder: (context, index) {
228- final country = state.countries[index];
229- final isSelected = _pageSelectedCountries.contains (country);
172+ // Handle empty state (after successful load but no countries found)
173+ if (state.status == CountriesFilterStatus .success &&
174+ state.countries.isEmpty) {
175+ return InitialStateWidget (
176+ icon: Icons .flag_circle_outlined,
177+ headline: l10n.countryFilterEmptyHeadline,
178+ subheadline: l10n.countryFilterEmptySubheadline,
179+ );
180+ }
230181
231- return CheckboxListTile (
232- title: Text (country.name, style: textTheme.titleMedium),
233- secondary: SizedBox (
234- width: AppSpacing .xl + AppSpacing .xs,
235- height: AppSpacing .lg + AppSpacing .sm,
236- child: ClipRRect (
237- // Clip the image for rounded corners if desired
238- borderRadius: BorderRadius .circular (
239- AppSpacing .xs / 2 ,
240- ),
241- child: Image .network (
242- country.flagUrl,
243- fit: BoxFit .cover,
244- errorBuilder: (context, error, stackTrace) => Icon (
245- Icons .flag_outlined,
246- color: theme.colorScheme.onSurfaceVariant,
247- size: AppSpacing .lg,
248- ),
249- loadingBuilder: (context, child, loadingProgress) {
250- if (loadingProgress == null ) return child;
251- return Center (
252- child: CircularProgressIndicator (
253- strokeWidth: 2 ,
254- value:
255- loadingProgress.expectedTotalBytes != null
256- ? loadingProgress.cumulativeBytesLoaded /
257- loadingProgress.expectedTotalBytes!
258- : null ,
259- ),
260- );
261- },
262- ),
263- ),
182+ // Handle loaded state (success)
183+ return ListView .builder (
184+ padding: const EdgeInsets .symmetric (
185+ vertical: AppSpacing .paddingSmall,
186+ ).copyWith (bottom: AppSpacing .xxl),
187+ itemCount: state.countries.length,
188+ itemBuilder: (context, index) {
189+ final country = state.countries[index];
190+ final isSelected = _pageSelectedCountries.contains (country);
191+
192+ return CheckboxListTile (
193+ title: Text (country.name, style: textTheme.titleMedium),
194+ secondary: SizedBox (
195+ width: AppSpacing .xl + AppSpacing .xs,
196+ height: AppSpacing .lg + AppSpacing .sm,
197+ child: ClipRRect (
198+ // Clip the image for rounded corners if desired
199+ borderRadius: BorderRadius .circular (
200+ AppSpacing .xs / 2 ,
201+ ),
202+ child: Image .network (
203+ country.flagUrl,
204+ fit: BoxFit .cover,
205+ errorBuilder: (context, error, stackTrace) => Icon (
206+ Icons .flag_outlined,
207+ color: theme.colorScheme.onSurfaceVariant,
208+ size: AppSpacing .lg,
264209 ),
265- value: isSelected,
266- onChanged: (bool ? value) {
267- setState (() {
268- if (value == true ) {
269- _pageSelectedCountries.add (country);
270- } else {
271- _pageSelectedCountries.remove (country);
272- }
273- });
210+ loadingBuilder: (context, child, loadingProgress) {
211+ if (loadingProgress == null ) return child;
212+ return Center (
213+ child: CircularProgressIndicator (
214+ strokeWidth: 2 ,
215+ value:
216+ loadingProgress.expectedTotalBytes != null
217+ ? loadingProgress.cumulativeBytesLoaded /
218+ loadingProgress.expectedTotalBytes!
219+ : null ,
220+ ),
221+ );
274222 },
275- controlAffinity: ListTileControlAffinity .leading,
276- contentPadding: const EdgeInsets .symmetric (
277- horizontal: AppSpacing .paddingMedium,
278- ),
279- );
280- },
281- ),
282- // Show loading overlay if followed countries are being fetched
283- if (isLoadingFollowedCountries)
284- Positioned .fill (
285- child: ColoredBox (
286- color: Colors .black54, // Semi-transparent overlay
287- child: Center (
288- child: Column (
289- mainAxisAlignment: MainAxisAlignment .center,
290- children: [
291- const CircularProgressIndicator (),
292- const SizedBox (height: AppSpacing .md),
293- Text (
294- l10n.headlinesFeedLoadingHeadline,
295- style: theme.textTheme.titleMedium? .copyWith (
296- color: Colors .white,
297- ),
298- ),
299- ],
300- ),
301- ),
302223 ),
303224 ),
304- ],
305- );
306- },
307- ),
225+ ),
226+ value: isSelected,
227+ onChanged: (bool ? value) {
228+ setState (() {
229+ if (value == true ) {
230+ _pageSelectedCountries.add (country);
231+ } else {
232+ _pageSelectedCountries.remove (country);
233+ }
234+ });
235+ },
236+ controlAffinity: ListTileControlAffinity .leading,
237+ contentPadding: const EdgeInsets .symmetric (
238+ horizontal: AppSpacing .paddingMedium,
239+ ),
240+ );
241+ },
242+ );
243+ },
308244 ),
309245 );
310246 }
0 commit comments