4
4
import 'package:flutter/material.dart' ;
5
5
import 'package:flutter_bloc/flutter_bloc.dart' ;
6
6
import 'package:go_router/go_router.dart' ;
7
+ import 'package:ht_data_repository/ht_data_repository.dart' ; // Added
8
+ import 'package:ht_main/app/bloc/app_bloc.dart' ; // Added
7
9
import 'package:ht_main/headlines-feed/bloc/headlines_feed_bloc.dart' ;
8
10
import 'package:ht_main/headlines-feed/models/headline_filter.dart' ;
9
11
import 'package:ht_main/l10n/l10n.dart' ;
10
12
import 'package:ht_main/router/routes.dart' ;
11
13
import 'package:ht_main/shared/constants/constants.dart' ;
12
- import 'package:ht_shared/ht_shared.dart' show Category, Source, SourceType;
14
+ import 'package:ht_shared/ht_shared.dart'
15
+ show
16
+ Category,
17
+ Source,
18
+ SourceType,
19
+ UserContentPreferences,
20
+ User,
21
+ HtHttpException, // Added
22
+ NotFoundException; // Added
13
23
14
24
// Keys for passing data to/from SourceFilterPage
15
25
const String keySelectedSources = 'selectedSources' ;
@@ -39,34 +49,129 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
39
49
/// and are only applied back to the BLoC when the user taps 'Apply'.
40
50
late List <Category > _tempSelectedCategories;
41
51
late List <Source > _tempSelectedSources;
42
- // State for source filter capsules, to be passed to and from SourceFilterPage
43
52
late Set <String > _tempSelectedSourceCountryIsoCodes;
44
53
late Set <SourceType > _tempSelectedSourceSourceTypes;
45
54
55
+ // New state variables for the "Apply my followed items" feature
56
+ bool _useFollowedFilters = false ;
57
+ bool _isLoadingFollowedFilters = false ;
58
+ String ? _loadFollowedFiltersError;
59
+ UserContentPreferences ? _currentUserPreferences;
60
+
46
61
@override
47
62
void initState () {
48
63
super .initState ();
49
- // Initialize the temporary selection state based on the currently
50
- // active filters held within the HeadlinesFeedBloc. This ensures that
51
- // when the filter page opens, it reflects the filters already applied.
52
- final currentState = BlocProvider .of <HeadlinesFeedBloc >(context).state;
53
- if (currentState is HeadlinesFeedLoaded ) {
54
- // Create copies of the lists to avoid modifying the BLoC state directly.
55
- _tempSelectedCategories = List .from (currentState.filter.categories ?? []);
56
- _tempSelectedSources = List .from (currentState.filter.sources ?? []);
57
- // Initialize source capsule states from the BLoC's current filter
58
- _tempSelectedSourceCountryIsoCodes = Set .from (
59
- currentState.filter.selectedSourceCountryIsoCodes ?? {},
60
- );
61
- _tempSelectedSourceSourceTypes = Set .from (
62
- currentState.filter.selectedSourceSourceTypes ?? {},
63
- );
64
+ final headlinesFeedState =
65
+ BlocProvider .of <HeadlinesFeedBloc >(context).state;
66
+
67
+ bool initialUseFollowedFilters = false ;
68
+
69
+ if (headlinesFeedState is HeadlinesFeedLoaded ) {
70
+ final currentFilter = headlinesFeedState.filter;
71
+ _tempSelectedCategories = List .from (currentFilter.categories ?? []);
72
+ _tempSelectedSources = List .from (currentFilter.sources ?? []);
73
+ _tempSelectedSourceCountryIsoCodes =
74
+ Set .from (currentFilter.selectedSourceCountryIsoCodes ?? {});
75
+ _tempSelectedSourceSourceTypes =
76
+ Set .from (currentFilter.selectedSourceSourceTypes ?? {});
77
+
78
+ // Use the new flag from the filter to set the checkbox state
79
+ initialUseFollowedFilters = currentFilter.isFromFollowedItems;
64
80
} else {
65
81
_tempSelectedCategories = [];
66
82
_tempSelectedSources = [];
67
83
_tempSelectedSourceCountryIsoCodes = {};
68
84
_tempSelectedSourceSourceTypes = {};
69
85
}
86
+
87
+ _useFollowedFilters = initialUseFollowedFilters;
88
+ _isLoadingFollowedFilters = false ;
89
+ _loadFollowedFiltersError = null ;
90
+ _currentUserPreferences = null ;
91
+
92
+ // If the checkbox should be initially checked, fetch the followed items
93
+ // to ensure the _temp lists are correctly populated with the *latest*
94
+ // followed items, and to correctly disable the manual filter tiles.
95
+ if (_useFollowedFilters) {
96
+ WidgetsBinding .instance.addPostFrameCallback ((_) {
97
+ // Ensure context is available for l10n and BLoC access
98
+ if (mounted) {
99
+ _fetchAndApplyFollowedFilters ();
100
+ }
101
+ });
102
+ }
103
+ }
104
+
105
+ Future <void > _fetchAndApplyFollowedFilters () async {
106
+ setState (() {
107
+ _isLoadingFollowedFilters = true ;
108
+ _loadFollowedFiltersError = null ;
109
+ });
110
+
111
+ final appState = context.read <AppBloc >().state;
112
+ final User ? currentUser = appState.user;
113
+
114
+ if (currentUser == null ) {
115
+ setState (() {
116
+ _isLoadingFollowedFilters = false ;
117
+ _useFollowedFilters = false ; // Uncheck the box
118
+ _loadFollowedFiltersError =
119
+ context.l10n.mustBeLoggedInToUseFeatureError;
120
+ });
121
+ return ;
122
+ }
123
+
124
+ try {
125
+ final preferencesRepo =
126
+ context.read <HtDataRepository <UserContentPreferences >>();
127
+ final preferences = await preferencesRepo.read (
128
+ id: currentUser.id,
129
+ userId: currentUser.id,
130
+ ); // Assuming read by user ID
131
+
132
+ setState (() {
133
+ _currentUserPreferences = preferences;
134
+ _tempSelectedCategories = List .from (preferences.followedCategories);
135
+ _tempSelectedSources = List .from (preferences.followedSources);
136
+ // We don't auto-apply source country/type filters from user preferences here
137
+ // as the "Apply my followed" checkbox is primarily for categories/sources.
138
+ // If needed, this logic could be expanded.
139
+ _isLoadingFollowedFilters = false ;
140
+ });
141
+ } on NotFoundException {
142
+ setState (() {
143
+ _currentUserPreferences =
144
+ UserContentPreferences (id: currentUser.id); // Empty prefs
145
+ _tempSelectedCategories = [];
146
+ _tempSelectedSources = [];
147
+ _isLoadingFollowedFilters = false ;
148
+ // Optionally, inform user they have no followed items:
149
+ // _loadFollowedFiltersError = context.l10n.noFollowedItemsFound;
150
+ });
151
+ } on HtHttpException catch (e) {
152
+ setState (() {
153
+ _isLoadingFollowedFilters = false ;
154
+ _useFollowedFilters = false ; // Uncheck the box
155
+ _loadFollowedFiltersError =
156
+ e.message; // Or a generic "Failed to load"
157
+ });
158
+ } catch (e) {
159
+ setState (() {
160
+ _isLoadingFollowedFilters = false ;
161
+ _useFollowedFilters = false ; // Uncheck the box
162
+ _loadFollowedFiltersError = context.l10n.unknownError;
163
+ });
164
+ }
165
+ }
166
+
167
+ void _clearTemporaryFilters () {
168
+ setState (() {
169
+ _tempSelectedCategories = [];
170
+ _tempSelectedSources = [];
171
+ // Keep source country/type filters as they are not part of this quick filter
172
+ // _tempSelectedSourceCountryIsoCodes = {};
173
+ // _tempSelectedSourceSourceTypes = {};
174
+ });
70
175
}
71
176
72
177
/// Builds a [ListTile] representing a filter criterion (e.g., Categories).
@@ -88,6 +193,7 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
88
193
// For sources, currentSelection will be a Map
89
194
required dynamic currentSelectionData,
90
195
required void Function (dynamic )? onResult, // Result can also be a Map
196
+ bool enabled = true ,
91
197
}) {
92
198
final l10n = context.l10n;
93
199
final allLabel = l10n.headlinesFeedFilterAllLabel;
@@ -101,22 +207,25 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
101
207
title: Text (title),
102
208
subtitle: Text (subtitle),
103
209
trailing: const Icon (Icons .chevron_right),
104
- onTap: () async {
105
- final result = await context.pushNamed <dynamic >(
106
- routeName,
107
- extra: currentSelectionData, // Pass the map or list
108
- );
109
- if (result != null && onResult != null ) {
110
- onResult (result);
111
- }
112
- },
210
+ enabled: enabled, // Use the enabled parameter
211
+ onTap: enabled // Only allow tap if enabled
212
+ ? () async {
213
+ final result = await context.pushNamed <dynamic >(
214
+ routeName,
215
+ extra: currentSelectionData, // Pass the map or list
216
+ );
217
+ if (result != null && onResult != null ) {
218
+ onResult (result);
219
+ }
220
+ }
221
+ : null ,
113
222
);
114
223
}
115
224
116
- @override
117
225
@override
118
226
Widget build (BuildContext context) {
119
227
final l10n = context.l10n;
228
+ final theme = Theme .of (context);
120
229
121
230
return Scaffold (
122
231
appBar: AppBar (
@@ -127,35 +236,34 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
127
236
),
128
237
title: Text (l10n.headlinesFeedFilterTitle),
129
238
actions: [
130
- // Clear Button
131
239
IconButton (
132
240
icon: const Icon (Icons .clear_all),
133
241
tooltip: l10n.headlinesFeedFilterResetButton,
134
242
onPressed: () {
135
- // Dispatch clear event immediately and pop
136
243
context.read <HeadlinesFeedBloc >().add (
137
- HeadlinesFeedFiltersCleared (),
138
- );
244
+ HeadlinesFeedFiltersCleared (),
245
+ );
246
+ // Also reset local state for the checkbox
247
+ setState (() {
248
+ _useFollowedFilters = false ;
249
+ _isLoadingFollowedFilters = false ;
250
+ _loadFollowedFiltersError = null ;
251
+ _clearTemporaryFilters ();
252
+ });
139
253
context.pop ();
140
254
},
141
255
),
142
- // Apply Button
143
256
IconButton (
144
257
icon: const Icon (Icons .check),
145
258
tooltip: l10n.headlinesFeedFilterApplyButton,
146
259
onPressed: () {
147
- // When the user confirms their filter choices on this page,
148
- // create a new HeadlineFilter object using the final temporary
149
- // selections gathered from the sub-pages.
150
260
final newFilter = HeadlineFilter (
151
- categories:
152
- _tempSelectedCategories.isNotEmpty
153
- ? _tempSelectedCategories
154
- : null ,
155
- sources:
156
- _tempSelectedSources.isNotEmpty
157
- ? _tempSelectedSources
158
- : null ,
261
+ categories: _tempSelectedCategories.isNotEmpty
262
+ ? _tempSelectedCategories
263
+ : null ,
264
+ sources: _tempSelectedSources.isNotEmpty
265
+ ? _tempSelectedSources
266
+ : null ,
159
267
selectedSourceCountryIsoCodes:
160
268
_tempSelectedSourceCountryIsoCodes.isNotEmpty
161
269
? _tempSelectedSourceCountryIsoCodes
@@ -164,24 +272,65 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
164
272
_tempSelectedSourceSourceTypes.isNotEmpty
165
273
? _tempSelectedSourceSourceTypes
166
274
: null ,
275
+ isFromFollowedItems:
276
+ _useFollowedFilters, // Set the new flag
167
277
);
168
-
169
- // Add an event to the main HeadlinesFeedBloc to apply the
170
- // newly constructed filter and trigger a data refresh.
171
278
context.read <HeadlinesFeedBloc >().add (
172
- HeadlinesFeedFiltersApplied (filter: newFilter),
173
- );
174
- context.pop (); // Close the filter page
279
+ HeadlinesFeedFiltersApplied (filter: newFilter),
280
+ );
281
+ context.pop ();
175
282
},
176
283
),
177
284
],
178
285
),
179
286
body: ListView (
180
287
padding: const EdgeInsets .symmetric (vertical: AppSpacing .md),
181
288
children: [
289
+ Padding (
290
+ padding: const EdgeInsets .symmetric (
291
+ horizontal: AppSpacing .paddingSmall, // Consistent with ListTiles
292
+ ),
293
+ child: CheckboxListTile (
294
+ title: Text (l10n.headlinesFeedFilterApplyFollowedLabel),
295
+ value: _useFollowedFilters,
296
+ onChanged: (bool ? newValue) {
297
+ setState (() {
298
+ _useFollowedFilters = newValue ?? false ;
299
+ if (_useFollowedFilters) {
300
+ _fetchAndApplyFollowedFilters ();
301
+ } else {
302
+ _isLoadingFollowedFilters = false ;
303
+ _loadFollowedFiltersError = null ;
304
+ _clearTemporaryFilters (); // Clear auto-applied filters
305
+ }
306
+ });
307
+ },
308
+ secondary: _isLoadingFollowedFilters
309
+ ? const SizedBox (
310
+ width: 24 ,
311
+ height: 24 ,
312
+ child: CircularProgressIndicator (strokeWidth: 2 ),
313
+ )
314
+ : null ,
315
+ controlAffinity: ListTileControlAffinity .leading,
316
+ ),
317
+ ),
318
+ if (_loadFollowedFiltersError != null )
319
+ Padding (
320
+ padding: const EdgeInsets .symmetric (
321
+ horizontal: AppSpacing .paddingLarge,
322
+ vertical: AppSpacing .sm,
323
+ ),
324
+ child: Text (
325
+ _loadFollowedFiltersError! ,
326
+ style: TextStyle (color: theme.colorScheme.error),
327
+ ),
328
+ ),
329
+ const Divider (),
182
330
_buildFilterTile (
183
331
context: context,
184
332
title: l10n.headlinesFeedFilterCategoryLabel,
333
+ enabled: ! _useFollowedFilters && ! _isLoadingFollowedFilters,
185
334
selectedCount: _tempSelectedCategories.length,
186
335
routeName: Routes .feedFilterCategoriesName,
187
336
currentSelectionData: _tempSelectedCategories,
@@ -194,6 +343,7 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
194
343
_buildFilterTile (
195
344
context: context,
196
345
title: l10n.headlinesFeedFilterSourceLabel,
346
+ enabled: ! _useFollowedFilters && ! _isLoadingFollowedFilters,
197
347
selectedCount: _tempSelectedSources.length,
198
348
routeName: Routes .feedFilterSourcesName,
199
349
currentSelectionData: {
@@ -214,7 +364,6 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
214
364
}
215
365
},
216
366
),
217
- // _buildFilterTile for eventCountries removed
218
367
],
219
368
),
220
369
);
0 commit comments