Skip to content

Commit 439b81f

Browse files
authored
Merge pull request #85 from flutter-news-app-full-source-code/refactor/ad-config-ui-role-based
Refactor/ad config UI role based
2 parents 5b85ee2 + fdfe26f commit 439b81f

16 files changed

+366
-408
lines changed

lib/app_configuration/widgets/ad_config_form.dart

Lines changed: 3 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import 'package:flutter/material.dart';
33
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart';
44

55
/// {@template ad_config_form}
6-
/// A form widget for configuring ad settings based on user role.
6+
/// A form widget for configuring global ad settings.
77
///
8-
/// This widget uses a [TabBar] to allow selection of an [AppUserRole]
9-
/// and then conditionally renders the relevant input fields for that role.
8+
/// This widget primarily controls the global enable/disable switch for ads.
109
/// {@endtemplate}
1110
class AdConfigForm extends StatefulWidget {
1211
/// {@macro ad_config_form}
@@ -26,96 +25,7 @@ class AdConfigForm extends StatefulWidget {
2625
State<AdConfigForm> createState() => _AdConfigFormState();
2726
}
2827

29-
class _AdConfigFormState extends State<AdConfigForm>
30-
with SingleTickerProviderStateMixin {
31-
late TabController _tabController;
32-
late final Map<AppUserRole, TextEditingController> _adFrequencyControllers;
33-
late final Map<AppUserRole, TextEditingController>
34-
_adPlacementIntervalControllers;
35-
36-
@override
37-
void initState() {
38-
super.initState();
39-
_tabController = TabController(
40-
length: AppUserRole.values.length,
41-
vsync: this,
42-
);
43-
_initializeControllers();
44-
}
45-
46-
@override
47-
void didUpdateWidget(covariant AdConfigForm oldWidget) {
48-
super.didUpdateWidget(oldWidget);
49-
if (widget.remoteConfig.adConfig != oldWidget.remoteConfig.adConfig) {
50-
_updateControllers();
51-
}
52-
}
53-
54-
void _initializeControllers() {
55-
final adConfig = widget.remoteConfig.adConfig;
56-
_adFrequencyControllers = {
57-
for (final role in AppUserRole.values)
58-
role:
59-
TextEditingController(
60-
text: _getAdFrequency(adConfig, role).toString(),
61-
)
62-
..selection = TextSelection.collapsed(
63-
offset: _getAdFrequency(adConfig, role).toString().length,
64-
),
65-
};
66-
_adPlacementIntervalControllers = {
67-
for (final role in AppUserRole.values)
68-
role:
69-
TextEditingController(
70-
text: _getAdPlacementInterval(adConfig, role).toString(),
71-
)
72-
..selection = TextSelection.collapsed(
73-
offset: _getAdPlacementInterval(
74-
adConfig,
75-
role,
76-
).toString().length,
77-
),
78-
};
79-
}
80-
81-
void _updateControllers() {
82-
final adConfig = widget.remoteConfig.adConfig;
83-
for (final role in AppUserRole.values) {
84-
final newFrequencyValue = _getAdFrequency(adConfig, role).toString();
85-
if (_adFrequencyControllers[role]?.text != newFrequencyValue) {
86-
_adFrequencyControllers[role]?.text = newFrequencyValue;
87-
_adFrequencyControllers[role]?.selection = TextSelection.collapsed(
88-
offset: newFrequencyValue.length,
89-
);
90-
}
91-
92-
final newPlacementIntervalValue = _getAdPlacementInterval(
93-
adConfig,
94-
role,
95-
).toString();
96-
if (_adPlacementIntervalControllers[role]?.text !=
97-
newPlacementIntervalValue) {
98-
_adPlacementIntervalControllers[role]?.text = newPlacementIntervalValue;
99-
_adPlacementIntervalControllers[role]?.selection =
100-
TextSelection.collapsed(
101-
offset: newPlacementIntervalValue.length,
102-
);
103-
}
104-
}
105-
}
106-
107-
@override
108-
void dispose() {
109-
_tabController.dispose();
110-
for (final controller in _adFrequencyControllers.values) {
111-
controller.dispose();
112-
}
113-
for (final controller in _adPlacementIntervalControllers.values) {
114-
controller.dispose();
115-
}
116-
super.dispose();
117-
}
118-
28+
class _AdConfigFormState extends State<AdConfigForm> {
11929
@override
12030
Widget build(BuildContext context) {
12131
final adConfig = widget.remoteConfig.adConfig;
@@ -138,38 +48,4 @@ class _AdConfigFormState extends State<AdConfigForm>
13848
],
13949
);
14050
}
141-
142-
int _getAdFrequency(AdConfig config, AppUserRole role) {
143-
switch (role) {
144-
case AppUserRole.guestUser:
145-
return config.feedAdConfiguration.frequencyConfig.guestAdFrequency;
146-
case AppUserRole.standardUser:
147-
return config
148-
.feedAdConfiguration
149-
.frequencyConfig
150-
.authenticatedAdFrequency;
151-
case AppUserRole.premiumUser:
152-
return config.feedAdConfiguration.frequencyConfig.premiumAdFrequency;
153-
}
154-
}
155-
156-
int _getAdPlacementInterval(AdConfig config, AppUserRole role) {
157-
switch (role) {
158-
case AppUserRole.guestUser:
159-
return config
160-
.feedAdConfiguration
161-
.frequencyConfig
162-
.guestAdPlacementInterval;
163-
case AppUserRole.standardUser:
164-
return config
165-
.feedAdConfiguration
166-
.frequencyConfig
167-
.authenticatedAdPlacementInterval;
168-
case AppUserRole.premiumUser:
169-
return config
170-
.feedAdConfiguration
171-
.frequencyConfig
172-
.premiumAdPlacementInterval;
173-
}
174-
}
17551
}

lib/app_configuration/widgets/article_ad_settings_form.dart

Lines changed: 133 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import 'package:core/core.dart';
22
import 'package:flutter/material.dart';
3+
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart';
34
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart';
45
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/banner_ad_shape_l10n.dart';
56
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/in_article_ad_slot_type_l10n.dart';
7+
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart';
68
import 'package:ui_kit/ui_kit.dart';
79

810
/// {@template article_ad_settings_form}
@@ -28,14 +30,28 @@ class ArticleAdSettingsForm extends StatefulWidget {
2830

2931
class _ArticleAdSettingsFormState extends State<ArticleAdSettingsForm>
3032
with SingleTickerProviderStateMixin {
33+
late TabController _tabController;
34+
3135
@override
3236
void initState() {
3337
super.initState();
38+
_tabController = TabController(
39+
length: AppUserRole.values.length,
40+
vsync: this,
41+
);
3442
}
3543

3644
@override
3745
void didUpdateWidget(covariant ArticleAdSettingsForm oldWidget) {
3846
super.didUpdateWidget(oldWidget);
47+
// No specific controller updates needed here as the UI rebuilds based on
48+
// the remoteConfig directly.
49+
}
50+
51+
@override
52+
void dispose() {
53+
_tabController.dispose();
54+
super.dispose();
3955
}
4056

4157
@override
@@ -134,34 +150,129 @@ class _ArticleAdSettingsFormState extends State<ArticleAdSettingsForm>
134150
textAlign: TextAlign.start,
135151
),
136152
const SizedBox(height: AppSpacing.lg),
137-
...articleAdConfig.inArticleAdSlotConfigurations.map(
138-
(slotConfig) => SwitchListTile(
139-
title: Text(slotConfig.slotType.l10n(context)),
140-
value: slotConfig.enabled,
141-
onChanged: (value) {
142-
final updatedSlots = articleAdConfig
143-
.inArticleAdSlotConfigurations
144-
.map(
145-
(e) => e.slotType == slotConfig.slotType
146-
? e.copyWith(enabled: value)
147-
: e,
148-
)
149-
.toList();
150-
widget.onConfigChanged(
151-
widget.remoteConfig.copyWith(
152-
adConfig: adConfig.copyWith(
153-
articleAdConfiguration: articleAdConfig.copyWith(
154-
inArticleAdSlotConfigurations: updatedSlots,
155-
),
153+
Align(
154+
alignment: AlignmentDirectional.centerStart,
155+
child: SizedBox(
156+
height: kTextTabBarHeight,
157+
child: TabBar(
158+
controller: _tabController,
159+
tabAlignment: TabAlignment.start,
160+
isScrollable: true,
161+
tabs: AppUserRole.values
162+
.map((role) => Tab(text: role.l10n(context)))
163+
.toList(),
164+
),
165+
),
166+
),
167+
const SizedBox(height: AppSpacing.lg),
168+
SizedBox(
169+
height: 250,
170+
child: TabBarView(
171+
controller: _tabController,
172+
children: AppUserRole.values
173+
.map(
174+
(role) => _buildRoleSpecificFields(
175+
context,
176+
l10n,
177+
role,
178+
articleAdConfig,
156179
),
157-
),
158-
);
159-
},
180+
)
181+
.toList(),
160182
),
161183
),
162184
],
163185
),
164186
],
165187
);
166188
}
189+
190+
/// Builds role-specific configuration fields for in-article ad slots.
191+
///
192+
/// This widget displays checkboxes for each [InArticleAdSlotType] for a
193+
/// given [AppUserRole], allowing to enable/disable specific ad slots.
194+
Widget _buildRoleSpecificFields(
195+
BuildContext context,
196+
AppLocalizations l10n,
197+
AppUserRole role,
198+
ArticleAdConfiguration config,
199+
) {
200+
final roleSlots = config.visibleTo[role];
201+
202+
return Column(
203+
children: [
204+
SwitchListTile(
205+
title: Text(l10n.enableInArticleAdsForRoleLabel(role.l10n(context))),
206+
value: roleSlots != null,
207+
onChanged: (value) {
208+
final newVisibleTo =
209+
Map<AppUserRole, Map<InArticleAdSlotType, bool>>.from(
210+
config.visibleTo,
211+
);
212+
if (value) {
213+
// Default values when enabling for a role
214+
newVisibleTo[role] = {
215+
InArticleAdSlotType.aboveArticleContinueReadingButton: true,
216+
InArticleAdSlotType.belowArticleContinueReadingButton: true,
217+
};
218+
} else {
219+
newVisibleTo.remove(role);
220+
}
221+
222+
widget.onConfigChanged(
223+
widget.remoteConfig.copyWith(
224+
adConfig: widget.remoteConfig.adConfig.copyWith(
225+
articleAdConfiguration: config.copyWith(
226+
visibleTo: newVisibleTo,
227+
),
228+
),
229+
),
230+
);
231+
},
232+
),
233+
if (roleSlots != null)
234+
Padding(
235+
padding: const EdgeInsets.symmetric(
236+
horizontal: AppSpacing.lg,
237+
vertical: AppSpacing.sm,
238+
),
239+
child: Column(
240+
children: [
241+
// SwitchListTile for each InArticleAdSlotType
242+
for (final slotType in InArticleAdSlotType.values)
243+
CheckboxListTile(
244+
title: Text(slotType.l10n(context)),
245+
value: roleSlots[slotType] ?? false,
246+
onChanged: (value) {
247+
final newRoleSlots = Map<InArticleAdSlotType, bool>.from(
248+
roleSlots,
249+
);
250+
if (value ?? false) {
251+
newRoleSlots[slotType] = true;
252+
} else {
253+
newRoleSlots.remove(slotType);
254+
}
255+
256+
final newVisibleTo =
257+
Map<AppUserRole, Map<InArticleAdSlotType, bool>>.from(
258+
config.visibleTo,
259+
)..[role] = newRoleSlots;
260+
261+
widget.onConfigChanged(
262+
widget.remoteConfig.copyWith(
263+
adConfig: widget.remoteConfig.adConfig.copyWith(
264+
articleAdConfiguration: config.copyWith(
265+
visibleTo: newVisibleTo,
266+
),
267+
),
268+
),
269+
);
270+
},
271+
),
272+
],
273+
),
274+
),
275+
],
276+
);
277+
}
167278
}

0 commit comments

Comments
 (0)