1- //
2- // ignore_for_file: lines_longer_than_80_chars
3-
41import 'package:core/core.dart' ;
52import 'package:flutter/material.dart' ;
63import 'package:flutter_bloc/flutter_bloc.dart' ;
@@ -43,10 +40,13 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
4340 /// `HeadlinesFilterPage` . This ensures the checkboxes reflect the state
4441 /// from the main filter page when this page loads.
4542 late Set <Country > _pageSelectedCountries;
43+ late final CountriesFilterBloc _countriesFilterBloc;
4644
4745 @override
4846 void initState () {
4947 super .initState ();
48+ _countriesFilterBloc = context.read <CountriesFilterBloc >();
49+
5050 // Initialization needs to happen after the first frame to safely access
5151 // GoRouterState.of(context).
5252 WidgetsBinding .instance.addPostFrameCallback ((_) {
@@ -65,9 +65,7 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
6565 // 3. Trigger the page-specific BLoC (CountriesFilterBloc) to start
6666 // fetching the list of *all available* countries that the user can
6767 // potentially select from, using the specified usage filter.
68- context.read <CountriesFilterBloc >().add (
69- CountriesFilterRequested (usage: widget.usage),
70- );
68+ _countriesFilterBloc.add (CountriesFilterRequested (usage: widget.usage));
7169 });
7270 }
7371
@@ -89,6 +87,39 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
8987 style: textTheme.titleLarge,
9088 ),
9189 actions: [
90+ // Apply My Followed Countries Button
91+ BlocBuilder <CountriesFilterBloc , CountriesFilterState >(
92+ builder: (context, state) {
93+ // Determine if the "Apply My Followed" icon should be filled
94+ final bool isFollowedFilterActive =
95+ state.followedCountries.isNotEmpty &&
96+ _pageSelectedCountries.length ==
97+ state.followedCountries.length &&
98+ _pageSelectedCountries.every (
99+ state.followedCountries.contains,
100+ );
101+
102+ return IconButton (
103+ icon: isFollowedFilterActive
104+ ? const Icon (Icons .favorite)
105+ : const Icon (Icons .favorite_border),
106+ color: isFollowedFilterActive
107+ ? theme.colorScheme.primary
108+ : null ,
109+ tooltip: l10n.headlinesFeedFilterApplyFollowedLabel,
110+ onPressed:
111+ state.followedCountriesStatus ==
112+ CountriesFilterStatus .loading
113+ ? null // Disable while loading
114+ : () {
115+ // Dispatch event to BLoC to fetch and apply followed countries
116+ _countriesFilterBloc.add (
117+ CountriesFilterApplyFollowedRequested (),
118+ );
119+ },
120+ );
121+ },
122+ ),
92123 IconButton (
93124 icon: const Icon (Icons .check),
94125 tooltip: l10n.headlinesFeedFilterApplyButton,
@@ -102,107 +133,172 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
102133 ),
103134 ],
104135 ),
105- // Use BlocBuilder to react to state changes from CountriesFilterBloc
106- body: BlocBuilder <CountriesFilterBloc , CountriesFilterState >(
107- builder: _buildBody,
108- ),
109- );
110- }
136+ // Use BlocListener to react to state changes from CountriesFilterBloc
137+ body: BlocListener <CountriesFilterBloc , CountriesFilterState >(
138+ listenWhen: (previous, current) =>
139+ previous.followedCountriesStatus !=
140+ current.followedCountriesStatus ||
141+ previous.followedCountries != current.followedCountries,
142+ listener: (context, state) {
143+ if (state.followedCountriesStatus == CountriesFilterStatus .success) {
144+ // Update local state with followed countries from BLoC
145+ setState (() {
146+ _pageSelectedCountries = Set .from (state.followedCountries);
147+ });
148+ if (state.followedCountries.isEmpty) {
149+ ScaffoldMessenger .of (context)
150+ ..hideCurrentSnackBar ()
151+ ..showSnackBar (
152+ SnackBar (
153+ content: Text (l10n.noFollowedItemsForFilterSnackbar),
154+ duration: const Duration (seconds: 3 ),
155+ ),
156+ );
157+ }
158+ } else if (state.followedCountriesStatus ==
159+ CountriesFilterStatus .failure) {
160+ // Show error message if fetching followed countries failed
161+ ScaffoldMessenger .of (context)
162+ ..hideCurrentSnackBar ()
163+ ..showSnackBar (
164+ SnackBar (
165+ content: Text (state.error? .message ?? l10n.unknownError),
166+ duration: const Duration (seconds: 3 ),
167+ ),
168+ );
169+ }
170+ },
171+ child: BlocBuilder <CountriesFilterBloc , CountriesFilterState >(
172+ builder: (context, state) {
173+ // Determine overall loading status for the main list
174+ final bool isLoadingMainList =
175+ state.status == CountriesFilterStatus .loading;
111176
112- /// Builds the main content body based on the current [CountriesFilterState] .
113- Widget _buildBody (BuildContext context, CountriesFilterState state) {
114- final l10n = AppLocalizationsX (context).l10n;
115- final theme = Theme .of (context);
116- final textTheme = theme.textTheme;
117- final colorScheme = theme.colorScheme;
118-
119- // Handle initial loading state
120- if (state.status == CountriesFilterStatus .loading) {
121- return LoadingStateWidget (
122- icon: Icons .public_outlined,
123- headline: l10n.countryFilterLoadingHeadline,
124- subheadline: l10n.countryFilterLoadingSubheadline,
125- );
126- }
127-
128- // Handle failure state (show error and retry button)
129- if (state.status == CountriesFilterStatus .failure &&
130- state.countries.isEmpty) {
131- return FailureStateWidget (
132- exception: state.error ?? const UnknownException ('Unknown error' ),
133- onRetry: () => context.read <CountriesFilterBloc >().add (
134- CountriesFilterRequested (usage: widget.usage),
135- ),
136- );
137- }
138-
139- // Handle empty state (after successful load but no countries found)
140- if (state.status == CountriesFilterStatus .success &&
141- state.countries.isEmpty) {
142- return InitialStateWidget (
143- icon: Icons .flag_circle_outlined,
144- headline: l10n.countryFilterEmptyHeadline,
145- subheadline: l10n.countryFilterEmptySubheadline,
146- );
147- }
148-
149- // Handle loaded state (success)
150- return ListView .builder (
151- padding: const EdgeInsets .symmetric (
152- vertical: AppSpacing .paddingSmall,
153- ).copyWith (bottom: AppSpacing .xxl),
154- itemCount: state.countries.length,
155- itemBuilder: (context, index) {
156- final country = state.countries[index];
157- final isSelected = _pageSelectedCountries.contains (country);
158-
159- return CheckboxListTile (
160- title: Text (country.name, style: textTheme.titleMedium),
161- secondary: SizedBox (
162- width: AppSpacing .xl + AppSpacing .xs,
163- height: AppSpacing .lg + AppSpacing .sm,
164- child: ClipRRect (
165- // Clip the image for rounded corners if desired
166- borderRadius: BorderRadius .circular (AppSpacing .xs / 2 ),
167- child: Image .network (
168- country.flagUrl,
169- fit: BoxFit .cover,
170- errorBuilder: (context, error, stackTrace) => Icon (
171- Icons .flag_outlined,
172- color: colorScheme.onSurfaceVariant,
173- size: AppSpacing .lg,
177+ // Determine if followed countries are currently loading
178+ final bool isLoadingFollowedCountries =
179+ state.followedCountriesStatus == CountriesFilterStatus .loading;
180+
181+ // Handle initial loading state
182+ if (isLoadingMainList) {
183+ return LoadingStateWidget (
184+ icon: Icons .public_outlined,
185+ headline: l10n.countryFilterLoadingHeadline,
186+ subheadline: l10n.countryFilterLoadingSubheadline,
187+ );
188+ }
189+
190+ // Handle failure state (show error and retry button)
191+ if (state.status == CountriesFilterStatus .failure &&
192+ state.countries.isEmpty) {
193+ return FailureStateWidget (
194+ exception:
195+ state.error ?? const UnknownException ('Unknown error' ),
196+ onRetry: () => _countriesFilterBloc.add (
197+ CountriesFilterRequested (usage: widget.usage),
198+ ),
199+ );
200+ }
201+
202+ // Handle empty state (after successful load but no countries found)
203+ if (state.status == CountriesFilterStatus .success &&
204+ state.countries.isEmpty) {
205+ return InitialStateWidget (
206+ icon: Icons .flag_circle_outlined,
207+ headline: l10n.countryFilterEmptyHeadline,
208+ subheadline: l10n.countryFilterEmptySubheadline,
209+ );
210+ }
211+
212+ // Handle loaded state (success)
213+ return Stack (
214+ children: [
215+ ListView .builder (
216+ padding: const EdgeInsets .symmetric (
217+ vertical: AppSpacing .paddingSmall,
218+ ).copyWith (bottom: AppSpacing .xxl),
219+ itemCount: state.countries.length,
220+ itemBuilder: (context, index) {
221+ final country = state.countries[index];
222+ final isSelected = _pageSelectedCountries.contains (country);
223+
224+ return CheckboxListTile (
225+ title: Text (country.name, style: textTheme.titleMedium),
226+ secondary: SizedBox (
227+ width: AppSpacing .xl + AppSpacing .xs,
228+ height: AppSpacing .lg + AppSpacing .sm,
229+ child: ClipRRect (
230+ // Clip the image for rounded corners if desired
231+ borderRadius: BorderRadius .circular (
232+ AppSpacing .xs / 2 ,
233+ ),
234+ child: Image .network (
235+ country.flagUrl,
236+ fit: BoxFit .cover,
237+ errorBuilder: (context, error, stackTrace) => Icon (
238+ Icons .flag_outlined,
239+ color: theme.colorScheme.onSurfaceVariant,
240+ size: AppSpacing .lg,
241+ ),
242+ loadingBuilder: (context, child, loadingProgress) {
243+ if (loadingProgress == null ) return child;
244+ return Center (
245+ child: CircularProgressIndicator (
246+ strokeWidth: 2 ,
247+ value:
248+ loadingProgress.expectedTotalBytes != null
249+ ? loadingProgress.cumulativeBytesLoaded /
250+ loadingProgress.expectedTotalBytes!
251+ : null ,
252+ ),
253+ );
254+ },
255+ ),
256+ ),
257+ ),
258+ value: isSelected,
259+ onChanged: (bool ? value) {
260+ setState (() {
261+ if (value == true ) {
262+ _pageSelectedCountries.add (country);
263+ } else {
264+ _pageSelectedCountries.remove (country);
265+ }
266+ });
267+ },
268+ controlAffinity: ListTileControlAffinity .leading,
269+ contentPadding: const EdgeInsets .symmetric (
270+ horizontal: AppSpacing .paddingMedium,
271+ ),
272+ );
273+ },
174274 ),
175- loadingBuilder: (context, child, loadingProgress) {
176- if (loadingProgress == null ) return child;
177- return Center (
178- child: CircularProgressIndicator (
179- strokeWidth: 2 ,
180- value: loadingProgress.expectedTotalBytes != null
181- ? loadingProgress.cumulativeBytesLoaded /
182- loadingProgress.expectedTotalBytes!
183- : null ,
275+ // Show loading overlay if followed countries are being fetched
276+ if (isLoadingFollowedCountries)
277+ Positioned .fill (
278+ child: Container (
279+ color: Colors .black54, // Semi-transparent overlay
280+ child: Center (
281+ child: Column (
282+ mainAxisAlignment: MainAxisAlignment .center,
283+ children: [
284+ const CircularProgressIndicator (),
285+ const SizedBox (height: AppSpacing .md),
286+ Text (
287+ l10n.headlinesFeedLoadingHeadline,
288+ style: theme.textTheme.titleMedium? .copyWith (
289+ color: Colors .white,
290+ ),
291+ ),
292+ ],
293+ ),
294+ ),
184295 ),
185- );
186- },
187- ),
188- ),
189- ),
190- value: isSelected,
191- onChanged: (bool ? value) {
192- setState (() {
193- if (value == true ) {
194- _pageSelectedCountries.add (country);
195- } else {
196- _pageSelectedCountries.remove (country);
197- }
198- });
296+ ),
297+ ],
298+ );
199299 },
200- controlAffinity: ListTileControlAffinity .leading,
201- contentPadding: const EdgeInsets .symmetric (
202- horizontal: AppSpacing .paddingMedium,
203- ),
204- );
205- },
300+ ),
301+ ),
206302 );
207303 }
208304}
0 commit comments