Skip to content

Commit d385834

Browse files
authored
Merge pull request #161 from flutter-news-app-full-source-code/feat-filter-bookmarks
Feat filter bookmarks
2 parents 6e690d0 + b97124e commit d385834

28 files changed

+1901
-575
lines changed

CHANGELOG.md

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,50 @@
11
# Changelog
22

3-
## 1.3.0
4-
5-
- feat: enforce content following and bookmarking limits ([#145](link), [#159](link))
6-
- feat: enforce mandatory app updates via remote configuration ([#134](link))
7-
- refactor: separate authentication and account linking flows ([#142](link))
8-
- refactor: use `flutter_native_splash` for web splash screen management ([#137](link))
9-
- refactor: implement centralized logging for GoRouter navigation ([#143](link))
10-
- fix: web splash screen remains visible indefinitely ([#136](link))
11-
- fix: back button is unresponsive during authentication flow ([#144](link))
12-
- fix: ensure proper redirection after successful authentication ([#148](link))
13-
- fix: content limitation bottom sheet overflows on small screens ([#149](link))
14-
- chore(deps): update bloc, go_router, and other key dependencies ([#135](link))
15-
- docs: clarify feed display settings in README ([#150](link))
3+
## 1.4.0 - 2025-10-14
4+
5+
- **feat**: implement saved feed filters with create, rename, and delete
6+
- **feat**: add horizontal filter bar to the headlines feed for quick selection
7+
- **refactor**: replace monolithic "apply followed items" with granular controls
8+
- **refactor**: Updated the Headline Details page with a fading scroll effect for metadata chips and a new style for the 'Continue Reading' button. The main feed's filter icon was removed from the AppBar in favor of the new filter bar.
9+
10+
## 1.3.0 - 2025-10-10
11+
12+
- **feat**: enforce content following and bookmarking limits
13+
- **feat**: enforce mandatory app updates via remote configuration
14+
- **refactor**: separate authentication and account linking flows
15+
- **refactor**: use `flutter_native_splash` for web splash screen management
16+
- **refactor**: implement centralized logging for GoRouter navigation
17+
- **fix**: web splash screen remains visible indefinitely
18+
- **fix**: back button is unresponsive during authentication flow
19+
- **fix**: ensure proper redirection after successful authentication
20+
- **fix**: content limitation bottom sheet overflows on small screens
21+
- **chore(deps)**: update bloc, go_router, and other key dependencies
22+
- **docs**: clarify feed display settings in README
1623

1724
## 1.2.1
1825

19-
- **Fix:** Resolved an issue where the web splash screen would not disappear after the application loaded due to an incorrect bootstrap script in `index.html`.
26+
- **fix**: resolved an issue where the web splash screen would not disappear after the application loaded due to an incorrect bootstrap script in `index.html`.
2027

2128
## 1.2.0
2229

23-
- **Refactor:** Updated `flutter_launcher_icons` configurations.
24-
- **Refactor:** Updated `flutter_native_splash` configurations.
25-
- **Dependency Update:** Bumped `bloc` to `^9.0.1`.
26-
- **Dependency Update:** Bumped `flutter_bloc` to `^9.1.1`.
27-
- **Dependency Update:** Bumped `go_router` to `^16.2.4`.
28-
- **Dependency Update:** Bumped `google_fonts` to `^6.3.2`.
29-
- **Dependency Update:** Bumped `share_plus` to `^11.1.0`.
30-
- **Dependency Update:** Bumped `very_good_analysis` to `^10.0.0`.
31-
- **Metadata Update:** Updated `homepage` and `documentation` URLs.
32-
- **Metadata Update:** Added `funding` link.
33-
- **Metadata Update:** Added `flutter` and `flutter-full-app` to `topics`.
34-
- **Metadata Update:** Added `screenshots` configuration.
30+
- **refactor**: updated `flutter_launcher_icons` configurations
31+
- **refactor**: updated `flutter_native_splash` configurations
32+
- **chore(deps)**: bumped `bloc` to `^9.0.1`
33+
- **chore(deps)**: bumped `flutter_bloc` to `^9.1.1`
34+
- **chore(deps)**: bumped `go_router` to `^16.2.4`
35+
- **chore(deps)**: bumped `google_fonts` to `^6.3.2`
36+
- **chore(deps)**: bumped `share_plus` to `^11.1.0`
37+
- **chore(deps)**: bumped `very_good_analysis` to `^10.0.0`
38+
- **docs**: updated `homepage` and `documentation` URLs
39+
- **docs**: added `funding` link
40+
- **docs**: added `flutter` and `flutter-full-app` to `topics`
41+
- **docs**: added `screenshots` configuration
3542

3643
## 1.1.0
3744

38-
- **Feature:** Implemented app version enforcement based on remote configuration, including a dedicated `UpdateRequiredPage` to guide users to update.
39-
- **Enhancement:** Integrated `package_info_plus` and `pub_semver` for accurate version retrieval and comparison across platforms (Android, iOS, Web).
45+
- **feat**: implemented app version enforcement based on remote configuration, including a dedicated `UpdateRequiredPage` to guide users to update
46+
- **feat**: integrated `package_info_plus` and `pub_semver` for accurate version retrieval and comparison across platforms (Android, iOS, Web)
4047

4148
## 1.0.1
4249

43-
- **Version Control:** Transitioned from date-based versioning to semantic versioning. This release marks the first version following the semantic versioning standard.
50+
- **chore**: transitioned from date-based versioning to semantic versioning. This release marks the first version following the semantic versioning standard

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Click on any category to explore.
3636
### 🔍 Advanced Content Filtering & Search
3737
- **Comprehensive Filtering:** Let users filter headlines by `Topic`, `Source`, and `Country` using a dedicated filter interface.
3838
- **"Followed Items" Filter:** Users can instantly filter the feed to show only content from their followed topics, sources, and countries.
39+
- **Saved & Quick-Access Filters:** Empower users to save complex filter combinations and recall them with a single tap from a new, persistent filter bar on the main feed.
3940
- **Unified Search:** Includes a dedicated search page to help users find specific content quickly, with the ability to search across headlines, topics, sources, and countries.
4041
> **🎯 Your Advantage:** Give your users powerful content discovery tools that keep them engaged and coming back for more.
4142

lib/app/bloc/app_bloc.dart

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ class AppBloc extends Bloc<AppEvent, AppState> {
7878
on<AppVersionCheckRequested>(_onAppVersionCheckRequested);
7979
on<AppUserFeedDecoratorShown>(_onAppUserFeedDecoratorShown);
8080
on<AppUserContentPreferencesChanged>(_onAppUserContentPreferencesChanged);
81+
on<SavedFilterAdded>(_onSavedFilterAdded);
82+
on<SavedFilterUpdated>(_onSavedFilterUpdated);
83+
on<SavedFilterDeleted>(_onSavedFilterDeleted);
8184
on<AppLogoutRequested>(_onLogoutRequested);
8285

8386
// Subscribe to the authentication repository's authStateChanges stream.
@@ -802,4 +805,105 @@ class AppBloc extends Bloc<AppEvent, AppState> {
802805
);
803806
}
804807
}
808+
809+
/// Handles adding a new saved filter to the user's content preferences.
810+
///
811+
/// This method optimistically updates the state by dispatching an
812+
/// [AppUserContentPreferencesChanged] event, which will then handle
813+
/// persistence.
814+
Future<void> _onSavedFilterAdded(
815+
SavedFilterAdded event,
816+
Emitter<AppState> emit,
817+
) async {
818+
if (state.userContentPreferences == null) {
819+
_logger.warning(
820+
'[AppBloc] Skipping SavedFilterAdded: UserContentPreferences not loaded.',
821+
);
822+
return;
823+
}
824+
825+
final updatedSavedFilters = List<SavedFilter>.from(
826+
state.userContentPreferences!.savedFilters,
827+
)..add(event.filter);
828+
829+
final updatedPreferences = state.userContentPreferences!.copyWith(
830+
savedFilters: updatedSavedFilters,
831+
);
832+
833+
add(AppUserContentPreferencesChanged(preferences: updatedPreferences));
834+
}
835+
836+
/// Handles updating an existing saved filter (e.g., renaming it).
837+
///
838+
/// This method finds the filter by its ID, replaces it with the updated
839+
/// version, and then dispatches an [AppUserContentPreferencesChanged] event
840+
/// to persist the changes.
841+
Future<void> _onSavedFilterUpdated(
842+
SavedFilterUpdated event,
843+
Emitter<AppState> emit,
844+
) async {
845+
if (state.userContentPreferences == null) {
846+
_logger.warning(
847+
'[AppBloc] Skipping SavedFilterUpdated: UserContentPreferences not loaded.',
848+
);
849+
return;
850+
}
851+
852+
final originalFilters = state.userContentPreferences!.savedFilters;
853+
final index = originalFilters.indexWhere((f) => f.id == event.filter.id);
854+
855+
if (index == -1) {
856+
_logger.warning(
857+
'[AppBloc] Skipping SavedFilterUpdated: Filter with id ${event.filter.id} not found.',
858+
);
859+
return;
860+
}
861+
862+
final updatedSavedFilters = List<SavedFilter>.from(originalFilters)
863+
..[index] = event.filter;
864+
865+
final updatedPreferences = state.userContentPreferences!.copyWith(
866+
savedFilters: updatedSavedFilters,
867+
);
868+
869+
add(AppUserContentPreferencesChanged(preferences: updatedPreferences));
870+
}
871+
872+
/// Handles deleting a saved filter from the user's content preferences.
873+
///
874+
/// This method removes the filter by its ID and then dispatches an
875+
/// [AppUserContentPreferencesChanged] event to persist the changes.
876+
Future<void> _onSavedFilterDeleted(
877+
SavedFilterDeleted event,
878+
Emitter<AppState> emit,
879+
) async {
880+
if (state.userContentPreferences == null) {
881+
_logger.warning(
882+
'[AppBloc] Skipping SavedFilterDeleted: UserContentPreferences not loaded.',
883+
);
884+
return;
885+
}
886+
887+
final updatedSavedFilters = List<SavedFilter>.from(
888+
state.userContentPreferences!.savedFilters,
889+
)..removeWhere((f) => f.id == event.filterId);
890+
891+
// Check if the list was actually modified to avoid unnecessary updates.
892+
if (updatedSavedFilters.length ==
893+
state.userContentPreferences!.savedFilters.length) {
894+
if (updatedSavedFilters.length ==
895+
state.userContentPreferences!.savedFilters.length) {
896+
_logger.warning(
897+
'[AppBloc] Skipping SavedFilterDeleted: Filter with id ${event.filterId} not found.',
898+
);
899+
return;
900+
}
901+
}
902+
903+
final updatedPreferences = state.userContentPreferences!.copyWith(
904+
savedFilters: updatedSavedFilters,
905+
);
906+
907+
add(AppUserContentPreferencesChanged(preferences: updatedPreferences));
908+
}
805909
}

lib/app/bloc/app_event.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,45 @@ class AppUserFeedDecoratorShown extends AppEvent {
156156
@override
157157
List<Object> get props => [userId, feedDecoratorType, isCompleted];
158158
}
159+
160+
/// {@template saved_filter_added}
161+
/// Dispatched when a new feed filter is saved by the user.
162+
/// {@endtemplate}
163+
class SavedFilterAdded extends AppEvent {
164+
/// {@macro saved_filter_added}
165+
const SavedFilterAdded({required this.filter});
166+
167+
/// The new [SavedFilter] to be added.
168+
final SavedFilter filter;
169+
170+
@override
171+
List<Object> get props => [filter];
172+
}
173+
174+
/// {@template saved_filter_updated}
175+
/// Dispatched when an existing saved feed filter is updated (e.g., renamed).
176+
/// {@endtemplate}
177+
class SavedFilterUpdated extends AppEvent {
178+
/// {@macro saved_filter_updated}
179+
const SavedFilterUpdated({required this.filter});
180+
181+
/// The updated [SavedFilter] object.
182+
final SavedFilter filter;
183+
184+
@override
185+
List<Object> get props => [filter];
186+
}
187+
188+
/// {@template saved_filter_deleted}
189+
/// Dispatched when a saved feed filter is deleted by the user.
190+
/// {@endtemplate}
191+
class SavedFilterDeleted extends AppEvent {
192+
/// {@macro saved_filter_deleted}
193+
const SavedFilterDeleted({required this.filterId});
194+
195+
/// The ID of the filter to be deleted.
196+
final String filterId;
197+
198+
@override
199+
List<Object> get props => [filterId];
200+
}

lib/app/services/demo_data_initializer_service.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ class DemoDataInitializerService {
118118
followedSources: const [],
119119
followedTopics: const [],
120120
savedHeadlines: const [],
121+
savedFilters: const [],
121122
);
122123
await _userContentPreferencesRepository.create(
123124
item: defaultPreferences,

0 commit comments

Comments
 (0)