Skip to content

Commit 388cf2d

Browse files
authored
Merge pull request #100 from flutter-news-app-full-source-code/feat-custom-filters-useres-limits-configuration
Feat custom filters useres limits configuration
2 parents a7eac4b + 18aa371 commit 388cf2d

File tree

13 files changed

+317
-50
lines changed

13 files changed

+317
-50
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
##1.0.0
2+
3+
- **BREAKING** feat!: migrated from date based versioning to semantic versioning.

lib/app_configuration/view/tabs/feed_configuration_tab.dart

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:core/core.dart';
22
import 'package:flutter/material.dart';
33
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_decorator_form.dart';
4+
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/saved_headlines_filters_limit_form.dart';
45
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/user_preference_limits_form.dart';
56
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart';
67
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/feed_decorator_type_l10n.dart';
@@ -67,6 +68,15 @@ class _FeedConfigurationTabState extends State<FeedConfigurationTab> {
6768
},
6869
initiallyExpanded: expandedIndex == tileIndex,
6970
children: [
71+
Text(
72+
l10n.userContentLimitsDescription,
73+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
74+
color: Theme.of(
75+
context,
76+
).colorScheme.onSurface.withOpacity(0.7),
77+
),
78+
),
79+
const SizedBox(height: AppSpacing.lg),
7080
UserPreferenceLimitsForm(
7181
remoteConfig: widget.remoteConfig,
7282
onConfigChanged: widget.onConfigChanged,
@@ -76,11 +86,48 @@ class _FeedConfigurationTabState extends State<FeedConfigurationTab> {
7686
},
7787
),
7888
const SizedBox(height: AppSpacing.lg),
79-
// New Top-level ExpansionTile for Feed Decorators
89+
// New Top-level ExpansionTile for User Preset Limits
8090
ValueListenableBuilder<int?>(
8191
valueListenable: _expandedTileIndex,
8292
builder: (context, expandedIndex, child) {
8393
const tileIndex = 1;
94+
return ExpansionTile(
95+
key: ValueKey('savedHeadlinesFilterLimitsTile_$expandedIndex'),
96+
title: Text(l10n.savedHeadlinesFilterLimitsTitle),
97+
childrenPadding: const EdgeInsetsDirectional.only(
98+
start: AppSpacing.lg,
99+
top: AppSpacing.md,
100+
bottom: AppSpacing.md,
101+
),
102+
expandedCrossAxisAlignment: CrossAxisAlignment.start,
103+
onExpansionChanged: (isExpanded) {
104+
_expandedTileIndex.value = isExpanded ? tileIndex : null;
105+
},
106+
initiallyExpanded: expandedIndex == tileIndex,
107+
children: [
108+
Text(
109+
l10n.savedHeadlinesFilterLimitsDescription,
110+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
111+
color: Theme.of(
112+
context,
113+
).colorScheme.onSurface.withOpacity(0.7),
114+
),
115+
),
116+
const SizedBox(height: AppSpacing.lg),
117+
SavedHeadlinesFiltersLimitForm(
118+
remoteConfig: widget.remoteConfig,
119+
onConfigChanged: widget.onConfigChanged,
120+
),
121+
],
122+
);
123+
},
124+
),
125+
const SizedBox(height: AppSpacing.lg),
126+
// New Top-level ExpansionTile for Feed Decorators
127+
ValueListenableBuilder<int?>(
128+
valueListenable: _expandedTileIndex,
129+
builder: (context, expandedIndex, child) {
130+
const tileIndex = 2;
84131
return ExpansionTile(
85132
key: ValueKey('feedDecoratorsTile_$expandedIndex'),
86133
title: Text(l10n.feedDecoratorsTitle),
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import 'package:core/core.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_config_form_fields.dart';
4+
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart';
5+
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart';
6+
import 'package:ui_kit/ui_kit.dart';
7+
8+
/// {@template saved_headlines_filters_limit_form}
9+
/// A form for configuring saved headlines filter limits within the
10+
/// [RemoteConfig].
11+
///
12+
/// This form provides fields to set the maximum number of saved filters
13+
/// for guest, authenticated, and premium users.
14+
/// {@endtemplate}
15+
class SavedHeadlinesFiltersLimitForm extends StatefulWidget {
16+
/// {@macro saved_headlines_filters_limit_form}
17+
const SavedHeadlinesFiltersLimitForm({
18+
required this.remoteConfig,
19+
required this.onConfigChanged,
20+
super.key,
21+
});
22+
23+
/// The current [RemoteConfig] object.
24+
final RemoteConfig remoteConfig;
25+
26+
/// Callback to notify parent of changes to the [RemoteConfig].
27+
final ValueChanged<RemoteConfig> onConfigChanged;
28+
29+
@override
30+
State<SavedHeadlinesFiltersLimitForm> createState() =>
31+
_SavedHeadlinesFiltersLimitFormState();
32+
}
33+
34+
class _SavedHeadlinesFiltersLimitFormState
35+
extends State<SavedHeadlinesFiltersLimitForm>
36+
with SingleTickerProviderStateMixin {
37+
late TabController _tabController;
38+
late final Map<AppUserRole, TextEditingController> _controllers;
39+
40+
@override
41+
void initState() {
42+
super.initState();
43+
_tabController = TabController(
44+
length: AppUserRole.values.length,
45+
vsync: this,
46+
);
47+
_initializeControllers();
48+
}
49+
50+
@override
51+
void didUpdateWidget(covariant SavedHeadlinesFiltersLimitForm oldWidget) {
52+
super.didUpdateWidget(oldWidget);
53+
if (widget.remoteConfig.userPreferenceConfig !=
54+
oldWidget.remoteConfig.userPreferenceConfig) {
55+
_updateControllerValues();
56+
}
57+
}
58+
59+
void _initializeControllers() {
60+
_controllers = {
61+
for (final role in AppUserRole.values)
62+
role: TextEditingController(
63+
text: _getSavedFiltersLimit(
64+
widget.remoteConfig.userPreferenceConfig,
65+
role,
66+
).toString(),
67+
)..selection = TextSelection.collapsed(
68+
offset: _getSavedFiltersLimit(
69+
widget.remoteConfig.userPreferenceConfig,
70+
role,
71+
).toString().length,
72+
),
73+
};
74+
}
75+
76+
void _updateControllerValues() {
77+
for (final role in AppUserRole.values) {
78+
final newLimit = _getSavedFiltersLimit(
79+
widget.remoteConfig.userPreferenceConfig,
80+
role,
81+
).toString();
82+
if (_controllers[role]?.text != newLimit) {
83+
_controllers[role]?.text = newLimit;
84+
_controllers[role]?.selection = TextSelection.collapsed(
85+
offset: newLimit.length,
86+
);
87+
}
88+
}
89+
}
90+
91+
@override
92+
void dispose() {
93+
_tabController.dispose();
94+
for (final controller in _controllers.values) {
95+
controller.dispose();
96+
}
97+
super.dispose();
98+
}
99+
100+
@override
101+
Widget build(BuildContext context) {
102+
final l10n = AppLocalizationsX(context).l10n;
103+
104+
return Column(
105+
crossAxisAlignment: CrossAxisAlignment.start,
106+
children: [
107+
Align(
108+
alignment: AlignmentDirectional.centerStart,
109+
child: SizedBox(
110+
height: kTextTabBarHeight,
111+
child: TabBar(
112+
controller: _tabController,
113+
tabAlignment: TabAlignment.start,
114+
isScrollable: true,
115+
tabs: AppUserRole.values
116+
.map((role) => Tab(text: role.l10n(context)))
117+
.toList(),
118+
),
119+
),
120+
),
121+
const SizedBox(height: AppSpacing.lg),
122+
SizedBox(
123+
height: 120,
124+
child: TabBarView(
125+
controller: _tabController,
126+
children: AppUserRole.values.map((role) {
127+
final config = widget.remoteConfig.userPreferenceConfig;
128+
return AppConfigIntField(
129+
label: l10n.savedHeadlinesLimitLabel,
130+
description: l10n.savedHeadlinesLimitDescription,
131+
value: _getSavedFiltersLimit(config, role),
132+
onChanged: (value) {
133+
widget.onConfigChanged(
134+
widget.remoteConfig.copyWith(
135+
userPreferenceConfig:
136+
_updateSavedFiltersLimit(config, value, role),
137+
),
138+
);
139+
},
140+
controller: _controllers[role],
141+
);
142+
}).toList(),
143+
),
144+
),
145+
],
146+
);
147+
}
148+
149+
/// Retrieves the saved filters limit for a given [AppUserRole].
150+
///
151+
/// This helper method abstracts the logic for accessing the correct limit
152+
/// from the [UserPreferenceConfig] based on the provided [role].
153+
int _getSavedFiltersLimit(UserPreferenceConfig config, AppUserRole role) {
154+
switch (role) {
155+
case AppUserRole.guestUser:
156+
return config.guestSavedFiltersLimit;
157+
case AppUserRole.standardUser:
158+
return config.authenticatedSavedFiltersLimit;
159+
case AppUserRole.premiumUser:
160+
return config.premiumSavedFiltersLimit;
161+
}
162+
}
163+
164+
/// Updates the saved filters limit for a given [AppUserRole].
165+
///
166+
/// This helper method abstracts the logic for updating the correct limit
167+
/// within the [UserPreferenceConfig] based on the provided [role] and [value].
168+
UserPreferenceConfig _updateSavedFiltersLimit(
169+
UserPreferenceConfig config,
170+
int value,
171+
AppUserRole role,
172+
) {
173+
switch (role) {
174+
case AppUserRole.guestUser:
175+
return config.copyWith(guestSavedFiltersLimit: value);
176+
case AppUserRole.standardUser:
177+
return config.copyWith(authenticatedSavedFiltersLimit: value);
178+
case AppUserRole.premiumUser:
179+
return config.copyWith(premiumSavedFiltersLimit: value);
180+
}
181+
}
182+
}

lib/app_configuration/widgets/user_preference_limits_form.dart

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,6 @@ class _UserPreferenceLimitsFormState extends State<UserPreferenceLimitsForm>
142142
return Column(
143143
crossAxisAlignment: CrossAxisAlignment.start,
144144
children: [
145-
Text(
146-
l10n.userContentLimitsDescription,
147-
style: Theme.of(context).textTheme.bodySmall?.copyWith(
148-
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
149-
),
150-
),
151-
const SizedBox(height: AppSpacing.lg),
152145
Align(
153146
alignment: AlignmentDirectional.centerStart,
154147
child: SizedBox(

lib/l10n/app_localizations.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,6 +1592,18 @@ abstract class AppLocalizations {
15921592
/// **'How often an ad can appear for this user role (e.g., a value of 5 means an ad could be placed after every 5 news items).'**
15931593
String get adFrequencyDescription;
15941594

1595+
/// Description for the Saved Headlines Filter Limits section
1596+
///
1597+
/// In en, this message translates to:
1598+
/// **'Set limits on the number of saved headlines filters for each user tier.'**
1599+
String get savedHeadlinesFilterLimitsDescription;
1600+
1601+
/// Title for the Saved Headlines Filter Limits section
1602+
///
1603+
/// In en, this message translates to:
1604+
/// **'Saved Headlines Filter Limits'**
1605+
String get savedHeadlinesFilterLimitsTitle;
1606+
15951607
/// Label for Ad Placement Interval
15961608
///
15971609
/// In en, this message translates to:

lib/l10n/app_localizations_ar.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,13 @@ class AppLocalizationsAr extends AppLocalizations {
839839
String get adFrequencyDescription =>
840840
'عدد مرات ظهور الإعلان لهذا الدور المستخدم (على سبيل المثال، قيمة 5 تعني أنه يمكن وضع إعلان بعد كل 5 عناصر إخبارية).';
841841

842+
@override
843+
String get savedHeadlinesFilterLimitsDescription =>
844+
'الحد الأقصى لعدد مرشحات العناوين المحفوظة التي يمكن لهذا الدور إنشاؤها.';
845+
846+
@override
847+
String get savedHeadlinesFilterLimitsTitle => 'حدود مرشحات العناوين المحفوظة';
848+
842849
@override
843850
String get adPlacementIntervalLabel => 'فترة وضع الإعلان';
844851

lib/l10n/app_localizations_en.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,13 @@ class AppLocalizationsEn extends AppLocalizations {
837837
String get adFrequencyDescription =>
838838
'How often an ad can appear for this user role (e.g., a value of 5 means an ad could be placed after every 5 news items).';
839839

840+
@override
841+
String get savedHeadlinesFilterLimitsDescription =>
842+
'Set limits on the number of saved headlines filters for each user tier.';
843+
844+
@override
845+
String get savedHeadlinesFilterLimitsTitle => 'Saved Headlines Filter Limits';
846+
840847
@override
841848
String get adPlacementIntervalLabel => 'Ad Placement Interval';
842849

lib/l10n/arb/app_ar.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,14 @@
10381038
"@adFrequencyDescription": {
10391039
"description": "وصف تكرار الإعلان"
10401040
},
1041+
"savedHeadlinesFilterLimitsTitle": "حدود مرشحات العناوين المحفوظة",
1042+
"@savedHeadlinesFilterLimitsTitle": {
1043+
"description": "وصف لحد المرشحات المحفوظة"
1044+
},
1045+
"savedHeadlinesFilterLimitsDescription": "الحد الأقصى لعدد مرشحات العناوين المحفوظة التي يمكن لهذا الدور إنشاؤها.",
1046+
"@savedHeadlinesFilterLimitsDescription": {
1047+
"description": "وصف لحد المرشحات المحفوظة"
1048+
},
10411049
"adPlacementIntervalLabel": "فترة وضع الإعلان",
10421050
"@adPlacementIntervalLabel": {
10431051
"description": "تسمية فترة وضع الإعلان"

lib/l10n/arb/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,14 @@
10381038
"@adFrequencyDescription": {
10391039
"description": "Description for Ad Frequency"
10401040
},
1041+
"savedHeadlinesFilterLimitsDescription": "Set limits on the number of saved headlines filters for each user tier.",
1042+
"@savedHeadlinesFilterLimitsDescription": {
1043+
"description": "Description for the Saved Headlines Filter Limits section"
1044+
},
1045+
"savedHeadlinesFilterLimitsTitle": "Saved Headlines Filter Limits",
1046+
"@savedHeadlinesFilterLimitsTitle": {
1047+
"description": "Title for the Saved Headlines Filter Limits section"
1048+
},
10411049
"adPlacementIntervalLabel": "Ad Placement Interval",
10421050
"@adPlacementIntervalLabel": {
10431051
"description": "Label for Ad Placement Interval"

lib/local_ads_management/view/local_ads_management_page.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_manage
88
import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/native_ads_page.dart';
99
import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/view/video_ads_page.dart';
1010
import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart';
11-
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart';
1211
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart';
1312
import 'package:go_router/go_router.dart';
1413
import 'package:ui_kit/ui_kit.dart';

0 commit comments

Comments
 (0)