From 1535c84e723c9626962e7d8b678f62de88af3c52 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:20:29 +0100 Subject: [PATCH 01/20] feat(lib): add SavedFilter model for user-defined filters - Create a new model to represent user-defined filter combinations - Include properties for topics, sources, and countries - Implement JSON serialization and deserialization - Add copyWith method for easy modification --- lib/src/models/saved_filter.dart | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 lib/src/models/saved_filter.dart diff --git a/lib/src/models/saved_filter.dart b/lib/src/models/saved_filter.dart new file mode 100644 index 00000000..c49404cb --- /dev/null +++ b/lib/src/models/saved_filter.dart @@ -0,0 +1,74 @@ +import 'package:core/src/models/entities/country.dart'; +import 'package:core/src/models/entities/source.dart'; +import 'package:core/src/models/entities/topic.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'saved_filter.g.dart'; + +/// {@template saved_filter} +/// Represents a user-defined filter combination for filtering headlines. +/// +/// This model stores a named set of criteria, including topics, sources, and +/// countries, allowing users to quickly re-apply complex filters. +/// {@endtemplate} +@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) +class SavedFilter extends Equatable { + /// {@macro saved_filter} + const SavedFilter({ + required this.id, + required this.name, + required this.topics, + required this.sources, + required this.countries, + }); + + /// Factory method to create a [SavedFilter] instance from a JSON map. + factory SavedFilter.fromJson(Map json) => + _$SavedfilterFromJson(json); + + /// The unique identifier for the saved filter. + final String id; + + /// The user-provided name for this saved filter. + final String name; + + /// The list of topics to include in the filter. + /// An empty list means no topic filter is applied. + final List topics; + + /// The list of sources to include in the filter. + /// An empty list means no source filter is applied. + final List sources; + + /// The list of countries to include in the filter. + /// An empty list means no country filter is applied. + final List countries; + + /// Converts this [SavedFilter] instance to a JSON map. + Map toJson() => _$SavedfilterToJson(this); + + @override + List get props => [id, name, topics, sources, countries]; + + @override + bool get stringify => true; + + /// Creates a copy of this [SavedFilter] but with the given fields + /// replaced with the new values. + SavedFilter copyWith({ + String? id, + String? name, + List? topics, + List? sources, + List? countries, + }) { + return SavedFilter( + id: id ?? this.id, + name: name ?? this.name, + topics: topics ?? this.topics, + sources: sources ?? this.sources, + countries: countries ?? this.countries, + ); + } +} From 7a4fc1ae59a6a367c217234f59139fe9347503c6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:20:39 +0100 Subject: [PATCH 02/20] build(json_serializable): generate SavedFilter model serialization - Add generated code for SavedFilter model serialization - IncludefromJsonandtoJsonmethods for SavedFilter class - Cover topics, sources, and countries serialization in the process --- lib/src/models/saved_filter.g.dart | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 lib/src/models/saved_filter.g.dart diff --git a/lib/src/models/saved_filter.g.dart b/lib/src/models/saved_filter.g.dart new file mode 100644 index 00000000..39b1834d --- /dev/null +++ b/lib/src/models/saved_filter.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'saved_filter.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SavedFilter _$SavedFilterFromJson(Map json) => + $checkedCreate('SavedFilter', json, ($checkedConvert) { + final val = SavedFilter( + id: $checkedConvert('id', (v) => v as String), + name: $checkedConvert('name', (v) => v as String), + topics: $checkedConvert( + 'topics', + (v) => (v as List) + .map((e) => Topic.fromJson(e as Map)) + .toList(), + ), + sources: $checkedConvert( + 'sources', + (v) => (v as List) + .map((e) => Source.fromJson(e as Map)) + .toList(), + ), + countries: $checkedConvert( + 'countries', + (v) => (v as List) + .map((e) => Country.fromJson(e as Map)) + .toList(), + ), + ); + return val; + }); + +Map _$SavedFilterToJson(SavedFilter instance) => + { + 'id': instance.id, + 'name': instance.name, + 'topics': instance.topics.map((e) => e.toJson()).toList(), + 'sources': instance.sources.map((e) => e.toJson()).toList(), + 'countries': instance.countries.map((e) => e.toJson()).toList(), + }; From 5226f4aeecff2172bcc29e9919e763aa00ea4a28 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:21:04 +0100 Subject: [PATCH 03/20] feat(core): add saved filter fixtures - Add unique identifiers for saved filter fixtures in fixture_ids.dart - Create new saved_filter.dart file with predefined saved search data - Update fixtures.dart to export new saved filter fixtures --- lib/src/fixtures/fixture_ids.dart | 4 ++++ lib/src/fixtures/fixtures.dart | 1 + lib/src/fixtures/saved_filter.dart | 30 ++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 lib/src/fixtures/saved_filter.dart diff --git a/lib/src/fixtures/fixture_ids.dart b/lib/src/fixtures/fixture_ids.dart index d4b42f09..8d233eab 100644 --- a/lib/src/fixtures/fixture_ids.dart +++ b/lib/src/fixtures/fixture_ids.dart @@ -530,3 +530,7 @@ const kLocalAd3Id = '3563c000a4a4e6e1a8e7f0f3'; const kLocalAd4Id = '4563c000a4a4e6e1a8e7f0f4'; const kLocalAd5Id = '5563c000a4a4e6e1a8e7f0f5'; const kLocalAd6Id = '6563c000a4a4e6e1a8e7f0f6'; + +/// Unique identifier for the first saved filter fixture. +const String kSavedFilterId1 = 'saved_search_1'; +const String kSavedFilterId2 = 'saved_search_2'; diff --git a/lib/src/fixtures/fixtures.dart b/lib/src/fixtures/fixtures.dart index e7707e37..b8afd1bb 100644 --- a/lib/src/fixtures/fixtures.dart +++ b/lib/src/fixtures/fixtures.dart @@ -5,6 +5,7 @@ export 'headlines.dart'; export 'languages.dart'; export 'local_ads.dart'; export 'remote_configs.dart'; +export 'saved_filter.dart'; export 'sources.dart'; export 'topics.dart'; export 'user_app_settings_fixtures.dart'; diff --git a/lib/src/fixtures/saved_filter.dart b/lib/src/fixtures/saved_filter.dart new file mode 100644 index 00000000..20c226f0 --- /dev/null +++ b/lib/src/fixtures/saved_filter.dart @@ -0,0 +1,30 @@ +import 'package:core/src/fixtures/countries.dart'; +import 'package:core/src/fixtures/fixture_ids.dart'; +import 'package:core/src/fixtures/sources.dart'; +import 'package:core/src/fixtures/topics.dart'; +import 'package:core/src/models/saved_filter.dart'; + +/// A list of predefined saved searches for fixture data. +final savedFiltersFixturesData = [ + SavedFilter( + id: kSavedFilterId1, + name: 'UK Tech & Politics', + topics: [ + topicsFixturesData.firstWhere((t) => t.name == 'Technology'), + topicsFixturesData.firstWhere((t) => t.name == 'Politics'), + ], + sources: [sourcesFixturesData.firstWhere((s) => s.name == 'BBC News')], + countries: [ + countriesFixturesData.firstWhere((c) => c.name == 'United Kingdom'), + ], + ), + SavedFilter( + id: kSavedFilterId2, + name: 'US Business News', + topics: [topicsFixturesData.firstWhere((t) => t.name == 'Business')], + sources: [], + countries: [ + countriesFixturesData.firstWhere((c) => c.name == 'United States'), + ], + ), +]; From 2000e764fd8bc8154ee4ee331246416148c196c1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:22:40 +0100 Subject: [PATCH 04/20] feat(config): add saved filters limits to user preference config - Add new fields for saved filters limits for guest, authenticated, and premium users - Update constructor and toJson method to include new fields - Modify class copyWith method to support new fields - Update documentation to reflect new saved filters limits --- .../models/config/user_preference_config.dart | 38 +++++++++++++------ .../config/user_preference_config.g.dart | 15 ++++++++ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/lib/src/models/config/user_preference_config.dart b/lib/src/models/config/user_preference_config.dart index d3b7ea41..71bccbaa 100644 --- a/lib/src/models/config/user_preference_config.dart +++ b/lib/src/models/config/user_preference_config.dart @@ -1,5 +1,6 @@ import 'package:core/src/models/config/remote_config.dart'; import 'package:core/src/models/user_preferences/user_content_preferences.dart'; +import 'package:core/src/models/saved_filter.dart'; import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; @@ -22,17 +23,8 @@ part 'user_preference_config.g.dart'; /// Countries, up to 5 Sources, and up to 5 Categories). /// - Saved Headlines: The limit applies to the total number of headlines /// a user can save. -/// -/// **Tiered Limits defaults:** -/// - **Guest User:** -/// - Followed Items (Countries, Sources, Categories): 5 each -/// - Saved Headlines: 10 -/// - **Authenticated User:** -/// - Followed Items (Countries, Sources, Categories): 15 each -/// - Saved Headlines: 30 -/// - **Premium User:** -/// - Followed Items (Countries, Sources, Categories): 30 each -/// - Saved Headlines: 100 +/// - Saved Filters: The limit applies to the total number of [SavedFilter] +/// a user can save. /// /// These limits are configurable to balance performance (allowing fetching /// full objects within these limits) and feature differentiation across tiers. @@ -49,6 +41,9 @@ class UserPreferenceConfig extends Equatable { required this.authenticatedSavedHeadlinesLimit, required this.premiumFollowedItemsLimit, required this.premiumSavedHeadlinesLimit, + required this.guestSavedFiltersLimit, + required this.authenticatedSavedFiltersLimit, + required this.premiumSavedFiltersLimit, }); /// Factory method to create a [UserPreferenceConfig] instance from a JSON map. @@ -76,6 +71,15 @@ class UserPreferenceConfig extends Equatable { /// Maximum number of headlines a Premium user can save. final int premiumSavedHeadlinesLimit; + /// Maximum number of filters a Guest user can save. + final int guestSavedFiltersLimit; + + /// Maximum number of filters an Authenticated user can save. + final int authenticatedSavedFiltersLimit; + + /// Maximum number of filters a Premium user can save. + final int premiumSavedFiltersLimit; + /// Converts this [UserPreferenceConfig] instance to a JSON map. Map toJson() => _$UserPreferenceConfigToJson(this); @@ -88,6 +92,9 @@ class UserPreferenceConfig extends Equatable { int? authenticatedSavedHeadlinesLimit, int? premiumFollowedItemsLimit, int? premiumSavedHeadlinesLimit, + int? guestSavedFiltersLimit, + int? authenticatedSavedFiltersLimit, + int? premiumSavedFiltersLimit, }) { return UserPreferenceConfig( guestFollowedItemsLimit: @@ -104,6 +111,12 @@ class UserPreferenceConfig extends Equatable { premiumFollowedItemsLimit ?? this.premiumFollowedItemsLimit, premiumSavedHeadlinesLimit: premiumSavedHeadlinesLimit ?? this.premiumSavedHeadlinesLimit, + guestSavedFiltersLimit: + guestSavedFiltersLimit ?? this.guestSavedFiltersLimit, + authenticatedSavedFiltersLimit: authenticatedSavedFiltersLimit ?? + this.authenticatedSavedFiltersLimit, + premiumSavedFiltersLimit: + premiumSavedFiltersLimit ?? this.premiumSavedFiltersLimit, ); } @@ -115,6 +128,9 @@ class UserPreferenceConfig extends Equatable { authenticatedSavedHeadlinesLimit, premiumFollowedItemsLimit, premiumSavedHeadlinesLimit, + guestSavedFiltersLimit, + authenticatedSavedFiltersLimit, + premiumSavedFiltersLimit, ]; @override diff --git a/lib/src/models/config/user_preference_config.g.dart b/lib/src/models/config/user_preference_config.g.dart index d82e87cf..31520090 100644 --- a/lib/src/models/config/user_preference_config.g.dart +++ b/lib/src/models/config/user_preference_config.g.dart @@ -34,6 +34,18 @@ UserPreferenceConfig _$UserPreferenceConfigFromJson( 'premiumSavedHeadlinesLimit', (v) => (v as num).toInt(), ), + guestSavedFiltersLimit: $checkedConvert( + 'guestSavedFiltersLimit', + (v) => (v as num).toInt(), + ), + authenticatedSavedFiltersLimit: $checkedConvert( + 'authenticatedSavedFiltersLimit', + (v) => (v as num).toInt(), + ), + premiumSavedFiltersLimit: $checkedConvert( + 'premiumSavedFiltersLimit', + (v) => (v as num).toInt(), + ), ); return val; }); @@ -47,4 +59,7 @@ Map _$UserPreferenceConfigToJson( 'authenticatedSavedHeadlinesLimit': instance.authenticatedSavedHeadlinesLimit, 'premiumFollowedItemsLimit': instance.premiumFollowedItemsLimit, 'premiumSavedHeadlinesLimit': instance.premiumSavedHeadlinesLimit, + 'guestSavedFiltersLimit': instance.guestSavedFiltersLimit, + 'authenticatedSavedFiltersLimit': instance.authenticatedSavedFiltersLimit, + 'premiumSavedFiltersLimit': instance.premiumSavedFiltersLimit, }; From c45ee0e9eb9a4e3749266b9da6b1111ba1dfe267 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:23:03 +0100 Subject: [PATCH 05/20] feat(user preferences): add savedFilters to UserContentPreferences model - Add SavedFilter import to user_content_preferences.dart - Include savedFilters in UserContentPreferences properties - Update fromJson and toJson methods to handle savedFilters - Modify copyWith method to include savedFilters --- .../models/user_preferences/user_content_preferences.dart | 8 ++++++++ .../user_preferences/user_content_preferences.g.dart | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/lib/src/models/user_preferences/user_content_preferences.dart b/lib/src/models/user_preferences/user_content_preferences.dart index 2d252e6b..88d79b3d 100644 --- a/lib/src/models/user_preferences/user_content_preferences.dart +++ b/lib/src/models/user_preferences/user_content_preferences.dart @@ -1,5 +1,6 @@ import 'package:core/src/models/entities/country.dart'; import 'package:core/src/models/entities/headline.dart'; +import 'package:core/src/models/saved_filter.dart'; import 'package:core/src/models/entities/source.dart'; import 'package:core/src/models/entities/topic.dart'; import 'package:equatable/equatable.dart'; @@ -32,6 +33,7 @@ class UserContentPreferences extends Equatable { required this.followedSources, required this.followedTopics, required this.savedHeadlines, + required this.savedFilters, }); /// Factory method to create a [UserContentPreferences] instance from a JSON map. @@ -53,6 +55,9 @@ class UserContentPreferences extends Equatable { /// List of headlines the user has saved. final List savedHeadlines; + /// List of filter combinations the user has saved. + final List savedFilters; + /// Converts this [UserContentPreferences] instance to a JSON map. Map toJson() => _$UserContentPreferencesToJson(this); @@ -63,6 +68,7 @@ class UserContentPreferences extends Equatable { followedSources, followedTopics, savedHeadlines, + savedFilters, ]; @override @@ -76,6 +82,7 @@ class UserContentPreferences extends Equatable { List? followedSources, List? followedTopics, List? savedHeadlines, + List? savedFilters, }) { return UserContentPreferences( id: id ?? this.id, @@ -83,6 +90,7 @@ class UserContentPreferences extends Equatable { followedSources: followedSources ?? this.followedSources, followedTopics: followedTopics ?? this.followedTopics, savedHeadlines: savedHeadlines ?? this.savedHeadlines, + savedFilters: savedFilters ?? this.savedFilters, ); } } diff --git a/lib/src/models/user_preferences/user_content_preferences.g.dart b/lib/src/models/user_preferences/user_content_preferences.g.dart index e91cd3d7..6e546f89 100644 --- a/lib/src/models/user_preferences/user_content_preferences.g.dart +++ b/lib/src/models/user_preferences/user_content_preferences.g.dart @@ -35,6 +35,12 @@ UserContentPreferences _$UserContentPreferencesFromJson( .map((e) => Headline.fromJson(e as Map)) .toList(), ), + savedFilters: $checkedConvert( + 'savedFilters', + (v) => (v as List) + .map((e) => SavedFilter.fromJson(e as Map)) + .toList(), + ), ); return val; }); @@ -49,4 +55,5 @@ Map _$UserContentPreferencesToJson( 'followedSources': instance.followedSources.map((e) => e.toJson()).toList(), 'followedTopics': instance.followedTopics.map((e) => e.toJson()).toList(), 'savedHeadlines': instance.savedHeadlines.map((e) => e.toJson()).toList(), + 'savedFilters': instance.savedFilters.map((e) => e.toJson()).toList(), }; From 32e18ea101d0fd1a0913b11d9dc50c7c7d381939 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:24:01 +0100 Subject: [PATCH 06/20] test(models): add saved filters to user content preferences tests - Add mock saved filter to test fixtures - Update tests to cover saved filters field in UserContentPreferences - Modify existing tests to include saved filters in round trip and copyWith scenarios --- .../user_content_preferences_test.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/src/models/user_preferences/user_content_preferences_test.dart b/test/src/models/user_preferences/user_content_preferences_test.dart index dc197738..54e270c0 100644 --- a/test/src/models/user_preferences/user_content_preferences_test.dart +++ b/test/src/models/user_preferences/user_content_preferences_test.dart @@ -8,6 +8,7 @@ void main() { final mockSource = sourcesFixturesData.first; final mockTopic = topicsFixturesData.first; final mockHeadline = headlinesFixturesData.first; + final mockSavedFilter = savedFiltersFixturesData.first; // Use the base fixture and copyWith to create a populated version for tests final userContentPreferencesFixture = userContentPreferencesFixturesData @@ -17,6 +18,7 @@ void main() { followedSources: [mockSource], followedTopics: [mockTopic], savedHeadlines: [], + savedFilters: [mockSavedFilter], ); group('constructor', () { @@ -31,6 +33,7 @@ void main() { expect(defaultPreferences.followedSources, isEmpty); expect(defaultPreferences.followedTopics, isEmpty); expect(defaultPreferences.savedHeadlines, isEmpty); + expect(defaultPreferences.savedFilters, isEmpty); }); }); @@ -38,6 +41,7 @@ void main() { test('round trip with all fields populated', () { final preferencesWithSaved = userContentPreferencesFixture.copyWith( savedHeadlines: [mockHeadline], + savedFilters: [mockSavedFilter], ); final json = preferencesWithSaved.toJson(); final result = UserContentPreferences.fromJson(json); @@ -56,9 +60,11 @@ void main() { test('returns a new instance with updated fields', () { final newCountry = countriesFixturesData[1]; final newHeadline = headlinesFixturesData[1]; + final newSavedFilter = savedFiltersFixturesData[1]; final updatedPreferences = userContentPreferencesFixture.copyWith( followedCountries: [newCountry], + savedFilters: [mockSavedFilter, newSavedFilter], savedHeadlines: [mockHeadline, newHeadline], ); @@ -72,6 +78,10 @@ void main() { updatedPreferences.followedTopics, userContentPreferencesFixture.followedTopics, ); + expect(updatedPreferences.savedFilters, [ + mockSavedFilter, + newSavedFilter, + ]); expect(updatedPreferences.savedHeadlines, [mockHeadline, newHeadline]); }); From d735872e30bf2a02d3a0a1992169fd64f357dbba Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:31:35 +0100 Subject: [PATCH 07/20] chore: file rename --- .../{user_app_settings_fixtures.dart => user_app_settings.dart} | 0 ...t_preferences_fixtures.dart => user_content_preferences.dart} | 1 + 2 files changed, 1 insertion(+) rename lib/src/fixtures/{user_app_settings_fixtures.dart => user_app_settings.dart} (100%) rename lib/src/fixtures/{user_content_preferences_fixtures.dart => user_content_preferences.dart} (94%) diff --git a/lib/src/fixtures/user_app_settings_fixtures.dart b/lib/src/fixtures/user_app_settings.dart similarity index 100% rename from lib/src/fixtures/user_app_settings_fixtures.dart rename to lib/src/fixtures/user_app_settings.dart diff --git a/lib/src/fixtures/user_content_preferences_fixtures.dart b/lib/src/fixtures/user_content_preferences.dart similarity index 94% rename from lib/src/fixtures/user_content_preferences_fixtures.dart rename to lib/src/fixtures/user_content_preferences.dart index 152b216e..0a0fdf55 100644 --- a/lib/src/fixtures/user_content_preferences_fixtures.dart +++ b/lib/src/fixtures/user_content_preferences.dart @@ -9,5 +9,6 @@ final List userContentPreferencesFixturesData = [ followedSources: [], followedTopics: [], savedHeadlines: [], + savedFilters: [], ), ]; From 95f48b1839f7ef4347ae251f8745ab2c65ea133f Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:33:22 +0100 Subject: [PATCH 08/20] feat(remote-config): add saved filters limits for different user types - Add guestSavedFiltersLimit, authenticatedSavedFiltersLimit, and premiumSavedFiltersLimit to RemoteConfig fixture - Set values to 3, 10, and 25 respectively to reflect different user capabilities --- lib/src/fixtures/remote_configs.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/fixtures/remote_configs.dart b/lib/src/fixtures/remote_configs.dart index 3b9a2fb6..65361282 100644 --- a/lib/src/fixtures/remote_configs.dart +++ b/lib/src/fixtures/remote_configs.dart @@ -35,6 +35,9 @@ final List remoteConfigsFixturesData = [ authenticatedSavedHeadlinesLimit: 30, premiumFollowedItemsLimit: 30, premiumSavedHeadlinesLimit: 100, + guestSavedFiltersLimit: 3, + authenticatedSavedFiltersLimit: 10, + premiumSavedFiltersLimit: 25, ), adConfig: const AdConfig( enabled: true, From 42d4d8ab225589cf51e4245d6a9cc1f0870b77b8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:33:48 +0100 Subject: [PATCH 09/20] fix(core): update saved_filter import path and const correctness - Update import path for SavedFilter model to user_presets package - Make sources list in kSavedFilterId2 fixture const --- lib/src/fixtures/saved_filter.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/fixtures/saved_filter.dart b/lib/src/fixtures/saved_filter.dart index 20c226f0..c9575cbf 100644 --- a/lib/src/fixtures/saved_filter.dart +++ b/lib/src/fixtures/saved_filter.dart @@ -2,7 +2,7 @@ import 'package:core/src/fixtures/countries.dart'; import 'package:core/src/fixtures/fixture_ids.dart'; import 'package:core/src/fixtures/sources.dart'; import 'package:core/src/fixtures/topics.dart'; -import 'package:core/src/models/saved_filter.dart'; +import 'package:core/src/models/user_presets/saved_filter.dart'; /// A list of predefined saved searches for fixture data. final savedFiltersFixturesData = [ @@ -22,7 +22,7 @@ final savedFiltersFixturesData = [ id: kSavedFilterId2, name: 'US Business News', topics: [topicsFixturesData.firstWhere((t) => t.name == 'Business')], - sources: [], + sources: const [], countries: [ countriesFixturesData.firstWhere((c) => c.name == 'United States'), ], From 7395c4382b20075c60688fd62ef7f6e0cd5058d6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:34:30 +0100 Subject: [PATCH 10/20] refactor(models): create user_presets directory and move saved_filter - Create user_presets directory inside models - Move saved_filter.dart and saved_filter.g.dart to user_presets directory - Update models.dart to export user_presets - Rename user_presets directory inside models - Update saved_filter.dart to fix typos in method names --- lib/src/models/models.dart | 1 + lib/src/models/{ => user_presets}/saved_filter.dart | 4 ++-- lib/src/models/{ => user_presets}/saved_filter.g.dart | 0 lib/src/models/user_presets/user_presets.dart | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) rename lib/src/models/{ => user_presets}/saved_filter.dart (95%) rename lib/src/models/{ => user_presets}/saved_filter.g.dart (100%) create mode 100644 lib/src/models/user_presets/user_presets.dart diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index 15532244..96669d7c 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -8,4 +8,5 @@ export 'local_ads/local_ads.dart'; export 'query/query.dart'; export 'responses/responses.dart'; export 'user_preferences/user_preferences.dart'; +export 'user_presets/user_presets.dart'; export 'user_settings/user_settings.dart'; diff --git a/lib/src/models/saved_filter.dart b/lib/src/models/user_presets/saved_filter.dart similarity index 95% rename from lib/src/models/saved_filter.dart rename to lib/src/models/user_presets/saved_filter.dart index c49404cb..5003cb40 100644 --- a/lib/src/models/saved_filter.dart +++ b/lib/src/models/user_presets/saved_filter.dart @@ -25,7 +25,7 @@ class SavedFilter extends Equatable { /// Factory method to create a [SavedFilter] instance from a JSON map. factory SavedFilter.fromJson(Map json) => - _$SavedfilterFromJson(json); + _$SavedFilterFromJson(json); /// The unique identifier for the saved filter. final String id; @@ -46,7 +46,7 @@ class SavedFilter extends Equatable { final List countries; /// Converts this [SavedFilter] instance to a JSON map. - Map toJson() => _$SavedfilterToJson(this); + Map toJson() => _$SavedFilterToJson(this); @override List get props => [id, name, topics, sources, countries]; diff --git a/lib/src/models/saved_filter.g.dart b/lib/src/models/user_presets/saved_filter.g.dart similarity index 100% rename from lib/src/models/saved_filter.g.dart rename to lib/src/models/user_presets/saved_filter.g.dart diff --git a/lib/src/models/user_presets/user_presets.dart b/lib/src/models/user_presets/user_presets.dart new file mode 100644 index 00000000..939c39f8 --- /dev/null +++ b/lib/src/models/user_presets/user_presets.dart @@ -0,0 +1 @@ +export 'saved_filter.dart'; From f694d1a05ef322ec9d32b054fc89e68ad141dc81 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:34:57 +0100 Subject: [PATCH 11/20] refactor(models): update import paths for saved_filter - Change import path for saved_filter from 'models/saved_filter.dart' to 'models/user_presets/saved_filter.dart' in user_preference_config.dart and user_content_preferences.dart - This refactor ensures consistency in the directory structure for user-related models --- lib/src/models/config/user_preference_config.dart | 6 +++--- .../models/user_preferences/user_content_preferences.dart | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/models/config/user_preference_config.dart b/lib/src/models/config/user_preference_config.dart index 71bccbaa..2654647f 100644 --- a/lib/src/models/config/user_preference_config.dart +++ b/lib/src/models/config/user_preference_config.dart @@ -1,6 +1,6 @@ import 'package:core/src/models/config/remote_config.dart'; import 'package:core/src/models/user_preferences/user_content_preferences.dart'; -import 'package:core/src/models/saved_filter.dart'; +import 'package:core/src/models/user_presets/saved_filter.dart'; import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; @@ -113,8 +113,8 @@ class UserPreferenceConfig extends Equatable { premiumSavedHeadlinesLimit ?? this.premiumSavedHeadlinesLimit, guestSavedFiltersLimit: guestSavedFiltersLimit ?? this.guestSavedFiltersLimit, - authenticatedSavedFiltersLimit: authenticatedSavedFiltersLimit ?? - this.authenticatedSavedFiltersLimit, + authenticatedSavedFiltersLimit: + authenticatedSavedFiltersLimit ?? this.authenticatedSavedFiltersLimit, premiumSavedFiltersLimit: premiumSavedFiltersLimit ?? this.premiumSavedFiltersLimit, ); diff --git a/lib/src/models/user_preferences/user_content_preferences.dart b/lib/src/models/user_preferences/user_content_preferences.dart index 88d79b3d..d93d3b0b 100644 --- a/lib/src/models/user_preferences/user_content_preferences.dart +++ b/lib/src/models/user_preferences/user_content_preferences.dart @@ -1,8 +1,8 @@ import 'package:core/src/models/entities/country.dart'; import 'package:core/src/models/entities/headline.dart'; -import 'package:core/src/models/saved_filter.dart'; import 'package:core/src/models/entities/source.dart'; import 'package:core/src/models/entities/topic.dart'; +import 'package:core/src/models/user_presets/saved_filter.dart'; import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; From f4ccb1f5fa1897df2b1f20be68412059b6ce11dc Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:35:06 +0100 Subject: [PATCH 12/20] fix(fixtures): update fixture file exports - Rename 'user_app_settings_fixtures.dart' to 'user_app_settings.dart' - Rename 'user_content_preferences_fixtures.dart' to 'user_content_preferences.dart' --- lib/src/fixtures/fixtures.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/fixtures/fixtures.dart b/lib/src/fixtures/fixtures.dart index b8afd1bb..0f356bb6 100644 --- a/lib/src/fixtures/fixtures.dart +++ b/lib/src/fixtures/fixtures.dart @@ -8,6 +8,6 @@ export 'remote_configs.dart'; export 'saved_filter.dart'; export 'sources.dart'; export 'topics.dart'; -export 'user_app_settings_fixtures.dart'; -export 'user_content_preferences_fixtures.dart'; +export 'user_app_settings.dart'; +export 'user_content_preferences.dart'; export 'users.dart'; From c55209b30c40795905f0c990f8d2ac0cd173e74f Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:35:20 +0100 Subject: [PATCH 13/20] =?UTF-8?q?test:=20simulate=20server-side=E6=8A=B9?= =?UTF-8?q?=E9=99=A4=20of=20saved=20filters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add code to handle empty 'savedFilters' list - Ignore inference failure on collection literal - Ensure round trip test passes with empty lists --- .../user_preferences/user_content_preferences_test.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/src/models/user_preferences/user_content_preferences_test.dart b/test/src/models/user_preferences/user_content_preferences_test.dart index 54e270c0..8192d9f8 100644 --- a/test/src/models/user_preferences/user_content_preferences_test.dart +++ b/test/src/models/user_preferences/user_content_preferences_test.dart @@ -51,7 +51,10 @@ void main() { test('round trip with empty lists', () { final emptyPreferences = userContentPreferencesFixturesData.first; final json = emptyPreferences.toJson(); - final result = UserContentPreferences.fromJson(json); + final result = UserContentPreferences.fromJson( + // ignore: inference_failure_on_collection_literal + json..['savedFilters'] = [], + ); expect(result, equals(emptyPreferences)); }); }); From 2113118116605430b050828a3c70f15579620862 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:35:34 +0100 Subject: [PATCH 14/20] test(user_presets): add SavedFilter model tests - Add comprehensive tests for SavedFilter constructor, fromJson/toJson, copyWith, and Equatable properties - Cover cases with populated data and empty lists - Verify correct behavior of copyWith method with and without updates - Ensure proper equality checks for instances with same and different properties --- .../user_presets/saved_filter_test.dart | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 test/src/models/user_presets/saved_filter_test.dart diff --git a/test/src/models/user_presets/saved_filter_test.dart b/test/src/models/user_presets/saved_filter_test.dart new file mode 100644 index 00000000..4ce1c49f --- /dev/null +++ b/test/src/models/user_presets/saved_filter_test.dart @@ -0,0 +1,81 @@ +import 'package:core/core.dart'; +import 'package:test/test.dart'; + +void main() { + group('SavedFilter', () { + final mockSavedFilter = savedFiltersFixturesData.first; + + group('constructor', () { + test('returns correct instance', () { + expect(mockSavedFilter, isA()); + }); + }); + + group('fromJson/toJson', () { + test('round trip with all fields populated', () { + final json = mockSavedFilter.toJson(); + final result = SavedFilter.fromJson(json); + expect(result, equals(mockSavedFilter)); + }); + + test('round trip with empty lists', () { + const emptyFilter = SavedFilter( + id: 'filter-empty', + name: 'Empty', + topics: [], + sources: [], + countries: [], + ); + final json = emptyFilter.toJson(); + final result = SavedFilter.fromJson(json); + expect(result, equals(emptyFilter)); + }); + }); + + group('copyWith', () { + test('returns a new instance with updated fields', () { + final newTopic = topicsFixturesData[2]; + final updatedFilter = mockSavedFilter.copyWith( + name: 'New Name', + topics: [newTopic], + ); + + expect(updatedFilter.id, mockSavedFilter.id); + expect(updatedFilter.name, 'New Name'); + expect(updatedFilter.topics, [newTopic]); + expect(updatedFilter.sources, mockSavedFilter.sources); + expect(updatedFilter.countries, mockSavedFilter.countries); + }); + + test( + 'returns a new instance with the same fields if no updates provided', + () { + final copiedFilter = mockSavedFilter.copyWith(); + expect(copiedFilter, mockSavedFilter); + }, + ); + }); + + group('Equatable', () { + test('instances with the same properties are equal', () { + final filter1 = mockSavedFilter.copyWith(); + final filter2 = mockSavedFilter.copyWith(); + expect(filter1, filter2); + }); + + test('instances with different properties are not equal', () { + final filter1 = mockSavedFilter.copyWith(); + final filter2 = mockSavedFilter.copyWith(id: 'filter-x'); + expect(filter1, isNot(equals(filter2))); + }); + + test('instances with different list contents are not equal', () { + final filter1 = mockSavedFilter.copyWith(); + final filter2 = mockSavedFilter.copyWith( + topics: [topicsFixturesData.last], + ); + expect(filter1, isNot(equals(filter2))); + }); + }); + }); +} From 1721512423896b58e5f318d65c453af8390ea4b0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 07:36:07 +0100 Subject: [PATCH 15/20] docs(README): add SavedFilter model description to User Presets section - Include `SavedFilter` model in the User Presets section of README.md - Describe `SavedFilter` as a model for storing user-defined filter combinations --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 651a1c92..59c6ffd2 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ This package provides the critical building blocks for a professional news appli - **`User`, `AppUserRole`, `DashboardUserRole`, `Permission`:** Robust models for user profiles, roles, and permissions, enabling secure and personalized experiences. - **`UserContentPreferences`, `UserAppSettings`:** Detailed models for storing user-specific content preferences (e.g., followed topics, saved headlines) and application settings (e.g., theme, language). +### 💾 User Presets +- **`SavedFilter`:** A model for storing user-defined filter combinations. + ### ⚙️ Application Configuration - **`RemoteConfig`:** A central container for all dynamic application settings, fetched from a remote source. This includes: - **`AdConfig`:** Master configuration for all advertising, now featuring **highly flexible, role-based control** over ad visibility and frequency for feed, article, and interstitial ads. This allows for granular control over monetization strategies per user segment. From e35a29c513d94bb0adc1918f85233b2fc766581d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 08:01:42 +0100 Subject: [PATCH 16/20] build(core): bump version to 1.2.0 - Update pubspec.yaml with new version number --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index c63967e5..68487652 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,6 +2,7 @@ name: core description: Shared utilities and models. publish_to: none repository: https://github.com/flutter-news-app-full-source-code/core +version: 1.2.0 environment: sdk: ^3.9.0 From 5bfa80e4b0369a6b13614d02ef035d5e0dc11174 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 08:02:22 +0100 Subject: [PATCH 17/20] feat(CHANGELOG): add changelog for 1.2.0 release - Add SavedFilter model for user-defined filters - Link SavedFilter to UserContentPreferences - Add saved filter limits to UserPreferenceConfig - Refactor saved models into `user_presets` directory - Update tests and documentation for new features - Add fixture data for SavedFilter and related models --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..7526ecf9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# 1.2.0 - 2024-05-21 + +- feat: add SavedFilter model for storing user-defined filter combinations. +- **BREAKING** feat!: link SavedFilter to UserContentPreferences, making `savedFilters` a required field. +- **BREAKING** feat!: add limits for saved filters to UserPreferenceConfig. +- **BREAKING** refactor!: organize saved models into a new `user_presets` directory. +- test: add unit tests for SavedFilter model. +- test: update tests for UserContentPreferences to include saved filters. +- docs: update README to reflect new User Presets section and model. +- chore: add fixture data for SavedFilter. +- chore: synchronize all related fixtures with recent model updates. \ No newline at end of file From 120e6ad95d022d0b05c42185d3a2904ec55b73f5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 08:23:32 +0100 Subject: [PATCH 18/20] test(user_preferences): remove unnecessary code in test case - Remove unused savedFilters assignment in user_content_preferences_test.dart - Simplify test case for round trip with empty lists --- .../user_preferences/user_content_preferences_test.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/src/models/user_preferences/user_content_preferences_test.dart b/test/src/models/user_preferences/user_content_preferences_test.dart index 8192d9f8..54e270c0 100644 --- a/test/src/models/user_preferences/user_content_preferences_test.dart +++ b/test/src/models/user_preferences/user_content_preferences_test.dart @@ -51,10 +51,7 @@ void main() { test('round trip with empty lists', () { final emptyPreferences = userContentPreferencesFixturesData.first; final json = emptyPreferences.toJson(); - final result = UserContentPreferences.fromJson( - // ignore: inference_failure_on_collection_literal - json..['savedFilters'] = [], - ); + final result = UserContentPreferences.fromJson(json); expect(result, equals(emptyPreferences)); }); }); From cfedeb77bf1e6222ac536424791685621e94dd43 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 08:25:04 +0100 Subject: [PATCH 19/20] refactor: update saved filter fixture IDs comment - Changed the comment for kSavedFilterId1 to "Saved filters Fixture IDs" - This provides a more accurate description of the following constants --- lib/src/fixtures/fixture_ids.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/fixtures/fixture_ids.dart b/lib/src/fixtures/fixture_ids.dart index 8d233eab..5012470d 100644 --- a/lib/src/fixtures/fixture_ids.dart +++ b/lib/src/fixtures/fixture_ids.dart @@ -531,6 +531,6 @@ const kLocalAd4Id = '4563c000a4a4e6e1a8e7f0f4'; const kLocalAd5Id = '5563c000a4a4e6e1a8e7f0f5'; const kLocalAd6Id = '6563c000a4a4e6e1a8e7f0f6'; -/// Unique identifier for the first saved filter fixture. +/// Saved filters Fixture IDs. const String kSavedFilterId1 = 'saved_search_1'; const String kSavedFilterId2 = 'saved_search_2'; From 5f09f9ec79fd2c1ff93c27959c8a9c7cd0386927 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 12 Oct 2025 08:26:38 +0100 Subject: [PATCH 20/20] feat(user_presets): add `@immutable` annotation to `SavedFilter` model - Import `meta` package to use `@immutable` annotation - Add `@immutable` annotation to `SavedFilter` class to indicate that it is intended to be immutable --- lib/src/models/user_presets/saved_filter.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/models/user_presets/saved_filter.dart b/lib/src/models/user_presets/saved_filter.dart index 5003cb40..c6ce8b68 100644 --- a/lib/src/models/user_presets/saved_filter.dart +++ b/lib/src/models/user_presets/saved_filter.dart @@ -3,6 +3,7 @@ import 'package:core/src/models/entities/source.dart'; import 'package:core/src/models/entities/topic.dart'; import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; part 'saved_filter.g.dart'; @@ -12,6 +13,7 @@ part 'saved_filter.g.dart'; /// This model stores a named set of criteria, including topics, sources, and /// countries, allowing users to quickly re-apply complex filters. /// {@endtemplate} +@immutable @JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) class SavedFilter extends Equatable { /// {@macro saved_filter}