diff --git a/lib/headlines-feed/bloc/headlines_feed_bloc.dart b/lib/headlines-feed/bloc/headlines_feed_bloc.dart index a3dc018d..e6e5e3e4 100644 --- a/lib/headlines-feed/bloc/headlines_feed_bloc.dart +++ b/lib/headlines-feed/bloc/headlines_feed_bloc.dart @@ -155,6 +155,7 @@ class HeadlinesFeedBloc extends Bloc { feedItems: processedFeedItems, hasMore: headlineResponse.hasMore, cursor: headlineResponse.cursor, + filter: const HeadlineFilter(), // Ensure filter is reset ), ); diff --git a/lib/headlines-feed/models/headline_filter.dart b/lib/headlines-feed/models/headline_filter.dart index e44d586c..ee286d1e 100644 --- a/lib/headlines-feed/models/headline_filter.dart +++ b/lib/headlines-feed/models/headline_filter.dart @@ -11,6 +11,7 @@ class HeadlineFilter extends Equatable { this.sources, this.selectedSourceCountryIsoCodes, this.selectedSourceSourceTypes, + this.isFromFollowedItems = false, // Added new field with default }); /// The list of selected category filters. @@ -27,13 +28,17 @@ class HeadlineFilter extends Equatable { /// The set of selected source types for source filtering. final Set? selectedSourceSourceTypes; + /// Indicates if this filter was generated from the user's followed items. + final bool isFromFollowedItems; + @override List get props => [ - categories, - sources, - selectedSourceCountryIsoCodes, - selectedSourceSourceTypes, - ]; + categories, + sources, + selectedSourceCountryIsoCodes, + selectedSourceSourceTypes, + isFromFollowedItems, // Added to props + ]; /// Creates a copy of this [HeadlineFilter] with the given fields /// replaced with the new values. @@ -42,6 +47,7 @@ class HeadlineFilter extends Equatable { List? sources, Set? selectedSourceCountryIsoCodes, Set? selectedSourceSourceTypes, + bool? isFromFollowedItems, // Added to copyWith }) { return HeadlineFilter( categories: categories ?? this.categories, @@ -50,6 +56,7 @@ class HeadlineFilter extends Equatable { selectedSourceCountryIsoCodes ?? this.selectedSourceCountryIsoCodes, selectedSourceSourceTypes: selectedSourceSourceTypes ?? this.selectedSourceSourceTypes, + isFromFollowedItems: isFromFollowedItems ?? this.isFromFollowedItems, // Added ); } } diff --git a/lib/headlines-feed/view/headlines_filter_page.dart b/lib/headlines-feed/view/headlines_filter_page.dart index 095d92f1..34008123 100644 --- a/lib/headlines-feed/view/headlines_filter_page.dart +++ b/lib/headlines-feed/view/headlines_filter_page.dart @@ -4,12 +4,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; // Added +import 'package:ht_main/app/bloc/app_bloc.dart'; // Added import 'package:ht_main/headlines-feed/bloc/headlines_feed_bloc.dart'; import 'package:ht_main/headlines-feed/models/headline_filter.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; import 'package:ht_main/shared/constants/constants.dart'; -import 'package:ht_shared/ht_shared.dart' show Category, Source, SourceType; +import 'package:ht_shared/ht_shared.dart' + show + Category, + Source, + SourceType, + UserContentPreferences, + User, + HtHttpException, // Added + NotFoundException; // Added // Keys for passing data to/from SourceFilterPage const String keySelectedSources = 'selectedSources'; @@ -39,34 +49,159 @@ class _HeadlinesFilterPageState extends State { /// and are only applied back to the BLoC when the user taps 'Apply'. late List _tempSelectedCategories; late List _tempSelectedSources; - // State for source filter capsules, to be passed to and from SourceFilterPage late Set _tempSelectedSourceCountryIsoCodes; late Set _tempSelectedSourceSourceTypes; + // New state variables for the "Apply my followed items" feature + bool _useFollowedFilters = false; + bool _isLoadingFollowedFilters = false; + String? _loadFollowedFiltersError; + UserContentPreferences? _currentUserPreferences; + @override void initState() { super.initState(); - // Initialize the temporary selection state based on the currently - // active filters held within the HeadlinesFeedBloc. This ensures that - // when the filter page opens, it reflects the filters already applied. - final currentState = BlocProvider.of(context).state; - if (currentState is HeadlinesFeedLoaded) { - // Create copies of the lists to avoid modifying the BLoC state directly. - _tempSelectedCategories = List.from(currentState.filter.categories ?? []); - _tempSelectedSources = List.from(currentState.filter.sources ?? []); - // Initialize source capsule states from the BLoC's current filter - _tempSelectedSourceCountryIsoCodes = Set.from( - currentState.filter.selectedSourceCountryIsoCodes ?? {}, - ); - _tempSelectedSourceSourceTypes = Set.from( - currentState.filter.selectedSourceSourceTypes ?? {}, - ); + final headlinesFeedState = + BlocProvider.of(context).state; + + bool initialUseFollowedFilters = false; + + if (headlinesFeedState is HeadlinesFeedLoaded) { + final currentFilter = headlinesFeedState.filter; + _tempSelectedCategories = List.from(currentFilter.categories ?? []); + _tempSelectedSources = List.from(currentFilter.sources ?? []); + _tempSelectedSourceCountryIsoCodes = + Set.from(currentFilter.selectedSourceCountryIsoCodes ?? {}); + _tempSelectedSourceSourceTypes = + Set.from(currentFilter.selectedSourceSourceTypes ?? {}); + + // Use the new flag from the filter to set the checkbox state + initialUseFollowedFilters = currentFilter.isFromFollowedItems; } else { _tempSelectedCategories = []; _tempSelectedSources = []; _tempSelectedSourceCountryIsoCodes = {}; _tempSelectedSourceSourceTypes = {}; } + + _useFollowedFilters = initialUseFollowedFilters; + _isLoadingFollowedFilters = false; + _loadFollowedFiltersError = null; + _currentUserPreferences = null; + + // If the checkbox should be initially checked, fetch the followed items + // to ensure the _temp lists are correctly populated with the *latest* + // followed items, and to correctly disable the manual filter tiles. + if (_useFollowedFilters) { + WidgetsBinding.instance.addPostFrameCallback((_) { + // Ensure context is available for l10n and BLoC access + if (mounted) { + _fetchAndApplyFollowedFilters(); + } + }); + } + } + + Future _fetchAndApplyFollowedFilters() async { + setState(() { + _isLoadingFollowedFilters = true; + _loadFollowedFiltersError = null; + }); + + final appState = context.read().state; + final User? currentUser = appState.user; + + if (currentUser == null) { + setState(() { + _isLoadingFollowedFilters = false; + _useFollowedFilters = false; // Uncheck the box + _loadFollowedFiltersError = + context.l10n.mustBeLoggedInToUseFeatureError; + }); + return; + } + + try { + final preferencesRepo = + context.read>(); + final preferences = await preferencesRepo.read( + id: currentUser.id, + userId: currentUser.id, + ); // Assuming read by user ID + + // NEW: Check if followed items are empty + if (preferences.followedCategories.isEmpty && + preferences.followedSources.isEmpty) { + setState(() { + _isLoadingFollowedFilters = false; + _useFollowedFilters = false; // Uncheck the box + _tempSelectedCategories = []; // Ensure lists are cleared + _tempSelectedSources = []; + }); + if (mounted) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(context.l10n.noFollowedItemsForFilterSnackbar), + duration: const Duration(seconds: 3), + ), + ); + } + return; // Exit the function as no filters to apply + } else { + setState(() { + _currentUserPreferences = preferences; + _tempSelectedCategories = List.from(preferences.followedCategories); + _tempSelectedSources = List.from(preferences.followedSources); + // We don't auto-apply source country/type filters from user preferences here + // as the "Apply my followed" checkbox is primarily for categories/sources. + _isLoadingFollowedFilters = false; + }); + } + } on NotFoundException { + setState(() { + _currentUserPreferences = + UserContentPreferences(id: currentUser.id); // Empty prefs + _tempSelectedCategories = []; + _tempSelectedSources = []; + _isLoadingFollowedFilters = false; + _useFollowedFilters = false; // Uncheck as no prefs found (implies no followed) + }); + if (mounted) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(context.l10n.noFollowedItemsForFilterSnackbar), + duration: const Duration(seconds: 3), + ), + ); + } + } on HtHttpException catch (e) { + setState(() { + _isLoadingFollowedFilters = false; + _useFollowedFilters = false; // Uncheck the box + _loadFollowedFiltersError = + e.message; // Or a generic "Failed to load" + }); + } catch (e) { + setState(() { + _isLoadingFollowedFilters = false; + _useFollowedFilters = false; // Uncheck the box + _loadFollowedFiltersError = context.l10n.unknownError; + }); + } + } + + void _clearTemporaryFilters() { + setState(() { + _tempSelectedCategories = []; + _tempSelectedSources = []; + // Keep source country/type filters as they are not part of this quick filter + // _tempSelectedSourceCountryIsoCodes = {}; + // _tempSelectedSourceSourceTypes = {}; + }); } /// Builds a [ListTile] representing a filter criterion (e.g., Categories). @@ -88,6 +223,7 @@ class _HeadlinesFilterPageState extends State { // For sources, currentSelection will be a Map required dynamic currentSelectionData, required void Function(dynamic)? onResult, // Result can also be a Map + bool enabled = true, }) { final l10n = context.l10n; final allLabel = l10n.headlinesFeedFilterAllLabel; @@ -101,22 +237,25 @@ class _HeadlinesFilterPageState extends State { title: Text(title), subtitle: Text(subtitle), trailing: const Icon(Icons.chevron_right), - onTap: () async { - final result = await context.pushNamed( - routeName, - extra: currentSelectionData, // Pass the map or list - ); - if (result != null && onResult != null) { - onResult(result); - } - }, + enabled: enabled, // Use the enabled parameter + onTap: enabled // Only allow tap if enabled + ? () async { + final result = await context.pushNamed( + routeName, + extra: currentSelectionData, // Pass the map or list + ); + if (result != null && onResult != null) { + onResult(result); + } + } + : null, ); } - @override @override Widget build(BuildContext context) { final l10n = context.l10n; + final theme = Theme.of(context); return Scaffold( appBar: AppBar( @@ -127,35 +266,34 @@ class _HeadlinesFilterPageState extends State { ), title: Text(l10n.headlinesFeedFilterTitle), actions: [ - // Clear Button IconButton( icon: const Icon(Icons.clear_all), tooltip: l10n.headlinesFeedFilterResetButton, onPressed: () { - // Dispatch clear event immediately and pop context.read().add( - HeadlinesFeedFiltersCleared(), - ); + HeadlinesFeedFiltersCleared(), + ); + // Also reset local state for the checkbox + setState(() { + _useFollowedFilters = false; + _isLoadingFollowedFilters = false; + _loadFollowedFiltersError = null; + _clearTemporaryFilters(); + }); context.pop(); }, ), - // Apply Button IconButton( icon: const Icon(Icons.check), tooltip: l10n.headlinesFeedFilterApplyButton, onPressed: () { - // When the user confirms their filter choices on this page, - // create a new HeadlineFilter object using the final temporary - // selections gathered from the sub-pages. final newFilter = HeadlineFilter( - categories: - _tempSelectedCategories.isNotEmpty - ? _tempSelectedCategories - : null, - sources: - _tempSelectedSources.isNotEmpty - ? _tempSelectedSources - : null, + categories: _tempSelectedCategories.isNotEmpty + ? _tempSelectedCategories + : null, + sources: _tempSelectedSources.isNotEmpty + ? _tempSelectedSources + : null, selectedSourceCountryIsoCodes: _tempSelectedSourceCountryIsoCodes.isNotEmpty ? _tempSelectedSourceCountryIsoCodes @@ -164,14 +302,13 @@ class _HeadlinesFilterPageState extends State { _tempSelectedSourceSourceTypes.isNotEmpty ? _tempSelectedSourceSourceTypes : null, + isFromFollowedItems: + _useFollowedFilters, // Set the new flag ); - - // Add an event to the main HeadlinesFeedBloc to apply the - // newly constructed filter and trigger a data refresh. context.read().add( - HeadlinesFeedFiltersApplied(filter: newFilter), - ); - context.pop(); // Close the filter page + HeadlinesFeedFiltersApplied(filter: newFilter), + ); + context.pop(); }, ), ], @@ -179,9 +316,51 @@ class _HeadlinesFilterPageState extends State { body: ListView( padding: const EdgeInsets.symmetric(vertical: AppSpacing.md), children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.paddingSmall, // Consistent with ListTiles + ), + child: CheckboxListTile( + title: Text(l10n.headlinesFeedFilterApplyFollowedLabel), + value: _useFollowedFilters, + onChanged: (bool? newValue) { + setState(() { + _useFollowedFilters = newValue ?? false; + if (_useFollowedFilters) { + _fetchAndApplyFollowedFilters(); + } else { + _isLoadingFollowedFilters = false; + _loadFollowedFiltersError = null; + _clearTemporaryFilters(); // Clear auto-applied filters + } + }); + }, + secondary: _isLoadingFollowedFilters + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : null, + controlAffinity: ListTileControlAffinity.leading, + ), + ), + if (_loadFollowedFiltersError != null) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.paddingLarge, + vertical: AppSpacing.sm, + ), + child: Text( + _loadFollowedFiltersError!, + style: TextStyle(color: theme.colorScheme.error), + ), + ), + const Divider(), _buildFilterTile( context: context, title: l10n.headlinesFeedFilterCategoryLabel, + enabled: !_useFollowedFilters && !_isLoadingFollowedFilters, selectedCount: _tempSelectedCategories.length, routeName: Routes.feedFilterCategoriesName, currentSelectionData: _tempSelectedCategories, @@ -194,6 +373,7 @@ class _HeadlinesFilterPageState extends State { _buildFilterTile( context: context, title: l10n.headlinesFeedFilterSourceLabel, + enabled: !_useFollowedFilters && !_isLoadingFollowedFilters, selectedCount: _tempSelectedSources.length, routeName: Routes.feedFilterSourcesName, currentSelectionData: { @@ -214,7 +394,6 @@ class _HeadlinesFilterPageState extends State { } }, ), - // _buildFilterTile for eventCountries removed ], ), ); diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index e661c01a..c7b701c5 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -808,28 +808,40 @@ "@similarHeadlinesEmpty": { "description": "Message shown when no similar headlines are found" }, - "detailsPageTitle": "[AR] Details", + "detailsPageTitle": "التفاصيل", "@detailsPageTitle": { "description": "Title for the category/source details page" }, - "followButtonLabel": "[AR] Follow", + "followButtonLabel": "متابعة", "@followButtonLabel": { "description": "Label for the follow button" }, - "unfollowButtonLabel": "[AR] Unfollow", + "unfollowButtonLabel": "إلغاء المتابعة", "@unfollowButtonLabel": { "description": "Label for the unfollow button" }, - "noHeadlinesFoundMessage": "[AR] No headlines found for this item.", + "noHeadlinesFoundMessage": "لم يتم العثور على عناوين لهذا العنصر.", "@noHeadlinesFoundMessage": { "description": "Message displayed when no headlines are available for a category/source" }, - "failedToLoadMoreHeadlines": "[AR] Failed to load more headlines.", + "failedToLoadMoreHeadlines": "فشل تحميل المزيد من العناوين.", "@failedToLoadMoreHeadlines": { "description": "Error message when loading more headlines fails on details page" }, - "headlinesSectionTitle": "[AR] Headlines", + "headlinesSectionTitle": "العناوين الرئيسية", "@headlinesSectionTitle": { "description": "Title for the headlines section on details page" + }, + "headlinesFeedFilterApplyFollowedLabel": "تطبيق الفئات والمصادر المتابَعة", + "@headlinesFeedFilterApplyFollowedLabel": { + "description": "Label for the checkbox to apply followed items as filters" + }, + "mustBeLoggedInToUseFeatureError": "يجب عليك تسجيل الدخول لاستخدام هذه الميزة.", + "@mustBeLoggedInToUseFeatureError": { + "description": "Error message shown when a logged-in user is required for a feature" + }, + "noFollowedItemsForFilterSnackbar": "أنت لا تتابع أي فئات أو مصادر لتطبيقها كفلتر.", + "@noFollowedItemsForFilterSnackbar": { + "description": "Snackbar message shown when user tries to apply followed filters but has none." } } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6cc4a6f5..8c6e14b1 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -831,5 +831,17 @@ "headlinesSectionTitle": "Headlines", "@headlinesSectionTitle": { "description": "Title for the headlines section on details page" + }, + "headlinesFeedFilterApplyFollowedLabel": "Apply my followed categories & sources", + "@headlinesFeedFilterApplyFollowedLabel": { + "description": "Label for the checkbox to apply followed items as filters" + }, + "mustBeLoggedInToUseFeatureError": "You must be logged in to use this feature.", + "@mustBeLoggedInToUseFeatureError": { + "description": "Error message shown when a logged-in user is required for a feature" + }, + "noFollowedItemsForFilterSnackbar": "You are not following any categories or sources to apply as a filter.", + "@noFollowedItemsForFilterSnackbar": { + "description": "Snackbar message shown when user tries to apply followed filters but has none." } }