@@ -4,14 +4,15 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuratio
44import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/user_preference_limits_form.dart' ;
55import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart' ;
66import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/feed_decorator_type_l10n.dart' ;
7+ import 'package:flutter/foundation.dart' ;
78import 'package:ui_kit/ui_kit.dart' ;
89
910/// {@template feed_configuration_tab}
1011/// A widget representing the "Feed" tab in the App Configuration page.
1112///
1213/// This tab allows configuration of user content limits and feed decorators.
1314/// {@endtemplate}
14- class FeedConfigurationTab extends StatelessWidget {
15+ class FeedConfigurationTab extends StatefulWidget {
1516 /// {@macro feed_configuration_tab}
1617 const FeedConfigurationTab ({
1718 required this .remoteConfig,
@@ -25,6 +26,22 @@ class FeedConfigurationTab extends StatelessWidget {
2526 /// Callback to notify parent of changes to the [RemoteConfig] .
2627 final ValueChanged <RemoteConfig > onConfigChanged;
2728
29+ @override
30+ State <FeedConfigurationTab > createState () => _FeedConfigurationTabState ();
31+ }
32+
33+ class _FeedConfigurationTabState extends State <FeedConfigurationTab > {
34+ /// Notifier for the index of the currently expanded top-level ExpansionTile.
35+ ///
36+ /// A value of `null` means no tile is expanded.
37+ final ValueNotifier <int ?> _expandedTileIndex = ValueNotifier <int ?>(null );
38+
39+ @override
40+ void dispose () {
41+ _expandedTileIndex.dispose ();
42+ super .dispose ();
43+ }
44+
2845 @override
2946 Widget build (BuildContext context) {
3047 final l10n = AppLocalizationsX (context).l10n;
@@ -33,86 +50,108 @@ class FeedConfigurationTab extends StatelessWidget {
3350 padding: const EdgeInsets .all (AppSpacing .lg),
3451 children: [
3552 // Top-level ExpansionTile for User Content Limits
36- ExpansionTile (
37- title: Text (l10n.userContentLimitsTitle),
38- childrenPadding: const EdgeInsetsDirectional .only (
39- start: AppSpacing .lg, // Adjusted padding for hierarchy
40- top: AppSpacing .md,
41- bottom: AppSpacing .md,
42- ),
43- expandedCrossAxisAlignment: CrossAxisAlignment .start, // Align content to start
44- children: [
45- UserPreferenceLimitsForm (
46- remoteConfig: remoteConfig,
47- onConfigChanged: onConfigChanged,
48- ),
49- ],
53+ ValueListenableBuilder <int ?>(
54+ valueListenable: _expandedTileIndex,
55+ builder: (context, expandedIndex, child) {
56+ const tileIndex = 0 ;
57+ return ExpansionTile (
58+ key: ValueKey ('userContentLimitsTile_$expandedIndex ' ),
59+ title: Text (l10n.userContentLimitsTitle),
60+ childrenPadding: const EdgeInsetsDirectional .only (
61+ start: AppSpacing .lg,
62+ top: AppSpacing .md,
63+ bottom: AppSpacing .md,
64+ ),
65+ expandedCrossAxisAlignment: CrossAxisAlignment .start,
66+ onExpansionChanged: (isExpanded) {
67+ _expandedTileIndex.value = isExpanded ? tileIndex : null ;
68+ },
69+ initiallyExpanded: expandedIndex == tileIndex,
70+ children: [
71+ UserPreferenceLimitsForm (
72+ remoteConfig: widget.remoteConfig,
73+ onConfigChanged: widget.onConfigChanged,
74+ ),
75+ ],
76+ );
77+ },
5078 ),
5179 const SizedBox (height: AppSpacing .lg),
5280 // New Top-level ExpansionTile for Feed Decorators
53- ExpansionTile (
54- title: Text (l10n.feedDecoratorsTitle),
55- childrenPadding: const EdgeInsetsDirectional .only (
56- start: AppSpacing .lg, // Adjusted padding for hierarchy
57- top: AppSpacing .md,
58- bottom: AppSpacing .md,
59- ),
60- expandedCrossAxisAlignment: CrossAxisAlignment .start, // Align content to start
61- children: [
62- Text (
63- l10n.feedDecoratorsDescription,
64- style: Theme .of (context).textTheme.bodySmall? .copyWith (
65- color: Theme .of (context).colorScheme.onSurface.withOpacity (0.7 ),
81+ ValueListenableBuilder <int ?>(
82+ valueListenable: _expandedTileIndex,
83+ builder: (context, expandedIndex, child) {
84+ const tileIndex = 1 ;
85+ return ExpansionTile (
86+ key: ValueKey ('feedDecoratorsTile_$expandedIndex ' ),
87+ title: Text (l10n.feedDecoratorsTitle),
88+ childrenPadding: const EdgeInsetsDirectional .only (
89+ start: AppSpacing .lg,
90+ top: AppSpacing .md,
91+ bottom: AppSpacing .md,
6692 ),
67- ),
68- const SizedBox (height: AppSpacing .lg),
69- // Individual ExpansionTiles for each Feed Decorator, nested
70- for (final decoratorType in FeedDecoratorType .values)
71- Padding (
72- padding: const EdgeInsets .only (bottom: AppSpacing .md),
73- child: ExpansionTile (
74- title: Text (decoratorType.l10n (context)),
75- childrenPadding: const EdgeInsetsDirectional .only (
76- start: AppSpacing .xl, // Further adjusted padding for nested hierarchy
77- top: AppSpacing .md,
78- bottom: AppSpacing .md,
93+ expandedCrossAxisAlignment: CrossAxisAlignment .start,
94+ onExpansionChanged: (isExpanded) {
95+ _expandedTileIndex.value = isExpanded ? tileIndex : null ;
96+ },
97+ initiallyExpanded: expandedIndex == tileIndex,
98+ children: [
99+ Text (
100+ l10n.feedDecoratorsDescription,
101+ style: Theme .of (context).textTheme.bodySmall? .copyWith (
102+ color: Theme .of (context).colorScheme.onSurface.withOpacity (0.7 ),
79103 ),
80- expandedCrossAxisAlignment: CrossAxisAlignment .start, // Align content to start
81- children: [
82- FeedDecoratorForm (
83- decoratorType: decoratorType,
84- remoteConfig: remoteConfig.copyWith (
85- feedDecoratorConfig:
86- Map .from (
87- remoteConfig.feedDecoratorConfig,
88- )..putIfAbsent (
89- decoratorType,
90- () => FeedDecoratorConfig (
91- category:
92- decoratorType ==
93- FeedDecoratorType .suggestedTopics ||
104+ ),
105+ const SizedBox (height: AppSpacing .lg),
106+ // Individual ExpansionTiles for each Feed Decorator, nested
107+ for (final decoratorType in FeedDecoratorType .values)
108+ Padding (
109+ padding: const EdgeInsets .only (bottom: AppSpacing .md),
110+ child: ExpansionTile (
111+ title: Text (decoratorType.l10n (context)),
112+ childrenPadding: const EdgeInsetsDirectional .only (
113+ start: AppSpacing .xl,
114+ top: AppSpacing .md,
115+ bottom: AppSpacing .md,
116+ ),
117+ expandedCrossAxisAlignment: CrossAxisAlignment .start,
118+ children: [
119+ FeedDecoratorForm (
120+ decoratorType: decoratorType,
121+ remoteConfig: widget.remoteConfig.copyWith (
122+ feedDecoratorConfig:
123+ Map .from (
124+ widget.remoteConfig.feedDecoratorConfig,
125+ )..putIfAbsent (
126+ decoratorType,
127+ () => FeedDecoratorConfig (
128+ category:
94129 decoratorType ==
95- FeedDecoratorType .suggestedSources
96- ? FeedDecoratorCategory .contentCollection
97- : FeedDecoratorCategory .callToAction,
98- enabled : false ,
99- visibleTo : const {} ,
100- itemsToDisplay :
101- decoratorType ==
102- FeedDecoratorType .suggestedTopics ||
130+ FeedDecoratorType .suggestedTopics ||
131+ decoratorType ==
132+ FeedDecoratorType .suggestedSources
133+ ? FeedDecoratorCategory .contentCollection
134+ : FeedDecoratorCategory .callToAction ,
135+ enabled : false ,
136+ visibleTo : const {},
137+ itemsToDisplay :
103138 decoratorType ==
104- FeedDecoratorType .suggestedSources
105- ? 0
106- : null ,
107- ),
108- ),
109- ),
110- onConfigChanged: onConfigChanged,
139+ FeedDecoratorType .suggestedTopics ||
140+ decoratorType ==
141+ FeedDecoratorType .suggestedSources
142+ ? 0
143+ : null ,
144+ ),
145+ ),
146+ ),
147+ onConfigChanged: widget.onConfigChanged,
148+ ),
149+ ],
111150 ),
112- ] ,
113- ) ,
114- ),
115- ] ,
151+ ) ,
152+ ] ,
153+ );
154+ } ,
116155 ),
117156 ],
118157 );
0 commit comments