From 7cecf97ad5e6dfc9bfeabe572274f86b22653664 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:05:39 +0100 Subject: [PATCH 01/15] refactor(sources): add all available sources to state - Added allAvailableSources property - Holds all available sources data --- lib/headlines-feed/bloc/sources_filter_state.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/headlines-feed/bloc/sources_filter_state.dart b/lib/headlines-feed/bloc/sources_filter_state.dart index 22402cd5..86a1c034 100644 --- a/lib/headlines-feed/bloc/sources_filter_state.dart +++ b/lib/headlines-feed/bloc/sources_filter_state.dart @@ -10,6 +10,7 @@ class SourcesFilterState extends Equatable { this.selectedCountryIsoCodes = const {}, this.availableSourceTypes = SourceType.values, this.selectedSourceTypes = const {}, + this.allAvailableSources = const [], // Added new property this.displayableSources = const [], this.finallySelectedSourceIds = const {}, this.dataLoadingStatus = SourceFilterDataLoadingStatus.initial, @@ -20,6 +21,7 @@ class SourcesFilterState extends Equatable { final Set selectedCountryIsoCodes; final List availableSourceTypes; final Set selectedSourceTypes; + final List allAvailableSources; // Added new property final List displayableSources; final Set finallySelectedSourceIds; final SourceFilterDataLoadingStatus dataLoadingStatus; @@ -30,6 +32,7 @@ class SourcesFilterState extends Equatable { Set? selectedCountryIsoCodes, List? availableSourceTypes, Set? selectedSourceTypes, + List? allAvailableSources, // Added new property List? displayableSources, Set? finallySelectedSourceIds, SourceFilterDataLoadingStatus? dataLoadingStatus, @@ -42,6 +45,7 @@ class SourcesFilterState extends Equatable { selectedCountryIsoCodes ?? this.selectedCountryIsoCodes, availableSourceTypes: availableSourceTypes ?? this.availableSourceTypes, selectedSourceTypes: selectedSourceTypes ?? this.selectedSourceTypes, + allAvailableSources: allAvailableSources ?? this.allAvailableSources, // Added displayableSources: displayableSources ?? this.displayableSources, finallySelectedSourceIds: finallySelectedSourceIds ?? this.finallySelectedSourceIds, @@ -57,6 +61,7 @@ class SourcesFilterState extends Equatable { selectedCountryIsoCodes, availableSourceTypes, selectedSourceTypes, + allAvailableSources, // Added new property displayableSources, finallySelectedSourceIds, dataLoadingStatus, From 12ec7a72c1b6d5cc2227b77ea87bbb17d59ee2a6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:05:47 +0100 Subject: [PATCH 02/15] feat(feed): add country/type filter persistence - Persist country ISO codes - Persist source types --- lib/headlines-feed/bloc/sources_filter_event.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/headlines-feed/bloc/sources_filter_event.dart b/lib/headlines-feed/bloc/sources_filter_event.dart index f41451a3..8e137391 100644 --- a/lib/headlines-feed/bloc/sources_filter_event.dart +++ b/lib/headlines-feed/bloc/sources_filter_event.dart @@ -10,12 +10,22 @@ abstract class SourcesFilterEvent extends Equatable { } class LoadSourceFilterData extends SourcesFilterEvent { - const LoadSourceFilterData({this.initialSelectedSources = const []}); + const LoadSourceFilterData({ + this.initialSelectedSources = const [], + this.initialSelectedCountryIsoCodes = const {}, + this.initialSelectedSourceTypes = const {}, + }); final List initialSelectedSources; + final Set initialSelectedCountryIsoCodes; + final Set initialSelectedSourceTypes; @override - List get props => [initialSelectedSources]; + List get props => [ + initialSelectedSources, + initialSelectedCountryIsoCodes, + initialSelectedSourceTypes, + ]; } class CountryCapsuleToggled extends SourcesFilterEvent { From d5036dd16c786f91dea5df718dbd1bafa87c09bc Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:05:55 +0100 Subject: [PATCH 03/15] refactor(sources): Filter sources locally - Remove _FetchFilteredSourcesRequested - Filter in bloc using helper method - Use initial capsule selections --- .../bloc/sources_filter_bloc.dart | 162 +++++++++--------- 1 file changed, 82 insertions(+), 80 deletions(-) diff --git a/lib/headlines-feed/bloc/sources_filter_bloc.dart b/lib/headlines-feed/bloc/sources_filter_bloc.dart index 703aab30..eb8347c4 100644 --- a/lib/headlines-feed/bloc/sources_filter_bloc.dart +++ b/lib/headlines-feed/bloc/sources_filter_bloc.dart @@ -18,11 +18,11 @@ class SourcesFilterBloc extends Bloc { super(const SourcesFilterState()) { on(_onLoadSourceFilterData); on(_onCountryCapsuleToggled); - on(_onAllSourceTypesCapsuleToggled); // Added + on(_onAllSourceTypesCapsuleToggled); on(_onSourceTypeCapsuleToggled); on(_onSourceCheckboxToggled); on(_onClearSourceFiltersRequested); - on<_FetchFilteredSourcesRequested>(_onFetchFilteredSourcesRequested); + // Removed _FetchFilteredSourcesRequested event listener } final HtDataRepository _sourcesRepository; @@ -40,32 +40,35 @@ class SourcesFilterBloc extends Bloc { final initialSelectedSourceIds = event.initialSelectedSources.map((s) => s.id).toSet(); - // Initialize selected capsules based on initialSelectedSources - final initialSelectedCountryIsoCodes = {}; - final initialSelectedSourceTypes = {}; - - if (event.initialSelectedSources.isNotEmpty) { - for (final source in event.initialSelectedSources) { - if (source.headquarters?.isoCode != null) { - initialSelectedCountryIsoCodes.add(source.headquarters!.isoCode); - } - if (source.sourceType != null) { - initialSelectedSourceTypes.add(source.sourceType!); - } - } - } + // Use the passed-in initial capsule selections directly + final initialSelectedCountryIsoCodes = + event.initialSelectedCountryIsoCodes; + final initialSelectedSourceTypes = event.initialSelectedSourceTypes; + + final allSourcesResponse = await _sourcesRepository.readAll(); + final allAvailableSources = allSourcesResponse.items; + + // Initially, display all sources. Capsules are visually set but don't filter the list yet. + // Filtering will occur if a capsule is manually toggled. + // However, if initial capsule filters ARE provided, we should respect them for the initial display. + final displayableSources = _getFilteredSources( + allSources: allAvailableSources, + selectedCountries: initialSelectedCountryIsoCodes, // Use event's data + selectedTypes: initialSelectedSourceTypes, // Use event's data + ); emit( state.copyWith( availableCountries: availableCountries.items, + allAvailableSources: allAvailableSources, + displayableSources: displayableSources, // Now correctly filtered if initial capsules were set finallySelectedSourceIds: initialSelectedSourceIds, - selectedCountryIsoCodes: initialSelectedCountryIsoCodes, - selectedSourceTypes: initialSelectedSourceTypes, - // Keep loading status until sources are fetched + selectedCountryIsoCodes: initialSelectedCountryIsoCodes, // Use event's data + selectedSourceTypes: initialSelectedSourceTypes, // Use event's data + dataLoadingStatus: SourceFilterDataLoadingStatus.success, + clearErrorMessage: true, ), ); - // Trigger initial fetch of displayable sources - add(const _FetchFilteredSourcesRequested()); } catch (e) { emit( state.copyWith( @@ -95,34 +98,57 @@ class SourcesFilterBloc extends Bloc { currentSelected.add(event.countryIsoCode); } } - emit(state.copyWith(selectedCountryIsoCodes: currentSelected)); - add(const _FetchFilteredSourcesRequested()); + final newDisplayableSources = _getFilteredSources( + allSources: state.allAvailableSources, + selectedCountries: currentSelected, + selectedTypes: state.selectedSourceTypes, + ); + emit( + state.copyWith( + selectedCountryIsoCodes: currentSelected, + displayableSources: newDisplayableSources, + ), + ); } - Future _onAllSourceTypesCapsuleToggled( + void _onAllSourceTypesCapsuleToggled( AllSourceTypesCapsuleToggled event, Emitter emit, - ) async { - // Toggling "All" for source types means clearing any specific selections. - // If already clear, it remains clear. - emit(state.copyWith(selectedSourceTypes: {})); - add(const _FetchFilteredSourcesRequested()); + ) { + final newDisplayableSources = _getFilteredSources( + allSources: state.allAvailableSources, + selectedCountries: state.selectedCountryIsoCodes, + selectedTypes: {}, // Cleared source types + ); + emit( + state.copyWith( + selectedSourceTypes: {}, + displayableSources: newDisplayableSources, + ), + ); } - Future _onSourceTypeCapsuleToggled( + void _onSourceTypeCapsuleToggled( SourceTypeCapsuleToggled event, Emitter emit, - ) async { + ) { final currentSelected = Set.from(state.selectedSourceTypes); if (currentSelected.contains(event.sourceType)) { currentSelected.remove(event.sourceType); } else { currentSelected.add(event.sourceType); } - // If specific types are selected, "All" is no longer true. - // The UI will derive "All" state from selectedSourceTypes.isEmpty - emit(state.copyWith(selectedSourceTypes: currentSelected)); - add(const _FetchFilteredSourcesRequested()); + final newDisplayableSources = _getFilteredSources( + allSources: state.allAvailableSources, + selectedCountries: state.selectedCountryIsoCodes, + selectedTypes: currentSelected, + ); + emit( + state.copyWith( + selectedSourceTypes: currentSelected, + displayableSources: newDisplayableSources, + ), + ); } void _onSourceCheckboxToggled( @@ -147,55 +173,31 @@ class SourcesFilterBloc extends Bloc { selectedCountryIsoCodes: {}, selectedSourceTypes: {}, finallySelectedSourceIds: {}, - // Keep availableCountries and availableSourceTypes - ), - ); - add(const _FetchFilteredSourcesRequested()); - } - - Future _onFetchFilteredSourcesRequested( - _FetchFilteredSourcesRequested event, - Emitter emit, - ) async { - emit( - state.copyWith( - dataLoadingStatus: SourceFilterDataLoadingStatus.loading, - displayableSources: [], // Clear previous sources + displayableSources: List.from(state.allAvailableSources), // Reset + dataLoadingStatus: SourceFilterDataLoadingStatus.success, clearErrorMessage: true, ), ); - try { - final queryParameters = {}; - if (state.selectedCountryIsoCodes.isNotEmpty) { - queryParameters['countries'] = state.selectedCountryIsoCodes.join(','); - } - if (state.selectedSourceTypes.isNotEmpty) { - queryParameters['sourceTypes'] = state.selectedSourceTypes - .map((st) => st.name) - .join(','); - } + } - final response = await _sourcesRepository.readAllByQuery(queryParameters); - emit( - state.copyWith( - displayableSources: response.items, - dataLoadingStatus: SourceFilterDataLoadingStatus.success, - ), - ); - } on HtHttpException catch (e) { - emit( - state.copyWith( - dataLoadingStatus: SourceFilterDataLoadingStatus.failure, - errorMessage: e.message, - ), - ); - } catch (e) { - emit( - state.copyWith( - dataLoadingStatus: SourceFilterDataLoadingStatus.failure, - errorMessage: 'An unexpected error occurred while fetching sources.', - ), - ); + // Helper method to filter sources based on selected countries and types + List _getFilteredSources({ + required List allSources, + required Set selectedCountries, + required Set selectedTypes, + }) { + if (selectedCountries.isEmpty && selectedTypes.isEmpty) { + return List.from(allSources); // Return all if no filters } + + return allSources.where((source) { + final matchesCountry = selectedCountries.isEmpty || + (source.headquarters != null && + selectedCountries.contains(source.headquarters!.isoCode)); + final matchesType = selectedTypes.isEmpty || + (source.sourceType != null && + selectedTypes.contains(source.sourceType!)); + return matchesCountry && matchesType; + }).toList(); } } From 617a61b27a494e198e5fa7d54765678d894e374c Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:06:04 +0100 Subject: [PATCH 04/15] feat(router): Pass initial filter data to page - Pass data via state.extra - Simplify SourceFilterPage init --- lib/router/router.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/router/router.dart b/lib/router/router.dart index 4c85defe..223de38d 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -400,7 +400,21 @@ GoRouter createRouter({ context .read>(), ), - child: const SourceFilterPage(), + // Pass initialSelectedSources, country ISO codes, and source types from state.extra + child: Builder( + builder: (context) { + final extraData = state.extra as Map? ?? const {}; + final initialSources = extraData[keySelectedSources] as List? ?? const []; + final initialCountryIsoCodes = extraData[keySelectedCountryIsoCodes] as Set? ?? const {}; + final initialSourceTypes = extraData[keySelectedSourceTypes] as Set? ?? const {}; + + return SourceFilterPage( + initialSelectedSources: initialSources, + initialSelectedCountryIsoCodes: initialCountryIsoCodes, + initialSelectedSourceTypes: initialSourceTypes, + ); + }, + ), ), ), // Sub-route for country selection REMOVED From e665de4caaec1364558440b092bf68b30875f5a2 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:06:11 +0100 Subject: [PATCH 05/15] feat(filter): support complex source filters - Pass map to source filter - Handle map result from source filter --- .../view/headlines_filter_page.dart | 71 +++++++++++-------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/lib/headlines-feed/view/headlines_filter_page.dart b/lib/headlines-feed/view/headlines_filter_page.dart index 43d2dc4d..64f2340e 100644 --- a/lib/headlines-feed/view/headlines_filter_page.dart +++ b/lib/headlines-feed/view/headlines_filter_page.dart @@ -1,5 +1,5 @@ // -// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -10,7 +10,12 @@ 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; // Import models from ht_shared, Country removed + show Category, Source, SourceType; + +// Keys for passing data to/from SourceFilterPage +const String keySelectedSources = 'selectedSources'; +const String keySelectedCountryIsoCodes = 'selectedCountryIsoCodes'; +const String keySelectedSourceTypes = 'selectedSourceTypes'; /// {@template headlines_filter_page} /// A full-screen dialog page for selecting headline filters. @@ -35,7 +40,9 @@ class _HeadlinesFilterPageState extends State { /// and are only applied back to the BLoC when the user taps 'Apply'. late List _tempSelectedCategories; late List _tempSelectedSources; - // late List _tempSelectedCountries; // Removed + // State for source filter capsules, to be passed to and from SourceFilterPage + late Set _tempSelectedSourceCountryIsoCodes; + late Set _tempSelectedSourceSourceTypes; @override void initState() { @@ -48,15 +55,18 @@ class _HeadlinesFilterPageState extends State { // Create copies of the lists to avoid modifying the BLoC state directly. _tempSelectedCategories = List.from(currentState.filter.categories ?? []); _tempSelectedSources = List.from(currentState.filter.sources ?? []); - // _tempSelectedCountries = List.from( - // currentState.filter.eventCountries ?? [], - // ); // Removed + // Initialize source capsule states. These are persisted here. + // For now, let's assume they start empty and are populated by SourceFilterPage's result. + // Or, if HeadlineFilter could store them, we'd get them from there. + // For this iteration, they start empty. SourceFilterPage will manage its own capsule + // state and return it. + _tempSelectedSourceCountryIsoCodes = {}; + _tempSelectedSourceSourceTypes = {}; } else { - // Default to empty lists if the feed isn't loaded yet. This might happen - // if the filter page is accessed before the initial feed load completes. _tempSelectedCategories = []; _tempSelectedSources = []; - // _tempSelectedCountries = []; // Removed + _tempSelectedSourceCountryIsoCodes = {}; + _tempSelectedSourceSourceTypes = {}; } } @@ -76,8 +86,9 @@ class _HeadlinesFilterPageState extends State { required String title, required int selectedCount, required String routeName, - required List currentSelection, // Pass current temp selection - required void Function(List?) onResult, + // For sources, currentSelection will be a Map + required dynamic currentSelectionData, + required void Function(dynamic)? onResult, // Result can also be a Map }) { final l10n = context.l10n; final allLabel = l10n.headlinesFeedFilterAllLabel; @@ -92,21 +103,12 @@ class _HeadlinesFilterPageState extends State { subtitle: Text(subtitle), trailing: const Icon(Icons.chevron_right), onTap: () async { - // Navigate to the specific filter selection page (e.g., SourceFilterPage). - // Use pushNamed to wait for a result when the page is popped. - final result = await context.pushNamed>( - routeName, // The route name for the specific filter page. - // Pass the current temporary selection for this filter type - // (e.g., _tempSelectedSources) to the next page. This allows - // the next page to initialize its UI reflecting the current state. - extra: currentSelection, + final result = await context.pushNamed( + routeName, + extra: currentSelectionData, // Pass the map or list ); - // When the filter selection page pops (usually via its 'Apply' button), - // it returns the potentially modified list of selected items. - // If the result is not null (meaning the user didn't just cancel/go back), - // update the corresponding temporary state list on *this* page. - if (result != null) { - onResult(result); // Calls setState to update the UI here. + if (result != null && onResult != null) { + onResult(result); } }, ); @@ -175,7 +177,7 @@ class _HeadlinesFilterPageState extends State { title: l10n.headlinesFeedFilterCategoryLabel, selectedCount: _tempSelectedCategories.length, routeName: Routes.feedFilterCategoriesName, - currentSelection: _tempSelectedCategories, + currentSelectionData: _tempSelectedCategories, onResult: (result) { if (result is List) { setState(() => _tempSelectedCategories = result); @@ -187,10 +189,21 @@ class _HeadlinesFilterPageState extends State { title: l10n.headlinesFeedFilterSourceLabel, selectedCount: _tempSelectedSources.length, routeName: Routes.feedFilterSourcesName, - currentSelection: _tempSelectedSources, + currentSelectionData: { + keySelectedSources: _tempSelectedSources, + keySelectedCountryIsoCodes: _tempSelectedSourceCountryIsoCodes, + keySelectedSourceTypes: _tempSelectedSourceSourceTypes, + }, onResult: (result) { - if (result is List) { - setState(() => _tempSelectedSources = result); + if (result is Map) { + setState(() { + _tempSelectedSources = + result[keySelectedSources] as List? ?? []; + _tempSelectedSourceCountryIsoCodes = + result[keySelectedCountryIsoCodes] as Set? ?? {}; + _tempSelectedSourceSourceTypes = + result[keySelectedSourceTypes] as Set? ?? {}; + }); } }, ), From ae723df9b032407cbb739f463444d5fef829523b Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:06:19 +0100 Subject: [PATCH 06/15] feat(feed): source filter preserves state - Pass and return full filter state - Retry reloads all sources/countries --- .../view/source_filter_page.dart | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/headlines-feed/view/source_filter_page.dart b/lib/headlines-feed/view/source_filter_page.dart index 170e3b30..3f5e8584 100644 --- a/lib/headlines-feed/view/source_filter_page.dart +++ b/lib/headlines-feed/view/source_filter_page.dart @@ -4,16 +4,30 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_main/headlines-feed/bloc/sources_filter_bloc.dart'; +import 'package:ht_main/headlines-feed/view/headlines_filter_page.dart' + show keySelectedCountryIsoCodes, keySelectedSourceTypes, keySelectedSources; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/shared/constants/app_spacing.dart'; import 'package:ht_main/shared/widgets/failure_state_widget.dart'; import 'package:ht_main/shared/widgets/loading_state_widget.dart'; -import 'package:ht_shared/ht_shared.dart' show Country, Source; +import 'package:ht_shared/ht_shared.dart' show Country, Source, SourceType; + +// Keys are defined in headlines_filter_page.dart and imported by router.dart +// const String keySelectedSources = 'selectedSources'; // REMOVED +// const String keySelectedCountryIsoCodes = 'selectedCountryIsoCodes'; // REMOVED +// const String keySelectedSourceTypes = 'selectedSourceTypes'; // REMOVED class SourceFilterPage extends StatelessWidget { - const SourceFilterPage({super.key, this.initialSelectedSources = const []}); + const SourceFilterPage({ + super.key, + this.initialSelectedSources = const [], + this.initialSelectedCountryIsoCodes = const {}, + this.initialSelectedSourceTypes = const {}, + }); final List initialSelectedSources; + final Set initialSelectedCountryIsoCodes; + final Set initialSelectedSourceTypes; @override Widget build(BuildContext context) { @@ -25,6 +39,8 @@ class SourceFilterPage extends StatelessWidget { )..add( LoadSourceFilterData( initialSelectedSources: initialSelectedSources, + initialSelectedCountryIsoCodes: initialSelectedCountryIsoCodes, + initialSelectedSourceTypes: initialSelectedSourceTypes, ), ), child: const _SourceFilterView(), @@ -63,7 +79,12 @@ class _SourceFilterView extends StatelessWidget { (s) => state.finallySelectedSourceIds.contains(s.id), ) .toList(); - Navigator.of(context).pop(selectedSources); + // Pop with a map containing all relevant filter state + Navigator.of(context).pop({ + keySelectedSources: selectedSources, + keySelectedCountryIsoCodes: state.selectedCountryIsoCodes, + keySelectedSourceTypes: state.selectedSourceTypes, + }); }, ), ], @@ -91,10 +112,15 @@ class _SourceFilterView extends StatelessWidget { message: state.errorMessage ?? l10n.headlinesFeedFilterErrorCriteria, onRetry: () { context.read().add( - LoadSourceFilterData( - initialSelectedSources: - ModalRoute.of(context)?.settings.arguments as List? ?? - const [], + // When retrying, we don't have initial capsule states from arguments + // So, we pass empty sets, BLoC will load all sources and countries. + // User can then re-apply capsule filters if needed. + // Or, we could try to persist/retrieve the last known good capsule state. + // For now, simple retry reloads all. + const LoadSourceFilterData( + initialSelectedSources: [], // Or pass current selections if needed + initialSelectedCountryIsoCodes: {}, + initialSelectedSourceTypes: {}, ), ); }, From c521661b9c99724668bcf74382df215fb779ff64 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:11:32 +0100 Subject: [PATCH 07/15] feat(feed): reset filter on refresh - Clear filter on refresh event --- lib/headlines-feed/bloc/headlines_feed_bloc.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/headlines-feed/bloc/headlines_feed_bloc.dart b/lib/headlines-feed/bloc/headlines_feed_bloc.dart index d07c6faa..73ce4318 100644 --- a/lib/headlines-feed/bloc/headlines_feed_bloc.dart +++ b/lib/headlines-feed/bloc/headlines_feed_bloc.dart @@ -110,6 +110,7 @@ class HeadlinesFeedBloc extends Bloc { headlines: response.items, hasMore: response.hasMore, cursor: response.cursor, + filter: const HeadlineFilter(), // Clear all filter aspects ), ); } on HtHttpException catch (e) { From 6f5645cf00b235050e61fde012c00a9c861fddd6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:11:40 +0100 Subject: [PATCH 08/15] feat(feed): add source type/country filtering - Added country ISO code filtering - Added source type filtering - Updated HeadlineFilter model --- .../models/headline_filter.dart | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/headlines-feed/models/headline_filter.dart b/lib/headlines-feed/models/headline_filter.dart index 6b617875..118acc28 100644 --- a/lib/headlines-feed/models/headline_filter.dart +++ b/lib/headlines-feed/models/headline_filter.dart @@ -6,7 +6,12 @@ import 'package:ht_shared/ht_shared.dart'; /// {@endtemplate} class HeadlineFilter extends Equatable { /// {@macro headline_filter} - const HeadlineFilter({this.categories, this.sources}); + const HeadlineFilter({ + this.categories, + this.sources, + this.selectedSourceCountryIsoCodes, + this.selectedSourceSourceTypes, + }); /// The list of selected category filters. /// Headlines matching *any* of these categories will be included (OR logic). @@ -16,15 +21,35 @@ class HeadlineFilter extends Equatable { /// Headlines matching *any* of these sources will be included (OR logic). final List? sources; + /// The set of selected country ISO codes for source filtering. + final Set? selectedSourceCountryIsoCodes; + + /// The set of selected source types for source filtering. + final Set? selectedSourceSourceTypes; + @override - List get props => [categories, sources]; + List get props => [ + categories, + sources, + selectedSourceCountryIsoCodes, + selectedSourceSourceTypes, + ]; /// Creates a copy of this [HeadlineFilter] with the given fields /// replaced with the new values. - HeadlineFilter copyWith({List? categories, List? sources}) { + HeadlineFilter copyWith({ + List? categories, + List? sources, + Set? selectedSourceCountryIsoCodes, + Set? selectedSourceSourceTypes, + }) { return HeadlineFilter( categories: categories ?? this.categories, sources: sources ?? this.sources, + selectedSourceCountryIsoCodes: selectedSourceCountryIsoCodes ?? + this.selectedSourceCountryIsoCodes, + selectedSourceSourceTypes: + selectedSourceSourceTypes ?? this.selectedSourceSourceTypes, ); } } From 9fe3eb48fe1eb3529f119018141ae68e550d3493 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:11:46 +0100 Subject: [PATCH 09/15] feat(filter): persist source filter selections - Initialize source filters from BLoC state - Pass filter states to HeadlinesFeedBloc --- .../view/headlines_filter_page.dart | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/headlines-feed/view/headlines_filter_page.dart b/lib/headlines-feed/view/headlines_filter_page.dart index 64f2340e..8449ce01 100644 --- a/lib/headlines-feed/view/headlines_filter_page.dart +++ b/lib/headlines-feed/view/headlines_filter_page.dart @@ -55,13 +55,11 @@ class _HeadlinesFilterPageState extends State { // 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. These are persisted here. - // For now, let's assume they start empty and are populated by SourceFilterPage's result. - // Or, if HeadlineFilter could store them, we'd get them from there. - // For this iteration, they start empty. SourceFilterPage will manage its own capsule - // state and return it. - _tempSelectedSourceCountryIsoCodes = {}; - _tempSelectedSourceSourceTypes = {}; + // Initialize source capsule states from the BLoC's current filter + _tempSelectedSourceCountryIsoCodes = + Set.from(currentState.filter.selectedSourceCountryIsoCodes ?? {}); + _tempSelectedSourceSourceTypes = + Set.from(currentState.filter.selectedSourceSourceTypes ?? {}); } else { _tempSelectedCategories = []; _tempSelectedSources = []; @@ -152,11 +150,19 @@ class _HeadlinesFilterPageState extends State { categories: _tempSelectedCategories.isNotEmpty ? _tempSelectedCategories - : null, // Use null if empty + : null, sources: _tempSelectedSources.isNotEmpty ? _tempSelectedSources - : null, // Use null if empty + : null, + selectedSourceCountryIsoCodes: + _tempSelectedSourceCountryIsoCodes.isNotEmpty + ? _tempSelectedSourceCountryIsoCodes + : null, + selectedSourceSourceTypes: + _tempSelectedSourceSourceTypes.isNotEmpty + ? _tempSelectedSourceSourceTypes + : null, ); // Add an event to the main HeadlinesFeedBloc to apply the From 6f7338e329e6a3e765fbed5c102bdc9496616a4f Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:12:28 +0100 Subject: [PATCH 10/15] style: misc --- .../bloc/sources_filter_bloc.dart | 12 ++++++--- .../bloc/sources_filter_event.dart | 8 +++--- .../bloc/sources_filter_state.dart | 3 ++- .../models/headline_filter.dart | 14 +++++------ .../view/headlines_filter_page.dart | 13 +++++----- .../view/source_filter_page.dart | 3 ++- lib/router/router.dart | 25 ++++++++++++++----- 7 files changed, 49 insertions(+), 29 deletions(-) diff --git a/lib/headlines-feed/bloc/sources_filter_bloc.dart b/lib/headlines-feed/bloc/sources_filter_bloc.dart index eb8347c4..f4ae1e50 100644 --- a/lib/headlines-feed/bloc/sources_filter_bloc.dart +++ b/lib/headlines-feed/bloc/sources_filter_bloc.dart @@ -61,9 +61,11 @@ class SourcesFilterBloc extends Bloc { state.copyWith( availableCountries: availableCountries.items, allAvailableSources: allAvailableSources, - displayableSources: displayableSources, // Now correctly filtered if initial capsules were set + displayableSources: + displayableSources, // Now correctly filtered if initial capsules were set finallySelectedSourceIds: initialSelectedSourceIds, - selectedCountryIsoCodes: initialSelectedCountryIsoCodes, // Use event's data + selectedCountryIsoCodes: + initialSelectedCountryIsoCodes, // Use event's data selectedSourceTypes: initialSelectedSourceTypes, // Use event's data dataLoadingStatus: SourceFilterDataLoadingStatus.success, clearErrorMessage: true, @@ -191,10 +193,12 @@ class SourcesFilterBloc extends Bloc { } return allSources.where((source) { - final matchesCountry = selectedCountries.isEmpty || + final matchesCountry = + selectedCountries.isEmpty || (source.headquarters != null && selectedCountries.contains(source.headquarters!.isoCode)); - final matchesType = selectedTypes.isEmpty || + final matchesType = + selectedTypes.isEmpty || (source.sourceType != null && selectedTypes.contains(source.sourceType!)); return matchesCountry && matchesType; diff --git a/lib/headlines-feed/bloc/sources_filter_event.dart b/lib/headlines-feed/bloc/sources_filter_event.dart index 8e137391..e5ff1220 100644 --- a/lib/headlines-feed/bloc/sources_filter_event.dart +++ b/lib/headlines-feed/bloc/sources_filter_event.dart @@ -22,10 +22,10 @@ class LoadSourceFilterData extends SourcesFilterEvent { @override List get props => [ - initialSelectedSources, - initialSelectedCountryIsoCodes, - initialSelectedSourceTypes, - ]; + initialSelectedSources, + initialSelectedCountryIsoCodes, + initialSelectedSourceTypes, + ]; } class CountryCapsuleToggled extends SourcesFilterEvent { diff --git a/lib/headlines-feed/bloc/sources_filter_state.dart b/lib/headlines-feed/bloc/sources_filter_state.dart index 86a1c034..818b10da 100644 --- a/lib/headlines-feed/bloc/sources_filter_state.dart +++ b/lib/headlines-feed/bloc/sources_filter_state.dart @@ -45,7 +45,8 @@ class SourcesFilterState extends Equatable { selectedCountryIsoCodes ?? this.selectedCountryIsoCodes, availableSourceTypes: availableSourceTypes ?? this.availableSourceTypes, selectedSourceTypes: selectedSourceTypes ?? this.selectedSourceTypes, - allAvailableSources: allAvailableSources ?? this.allAvailableSources, // Added + allAvailableSources: + allAvailableSources ?? this.allAvailableSources, // Added displayableSources: displayableSources ?? this.displayableSources, finallySelectedSourceIds: finallySelectedSourceIds ?? this.finallySelectedSourceIds, diff --git a/lib/headlines-feed/models/headline_filter.dart b/lib/headlines-feed/models/headline_filter.dart index 118acc28..e44d586c 100644 --- a/lib/headlines-feed/models/headline_filter.dart +++ b/lib/headlines-feed/models/headline_filter.dart @@ -29,11 +29,11 @@ class HeadlineFilter extends Equatable { @override List get props => [ - categories, - sources, - selectedSourceCountryIsoCodes, - selectedSourceSourceTypes, - ]; + categories, + sources, + selectedSourceCountryIsoCodes, + selectedSourceSourceTypes, + ]; /// Creates a copy of this [HeadlineFilter] with the given fields /// replaced with the new values. @@ -46,8 +46,8 @@ class HeadlineFilter extends Equatable { return HeadlineFilter( categories: categories ?? this.categories, sources: sources ?? this.sources, - selectedSourceCountryIsoCodes: selectedSourceCountryIsoCodes ?? - this.selectedSourceCountryIsoCodes, + selectedSourceCountryIsoCodes: + selectedSourceCountryIsoCodes ?? this.selectedSourceCountryIsoCodes, selectedSourceSourceTypes: selectedSourceSourceTypes ?? this.selectedSourceSourceTypes, ); diff --git a/lib/headlines-feed/view/headlines_filter_page.dart b/lib/headlines-feed/view/headlines_filter_page.dart index 8449ce01..095d92f1 100644 --- a/lib/headlines-feed/view/headlines_filter_page.dart +++ b/lib/headlines-feed/view/headlines_filter_page.dart @@ -9,8 +9,7 @@ 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; // Keys for passing data to/from SourceFilterPage const String keySelectedSources = 'selectedSources'; @@ -56,10 +55,12 @@ class _HeadlinesFilterPageState extends State { _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 ?? {}); + _tempSelectedSourceCountryIsoCodes = Set.from( + currentState.filter.selectedSourceCountryIsoCodes ?? {}, + ); + _tempSelectedSourceSourceTypes = Set.from( + currentState.filter.selectedSourceSourceTypes ?? {}, + ); } else { _tempSelectedCategories = []; _tempSelectedSources = []; diff --git a/lib/headlines-feed/view/source_filter_page.dart b/lib/headlines-feed/view/source_filter_page.dart index 3f5e8584..ee93e7ab 100644 --- a/lib/headlines-feed/view/source_filter_page.dart +++ b/lib/headlines-feed/view/source_filter_page.dart @@ -118,7 +118,8 @@ class _SourceFilterView extends StatelessWidget { // Or, we could try to persist/retrieve the last known good capsule state. // For now, simple retry reloads all. const LoadSourceFilterData( - initialSelectedSources: [], // Or pass current selections if needed + initialSelectedSources: + [], // Or pass current selections if needed initialSelectedCountryIsoCodes: {}, initialSelectedSourceTypes: {}, ), diff --git a/lib/router/router.dart b/lib/router/router.dart index 223de38d..92473519 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -403,15 +403,28 @@ GoRouter createRouter({ // Pass initialSelectedSources, country ISO codes, and source types from state.extra child: Builder( builder: (context) { - final extraData = state.extra as Map? ?? const {}; - final initialSources = extraData[keySelectedSources] as List? ?? const []; - final initialCountryIsoCodes = extraData[keySelectedCountryIsoCodes] as Set? ?? const {}; - final initialSourceTypes = extraData[keySelectedSourceTypes] as Set? ?? const {}; + final extraData = + state.extra as Map? ?? + const {}; + final initialSources = + extraData[keySelectedSources] + as List? ?? + const []; + final initialCountryIsoCodes = + extraData[keySelectedCountryIsoCodes] + as Set? ?? + const {}; + final initialSourceTypes = + extraData[keySelectedSourceTypes] + as Set? ?? + const {}; return SourceFilterPage( initialSelectedSources: initialSources, - initialSelectedCountryIsoCodes: initialCountryIsoCodes, - initialSelectedSourceTypes: initialSourceTypes, + initialSelectedCountryIsoCodes: + initialCountryIsoCodes, + initialSelectedSourceTypes: + initialSourceTypes, ); }, ), From d8f3f5608e856bd46816a43c38ba050238a58562 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:12:42 +0100 Subject: [PATCH 11/15] style: misc --- lib/headlines-feed/bloc/headlines_feed_bloc.dart | 5 ++--- lib/headlines-feed/bloc/sources_filter_bloc.dart | 2 +- lib/headlines-feed/view/source_filter_page.dart | 3 --- lib/headlines-search/bloc/headlines_search_bloc.dart | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/headlines-feed/bloc/headlines_feed_bloc.dart b/lib/headlines-feed/bloc/headlines_feed_bloc.dart index 73ce4318..a961604e 100644 --- a/lib/headlines-feed/bloc/headlines_feed_bloc.dart +++ b/lib/headlines-feed/bloc/headlines_feed_bloc.dart @@ -73,7 +73,7 @@ class HeadlinesFeedBloc extends Bloc { .whereType() .map((s) => s.id) .toList(), - }, limit: _headlinesFetchLimit); + }, limit: _headlinesFetchLimit,); emit( HeadlinesFeedLoaded( headlines: response.items, @@ -110,7 +110,6 @@ class HeadlinesFeedBloc extends Bloc { headlines: response.items, hasMore: response.hasMore, cursor: response.cursor, - filter: const HeadlineFilter(), // Clear all filter aspects ), ); } on HtHttpException catch (e) { @@ -237,7 +236,7 @@ class HeadlinesFeedBloc extends Bloc { .whereType() .map((s) => s.id) .toList(), - }, limit: _headlinesFetchLimit); + }, limit: _headlinesFetchLimit,); emit( HeadlinesFeedLoaded( headlines: response.items, // Replace headlines on refresh diff --git a/lib/headlines-feed/bloc/sources_filter_bloc.dart b/lib/headlines-feed/bloc/sources_filter_bloc.dart index f4ae1e50..048a98bf 100644 --- a/lib/headlines-feed/bloc/sources_filter_bloc.dart +++ b/lib/headlines-feed/bloc/sources_filter_bloc.dart @@ -200,7 +200,7 @@ class SourcesFilterBloc extends Bloc { final matchesType = selectedTypes.isEmpty || (source.sourceType != null && - selectedTypes.contains(source.sourceType!)); + selectedTypes.contains(source.sourceType)); return matchesCountry && matchesType; }).toList(); } diff --git a/lib/headlines-feed/view/source_filter_page.dart b/lib/headlines-feed/view/source_filter_page.dart index ee93e7ab..0c857c7a 100644 --- a/lib/headlines-feed/view/source_filter_page.dart +++ b/lib/headlines-feed/view/source_filter_page.dart @@ -118,10 +118,7 @@ class _SourceFilterView extends StatelessWidget { // Or, we could try to persist/retrieve the last known good capsule state. // For now, simple retry reloads all. const LoadSourceFilterData( - initialSelectedSources: - [], // Or pass current selections if needed initialSelectedCountryIsoCodes: {}, - initialSelectedSourceTypes: {}, ), ); }, diff --git a/lib/headlines-search/bloc/headlines_search_bloc.dart b/lib/headlines-search/bloc/headlines_search_bloc.dart index 23a5e37f..14494a6c 100644 --- a/lib/headlines-search/bloc/headlines_search_bloc.dart +++ b/lib/headlines-search/bloc/headlines_search_bloc.dart @@ -82,7 +82,7 @@ class HeadlinesSearchBloc try { final response = await _headlinesRepository.readAllByQuery({ 'query': event.searchTerm, - }, limit: _limit); + }, limit: _limit,); emit( HeadlinesSearchSuccess( headlines: response.items, From c94d7f00919fcd141978da92a6fa3832be27ce18 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:30:50 +0100 Subject: [PATCH 12/15] fix: The HeadlinesFeedBloc has been updated to correctly format multiple selected category IDs and source IDs as comma-separated strings when making API requests. This aligns with the backend's expectation and should resolve the bug where new filter selections were overriding previous ones instead of being combined. Filtering by multiple categories and/or multiple sources simultaneously should now work as intended. --- .../bloc/headlines_feed_bloc.dart | 93 ++++++++++--------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/lib/headlines-feed/bloc/headlines_feed_bloc.dart b/lib/headlines-feed/bloc/headlines_feed_bloc.dart index a961604e..fe08dde4 100644 --- a/lib/headlines-feed/bloc/headlines_feed_bloc.dart +++ b/lib/headlines-feed/bloc/headlines_feed_bloc.dart @@ -60,20 +60,22 @@ class HeadlinesFeedBloc extends Bloc { ) async { emit(HeadlinesFeedLoading()); // Show loading for filter application try { - final response = await _headlinesRepository.readAllByQuery({ - if (event.filter.categories?.isNotEmpty ?? false) - 'categories': - event.filter.categories! - .whereType() - .map((c) => c.id) - .toList(), - if (event.filter.sources?.isNotEmpty ?? false) - 'sources': - event.filter.sources! - .whereType() - .map((s) => s.id) - .toList(), - }, limit: _headlinesFetchLimit,); + final queryParams = {}; + if (event.filter.categories?.isNotEmpty ?? false) { + queryParams['categories'] = event.filter.categories! + .map((c) => c.id) + .join(','); + } + if (event.filter.sources?.isNotEmpty ?? false) { + queryParams['sources'] = event.filter.sources! + .map((s) => s.id) + .join(','); + } + + final response = await _headlinesRepository.readAllByQuery( + queryParams, + limit: _headlinesFetchLimit, + ); emit( HeadlinesFeedLoaded( headlines: response.items, @@ -133,8 +135,8 @@ class HeadlinesFeedBloc extends Bloc { Emitter emit, ) async { // Determine current filter and cursor based on state - var currentFilter = const HeadlineFilter(); - var currentCursor = + HeadlineFilter currentFilter = const HeadlineFilter(); // Made type explicit + String? currentCursor = // Made type explicit event.cursor; // Use event's cursor if provided (for pagination) var currentHeadlines = []; var isPaginating = false; @@ -169,21 +171,20 @@ class HeadlinesFeedBloc extends Bloc { } try { + final queryParams = {}; + if (currentFilter.categories?.isNotEmpty ?? false) { + queryParams['categories'] = currentFilter.categories! + .map((c) => c.id) + .join(','); + } + if (currentFilter.sources?.isNotEmpty ?? false) { + queryParams['sources'] = currentFilter.sources! + .map((s) => s.id) + .join(','); + } + final response = await _headlinesRepository.readAllByQuery( - { - if (currentFilter.categories?.isNotEmpty ?? false) - 'categories': - currentFilter.categories! - .whereType() - .map((c) => c.id) - .toList(), - if (currentFilter.sources?.isNotEmpty ?? false) - 'sources': - currentFilter.sources! - .whereType() - .map((s) => s.id) - .toList(), - }, + queryParams, limit: _headlinesFetchLimit, startAfterId: currentCursor, // Use determined cursor ); @@ -216,27 +217,29 @@ class HeadlinesFeedBloc extends Bloc { emit(HeadlinesFeedLoading()); // Show loading indicator for refresh // Determine the filter currently applied in the state - var currentFilter = const HeadlineFilter(); + HeadlineFilter currentFilter = const HeadlineFilter(); // Made type explicit if (state is HeadlinesFeedLoaded) { currentFilter = (state as HeadlinesFeedLoaded).filter; } try { + final queryParams = {}; + if (currentFilter.categories?.isNotEmpty ?? false) { + queryParams['categories'] = currentFilter.categories! + .map((c) => c.id) + .join(','); + } + if (currentFilter.sources?.isNotEmpty ?? false) { + queryParams['sources'] = currentFilter.sources! + .map((s) => s.id) + .join(','); + } + // Fetch the first page using the current filter - final response = await _headlinesRepository.readAllByQuery({ - if (currentFilter.categories?.isNotEmpty ?? false) - 'categories': - currentFilter.categories! - .whereType() - .map((c) => c.id) - .toList(), - if (currentFilter.sources?.isNotEmpty ?? false) - 'sources': - currentFilter.sources! - .whereType() - .map((s) => s.id) - .toList(), - }, limit: _headlinesFetchLimit,); + final response = await _headlinesRepository.readAllByQuery( + queryParams, + limit: _headlinesFetchLimit, + ); emit( HeadlinesFeedLoaded( headlines: response.items, // Replace headlines on refresh From 59dea22bf819caba7a8c21f439607323cf6a01e1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:31:27 +0100 Subject: [PATCH 13/15] refactor(feed): remove unnecessary type declarations - Removed explicit type declarations - Removed unused parameter --- lib/headlines-feed/bloc/headlines_feed_bloc.dart | 6 +++--- lib/headlines-feed/view/source_filter_page.dart | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/headlines-feed/bloc/headlines_feed_bloc.dart b/lib/headlines-feed/bloc/headlines_feed_bloc.dart index fe08dde4..687a83c5 100644 --- a/lib/headlines-feed/bloc/headlines_feed_bloc.dart +++ b/lib/headlines-feed/bloc/headlines_feed_bloc.dart @@ -135,8 +135,8 @@ class HeadlinesFeedBloc extends Bloc { Emitter emit, ) async { // Determine current filter and cursor based on state - HeadlineFilter currentFilter = const HeadlineFilter(); // Made type explicit - String? currentCursor = // Made type explicit + var currentFilter = const HeadlineFilter(); // Made type explicit + var currentCursor = // Made type explicit event.cursor; // Use event's cursor if provided (for pagination) var currentHeadlines = []; var isPaginating = false; @@ -217,7 +217,7 @@ class HeadlinesFeedBloc extends Bloc { emit(HeadlinesFeedLoading()); // Show loading indicator for refresh // Determine the filter currently applied in the state - HeadlineFilter currentFilter = const HeadlineFilter(); // Made type explicit + var currentFilter = const HeadlineFilter(); // Made type explicit if (state is HeadlinesFeedLoaded) { currentFilter = (state as HeadlinesFeedLoaded).filter; } diff --git a/lib/headlines-feed/view/source_filter_page.dart b/lib/headlines-feed/view/source_filter_page.dart index 0c857c7a..91a89ecc 100644 --- a/lib/headlines-feed/view/source_filter_page.dart +++ b/lib/headlines-feed/view/source_filter_page.dart @@ -117,9 +117,7 @@ class _SourceFilterView extends StatelessWidget { // User can then re-apply capsule filters if needed. // Or, we could try to persist/retrieve the last known good capsule state. // For now, simple retry reloads all. - const LoadSourceFilterData( - initialSelectedCountryIsoCodes: {}, - ), + const LoadSourceFilterData(), ); }, ); From 76c81a9ef3205524d20017f0f2e24cb00b9a7bea Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:34:15 +0100 Subject: [PATCH 14/15] style(feed): add padding to source filter page - Added top padding to column --- lib/headlines-feed/view/source_filter_page.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/headlines-feed/view/source_filter_page.dart b/lib/headlines-feed/view/source_filter_page.dart index 91a89ecc..b03abff0 100644 --- a/lib/headlines-feed/view/source_filter_page.dart +++ b/lib/headlines-feed/view/source_filter_page.dart @@ -123,15 +123,18 @@ class _SourceFilterView extends StatelessWidget { ); } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + return Padding( + padding: const EdgeInsets.only(top: AppSpacing.md), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ _buildCountryCapsules(context, state, l10n), const SizedBox(height: AppSpacing.lg), _buildSourceTypeCapsules(context, state, l10n), const SizedBox(height: AppSpacing.lg), Expanded(child: _buildSourcesList(context, state, l10n)), - ], + ], + ), ); } From a0bf11cadf10b5bc9e6bb19a90e6f1b85272adf5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 02:34:36 +0100 Subject: [PATCH 15/15] style: format source filter page --- lib/headlines-feed/view/source_filter_page.dart | 10 +++++----- lib/headlines-search/bloc/headlines_search_bloc.dart | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/headlines-feed/view/source_filter_page.dart b/lib/headlines-feed/view/source_filter_page.dart index b03abff0..7a652f4b 100644 --- a/lib/headlines-feed/view/source_filter_page.dart +++ b/lib/headlines-feed/view/source_filter_page.dart @@ -128,11 +128,11 @@ class _SourceFilterView extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildCountryCapsules(context, state, l10n), - const SizedBox(height: AppSpacing.lg), - _buildSourceTypeCapsules(context, state, l10n), - const SizedBox(height: AppSpacing.lg), - Expanded(child: _buildSourcesList(context, state, l10n)), + _buildCountryCapsules(context, state, l10n), + const SizedBox(height: AppSpacing.lg), + _buildSourceTypeCapsules(context, state, l10n), + const SizedBox(height: AppSpacing.lg), + Expanded(child: _buildSourcesList(context, state, l10n)), ], ), ); diff --git a/lib/headlines-search/bloc/headlines_search_bloc.dart b/lib/headlines-search/bloc/headlines_search_bloc.dart index 14494a6c..23a5e37f 100644 --- a/lib/headlines-search/bloc/headlines_search_bloc.dart +++ b/lib/headlines-search/bloc/headlines_search_bloc.dart @@ -82,7 +82,7 @@ class HeadlinesSearchBloc try { final response = await _headlinesRepository.readAllByQuery({ 'query': event.searchTerm, - }, limit: _limit,); + }, limit: _limit); emit( HeadlinesSearchSuccess( headlines: response.items,