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
1535c84
feat(lib): add SavedFilter model for user-defined filters
fulleni Oct 12, 2025
7a4fc1a
build(json_serializable): generate SavedFilter model serialization
fulleni Oct 12, 2025
5226f4a
feat(core): add saved filter fixtures
fulleni Oct 12, 2025
2000e76
feat(config): add saved filters limits to user preference config
fulleni Oct 12, 2025
c45ee0e
feat(user preferences): add savedFilters to UserContentPreferences model
fulleni Oct 12, 2025
32e18ea
test(models): add saved filters to user content preferences tests
fulleni Oct 12, 2025
d735872
chore: file rename
fulleni Oct 12, 2025
95f48b1
feat(remote-config): add saved filters limits for different user types
fulleni Oct 12, 2025
42d4d8a
fix(core): update saved_filter import path and const correctness
fulleni Oct 12, 2025
7395c43
refactor(models): create user_presets directory and move saved_filter
fulleni Oct 12, 2025
f694d1a
refactor(models): update import paths for saved_filter
fulleni Oct 12, 2025
f4ccb1f
fix(fixtures): update fixture file exports
fulleni Oct 12, 2025
c55209b
test: simulate server-side抹除 of saved filters
fulleni Oct 12, 2025
2113118
test(user_presets): add SavedFilter model tests
fulleni Oct 12, 2025
1721512
docs(README): add SavedFilter model description to User Presets section
fulleni Oct 12, 2025
e35a29c
build(core): bump version to 1.2.0
fulleni Oct 12, 2025
5bfa80e
feat(CHANGELOG): add changelog for 1.2.0 release
fulleni Oct 12, 2025
120e6ad
test(user_preferences): remove unnecessary code in test case
fulleni Oct 12, 2025
cfedeb7
refactor: update saved filter fixture IDs comment
fulleni Oct 12, 2025
5f09f9e
feat(user_presets): add `@immutable` annotation to `SavedFilter` model
fulleni Oct 12, 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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions lib/src/fixtures/fixture_ids.dart
Original file line number Diff line number Diff line change
Expand Up @@ -530,3 +530,7 @@ const kLocalAd3Id = '3563c000a4a4e6e1a8e7f0f3';
const kLocalAd4Id = '4563c000a4a4e6e1a8e7f0f4';
const kLocalAd5Id = '5563c000a4a4e6e1a8e7f0f5';
const kLocalAd6Id = '6563c000a4a4e6e1a8e7f0f6';

/// Saved filters Fixture IDs.
const String kSavedFilterId1 = 'saved_search_1';
const String kSavedFilterId2 = 'saved_search_2';
5 changes: 3 additions & 2 deletions lib/src/fixtures/fixtures.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ 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';
export 'user_content_preferences_fixtures.dart';
export 'user_app_settings.dart';
export 'user_content_preferences.dart';
export 'users.dart';
3 changes: 3 additions & 0 deletions lib/src/fixtures/remote_configs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ final List<RemoteConfig> remoteConfigsFixturesData = [
authenticatedSavedHeadlinesLimit: 30,
premiumFollowedItemsLimit: 30,
premiumSavedHeadlinesLimit: 100,
guestSavedFiltersLimit: 3,
authenticatedSavedFiltersLimit: 10,
premiumSavedFiltersLimit: 25,
),
adConfig: const AdConfig(
enabled: true,
Expand Down
30 changes: 30 additions & 0 deletions lib/src/fixtures/saved_filter.dart
Original file line number Diff line number Diff line change
@@ -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/user_presets/saved_filter.dart';

/// A list of predefined saved searches for fixture data.
final savedFiltersFixturesData = <SavedFilter>[
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: const [],
countries: [
countriesFixturesData.firstWhere((c) => c.name == 'United States'),
],
),
];
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ final List<UserContentPreferences> userContentPreferencesFixturesData = [
followedSources: [],
followedTopics: [],
savedHeadlines: [],
savedFilters: [],
),
];
38 changes: 27 additions & 11 deletions lib/src/models/config/user_preference_config.dart
Original file line number Diff line number Diff line change
@@ -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/user_presets/saved_filter.dart';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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<String, dynamic> toJson() => _$UserPreferenceConfigToJson(this);

Expand All @@ -88,6 +92,9 @@ class UserPreferenceConfig extends Equatable {
int? authenticatedSavedHeadlinesLimit,
int? premiumFollowedItemsLimit,
int? premiumSavedHeadlinesLimit,
int? guestSavedFiltersLimit,
int? authenticatedSavedFiltersLimit,
int? premiumSavedFiltersLimit,
}) {
return UserPreferenceConfig(
guestFollowedItemsLimit:
Expand All @@ -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,
);
}

Expand All @@ -115,6 +128,9 @@ class UserPreferenceConfig extends Equatable {
authenticatedSavedHeadlinesLimit,
premiumFollowedItemsLimit,
premiumSavedHeadlinesLimit,
guestSavedFiltersLimit,
authenticatedSavedFiltersLimit,
premiumSavedFiltersLimit,
];

@override
Expand Down
15 changes: 15 additions & 0 deletions lib/src/models/config/user_preference_config.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/src/models/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
8 changes: 8 additions & 0 deletions lib/src/models/user_preferences/user_content_preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:core/src/models/entities/country.dart';
import 'package:core/src/models/entities/headline.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';
Expand Down Expand Up @@ -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.
Expand All @@ -53,6 +55,9 @@ class UserContentPreferences extends Equatable {
/// List of headlines the user has saved.
final List<Headline> savedHeadlines;

/// List of filter combinations the user has saved.
final List<SavedFilter> savedFilters;

/// Converts this [UserContentPreferences] instance to a JSON map.
Map<String, dynamic> toJson() => _$UserContentPreferencesToJson(this);

Expand All @@ -63,6 +68,7 @@ class UserContentPreferences extends Equatable {
followedSources,
followedTopics,
savedHeadlines,
savedFilters,
];

@override
Expand All @@ -76,13 +82,15 @@ class UserContentPreferences extends Equatable {
List<Source>? followedSources,
List<Topic>? followedTopics,
List<Headline>? savedHeadlines,
List<SavedFilter>? savedFilters,
}) {
return UserContentPreferences(
id: id ?? this.id,
followedCountries: followedCountries ?? this.followedCountries,
followedSources: followedSources ?? this.followedSources,
followedTopics: followedTopics ?? this.followedTopics,
savedHeadlines: savedHeadlines ?? this.savedHeadlines,
savedFilters: savedFilters ?? this.savedFilters,
);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 76 additions & 0 deletions lib/src/models/user_presets/saved_filter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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';
import 'package:meta/meta.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}
@immutable
@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<String, dynamic> 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<Topic> topics;

/// The list of sources to include in the filter.
/// An empty list means no source filter is applied.
final List<Source> sources;

/// The list of countries to include in the filter.
/// An empty list means no country filter is applied.
final List<Country> countries;

/// Converts this [SavedFilter] instance to a JSON map.
Map<String, dynamic> toJson() => _$SavedFilterToJson(this);

@override
List<Object?> 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<Topic>? topics,
List<Source>? sources,
List<Country>? countries,
}) {
return SavedFilter(
id: id ?? this.id,
name: name ?? this.name,
topics: topics ?? this.topics,
sources: sources ?? this.sources,
countries: countries ?? this.countries,
);
}
}
43 changes: 43 additions & 0 deletions lib/src/models/user_presets/saved_filter.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading