Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba55ecc
feat(app): add event for reordering saved filters
fulleni Oct 15, 2025
a2a508a
feat(app): implement saved filters reordering in AppBloc
fulleni Oct 15, 2025
b7b9b87
Refactor(filters): implement reorderable UI for saved filters
fulleni Oct 15, 2025
c391cfe
refactor(filter): remove saved filter management from filter page
fulleni Oct 15, 2025
af8312f
feat(account): add entry point for saved filters management
fulleni Oct 15, 2025
8ed6c8d
refactor(router): update route constants for saved filters
fulleni Oct 15, 2025
eb65f46
refactor(account): move saved filters management to account section
fulleni Oct 15, 2025
7d9078f
feat(l10n): add localizations for saved filters feature
fulleni Oct 15, 2025
dfe48df
refactor(account): update saved filters tile and routing
fulleni Oct 15, 2025
7c0e963
feat(localization): update localization keys for saved filters page
fulleni Oct 15, 2025
b73c10b
fix(account): use correct localization key for saved filters page title
fulleni Oct 15, 2025
5ced3f1
feat(l10n): add Arabic translations for saved filters feature
fulleni Oct 15, 2025
954b39b
build: l10n
fulleni Oct 15, 2025
c3c58f5
refactor(filters): relocate management and introduce reordering
fulleni Oct 15, 2025
c00b19e
feat(readme): update features description for saved filters and manag…
fulleni Oct 15, 2025
09d6c11
refactor(router): move saved filters route under account section
fulleni Oct 15, 2025
7cce4e2
style(account): update filter icon in account page
fulleni Oct 15, 2025
59f0f3a
feat(account): disable default drag handles in saved filters list
fulleni Oct 15, 2025
d899176
style: format
fulleni Oct 15, 2025
95e68bd
refactor(router): reorganize saved filters route definition
fulleni Oct 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 1.4.0 - Upcoming Release

- **refactor**: relocated saved filters management to the account page and introduced reordering capability.
- **feat**: created a reusable, searchable multi-select page for filtering
- **feat**: add 'Followed' filter to quickly view content from followed items
- **feat(demo)**: pre-populate saved filters and settings for new anonymous users
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Click on any category to explore.
### 🔍 Powerful Filtering & Search
- **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.
- **Advanced Filter Creation:** A dedicated, full-screen interface allows users to build complex filters by combining multiple `Topics`, `Sources`, and `Countries`.
- **Saved Filters:** Users can name and save their custom filter combinations, which then appear in the quick-access filter bar for easy reuse.
- **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.
- **Unified Search:** A dedicated search page helps users find specific content quickly, with the ability to search across headlines, topics, sources, and countries.
> **🎯 Your Advantage:** Give your users powerful content discovery tools that keep them engaged and coming back for more.

Expand All @@ -56,6 +56,7 @@ Click on any category to explore.
### 🧑‍🎨 Personalized User Accounts & Preferences
- **Content Preferences:** Follow/unfollow `Topic`s, `Source`s, and `Country`s.
- **Saved Headlines:** Bookmark articles for easy access later.
- **Saved Filters Management:** Rename, delete, and reorder saved filters to control your content discovery shortcuts.
- **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.
> **❤️ Your Advantage:** Built-in personalization features that drive user retention and create a sticky app experience.

Expand Down
18 changes: 18 additions & 0 deletions lib/account/view/account_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ class AccountPage extends StatelessWidget {
indent: AppSpacing.paddingMedium,
endIndent: AppSpacing.paddingMedium,
),
ListTile(
leading: Icon(
Icons.filter_alt_outlined,
color: theme.colorScheme.primary,
),
title: Text(
l10n.accountSavedFiltersTile,
style: textTheme.titleMedium,
),
trailing: const Icon(Icons.chevron_right),
onTap: () {
context.goNamed(Routes.accountSavedFiltersName);
},
),
const Divider(
indent: AppSpacing.paddingMedium,
endIndent: AppSpacing.paddingMedium,
),
_buildSettingsTile(context),
const Divider(
indent: AppSpacing.paddingMedium,
Expand Down
127 changes: 127 additions & 0 deletions lib/account/view/saved_filters_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import 'package:core/core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/widgets/save_filter_dialog.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
import 'package:ui_kit/ui_kit.dart';

/// {@template saved_filters_page}
/// A page for managing saved feed filters, allowing users to reorder,
/// rename, or delete them.
///
/// Reordering is handled via a [ReorderableListView], which dispatches a
/// [SavedFiltersReordered] event to the [AppBloc] to persist the new order.
/// Renaming and deletion are handled via a [PopupMenuButton] on each list item.
/// {@endtemplate}
class SavedFiltersPage extends StatelessWidget {
/// {@macro saved_filters_page}
const SavedFiltersPage({super.key});

@override
Widget build(BuildContext context) {
final l10n = AppLocalizationsX(context).l10n;
final theme = Theme.of(context);

return Scaffold(
appBar: AppBar(
title: Text(
// Use the correct localization key for the page title.
l10n.savedFiltersPageTitle,
style: theme.textTheme.titleLarge,
),
),
body: BlocBuilder<AppBloc, AppState>(
builder: (context, state) {
final savedFilters = state.userContentPreferences?.savedFilters ?? [];

if (savedFilters.isEmpty) {
return InitialStateWidget(
icon: Icons.filter_list_off_outlined,
headline: l10n.savedFiltersEmptyHeadline,
subheadline: l10n.savedFiltersEmptySubheadline,
);
}

return ReorderableListView.builder(
// Disable the default trailing drag handles to use only the
// custom leading handle.
buildDefaultDragHandles: false,
itemCount: savedFilters.length,
itemBuilder: (context, index) {
final filter = savedFilters[index];
return ListTile(
// A key is required for ReorderableListView to work correctly.
key: ValueKey(filter.id),
leading: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
title: Text(filter.name),
trailing: PopupMenuButton<String>(
onSelected: (value) {
switch (value) {
case 'rename':
showDialog<void>(
context: context,
builder: (_) => SaveFilterDialog(
initialValue: filter.name,
onSave: (newName) {
final updatedFilter = filter.copyWith(
name: newName,
);
context.read<AppBloc>().add(
SavedFilterUpdated(filter: updatedFilter),
);
},
),
);
case 'delete':
context.read<AppBloc>().add(
SavedFilterDeleted(filterId: filter.id),
);
}
},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'rename',
child: Text(l10n.savedFiltersMenuRename),
),
PopupMenuItem<String>(
value: 'delete',
child: Text(
l10n.savedFiltersMenuDelete,
style: TextStyle(color: theme.colorScheme.error),
),
),
],
),
);
},
onReorder: (oldIndex, newIndex) {
// This adjustment is necessary when moving an item downwards
// in the list.
if (oldIndex < newIndex) {
newIndex -= 1;
}

// Create a mutable copy of the list.
final reorderedList = List<SavedFilter>.from(savedFilters);

// Remove the item from its old position and insert it into the
// new position.
final movedFilter = reorderedList.removeAt(oldIndex);
reorderedList.insert(newIndex, movedFilter);

// Dispatch the event to the AppBloc to persist the new order.
context.read<AppBloc>().add(
SavedFiltersReordered(reorderedFilters: reorderedList),
);
},
);
},
),
);
}
}
35 changes: 35 additions & 0 deletions lib/app/bloc/app_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
on<SavedFilterAdded>(_onSavedFilterAdded);
on<SavedFilterUpdated>(_onSavedFilterUpdated);
on<SavedFilterDeleted>(_onSavedFilterDeleted);
on<SavedFiltersReordered>(_onSavedFiltersReordered);
on<AppLogoutRequested>(_onLogoutRequested);

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

add(AppUserContentPreferencesChanged(preferences: updatedPreferences));
}

/// Handles reordering the list of saved filters.
///
/// This method receives the complete list of filters in their new order
/// and dispatches an [AppUserContentPreferencesChanged] event to persist
/// the change. This approach leverages the natural order of the list for
/// persistence, avoiding the need for a separate 'order' property on the
/// [SavedFilter] model.
Future<void> _onSavedFiltersReordered(
SavedFiltersReordered event,
Emitter<AppState> emit,
) async {
_logger.info('[AppBloc] SavedFiltersReordered event received.');

if (state.userContentPreferences == null) {
_logger.warning(
'[AppBloc] Skipping SavedFiltersReordered: UserContentPreferences not loaded.',
);
return;
}

// Create an updated preferences object with the reordered list.
final updatedPreferences = state.userContentPreferences!.copyWith(
savedFilters: event.reorderedFilters,
);

// Dispatch the existing event to handle persistence and state updates.
// This reuses the existing logic for updating user preferences.
add(AppUserContentPreferencesChanged(preferences: updatedPreferences));
_logger.info(
'[AppBloc] Dispatched AppUserContentPreferencesChanged '
'with reordered filters.',
);
}
}
14 changes: 14 additions & 0 deletions lib/app/bloc/app_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,17 @@ class SavedFilterDeleted extends AppEvent {
@override
List<Object> get props => [filterId];
}

/// {@template saved_filters_reordered}
/// Dispatched when the user reorders their saved filters in the UI.
/// {@endtemplate}
class SavedFiltersReordered extends AppEvent {
/// {@macro saved_filters_reordered}
const SavedFiltersReordered({required this.reorderedFilters});

/// The complete list of saved filters in their new order.
final List<SavedFilter> reorderedFilters;

@override
List<Object> get props => [reorderedFilters];
}
7 changes: 0 additions & 7 deletions lib/headlines-feed/view/headlines_filter_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,6 @@ class _HeadlinesFilterViewState extends State<_HeadlinesFilterView> {
context.pop();
},
),
// Manage Saved Filters Button
IconButton(
tooltip: l10n.headlinesFilterManageTooltip,
icon: const Icon(Icons.edit_note_outlined),
onPressed: () =>
context.pushNamed(Routes.manageSavedFiltersName),
),
// Apply Filters Button
IconButton(
icon: const Icon(Icons.check),
Expand Down
93 changes: 0 additions & 93 deletions lib/headlines-feed/view/manage_saved_filters_page.dart

This file was deleted.

36 changes: 36 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1921,6 +1921,42 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Filter source list'**
String get sourceListFilterPageFilterButtonTooltip;

/// The title for the list tile on the account page that navigates to the saved filters management page.
///
/// In en, this message translates to:
/// **'Saved Filters'**
String get accountSavedFiltersTile;

/// The title for the app bar on the saved filters management page.
///
/// In en, this message translates to:
/// **'Saved Filters'**
String get savedFiltersPageTitle;

/// The main headline text displayed on the saved filters page when the user has no saved filters.
///
/// In en, this message translates to:
/// **'No Saved Filters'**
String get savedFiltersEmptyHeadline;

/// The sub-headline text displayed on the saved filters page when the user has no saved filters.
///
/// In en, this message translates to:
/// **'Filters you save from the feed will appear here.'**
String get savedFiltersEmptySubheadline;

/// The text for the 'Rename' option in the popup menu on a saved filter item.
///
/// In en, this message translates to:
/// **'Rename'**
String get savedFiltersMenuRename;

/// The text for the 'Delete' option in the popup menu on a saved filter item.
///
/// In en, this message translates to:
/// **'Delete'**
String get savedFiltersMenuDelete;
}

class _AppLocalizationsDelegate
Expand Down
Loading
Loading