Skip to content

Commit 102818d

Browse files
authored
Merge pull request #169 from flutter-news-app-full-source-code/feat/saved-filters-reordering
Feat/saved filters reordering
2 parents 72bebba + 95e68bd commit 102818d

File tree

15 files changed

+328
-111
lines changed

15 files changed

+328
-111
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 1.4.0 - Upcoming Release
44

5+
- **refactor**: relocated saved filters management to the account page and introduced reordering capability.
56
- **feat**: created a reusable, searchable multi-select page for filtering
67
- **feat**: add 'Followed' filter to quickly view content from followed items
78
- **feat(demo)**: pre-populate saved filters and settings for new anonymous users

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Click on any category to explore.
3636
### 🔍 Powerful Filtering & Search
3737
- **Quick-Access Filter Bar:** A persistent, horizontal filter bar on the main feed gives users one-tap access to their favorite content views. It includes built-in filters for "All" and "Followed" items, alongside any custom filters the user has saved.
3838
- **Advanced Filter Creation:** A dedicated, full-screen interface allows users to build complex filters by combining multiple `Topics`, `Sources`, and `Countries`.
39-
- **Saved Filters:** Users can name and save their custom filter combinations, which then appear in the quick-access filter bar for easy reuse.
39+
- **Saved Filters:** Users can name and save their custom filter combinations. These filters appear in the quick-access bar and can be reordered for a fully customized workflow.
4040
- **Unified Search:** A dedicated search page helps users find specific content quickly, with the ability to search across headlines, topics, sources, and countries.
4141
> **🎯 Your Advantage:** Give your users powerful content discovery tools that keep them engaged and coming back for more.
4242
@@ -56,6 +56,7 @@ Click on any category to explore.
5656
### 🧑‍🎨 Personalized User Accounts & Preferences
5757
- **Content Preferences:** Follow/unfollow `Topic`s, `Source`s, and `Country`s.
5858
- **Saved Headlines:** Bookmark articles for easy access later.
59+
- **Saved Filters Management:** Rename, delete, and reorder saved filters to control your content discovery shortcuts.
5960
- **Decorator Interaction Tracking:** User interactions with in-feed decorators (e.g., "Link Account" prompts) are tracked and persisted, ensuring a personalized and non-repetitive experience.
6061
> **❤️ Your Advantage:** Built-in personalization features that drive user retention and create a sticky app experience.
6162

lib/account/view/account_page.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,24 @@ class AccountPage extends StatelessWidget {
7272
indent: AppSpacing.paddingMedium,
7373
endIndent: AppSpacing.paddingMedium,
7474
),
75+
ListTile(
76+
leading: Icon(
77+
Icons.filter_alt_outlined,
78+
color: theme.colorScheme.primary,
79+
),
80+
title: Text(
81+
l10n.accountSavedFiltersTile,
82+
style: textTheme.titleMedium,
83+
),
84+
trailing: const Icon(Icons.chevron_right),
85+
onTap: () {
86+
context.goNamed(Routes.accountSavedFiltersName);
87+
},
88+
),
89+
const Divider(
90+
indent: AppSpacing.paddingMedium,
91+
endIndent: AppSpacing.paddingMedium,
92+
),
7593
_buildSettingsTile(context),
7694
const Divider(
7795
indent: AppSpacing.paddingMedium,
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import 'package:core/core.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_bloc/flutter_bloc.dart';
4+
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
5+
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/widgets/save_filter_dialog.dart';
6+
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
7+
import 'package:ui_kit/ui_kit.dart';
8+
9+
/// {@template saved_filters_page}
10+
/// A page for managing saved feed filters, allowing users to reorder,
11+
/// rename, or delete them.
12+
///
13+
/// Reordering is handled via a [ReorderableListView], which dispatches a
14+
/// [SavedFiltersReordered] event to the [AppBloc] to persist the new order.
15+
/// Renaming and deletion are handled via a [PopupMenuButton] on each list item.
16+
/// {@endtemplate}
17+
class SavedFiltersPage extends StatelessWidget {
18+
/// {@macro saved_filters_page}
19+
const SavedFiltersPage({super.key});
20+
21+
@override
22+
Widget build(BuildContext context) {
23+
final l10n = AppLocalizationsX(context).l10n;
24+
final theme = Theme.of(context);
25+
26+
return Scaffold(
27+
appBar: AppBar(
28+
title: Text(
29+
// Use the correct localization key for the page title.
30+
l10n.savedFiltersPageTitle,
31+
style: theme.textTheme.titleLarge,
32+
),
33+
),
34+
body: BlocBuilder<AppBloc, AppState>(
35+
builder: (context, state) {
36+
final savedFilters = state.userContentPreferences?.savedFilters ?? [];
37+
38+
if (savedFilters.isEmpty) {
39+
return InitialStateWidget(
40+
icon: Icons.filter_list_off_outlined,
41+
headline: l10n.savedFiltersEmptyHeadline,
42+
subheadline: l10n.savedFiltersEmptySubheadline,
43+
);
44+
}
45+
46+
return ReorderableListView.builder(
47+
// Disable the default trailing drag handles to use only the
48+
// custom leading handle.
49+
buildDefaultDragHandles: false,
50+
itemCount: savedFilters.length,
51+
itemBuilder: (context, index) {
52+
final filter = savedFilters[index];
53+
return ListTile(
54+
// A key is required for ReorderableListView to work correctly.
55+
key: ValueKey(filter.id),
56+
leading: ReorderableDragStartListener(
57+
index: index,
58+
child: const Icon(Icons.drag_handle),
59+
),
60+
title: Text(filter.name),
61+
trailing: PopupMenuButton<String>(
62+
onSelected: (value) {
63+
switch (value) {
64+
case 'rename':
65+
showDialog<void>(
66+
context: context,
67+
builder: (_) => SaveFilterDialog(
68+
initialValue: filter.name,
69+
onSave: (newName) {
70+
final updatedFilter = filter.copyWith(
71+
name: newName,
72+
);
73+
context.read<AppBloc>().add(
74+
SavedFilterUpdated(filter: updatedFilter),
75+
);
76+
},
77+
),
78+
);
79+
case 'delete':
80+
context.read<AppBloc>().add(
81+
SavedFilterDeleted(filterId: filter.id),
82+
);
83+
}
84+
},
85+
itemBuilder: (BuildContext context) =>
86+
<PopupMenuEntry<String>>[
87+
PopupMenuItem<String>(
88+
value: 'rename',
89+
child: Text(l10n.savedFiltersMenuRename),
90+
),
91+
PopupMenuItem<String>(
92+
value: 'delete',
93+
child: Text(
94+
l10n.savedFiltersMenuDelete,
95+
style: TextStyle(color: theme.colorScheme.error),
96+
),
97+
),
98+
],
99+
),
100+
);
101+
},
102+
onReorder: (oldIndex, newIndex) {
103+
// This adjustment is necessary when moving an item downwards
104+
// in the list.
105+
if (oldIndex < newIndex) {
106+
newIndex -= 1;
107+
}
108+
109+
// Create a mutable copy of the list.
110+
final reorderedList = List<SavedFilter>.from(savedFilters);
111+
112+
// Remove the item from its old position and insert it into the
113+
// new position.
114+
final movedFilter = reorderedList.removeAt(oldIndex);
115+
reorderedList.insert(newIndex, movedFilter);
116+
117+
// Dispatch the event to the AppBloc to persist the new order.
118+
context.read<AppBloc>().add(
119+
SavedFiltersReordered(reorderedFilters: reorderedList),
120+
);
121+
},
122+
);
123+
},
124+
),
125+
);
126+
}
127+
}

lib/app/bloc/app_bloc.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
8181
on<SavedFilterAdded>(_onSavedFilterAdded);
8282
on<SavedFilterUpdated>(_onSavedFilterUpdated);
8383
on<SavedFilterDeleted>(_onSavedFilterDeleted);
84+
on<SavedFiltersReordered>(_onSavedFiltersReordered);
8485
on<AppLogoutRequested>(_onLogoutRequested);
8586

8687
// Subscribe to the authentication repository's authStateChanges stream.
@@ -914,4 +915,38 @@ class AppBloc extends Bloc<AppEvent, AppState> {
914915

915916
add(AppUserContentPreferencesChanged(preferences: updatedPreferences));
916917
}
918+
919+
/// Handles reordering the list of saved filters.
920+
///
921+
/// This method receives the complete list of filters in their new order
922+
/// and dispatches an [AppUserContentPreferencesChanged] event to persist
923+
/// the change. This approach leverages the natural order of the list for
924+
/// persistence, avoiding the need for a separate 'order' property on the
925+
/// [SavedFilter] model.
926+
Future<void> _onSavedFiltersReordered(
927+
SavedFiltersReordered event,
928+
Emitter<AppState> emit,
929+
) async {
930+
_logger.info('[AppBloc] SavedFiltersReordered event received.');
931+
932+
if (state.userContentPreferences == null) {
933+
_logger.warning(
934+
'[AppBloc] Skipping SavedFiltersReordered: UserContentPreferences not loaded.',
935+
);
936+
return;
937+
}
938+
939+
// Create an updated preferences object with the reordered list.
940+
final updatedPreferences = state.userContentPreferences!.copyWith(
941+
savedFilters: event.reorderedFilters,
942+
);
943+
944+
// Dispatch the existing event to handle persistence and state updates.
945+
// This reuses the existing logic for updating user preferences.
946+
add(AppUserContentPreferencesChanged(preferences: updatedPreferences));
947+
_logger.info(
948+
'[AppBloc] Dispatched AppUserContentPreferencesChanged '
949+
'with reordered filters.',
950+
);
951+
}
917952
}

lib/app/bloc/app_event.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,17 @@ class SavedFilterDeleted extends AppEvent {
198198
@override
199199
List<Object> get props => [filterId];
200200
}
201+
202+
/// {@template saved_filters_reordered}
203+
/// Dispatched when the user reorders their saved filters in the UI.
204+
/// {@endtemplate}
205+
class SavedFiltersReordered extends AppEvent {
206+
/// {@macro saved_filters_reordered}
207+
const SavedFiltersReordered({required this.reorderedFilters});
208+
209+
/// The complete list of saved filters in their new order.
210+
final List<SavedFilter> reorderedFilters;
211+
212+
@override
213+
List<Object> get props => [reorderedFilters];
214+
}

lib/headlines-feed/view/headlines_filter_page.dart

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,6 @@ class _HeadlinesFilterViewState extends State<_HeadlinesFilterView> {
274274
context.pop();
275275
},
276276
),
277-
// Manage Saved Filters Button
278-
IconButton(
279-
tooltip: l10n.headlinesFilterManageTooltip,
280-
icon: const Icon(Icons.edit_note_outlined),
281-
onPressed: () =>
282-
context.pushNamed(Routes.manageSavedFiltersName),
283-
),
284277
// Apply Filters Button
285278
IconButton(
286279
icon: const Icon(Icons.check),

lib/headlines-feed/view/manage_saved_filters_page.dart

Lines changed: 0 additions & 93 deletions
This file was deleted.

lib/l10n/app_localizations.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1921,6 +1921,42 @@ abstract class AppLocalizations {
19211921
/// In en, this message translates to:
19221922
/// **'Filter source list'**
19231923
String get sourceListFilterPageFilterButtonTooltip;
1924+
1925+
/// The title for the list tile on the account page that navigates to the saved filters management page.
1926+
///
1927+
/// In en, this message translates to:
1928+
/// **'Saved Filters'**
1929+
String get accountSavedFiltersTile;
1930+
1931+
/// The title for the app bar on the saved filters management page.
1932+
///
1933+
/// In en, this message translates to:
1934+
/// **'Saved Filters'**
1935+
String get savedFiltersPageTitle;
1936+
1937+
/// The main headline text displayed on the saved filters page when the user has no saved filters.
1938+
///
1939+
/// In en, this message translates to:
1940+
/// **'No Saved Filters'**
1941+
String get savedFiltersEmptyHeadline;
1942+
1943+
/// The sub-headline text displayed on the saved filters page when the user has no saved filters.
1944+
///
1945+
/// In en, this message translates to:
1946+
/// **'Filters you save from the feed will appear here.'**
1947+
String get savedFiltersEmptySubheadline;
1948+
1949+
/// The text for the 'Rename' option in the popup menu on a saved filter item.
1950+
///
1951+
/// In en, this message translates to:
1952+
/// **'Rename'**
1953+
String get savedFiltersMenuRename;
1954+
1955+
/// The text for the 'Delete' option in the popup menu on a saved filter item.
1956+
///
1957+
/// In en, this message translates to:
1958+
/// **'Delete'**
1959+
String get savedFiltersMenuDelete;
19241960
}
19251961

19261962
class _AppLocalizationsDelegate

0 commit comments

Comments
 (0)