Skip to content

Commit 7bc5c90

Browse files
committed
feat(filters): add followed items filter
- Added checkbox for followed items - Fetches and applies user preferences - Disables manual filters when checked
1 parent c0f2405 commit 7bc5c90

File tree

1 file changed

+199
-50
lines changed

1 file changed

+199
-50
lines changed

lib/headlines-feed/view/headlines_filter_page.dart

Lines changed: 199 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,22 @@
44
import 'package:flutter/material.dart';
55
import 'package:flutter_bloc/flutter_bloc.dart';
66
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
79
import 'package:ht_main/headlines-feed/bloc/headlines_feed_bloc.dart';
810
import 'package:ht_main/headlines-feed/models/headline_filter.dart';
911
import 'package:ht_main/l10n/l10n.dart';
1012
import 'package:ht_main/router/routes.dart';
1113
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
1323

1424
// Keys for passing data to/from SourceFilterPage
1525
const String keySelectedSources = 'selectedSources';
@@ -39,34 +49,129 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
3949
/// and are only applied back to the BLoC when the user taps 'Apply'.
4050
late List<Category> _tempSelectedCategories;
4151
late List<Source> _tempSelectedSources;
42-
// State for source filter capsules, to be passed to and from SourceFilterPage
4352
late Set<String> _tempSelectedSourceCountryIsoCodes;
4453
late Set<SourceType> _tempSelectedSourceSourceTypes;
4554

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+
4661
@override
4762
void initState() {
4863
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;
6480
} else {
6581
_tempSelectedCategories = [];
6682
_tempSelectedSources = [];
6783
_tempSelectedSourceCountryIsoCodes = {};
6884
_tempSelectedSourceSourceTypes = {};
6985
}
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+
});
70175
}
71176

72177
/// Builds a [ListTile] representing a filter criterion (e.g., Categories).
@@ -88,6 +193,7 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
88193
// For sources, currentSelection will be a Map
89194
required dynamic currentSelectionData,
90195
required void Function(dynamic)? onResult, // Result can also be a Map
196+
bool enabled = true,
91197
}) {
92198
final l10n = context.l10n;
93199
final allLabel = l10n.headlinesFeedFilterAllLabel;
@@ -101,22 +207,25 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
101207
title: Text(title),
102208
subtitle: Text(subtitle),
103209
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,
113222
);
114223
}
115224

116-
@override
117225
@override
118226
Widget build(BuildContext context) {
119227
final l10n = context.l10n;
228+
final theme = Theme.of(context);
120229

121230
return Scaffold(
122231
appBar: AppBar(
@@ -127,35 +236,34 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
127236
),
128237
title: Text(l10n.headlinesFeedFilterTitle),
129238
actions: [
130-
// Clear Button
131239
IconButton(
132240
icon: const Icon(Icons.clear_all),
133241
tooltip: l10n.headlinesFeedFilterResetButton,
134242
onPressed: () {
135-
// Dispatch clear event immediately and pop
136243
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+
});
139253
context.pop();
140254
},
141255
),
142-
// Apply Button
143256
IconButton(
144257
icon: const Icon(Icons.check),
145258
tooltip: l10n.headlinesFeedFilterApplyButton,
146259
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.
150260
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,
159267
selectedSourceCountryIsoCodes:
160268
_tempSelectedSourceCountryIsoCodes.isNotEmpty
161269
? _tempSelectedSourceCountryIsoCodes
@@ -164,24 +272,65 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
164272
_tempSelectedSourceSourceTypes.isNotEmpty
165273
? _tempSelectedSourceSourceTypes
166274
: null,
275+
isFromFollowedItems:
276+
_useFollowedFilters, // Set the new flag
167277
);
168-
169-
// Add an event to the main HeadlinesFeedBloc to apply the
170-
// newly constructed filter and trigger a data refresh.
171278
context.read<HeadlinesFeedBloc>().add(
172-
HeadlinesFeedFiltersApplied(filter: newFilter),
173-
);
174-
context.pop(); // Close the filter page
279+
HeadlinesFeedFiltersApplied(filter: newFilter),
280+
);
281+
context.pop();
175282
},
176283
),
177284
],
178285
),
179286
body: ListView(
180287
padding: const EdgeInsets.symmetric(vertical: AppSpacing.md),
181288
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(),
182330
_buildFilterTile(
183331
context: context,
184332
title: l10n.headlinesFeedFilterCategoryLabel,
333+
enabled: !_useFollowedFilters && !_isLoadingFollowedFilters,
185334
selectedCount: _tempSelectedCategories.length,
186335
routeName: Routes.feedFilterCategoriesName,
187336
currentSelectionData: _tempSelectedCategories,
@@ -194,6 +343,7 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
194343
_buildFilterTile(
195344
context: context,
196345
title: l10n.headlinesFeedFilterSourceLabel,
346+
enabled: !_useFollowedFilters && !_isLoadingFollowedFilters,
197347
selectedCount: _tempSelectedSources.length,
198348
routeName: Routes.feedFilterSourcesName,
199349
currentSelectionData: {
@@ -214,7 +364,6 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
214364
}
215365
},
216366
),
217-
// _buildFilterTile for eventCountries removed
218367
],
219368
),
220369
);

0 commit comments

Comments
 (0)