Skip to content

Commit 264f571

Browse files
committed
fix(headlines-feed): disable apply button for empty or duplicate filters
- Add check for empty filter selection - Compare current selection with saved filters for duplicates - Enable apply button only if filter is not empty and not a duplicate - Refactor UI build process for better readability
1 parent 1022797 commit 264f571

File tree

1 file changed

+142
-97
lines changed

1 file changed

+142
-97
lines changed

lib/headlines-feed/view/headlines_filter_page.dart

Lines changed: 142 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:core/core.dart';
22
import 'package:data_repository/data_repository.dart';
3+
import 'package:flutter/foundation.dart';
34
import 'package:flutter/material.dart';
45
import 'package:flutter_bloc/flutter_bloc.dart';
56
import '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
89
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/bloc/headlines_filter_bloc.dart';
910
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/models/headline_filter.dart';
1011
import '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';
1113
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
1214
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
1315
import '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

Comments
 (0)