11import 'package:core/core.dart' ;
22import 'package:data_repository/data_repository.dart' ;
3+ import 'package:flutter/foundation.dart' ;
34import 'package:flutter/material.dart' ;
45import 'package:flutter_bloc/flutter_bloc.dart' ;
56import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart' ;
@@ -8,6 +9,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/b
89import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/bloc/headlines_filter_bloc.dart' ;
910import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/models/headline_filter.dart' ;
1011import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/widgets/save_filter_dialog.dart' ;
12+ import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart' ;
1113import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart' ;
1214import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart' ;
1315import 'package:go_router/go_router.dart' ;
@@ -211,114 +213,157 @@ class _HeadlinesFilterViewState extends State<_HeadlinesFilterView> {
211213 @override
212214 Widget build (BuildContext context) {
213215 final l10n = AppLocalizationsX (context).l10n;
214- final theme = Theme .of (context);
215216
216- return Scaffold (
217- appBar: AppBar (
218- leading: IconButton (
219- icon: const Icon (Icons .close),
220- tooltip: MaterialLocalizations .of (context).closeButtonTooltip,
221- onPressed: () => context.pop (),
222- ),
223- title: Text (
224- l10n.headlinesFeedFilterTitle,
225- style: theme.textTheme.titleLarge,
226- ),
227- actions: [
228- // Reset All Filters Button
229- IconButton (
230- icon: const Icon (Icons .refresh),
231- tooltip: l10n.headlinesFeedFilterResetButton,
232- onPressed: () {
233- context.read <HeadlinesFilterBloc >().add (
234- const FilterSelectionsCleared (),
235- );
236- },
237- ),
238- // Manage Saved Filters Button
239- IconButton (
240- tooltip: l10n.headlinesFilterManageTooltip,
241- icon: const Icon (Icons .edit_note_outlined),
242- onPressed: () => context.pushNamed (Routes .manageSavedFiltersName),
243- ),
244- // Apply Filters Button
245- IconButton (
246- icon: const Icon (Icons .check),
247- tooltip: l10n.headlinesFeedFilterApplyButton,
248- onPressed: _showApplyOptionsDialog,
249- ),
250- ],
251- ),
252- body: BlocBuilder <HeadlinesFilterBloc , HeadlinesFilterState >(
253- builder: (context, filterState) {
254- // Determine if the "Apply my followed items" feature is active.
255- // This will disable the individual filter tiles.
256- final isFollowedFilterActive = filterState.isUsingFollowedItems;
217+ // Watch both AppBloc and HeadlinesFilterBloc to react to changes in either.
218+ return BlocBuilder <AppBloc , AppState >(
219+ builder: (context, appState) {
220+ return BlocBuilder <HeadlinesFilterBloc , HeadlinesFilterState >(
221+ builder: (context, filterState) {
222+ final theme = Theme .of (context);
257223
258- if (filterState.status == HeadlinesFilterStatus .loading) {
259- return LoadingStateWidget (
260- icon: Icons .filter_list,
261- headline: l10n.headlinesFeedFilterLoadingHeadline,
262- subheadline: l10n.pleaseWait,
263- );
264- }
224+ // Determine if the "Apply" button should be enabled.
225+ final isFilterEmpty =
226+ filterState.selectedTopics.isEmpty &&
227+ filterState.selectedSources.isEmpty &&
228+ filterState.selectedCountries.isEmpty;
229+
230+ final savedFilters =
231+ appState.userContentPreferences? .savedFilters ?? [];
265232
266- if (filterState.status == HeadlinesFilterStatus .failure) {
267- return FailureStateWidget (
268- exception:
269- filterState.error ??
270- const UnknownException ('Failed to load filter data.' ),
271- onRetry: () {
272- final headlinesFeedBloc = context.read <HeadlinesFeedBloc >();
273- final currentFilter = headlinesFeedBloc.state.filter;
274- context.read <HeadlinesFilterBloc >().add (
275- FilterDataLoaded (
276- initialSelectedTopics: currentFilter.topics ?? [],
277- initialSelectedSources: currentFilter.sources ?? [],
278- initialSelectedCountries:
279- currentFilter.eventCountries ?? [],
280- isUsingFollowedItems: currentFilter.isFromFollowedItems,
233+ // Check if the current selection matches any existing saved filter.
234+ final isDuplicate = savedFilters.any (
235+ (savedFilter) =>
236+ setEquals (
237+ savedFilter.topics.toSet (),
238+ filterState.selectedTopics,
239+ ) &&
240+ setEquals (
241+ savedFilter.sources.toSet (),
242+ filterState.selectedSources,
243+ ) &&
244+ setEquals (
245+ savedFilter.countries.toSet (),
246+ filterState.selectedCountries,
281247 ),
282- );
283- },
284248 );
285- }
286249
287- // Use a Map to define the filter tiles for cleaner code.
288- final filterTiles = {
289- l10n.headlinesFeedFilterTopicLabel: Routes .feedFilterTopicsName,
290- l10n.headlinesFeedFilterSourceLabel: Routes .feedFilterSourcesName,
291- l10n.headlinesFeedFilterEventCountryLabel:
292- Routes .feedFilterEventCountriesName,
293- };
250+ final isApplyEnabled = ! isFilterEmpty && ! isDuplicate;
251+
252+ return Scaffold (
253+ appBar: AppBar (
254+ leading: IconButton (
255+ icon: const Icon (Icons .close),
256+ tooltip: MaterialLocalizations .of (context).closeButtonTooltip,
257+ onPressed: () => context.pop (),
258+ ),
259+ title: Text (
260+ l10n.headlinesFeedFilterTitle,
261+ style: theme.textTheme.titleLarge,
262+ ),
263+ actions: [
264+ // Reset All Filters Button
265+ IconButton (
266+ icon: const Icon (Icons .refresh),
267+ tooltip: l10n.headlinesFeedFilterResetButton,
268+ onPressed: () {
269+ context.read <HeadlinesFilterBloc >().add (
270+ const FilterSelectionsCleared (),
271+ );
272+ },
273+ ),
274+ // Manage Saved Filters Button
275+ IconButton (
276+ tooltip: l10n.headlinesFilterManageTooltip,
277+ icon: const Icon (Icons .edit_note_outlined),
278+ onPressed: () =>
279+ context.pushNamed (Routes .manageSavedFiltersName),
280+ ),
281+ // Apply Filters Button
282+ IconButton (
283+ icon: const Icon (Icons .check),
284+ tooltip: l10n.headlinesFeedFilterApplyButton,
285+ // Disable the button if the filter is empty or a duplicate.
286+ onPressed: isApplyEnabled ? _showApplyOptionsDialog : null ,
287+ ),
288+ ],
289+ ),
290+ body: _buildBody (context, l10n, filterState),
291+ );
292+ },
293+ );
294+ },
295+ );
296+ }
294297
295- return ListView .separated (
296- itemCount: filterTiles.length,
297- separatorBuilder: (context, index) => const Divider (height: 1 ),
298- itemBuilder: (context, index) {
299- final title = filterTiles.keys.elementAt (index);
300- final routeName = filterTiles.values.elementAt (index);
301- int selectedCount;
298+ Widget _buildBody (
299+ BuildContext context,
300+ AppLocalizations l10n,
301+ HeadlinesFilterState filterState,
302+ ) {
303+ // Determine if the "Apply my followed items" feature is active.
304+ // This will disable the individual filter tiles.
305+ final isFollowedFilterActive = filterState.isUsingFollowedItems;
302306
303- if (routeName == Routes .feedFilterTopicsName ) {
304- selectedCount = filterState.selectedTopics.length;
305- } else if (routeName == Routes .feedFilterSourcesName) {
306- selectedCount = filterState.selectedSources.length;
307- } else {
308- selectedCount = filterState.selectedCountries.length ;
309- }
307+ if (filterState.status == HeadlinesFilterStatus .loading ) {
308+ return LoadingStateWidget (
309+ icon : Icons .filter_list,
310+ headline : l10n.headlinesFeedFilterLoadingHeadline,
311+ subheadline : l10n.pleaseWait,
312+ ) ;
313+ }
310314
311- return _buildFilterTile (
312- context: context,
313- title: title,
314- enabled: ! isFollowedFilterActive,
315- selectedCount: selectedCount,
316- routeName: routeName,
317- );
318- },
315+ if (filterState.status == HeadlinesFilterStatus .failure) {
316+ return FailureStateWidget (
317+ exception:
318+ filterState.error ??
319+ const UnknownException ('Failed to load filter data.' ),
320+ onRetry: () {
321+ final headlinesFeedBloc = context.read <HeadlinesFeedBloc >();
322+ final currentFilter = headlinesFeedBloc.state.filter;
323+ context.read <HeadlinesFilterBloc >().add (
324+ FilterDataLoaded (
325+ initialSelectedTopics: currentFilter.topics ?? [],
326+ initialSelectedSources: currentFilter.sources ?? [],
327+ initialSelectedCountries: currentFilter.eventCountries ?? [],
328+ isUsingFollowedItems: currentFilter.isFromFollowedItems,
329+ ),
319330 );
320331 },
321- ),
332+ );
333+ }
334+
335+ // Use a Map to define the filter tiles for cleaner code.
336+ final filterTiles = {
337+ l10n.headlinesFeedFilterTopicLabel: Routes .feedFilterTopicsName,
338+ l10n.headlinesFeedFilterSourceLabel: Routes .feedFilterSourcesName,
339+ l10n.headlinesFeedFilterEventCountryLabel:
340+ Routes .feedFilterEventCountriesName,
341+ };
342+
343+ return ListView .separated (
344+ itemCount: filterTiles.length,
345+ separatorBuilder: (context, index) => const Divider (height: 1 ),
346+ itemBuilder: (context, index) {
347+ final title = filterTiles.keys.elementAt (index);
348+ final routeName = filterTiles.values.elementAt (index);
349+ int selectedCount;
350+
351+ if (routeName == Routes .feedFilterTopicsName) {
352+ selectedCount = filterState.selectedTopics.length;
353+ } else if (routeName == Routes .feedFilterSourcesName) {
354+ selectedCount = filterState.selectedSources.length;
355+ } else {
356+ selectedCount = filterState.selectedCountries.length;
357+ }
358+
359+ return _buildFilterTile (
360+ context: context,
361+ title: title,
362+ enabled: ! isFollowedFilterActive,
363+ selectedCount: selectedCount,
364+ routeName: routeName,
365+ );
366+ },
322367 );
323368 }
324369}
0 commit comments