From fab1673b934ef3716b8d98efb8167d3724f3957a Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 03:59:45 +0100 Subject: [PATCH 01/17] feat(router): inject repositories into search bloc - Added category repository - Added source repository - Added country repository --- lib/router/router.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/router/router.dart b/lib/router/router.dart index 52acb83f..613c8549 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -293,11 +293,14 @@ GoRouter createRouter({ )..add(const HeadlinesFeedFetchRequested()), ), BlocProvider( - create: - (context) => HeadlinesSearchBloc( - headlinesRepository: - context.read>(), - ), + create: (context) => HeadlinesSearchBloc( + headlinesRepository: + context.read>(), + categoryRepository: + context.read>(), + sourceRepository: context.read>(), + countryRepository: context.read>(), + ), ), BlocProvider( create: From d13cf5dd428dc60410bc7ef5d3b620636e2e40cb Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 03:59:53 +0100 Subject: [PATCH 02/17] feat: create source item widget - Display source name and description - Handle tap event with snackbar --- .../widgets/source_item_widget.dart | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 lib/headlines-search/widgets/source_item_widget.dart diff --git a/lib/headlines-search/widgets/source_item_widget.dart b/lib/headlines-search/widgets/source_item_widget.dart new file mode 100644 index 00000000..079b60a4 --- /dev/null +++ b/lib/headlines-search/widgets/source_item_widget.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:ht_shared/ht_shared.dart'; // Import Source model + +/// A simple widget to display a Source search result. +class SourceItemWidget extends StatelessWidget { + const SourceItemWidget({required this.source, super.key}); + + final Source source; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(source.name), + subtitle: source.description != null + ? Text( + source.description!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ) + : null, + // TODO(you): Implement onTap navigation if needed for sources + onTap: () { + // Example: Navigate to a page showing headlines from this source + // context.goNamed('someSourceFeedRoute', params: {'id': source.id}); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Tapped on source: ${source.name}')), + ); + }, + ); + } +} From 935eb1a41d6716e14a8236d01d2e338305ca30aa Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:00:05 +0100 Subject: [PATCH 03/17] feat: add category item widget - Display category search result - Added onTap snackbar message --- .../widgets/category_item_widget.dart | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 lib/headlines-search/widgets/category_item_widget.dart diff --git a/lib/headlines-search/widgets/category_item_widget.dart b/lib/headlines-search/widgets/category_item_widget.dart new file mode 100644 index 00000000..3708253c --- /dev/null +++ b/lib/headlines-search/widgets/category_item_widget.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:ht_shared/ht_shared.dart'; // Import Category model + +/// A simple widget to display a Category search result. +class CategoryItemWidget extends StatelessWidget { + const CategoryItemWidget({required this.category, super.key}); + + final Category category; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(category.name), + subtitle: category.description != null + ? Text( + category.description!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ) + : null, + // TODO(you): Implement onTap navigation if needed for categories + onTap: () { + // Example: Navigate to a filtered feed for this category + // context.goNamed('someCategoryFeedRoute', params: {'id': category.id}); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Tapped on category: ${category.name}')), + ); + }, + ); + } +} From 176436be069b0e7a513191294fb7b46e0708eacf Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:00:13 +0100 Subject: [PATCH 04/17] feat: create country item widget - Displays country flag and name - Handles flag loading errors - Placeholder for no flag image - Shows snackbar on tap --- .../widgets/country_item_widget.dart | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 lib/headlines-search/widgets/country_item_widget.dart diff --git a/lib/headlines-search/widgets/country_item_widget.dart b/lib/headlines-search/widgets/country_item_widget.dart new file mode 100644 index 00000000..52cddd3e --- /dev/null +++ b/lib/headlines-search/widgets/country_item_widget.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:ht_shared/ht_shared.dart'; // Import Country model + +/// A simple widget to display a Country search result. +class CountryItemWidget extends StatelessWidget { + const CountryItemWidget({required this.country, super.key}); + + final Country country; + + @override + Widget build(BuildContext context) { + return ListTile( + leading: CircleAvatar( + backgroundImage: NetworkImage(country.flagUrl), + onBackgroundImageError: (exception, stackTrace) { + debugPrint('Error loading country flag: $exception'); + }, + child: country.flagUrl.isEmpty + ? const Icon(Icons.public_off_outlined) // Placeholder if no flag + : null, + ), + title: Text(country.name), + // TODO(you): Implement onTap navigation if needed for countries + onTap: () { + // Example: Navigate to a page showing headlines from this country + // context.goNamed('someCountryFeedRoute', params: {'isoCode': country.isoCode}); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Tapped on country: ${country.name}')), + ); + }, + ); + } +} From dfdd6776fbb758f05402d1db7aa520609af57049 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:00:19 +0100 Subject: [PATCH 05/17] feat(search): add search by category, source, country - Added dropdown for model type - Implemented new item widgets - Refactored search logic --- .../view/headlines_search_page.dart | 279 ++++++++++-------- 1 file changed, 163 insertions(+), 116 deletions(-) diff --git a/lib/headlines-search/view/headlines_search_page.dart b/lib/headlines-search/view/headlines_search_page.dart index 335e116d..80bcc5ae 100644 --- a/lib/headlines-search/view/headlines_search_page.dart +++ b/lib/headlines-search/view/headlines_search_page.dart @@ -4,7 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ht_main/headlines-feed/widgets/headline_item_widget.dart'; +import 'package:ht_main/headlines-search/models/search_model_type.dart'; // Import SearchModelType import 'package:ht_main/router/routes.dart'; // Import Routes +import 'package:ht_shared/ht_shared.dart'; // Import shared models +// Import new item widgets +import 'package:ht_main/headlines-search/widgets/category_item_widget.dart'; +import 'package:ht_main/headlines-search/widgets/country_item_widget.dart'; +import 'package:ht_main/headlines-search/widgets/source_item_widget.dart'; import 'package:ht_main/headlines-search/bloc/headlines_search_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/shared/constants/app_spacing.dart'; // Import AppSpacing @@ -37,58 +43,54 @@ class _HeadlinesSearchView extends StatefulWidget { class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { final _scrollController = ScrollController(); - final _textController = - TextEditingController(); // Controller for the TextField - bool _showClearButton = false; // State to control clear button visibility + final _textController = TextEditingController(); + bool _showClearButton = false; + SearchModelType _selectedModelType = SearchModelType.headline; // Initial selection @override void initState() { super.initState(); _scrollController.addListener(_onScroll); - // Listen to text changes to control clear button visibility _textController.addListener(() { setState(() { _showClearButton = _textController.text.isNotEmpty; }); }); + // Set initial model type in BLoC if not already set (e.g. on first load) + // Though BLoC state now defaults, this ensures UI and BLoC are in sync. + context + .read() + .add(HeadlinesSearchModelTypeChanged(_selectedModelType)); } @override void dispose() { - _scrollController - ..removeListener(_onScroll) - ..dispose(); - _textController.dispose(); // Dispose the text controller + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + _textController.dispose(); super.dispose(); } - /// Handles scroll events to trigger fetching more results when near the bottom. void _onScroll() { final state = context.read().state; - if (_isBottom && state is HeadlinesSearchSuccess) { - final searchTerm = state.lastSearchTerm; - if (state.hasMore) { - context.read().add( - HeadlinesSearchFetchRequested(searchTerm: searchTerm), - ); - } + if (_isBottom && state is HeadlinesSearchSuccess && state.hasMore) { + context.read().add( + HeadlinesSearchFetchRequested(searchTerm: state.lastSearchTerm), + ); } } - /// Checks if the scroll position is near the bottom of the list. bool get _isBottom { if (!_scrollController.hasClients) return false; final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.offset; - // Trigger slightly before the absolute bottom for a smoother experience return currentScroll >= (maxScroll * 0.98); } - /// Triggers a search request based on the current text input. void _performSearch() { context.read().add( - HeadlinesSearchFetchRequested(searchTerm: _textController.text), - ); + HeadlinesSearchFetchRequested(searchTerm: _textController.text), + ); } @override @@ -100,130 +102,175 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { return Scaffold( appBar: AppBar( - // Enhanced TextField integrated into the AppBar title + leadingWidth: 150, // Adjust width to accommodate dropdown + leading: Padding( + padding: const EdgeInsets.only(left: AppSpacing.md, right: AppSpacing.sm), + child: DropdownButtonFormField( + value: _selectedModelType, + // Use a more subtle underline or remove it if it clashes + decoration: const InputDecoration( + border: InputBorder.none, // Removes underline + contentPadding: EdgeInsets.symmetric( + horizontal: AppSpacing.xs, // Minimal horizontal padding + ), + ), + // Style the dropdown text to match AppBar title + style: appBarTheme.titleTextStyle ?? theme.textTheme.titleLarge, + dropdownColor: colorScheme.surfaceContainerHighest, // Match theme + icon: Icon( + Icons.arrow_drop_down, + color: appBarTheme.iconTheme?.color ?? colorScheme.onSurface, + ), + items: SearchModelType.values.map((SearchModelType type) { + return DropdownMenuItem( + value: type, + child: Text( + type.displayName, // Using the new getter + // Ensure text color contrasts with dropdownColor + style: appBarTheme.titleTextStyle?.copyWith( + color: colorScheme.onSurface, // Example color + ) ?? + theme.textTheme.titleLarge?.copyWith( + color: colorScheme.onSurface, + ), + ), + ); + }).toList(), + onChanged: (SearchModelType? newValue) { + if (newValue != null) { + setState(() { + _selectedModelType = newValue; + }); + context + .read() + .add(HeadlinesSearchModelTypeChanged(newValue)); + if (_textController.text.isNotEmpty) { + _performSearch(); // Re-trigger search with new model type + } + } + }, + ), + ), title: TextField( controller: _textController, - style: appBarTheme.titleTextStyle ?? theme.textTheme.titleLarge, decoration: InputDecoration( hintText: l10n.headlinesSearchHintText, - - hintStyle: - appBarTheme.titleTextStyle?.copyWith( - color: (appBarTheme.titleTextStyle?.color ?? - colorScheme.onSurface) - .withAlpha(153), // Replaced withOpacity(0.6) - ) ?? - theme.textTheme.titleLarge?.copyWith( - color: colorScheme.onSurface.withAlpha( - 153, - ), // Replaced withOpacity(0.6) - ), - // Remove the default border + hintStyle: (appBarTheme.titleTextStyle ?? + theme.textTheme.titleLarge) + ?.copyWith(color: colorScheme.onSurface.withAlpha(153)), border: InputBorder.none, - // Remove focused border highlight if any - focusedBorder: InputBorder.none, - // Remove enabled border highlight if any - enabledBorder: InputBorder.none, - // Add a subtle background fill filled: true, - - fillColor: colorScheme.surface.withAlpha( - 26, - ), // Replaced withOpacity(0.1) - // Apply consistent padding using AppSpacing + fillColor: colorScheme.surface.withAlpha(26), contentPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.paddingMedium, - vertical: - AppSpacing.paddingSmall, // Adjust vertical padding as needed + vertical: AppSpacing.paddingSmall, ), - // Add a clear button that appears when text is entered - suffixIcon: - _showClearButton - ? IconButton( - icon: Icon( - Icons.clear, - - color: - appBarTheme.iconTheme?.color ?? - colorScheme.onSurface, - ), - onPressed: _textController.clear, - ) - : null, // No icon when text field is empty + suffixIcon: _showClearButton + ? IconButton( + icon: Icon( + Icons.clear, + color: appBarTheme.iconTheme?.color ?? + colorScheme.onSurface, + ), + onPressed: () { + _textController.clear(); + // Optionally clear search results when text is cleared + // context.read().add(HeadlinesSearchModelTypeChanged(_selectedModelType)); + }, + ) + : null, ), - // Trigger search on submit (e.g., pressing Enter on keyboard) onSubmitted: (_) => _performSearch(), ), actions: [ - // Search action button IconButton( icon: const Icon(Icons.search), - tooltip: l10n.headlinesSearchActionTooltip, // Re-added tooltip + tooltip: l10n.headlinesSearchActionTooltip, onPressed: _performSearch, ), ], ), body: BlocBuilder( builder: (context, state) { - // Handle different states of the search BLoC return switch (state) { - // Loading state + HeadlinesSearchInitial() => InitialStateWidget( + icon: Icons.search_off_rounded, + headline: l10n.headlinesSearchInitialHeadline, + subheadline: l10n.headlinesSearchInitialSubheadline, + ), + // Use more generic loading text or existing keys HeadlinesSearchLoading() => InitialStateWidget( - icon: Icons.manage_search, // Changed icon - headline: - l10n.headlinesSearchInitialHeadline, // Keep initial text for loading phase - subheadline: l10n.headlinesSearchInitialSubheadline, - ), - // Success state with results + icon: Icons.manage_search, + headline: l10n.headlinesFeedLoadingHeadline, // Re-use feed loading + subheadline: + 'Searching ${state.selectedModelType.displayName.toLowerCase()}...', + ), HeadlinesSearchSuccess( - :final headlines, - :final hasMore, - :final errorMessage, // Check for specific error message within success - :final lastSearchTerm, + results: final results, + hasMore: final hasMore, + errorMessage: final errorMessage, + lastSearchTerm: final lastSearchTerm, + selectedModelType: final resultsModelType, ) => errorMessage != null - // Display error if present within success state ? FailureStateWidget( - message: errorMessage, - onRetry: () { - // Retry with the last successful search term - context.read().add( - HeadlinesSearchFetchRequested( - searchTerm: lastSearchTerm, + message: errorMessage, + onRetry: () => context.read().add( + HeadlinesSearchFetchRequested( + searchTerm: lastSearchTerm), + ), + ) + : results.isEmpty + ? FailureStateWidget( + message: + '${l10n.headlinesSearchNoResultsHeadline} for "${lastSearchTerm}" in ${resultsModelType.displayName.toLowerCase()}.\n${l10n.headlinesSearchNoResultsSubheadline}', + ) + : ListView.builder( + controller: _scrollController, + itemCount: + hasMore ? results.length + 1 : results.length, + itemBuilder: (context, index) { + if (index >= results.length) { + return const Padding( + padding: + EdgeInsets.all(AppSpacing.paddingLarge), + child: + Center(child: CircularProgressIndicator()), + ); + } + final item = results[index]; + switch (resultsModelType) { + case SearchModelType.headline: + return HeadlineItemWidget( + headline: item as Headline, + targetRouteName: + Routes.searchArticleDetailsName, + ); + case SearchModelType.category: + return CategoryItemWidget( + category: item as Category); + case SearchModelType.source: + return SourceItemWidget(source: item as Source); + case SearchModelType.country: + return CountryItemWidget( + country: item as Country); + } + }, ), - ); - }, - ) - // Display "no results" if list is empty - : headlines.isEmpty - ? FailureStateWidget( - message: - '${l10n.headlinesSearchNoResultsHeadline}\n${l10n.headlinesSearchNoResultsSubheadline}', - ) - // Display the list of headlines - : ListView.builder( - controller: _scrollController, - // Add 1 for loading indicator if more items exist - itemCount: - hasMore ? headlines.length + 1 : headlines.length, - itemBuilder: (context, index) { - // Show loading indicator at the end if hasMore - if (index >= headlines.length) { - // Ensure loading indicator is visible - return const Padding( - padding: EdgeInsets.all(AppSpacing.paddingLarge), - child: Center(child: CircularProgressIndicator()), - ); - } - // Display headline item - return HeadlineItemWidget( - headline: headlines[index], - targetRouteName: Routes.searchArticleDetailsName, - ); - }, - ), - // Default case (should ideally not be reached if states are handled) + HeadlinesSearchFailure( + errorMessage: final errorMessage, + lastSearchTerm: final lastSearchTerm, + selectedModelType: final failedModelType + ) => + FailureStateWidget( + message: + 'Failed to search $lastSearchTerm in ${failedModelType.displayName.toLowerCase()}:\n$errorMessage', + onRetry: () => context.read().add( + HeadlinesSearchFetchRequested(searchTerm: lastSearchTerm), + ), + ), + // Add default case for exhaustiveness _ => const SizedBox.shrink(), }; }, From 828a590846d6f62b06e12bdc522698f4acd7d99f Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:00:25 +0100 Subject: [PATCH 06/17] feat(search): add SearchModelType enum - Defines searchable model types - Includes display names - Includes toJson method --- .../models/search_model_type.dart | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 lib/headlines-search/models/search_model_type.dart diff --git a/lib/headlines-search/models/search_model_type.dart b/lib/headlines-search/models/search_model_type.dart new file mode 100644 index 00000000..de508d5d --- /dev/null +++ b/lib/headlines-search/models/search_model_type.dart @@ -0,0 +1,28 @@ +/// Defines the types of models that can be searched. +enum SearchModelType { + headline, + category, + country, + source; + + /// Returns a user-friendly display name for the enum value. + /// + /// This should ideally be localized using context.l10n, + /// but for simplicity in this step, we'll use direct strings. + /// TODO(Cline): Localize these display names. + String get displayName { + switch (this) { + case SearchModelType.headline: + return 'Headlines'; + case SearchModelType.category: + return 'Categories'; + case SearchModelType.country: + return 'Countries'; + case SearchModelType.source: + return 'Sources'; + } + } + + /// Returns the string representation for API query parameters. + String toJson() => name; +} From 3e5bf5cbed8cd75bbcf226d58e546f1133be6075 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:00:31 +0100 Subject: [PATCH 07/17] refactor(search): support multiple search models - Generalize results list - Add selectedModelType to state - Update props for states --- .../bloc/headlines_search_state.dart | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/headlines-search/bloc/headlines_search_state.dart b/lib/headlines-search/bloc/headlines_search_state.dart index 2a0007d1..09d35295 100644 --- a/lib/headlines-search/bloc/headlines_search_state.dart +++ b/lib/headlines-search/bloc/headlines_search_state.dart @@ -1,64 +1,74 @@ part of 'headlines_search_bloc.dart'; abstract class HeadlinesSearchState extends Equatable { - const HeadlinesSearchState(); - // lastSearchTerm will be defined in specific states that need it. + const HeadlinesSearchState({ + this.selectedModelType = SearchModelType.headline, + }); + + final SearchModelType selectedModelType; + @override - List get props => []; + List get props => [selectedModelType]; } /// Initial state before any search is performed. class HeadlinesSearchInitial extends HeadlinesSearchState { - const HeadlinesSearchInitial(); - // No lastSearchTerm needed for initial state. + const HeadlinesSearchInitial({super.selectedModelType}); } /// State when a search is actively in progress. class HeadlinesSearchLoading extends HeadlinesSearchState { - const HeadlinesSearchLoading({this.lastSearchTerm}); - final String? lastSearchTerm; // Term being loaded + const HeadlinesSearchLoading({ + required this.lastSearchTerm, + required super.selectedModelType, + }); + final String lastSearchTerm; // Term being loaded @override - List get props => [lastSearchTerm]; + List get props => [...super.props, lastSearchTerm]; } /// State when a search has successfully returned results. class HeadlinesSearchSuccess extends HeadlinesSearchState { const HeadlinesSearchSuccess({ - required this.headlines, + required this.results, required this.hasMore, required this.lastSearchTerm, + required super.selectedModelType, // The model type for these results this.cursor, this.errorMessage, // For non-critical errors like pagination failure }); - final List headlines; + final List results; // Can hold Headline, Category, Source, Country final bool hasMore; final String? cursor; final String? errorMessage; // e.g., for pagination errors final String lastSearchTerm; // The term that yielded these results HeadlinesSearchSuccess copyWith({ - List? headlines, + List? results, bool? hasMore, String? cursor, - String? errorMessage, // Allow clearing/setting error + String? errorMessage, String? lastSearchTerm, + SearchModelType? selectedModelType, bool clearErrorMessage = false, }) { return HeadlinesSearchSuccess( - headlines: headlines ?? this.headlines, + results: results ?? this.results, hasMore: hasMore ?? this.hasMore, cursor: cursor ?? this.cursor, errorMessage: clearErrorMessage ? null : errorMessage ?? this.errorMessage, lastSearchTerm: lastSearchTerm ?? this.lastSearchTerm, + selectedModelType: selectedModelType ?? this.selectedModelType, ); } @override List get props => [ - headlines, + ...super.props, + results, hasMore, cursor, errorMessage, @@ -71,11 +81,12 @@ class HeadlinesSearchFailure extends HeadlinesSearchState { const HeadlinesSearchFailure({ required this.errorMessage, required this.lastSearchTerm, + required super.selectedModelType, }); final String errorMessage; final String lastSearchTerm; // The term that failed @override - List get props => [errorMessage, lastSearchTerm]; + List get props => [...super.props, errorMessage, lastSearchTerm]; } From 6a5bd19b292d0760ceca2a73f6da05db34510b25 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:00:41 +0100 Subject: [PATCH 08/17] feat(search): add model type change event - Allow changing search model type - Added HeadlinesSearchModelTypeChanged --- lib/headlines-search/bloc/headlines_search_event.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/headlines-search/bloc/headlines_search_event.dart b/lib/headlines-search/bloc/headlines_search_event.dart index 8a04558b..ab1a105d 100644 --- a/lib/headlines-search/bloc/headlines_search_event.dart +++ b/lib/headlines-search/bloc/headlines_search_event.dart @@ -7,6 +7,15 @@ sealed class HeadlinesSearchEvent extends Equatable { List get props => []; } +final class HeadlinesSearchModelTypeChanged extends HeadlinesSearchEvent { + const HeadlinesSearchModelTypeChanged(this.newModelType); + + final SearchModelType newModelType; + + @override + List get props => [newModelType]; +} + final class HeadlinesSearchFetchRequested extends HeadlinesSearchEvent { const HeadlinesSearchFetchRequested({required this.searchTerm}); From 45943f36970a64633de73c262c3c5a1a034bda29 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:00:46 +0100 Subject: [PATCH 09/17] feat(search): add multi-model search support - Implemented model type selection - Added category, source, country search - Refactored search logic - Updated state management --- .../bloc/headlines_search_bloc.dart | 163 +++++++++++++----- 1 file changed, 119 insertions(+), 44 deletions(-) diff --git a/lib/headlines-search/bloc/headlines_search_bloc.dart b/lib/headlines-search/bloc/headlines_search_bloc.dart index 3cb8bef1..335736d2 100644 --- a/lib/headlines-search/bloc/headlines_search_bloc.dart +++ b/lib/headlines-search/bloc/headlines_search_bloc.dart @@ -2,6 +2,7 @@ import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository +import 'package:ht_main/headlines-search/models/search_model_type.dart'; // Import SearchModelType import 'package:ht_shared/ht_shared.dart'; // Shared models, including Headline part 'headlines_search_event.dart'; @@ -9,10 +10,17 @@ part 'headlines_search_state.dart'; class HeadlinesSearchBloc extends Bloc { - HeadlinesSearchBloc({required HtDataRepository headlinesRepository}) - : _headlinesRepository = headlinesRepository, - super(const HeadlinesSearchInitial()) { - // Start with Initial state + HeadlinesSearchBloc({ + required HtDataRepository headlinesRepository, + required HtDataRepository categoryRepository, + required HtDataRepository sourceRepository, + required HtDataRepository countryRepository, + }) : _headlinesRepository = headlinesRepository, + _categoryRepository = categoryRepository, + _sourceRepository = sourceRepository, + _countryRepository = countryRepository, + super(const HeadlinesSearchInitial()) { + on(_onHeadlinesSearchModelTypeChanged); on( _onSearchFetchRequested, transformer: restartable(), // Process only the latest search @@ -20,90 +28,157 @@ class HeadlinesSearchBloc } final HtDataRepository _headlinesRepository; + final HtDataRepository _categoryRepository; + final HtDataRepository _sourceRepository; + final HtDataRepository _countryRepository; static const _limit = 10; + Future _onHeadlinesSearchModelTypeChanged( + HeadlinesSearchModelTypeChanged event, + Emitter emit, + ) async { + // If there's an active search term, re-trigger search with new model type + final currentSearchTerm = state is HeadlinesSearchLoading + ? (state as HeadlinesSearchLoading).lastSearchTerm + : state is HeadlinesSearchSuccess + ? (state as HeadlinesSearchSuccess).lastSearchTerm + : state is HeadlinesSearchFailure + ? (state as HeadlinesSearchFailure).lastSearchTerm + : null; + + emit(HeadlinesSearchInitial(selectedModelType: event.newModelType)); + + if (currentSearchTerm != null && currentSearchTerm.isNotEmpty) { + add(HeadlinesSearchFetchRequested(searchTerm: currentSearchTerm)); + } + } + Future _onSearchFetchRequested( HeadlinesSearchFetchRequested event, Emitter emit, ) async { - if (event.searchTerm.isEmpty) { + final searchTerm = event.searchTerm; + final modelType = state.selectedModelType; + + if (searchTerm.isEmpty) { emit( - const HeadlinesSearchSuccess( - headlines: [], + HeadlinesSearchSuccess( + results: const [], hasMore: false, lastSearchTerm: '', + selectedModelType: modelType, ), ); return; } - // Check if current state is success and if the search term is the same for pagination + // Handle pagination if (state is HeadlinesSearchSuccess) { final successState = state as HeadlinesSearchSuccess; - if (event.searchTerm == successState.lastSearchTerm) { - // This is a pagination request for the current search term - if (!successState.hasMore) return; // No more items to paginate + if (searchTerm == successState.lastSearchTerm && + modelType == successState.selectedModelType) { + if (!successState.hasMore) return; - // It's a bit unusual to emit Loading here for pagination, - // typically UI handles this. Let's keep it simple for now. - // emit(HeadlinesSearchLoading(lastSearchTerm: event.searchTerm)); try { - final response = await _headlinesRepository.readAllByQuery( - {'q': event.searchTerm}, - limit: _limit, - startAfterId: successState.cursor, - ); + PaginatedResponse response; + switch (modelType) { + case SearchModelType.headline: + response = await _headlinesRepository.readAllByQuery( + {'q': searchTerm, 'model': modelType.toJson()}, + limit: _limit, + startAfterId: successState.cursor, + ); + case SearchModelType.category: + response = await _categoryRepository.readAllByQuery( + {'q': searchTerm, 'model': modelType.toJson()}, + limit: _limit, + startAfterId: successState.cursor, + ); + case SearchModelType.source: + response = await _sourceRepository.readAllByQuery( + {'q': searchTerm, 'model': modelType.toJson()}, + limit: _limit, + startAfterId: successState.cursor, + ); + case SearchModelType.country: + response = await _countryRepository.readAllByQuery( + {'q': searchTerm, 'model': modelType.toJson()}, + limit: _limit, + startAfterId: successState.cursor, + ); + } emit( - response.items.isEmpty - ? successState.copyWith(hasMore: false) - : successState.copyWith( - headlines: List.of(successState.headlines) - ..addAll(response.items), - hasMore: response.hasMore, - cursor: response.cursor, - ), + successState.copyWith( + results: List.of(successState.results)..addAll(response.items), + hasMore: response.hasMore, + cursor: response.cursor, + ), ); } on HtHttpException catch (e) { emit(successState.copyWith(errorMessage: e.message)); } catch (e, st) { - print('Search pagination error: $e\n$st'); - emit( - successState.copyWith(errorMessage: 'Failed to load more results.'), - ); + print('Search pagination error ($modelType): $e\n$st'); + emit(successState.copyWith( + errorMessage: 'Failed to load more results.', + )); } - return; // Pagination handled + return; } } - // If not paginating for the same term, it's a new search or different term - emit( - HeadlinesSearchLoading(lastSearchTerm: event.searchTerm), - ); // Show loading for new search + // New search + emit(HeadlinesSearchLoading( + lastSearchTerm: searchTerm, + selectedModelType: modelType, + )); try { - final response = await _headlinesRepository.readAllByQuery({ - 'q': event.searchTerm, - }, limit: _limit); + PaginatedResponse response; + switch (modelType) { + case SearchModelType.headline: + response = await _headlinesRepository.readAllByQuery( + {'q': searchTerm, 'model': modelType.toJson()}, + limit: _limit, + ); + case SearchModelType.category: + response = await _categoryRepository.readAllByQuery( + {'q': searchTerm, 'model': modelType.toJson()}, + limit: _limit, + ); + case SearchModelType.source: + response = await _sourceRepository.readAllByQuery( + {'q': searchTerm, 'model': modelType.toJson()}, + limit: _limit, + ); + case SearchModelType.country: + response = await _countryRepository.readAllByQuery( + {'q': searchTerm, 'model': modelType.toJson()}, + limit: _limit, + ); + } emit( HeadlinesSearchSuccess( - headlines: response.items, + results: response.items, hasMore: response.hasMore, cursor: response.cursor, - lastSearchTerm: event.searchTerm, + lastSearchTerm: searchTerm, + selectedModelType: modelType, ), ); } on HtHttpException catch (e) { emit( HeadlinesSearchFailure( errorMessage: e.message, - lastSearchTerm: event.searchTerm, + lastSearchTerm: searchTerm, + selectedModelType: modelType, ), ); } catch (e, st) { - print('Search error: $e\n$st'); + print('Search error ($modelType): $e\n$st'); emit( HeadlinesSearchFailure( errorMessage: 'An unexpected error occurred during search.', - lastSearchTerm: event.searchTerm, + lastSearchTerm: searchTerm, + selectedModelType: modelType, ), ); } From c293e0d609ecf298b5b658c1225f03606786c945 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:10:45 +0100 Subject: [PATCH 10/17] feat: add search localization strings - Added search model types - Added search hint texts --- lib/l10n/arb/app_ar.arb | 33 +++++++++++++++++++++++++++++++++ lib/l10n/arb/app_en.arb | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index d7c4779e..bc59b3fe 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -589,5 +589,38 @@ "headlinesFeedFilterNoSourcesMatch": "لا توجد مصادر تطابق الفلاتر المحددة.", "@headlinesFeedFilterNoSourcesMatch": { "description": "Message shown when no sources match the selected filters" + }, + + "searchModelTypeHeadline": "العناوين الرئيسية", + "@searchModelTypeHeadline": { + "description": "Dropdown display name for Headline search type" + }, + "searchModelTypeCategory": "الفئات", + "@searchModelTypeCategory": { + "description": "Dropdown display name for Category search type" + }, + "searchModelTypeSource": "المصادر", + "@searchModelTypeSource": { + "description": "Dropdown display name for Source search type" + }, + "searchModelTypeCountry": "الدول", + "@searchModelTypeCountry": { + "description": "Dropdown display name for Country search type" + }, + "searchHintTextHeadline": "مثال: تطورات الذكاء الاصطناعي, مركبة المريخ...", + "@searchHintTextHeadline": { + "description": "Hint text for searching headlines" + }, + "searchHintTextCategory": "مثال: تكنولوجيا, رياضة, مالية...", + "@searchHintTextCategory": { + "description": "Hint text for searching categories" + }, + "searchHintTextSource": "مثال: بي بي سي نيوز, تك كرانش, رويترز...", + "@searchHintTextSource": { + "description": "Hint text for searching sources" + }, + "searchHintTextCountry": "مثال: الولايات المتحدة, اليابان, البرازيل...", + "@searchHintTextCountry": { + "description": "Hint text for searching countries" } } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a78e8ea4..165246b5 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -589,5 +589,38 @@ "headlinesFeedFilterNoSourcesMatch": "No sources match your selected filters.", "@headlinesFeedFilterNoSourcesMatch": { "description": "Message shown when no sources match the selected filters" + }, + + "searchModelTypeHeadline": "Headlines", + "@searchModelTypeHeadline": { + "description": "Dropdown display name for Headline search type" + }, + "searchModelTypeCategory": "Categories", + "@searchModelTypeCategory": { + "description": "Dropdown display name for Category search type" + }, + "searchModelTypeSource": "Sources", + "@searchModelTypeSource": { + "description": "Dropdown display name for Source search type" + }, + "searchModelTypeCountry": "Countries", + "@searchModelTypeCountry": { + "description": "Dropdown display name for Country search type" + }, + "searchHintTextHeadline": "e.g., AI advancements, Mars rover...", + "@searchHintTextHeadline": { + "description": "Hint text for searching headlines" + }, + "searchHintTextCategory": "e.g., Technology, Sports, Finance...", + "@searchHintTextCategory": { + "description": "Hint text for searching categories" + }, + "searchHintTextSource": "e.g., BBC News, TechCrunch, Reuters...", + "@searchHintTextSource": { + "description": "Hint text for searching sources" + }, + "searchHintTextCountry": "e.g., USA, Japan, Brazil...", + "@searchHintTextCountry": { + "description": "Hint text for searching countries" } } From 7d5ca25033594ec82cd3943fdff7addcbb80fa4f Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:10:54 +0100 Subject: [PATCH 11/17] feat(search): Localize search model type dropdown - Localized dropdown menu - Added dynamic hint text --- .../view/headlines_search_page.dart | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/lib/headlines-search/view/headlines_search_page.dart b/lib/headlines-search/view/headlines_search_page.dart index 80bcc5ae..47addf53 100644 --- a/lib/headlines-search/view/headlines_search_page.dart +++ b/lib/headlines-search/view/headlines_search_page.dart @@ -122,13 +122,27 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { color: appBarTheme.iconTheme?.color ?? colorScheme.onSurface, ), items: SearchModelType.values.map((SearchModelType type) { + String displayLocalizedName; + switch (type) { + case SearchModelType.headline: + displayLocalizedName = l10n.searchModelTypeHeadline; + break; + case SearchModelType.category: + displayLocalizedName = l10n.searchModelTypeCategory; + break; + case SearchModelType.source: + displayLocalizedName = l10n.searchModelTypeSource; + break; + case SearchModelType.country: + displayLocalizedName = l10n.searchModelTypeCountry; + break; + } return DropdownMenuItem( value: type, child: Text( - type.displayName, // Using the new getter - // Ensure text color contrasts with dropdownColor + displayLocalizedName, style: appBarTheme.titleTextStyle?.copyWith( - color: colorScheme.onSurface, // Example color + color: colorScheme.onSurface, ) ?? theme.textTheme.titleLarge?.copyWith( color: colorScheme.onSurface, @@ -144,9 +158,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { context .read() .add(HeadlinesSearchModelTypeChanged(newValue)); - if (_textController.text.isNotEmpty) { - _performSearch(); // Re-trigger search with new model type - } + // DO NOT automatically perform search here } }, ), @@ -155,7 +167,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { controller: _textController, style: appBarTheme.titleTextStyle ?? theme.textTheme.titleLarge, decoration: InputDecoration( - hintText: l10n.headlinesSearchHintText, + hintText: _getHintTextForModelType(_selectedModelType, l10n), hintStyle: (appBarTheme.titleTextStyle ?? theme.textTheme.titleLarge) ?.copyWith(color: colorScheme.onSurface.withAlpha(153)), @@ -277,4 +289,17 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ), ); } + + String _getHintTextForModelType(SearchModelType modelType, AppLocalizations l10n) { + switch (modelType) { + case SearchModelType.headline: + return l10n.searchHintTextHeadline; + case SearchModelType.category: + return l10n.searchHintTextCategory; + case SearchModelType.source: + return l10n.searchHintTextSource; + case SearchModelType.country: + return l10n.searchHintTextCountry; + } + } } From 0e5528af8845345691b2f3fc62ac21e81c5f056a Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:11:00 +0100 Subject: [PATCH 12/17] refactor(search): remove automatic re-search - Removed automatic re-search - Simplifies state management --- lib/headlines-search/bloc/headlines_search_bloc.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/headlines-search/bloc/headlines_search_bloc.dart b/lib/headlines-search/bloc/headlines_search_bloc.dart index 335736d2..33ea9f66 100644 --- a/lib/headlines-search/bloc/headlines_search_bloc.dart +++ b/lib/headlines-search/bloc/headlines_search_bloc.dart @@ -48,9 +48,10 @@ class HeadlinesSearchBloc emit(HeadlinesSearchInitial(selectedModelType: event.newModelType)); - if (currentSearchTerm != null && currentSearchTerm.isNotEmpty) { - add(HeadlinesSearchFetchRequested(searchTerm: currentSearchTerm)); - } + // Removed automatic re-search: + // if (currentSearchTerm != null && currentSearchTerm.isNotEmpty) { + // add(HeadlinesSearchFetchRequested(searchTerm: currentSearchTerm)); + // } } Future _onSearchFetchRequested( From 50c2bd187f16de186186e39dadcba93b7104539d Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:14:05 +0100 Subject: [PATCH 13/17] style(search): refine headlines search page UI - Adjust dropdown text style - Update hint text appearance - Change initial state icon - Add list item spacing --- .../view/headlines_search_page.dart | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/headlines-search/view/headlines_search_page.dart b/lib/headlines-search/view/headlines_search_page.dart index 47addf53..68282eab 100644 --- a/lib/headlines-search/view/headlines_search_page.dart +++ b/lib/headlines-search/view/headlines_search_page.dart @@ -114,8 +114,10 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { horizontal: AppSpacing.xs, // Minimal horizontal padding ), ), - // Style the dropdown text to match AppBar title - style: appBarTheme.titleTextStyle ?? theme.textTheme.titleLarge, + // Style the dropdown text to match AppBar title - Adjusted + style: theme.textTheme.titleMedium?.copyWith( + color: appBarTheme.titleTextStyle?.color ?? colorScheme.onSurface, + ), dropdownColor: colorScheme.surfaceContainerHighest, // Match theme icon: Icon( Icons.arrow_drop_down, @@ -141,12 +143,10 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { value: type, child: Text( displayLocalizedName, - style: appBarTheme.titleTextStyle?.copyWith( - color: colorScheme.onSurface, - ) ?? - theme.textTheme.titleLarge?.copyWith( - color: colorScheme.onSurface, - ), + // Adjusted style for dropdown items + style: theme.textTheme.titleMedium?.copyWith( + color: colorScheme.onSurface, + ), ), ); }).toList(), @@ -168,9 +168,12 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { style: appBarTheme.titleTextStyle ?? theme.textTheme.titleLarge, decoration: InputDecoration( hintText: _getHintTextForModelType(_selectedModelType, l10n), - hintStyle: (appBarTheme.titleTextStyle ?? - theme.textTheme.titleLarge) - ?.copyWith(color: colorScheme.onSurface.withAlpha(153)), + // Adjusted hintStyle to use a smaller font + hintStyle: theme.textTheme.bodyMedium?.copyWith( + color: (appBarTheme.titleTextStyle?.color ?? + colorScheme.onSurface) + .withAlpha(153), + ), border: InputBorder.none, filled: true, fillColor: colorScheme.surface.withAlpha(26), @@ -207,7 +210,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { builder: (context, state) { return switch (state) { HeadlinesSearchInitial() => InitialStateWidget( - icon: Icons.search_off_rounded, + icon: Icons.search, // Changed icon headline: l10n.headlinesSearchInitialHeadline, subheadline: l10n.headlinesSearchInitialSubheadline, ), @@ -238,15 +241,18 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { message: '${l10n.headlinesSearchNoResultsHeadline} for "${lastSearchTerm}" in ${resultsModelType.displayName.toLowerCase()}.\n${l10n.headlinesSearchNoResultsSubheadline}', ) - : ListView.builder( + : ListView.separated( controller: _scrollController, + padding: const EdgeInsets.all(AppSpacing.paddingMedium), // Add overall padding itemCount: hasMore ? results.length + 1 : results.length, + separatorBuilder: (context, index) => + const SizedBox(height: AppSpacing.md), // Add separator itemBuilder: (context, index) { if (index >= results.length) { return const Padding( padding: - EdgeInsets.all(AppSpacing.paddingLarge), + EdgeInsets.symmetric(vertical: AppSpacing.lg), // Adjusted padding for loader child: Center(child: CircularProgressIndicator()), ); From bd994e16ed4b053a1fa3f52d21df3b2ef065df94 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:20:23 +0100 Subject: [PATCH 14/17] refactor(search): use generic keys for initial state - Updated headline text key - Updated subheadline text key --- lib/headlines-search/view/headlines_search_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/headlines-search/view/headlines_search_page.dart b/lib/headlines-search/view/headlines_search_page.dart index 68282eab..78356421 100644 --- a/lib/headlines-search/view/headlines_search_page.dart +++ b/lib/headlines-search/view/headlines_search_page.dart @@ -211,8 +211,8 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { return switch (state) { HeadlinesSearchInitial() => InitialStateWidget( icon: Icons.search, // Changed icon - headline: l10n.headlinesSearchInitialHeadline, - subheadline: l10n.headlinesSearchInitialSubheadline, + headline: l10n.searchPageInitialHeadline, // Use new generic key + subheadline: l10n.searchPageInitialSubheadline, // Use new generic key ), // Use more generic loading text or existing keys HeadlinesSearchLoading() => InitialStateWidget( From 5ca32f372588806210c3a51c8fa3ed7907079bad Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:20:31 +0100 Subject: [PATCH 15/17] feat: Add initial search page text to l10n - Added headline to search page - Added subheadline to search page --- lib/l10n/arb/app_ar.arb | 9 +++++++++ lib/l10n/arb/app_en.arb | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index bc59b3fe..46fa356b 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -622,5 +622,14 @@ "searchHintTextCountry": "مثال: الولايات المتحدة, اليابان, البرازيل...", "@searchHintTextCountry": { "description": "Hint text for searching countries" + }, + + "searchPageInitialHeadline": "ابدأ بحثك", + "@searchPageInitialHeadline": { + "description": "Generic headline for the initial state of the search page" + }, + "searchPageInitialSubheadline": "اختر نوعًا وأدخل كلمات رئيسية للبدء.", + "@searchPageInitialSubheadline": { + "description": "Generic subheadline for the initial state of the search page" } } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 165246b5..cae4c976 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -622,5 +622,14 @@ "searchHintTextCountry": "e.g., USA, Japan, Brazil...", "@searchHintTextCountry": { "description": "Hint text for searching countries" + }, + + "searchPageInitialHeadline": "Start Your Search", + "@searchPageInitialHeadline": { + "description": "Generic headline for the initial state of the search page" + }, + "searchPageInitialSubheadline": "Select a type and enter keywords to begin.", + "@searchPageInitialSubheadline": { + "description": "Generic subheadline for the initial state of the search page" } } From ab3b5b736ff6c74837e9ec7fc9f93b095f4ddb97 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:22:16 +0100 Subject: [PATCH 16/17] refactor(search): improve search bar in app bar ui - Replaced leading with title row - Added dropdown for search type - Improved textfield styling - Added clear button to textfield --- .../view/headlines_search_page.dart | 184 +++++++++--------- 1 file changed, 94 insertions(+), 90 deletions(-) diff --git a/lib/headlines-search/view/headlines_search_page.dart b/lib/headlines-search/view/headlines_search_page.dart index 78356421..fa07f622 100644 --- a/lib/headlines-search/view/headlines_search_page.dart +++ b/lib/headlines-search/view/headlines_search_page.dart @@ -102,101 +102,105 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { return Scaffold( appBar: AppBar( - leadingWidth: 150, // Adjust width to accommodate dropdown - leading: Padding( - padding: const EdgeInsets.only(left: AppSpacing.md, right: AppSpacing.sm), - child: DropdownButtonFormField( - value: _selectedModelType, - // Use a more subtle underline or remove it if it clashes - decoration: const InputDecoration( - border: InputBorder.none, // Removes underline - contentPadding: EdgeInsets.symmetric( - horizontal: AppSpacing.xs, // Minimal horizontal padding + // Removed leading and leadingWidth + titleSpacing: AppSpacing.paddingSmall, // Adjust title spacing if needed + title: Row( + crossAxisAlignment: CrossAxisAlignment.center, // Center items vertically + children: [ + SizedBox( + width: 140, // Constrain dropdown width + child: DropdownButtonFormField( + value: _selectedModelType, + isDense: true, // Makes the dropdown more compact + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: AppSpacing.xs, // Minimal horizontal padding + vertical: AppSpacing.xs, // Reduce vertical padding + ), + ), + style: theme.textTheme.titleMedium?.copyWith( + color: appBarTheme.titleTextStyle?.color ?? colorScheme.onSurface, + ), + dropdownColor: colorScheme.surfaceContainerHighest, + icon: Icon( + Icons.arrow_drop_down, + color: appBarTheme.iconTheme?.color ?? colorScheme.onSurface, + ), + items: SearchModelType.values.map((SearchModelType type) { + String displayLocalizedName; + switch (type) { + case SearchModelType.headline: + displayLocalizedName = l10n.searchModelTypeHeadline; + break; + case SearchModelType.category: + displayLocalizedName = l10n.searchModelTypeCategory; + break; + case SearchModelType.source: + displayLocalizedName = l10n.searchModelTypeSource; + break; + case SearchModelType.country: + displayLocalizedName = l10n.searchModelTypeCountry; + break; + } + return DropdownMenuItem( + value: type, + child: Text( + displayLocalizedName, + style: theme.textTheme.titleMedium?.copyWith( // Consistent style + color: colorScheme.onSurface, + ), + ), + ); + }).toList(), + onChanged: (SearchModelType? newValue) { + if (newValue != null) { + setState(() { + _selectedModelType = newValue; + }); + context + .read() + .add(HeadlinesSearchModelTypeChanged(newValue)); + } + }, ), ), - // Style the dropdown text to match AppBar title - Adjusted - style: theme.textTheme.titleMedium?.copyWith( - color: appBarTheme.titleTextStyle?.color ?? colorScheme.onSurface, - ), - dropdownColor: colorScheme.surfaceContainerHighest, // Match theme - icon: Icon( - Icons.arrow_drop_down, - color: appBarTheme.iconTheme?.color ?? colorScheme.onSurface, - ), - items: SearchModelType.values.map((SearchModelType type) { - String displayLocalizedName; - switch (type) { - case SearchModelType.headline: - displayLocalizedName = l10n.searchModelTypeHeadline; - break; - case SearchModelType.category: - displayLocalizedName = l10n.searchModelTypeCategory; - break; - case SearchModelType.source: - displayLocalizedName = l10n.searchModelTypeSource; - break; - case SearchModelType.country: - displayLocalizedName = l10n.searchModelTypeCountry; - break; - } - return DropdownMenuItem( - value: type, - child: Text( - displayLocalizedName, - // Adjusted style for dropdown items - style: theme.textTheme.titleMedium?.copyWith( - color: colorScheme.onSurface, + const SizedBox(width: AppSpacing.sm), // Spacing between dropdown and textfield + Expanded( + child: TextField( + controller: _textController, + style: appBarTheme.titleTextStyle ?? theme.textTheme.titleLarge, + decoration: InputDecoration( + hintText: _getHintTextForModelType(_selectedModelType, l10n), + hintStyle: theme.textTheme.bodyMedium?.copyWith( + color: (appBarTheme.titleTextStyle?.color ?? + colorScheme.onSurface) + .withAlpha(153), ), + border: InputBorder.none, + filled: true, + fillColor: colorScheme.surface.withAlpha(26), + contentPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.paddingMedium, + vertical: AppSpacing.paddingSmall + 3, // Fine-tune vertical padding for alignment + ), + suffixIcon: _showClearButton + ? IconButton( + icon: Icon( + Icons.clear, + color: appBarTheme.iconTheme?.color ?? + colorScheme.onSurface, + ), + onPressed: () { + _textController.clear(); + }, + ) + : null, ), - ); - }).toList(), - onChanged: (SearchModelType? newValue) { - if (newValue != null) { - setState(() { - _selectedModelType = newValue; - }); - context - .read() - .add(HeadlinesSearchModelTypeChanged(newValue)); - // DO NOT automatically perform search here - } - }, - ), - ), - title: TextField( - controller: _textController, - style: appBarTheme.titleTextStyle ?? theme.textTheme.titleLarge, - decoration: InputDecoration( - hintText: _getHintTextForModelType(_selectedModelType, l10n), - // Adjusted hintStyle to use a smaller font - hintStyle: theme.textTheme.bodyMedium?.copyWith( - color: (appBarTheme.titleTextStyle?.color ?? - colorScheme.onSurface) - .withAlpha(153), - ), - border: InputBorder.none, - filled: true, - fillColor: colorScheme.surface.withAlpha(26), - contentPadding: const EdgeInsets.symmetric( - horizontal: AppSpacing.paddingMedium, - vertical: AppSpacing.paddingSmall, + onSubmitted: (_) => _performSearch(), + ), ), - suffixIcon: _showClearButton - ? IconButton( - icon: Icon( - Icons.clear, - color: appBarTheme.iconTheme?.color ?? - colorScheme.onSurface, - ), - onPressed: () { - _textController.clear(); - // Optionally clear search results when text is cleared - // context.read().add(HeadlinesSearchModelTypeChanged(_selectedModelType)); - }, - ) - : null, - ), - onSubmitted: (_) => _performSearch(), + ], ), actions: [ IconButton( From 716423094578e42e1b57a876522fd6e296d8bfa5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 30 May 2025 04:26:33 +0100 Subject: [PATCH 17/17] style: misc --- .../bloc/headlines_feed_bloc.dart | 7 +- .../bloc/sources_filter_bloc.dart | 3 +- .../bloc/sources_filter_event.dart | 5 - .../widgets/headline_item_widget.dart | 1 - .../bloc/headlines_search_bloc.dart | 70 +++--- .../view/headlines_search_page.dart | 237 +++++++++--------- .../widgets/category_item_widget.dart | 15 +- .../widgets/country_item_widget.dart | 9 +- .../widgets/source_item_widget.dart | 15 +- lib/router/router.dart | 19 +- 10 files changed, 196 insertions(+), 185 deletions(-) diff --git a/lib/headlines-feed/bloc/headlines_feed_bloc.dart b/lib/headlines-feed/bloc/headlines_feed_bloc.dart index 687a83c5..fbb73ac2 100644 --- a/lib/headlines-feed/bloc/headlines_feed_bloc.dart +++ b/lib/headlines-feed/bloc/headlines_feed_bloc.dart @@ -6,12 +6,7 @@ import 'package:equatable/equatable.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository import 'package:ht_main/headlines-feed/models/headline_filter.dart'; import 'package:ht_shared/ht_shared.dart' - show - Category, - // Country, // Removed as it's no longer used for headline filtering - Headline, - HtHttpException, - Source; // Shared models and standardized exceptions + show Headline, HtHttpException; // Shared models and standardized exceptions part 'headlines_feed_event.dart'; part 'headlines_feed_state.dart'; diff --git a/lib/headlines-feed/bloc/sources_filter_bloc.dart b/lib/headlines-feed/bloc/sources_filter_bloc.dart index 048a98bf..272aea1d 100644 --- a/lib/headlines-feed/bloc/sources_filter_bloc.dart +++ b/lib/headlines-feed/bloc/sources_filter_bloc.dart @@ -3,8 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; -import 'package:ht_shared/ht_shared.dart' - show Country, HtHttpException, Source, SourceType; +import 'package:ht_shared/ht_shared.dart' show Country, Source, SourceType; part 'sources_filter_event.dart'; part 'sources_filter_state.dart'; diff --git a/lib/headlines-feed/bloc/sources_filter_event.dart b/lib/headlines-feed/bloc/sources_filter_event.dart index e5ff1220..1ba408f9 100644 --- a/lib/headlines-feed/bloc/sources_filter_event.dart +++ b/lib/headlines-feed/bloc/sources_filter_event.dart @@ -64,8 +64,3 @@ class SourceCheckboxToggled extends SourcesFilterEvent { class ClearSourceFiltersRequested extends SourcesFilterEvent { const ClearSourceFiltersRequested(); } - -// Internal event - not part of public API, hence leading underscore -class _FetchFilteredSourcesRequested extends SourcesFilterEvent { - const _FetchFilteredSourcesRequested(); -} diff --git a/lib/headlines-feed/widgets/headline_item_widget.dart b/lib/headlines-feed/widgets/headline_item_widget.dart index f48cb161..78a48d08 100644 --- a/lib/headlines-feed/widgets/headline_item_widget.dart +++ b/lib/headlines-feed/widgets/headline_item_widget.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_main/router/routes.dart'; import 'package:ht_main/shared/constants/constants.dart'; // Import AppSpacing import 'package:ht_shared/ht_shared.dart'; // Import models from ht_shared import 'package:intl/intl.dart'; // For date formatting diff --git a/lib/headlines-search/bloc/headlines_search_bloc.dart b/lib/headlines-search/bloc/headlines_search_bloc.dart index 33ea9f66..14d6c39f 100644 --- a/lib/headlines-search/bloc/headlines_search_bloc.dart +++ b/lib/headlines-search/bloc/headlines_search_bloc.dart @@ -15,11 +15,11 @@ class HeadlinesSearchBloc required HtDataRepository categoryRepository, required HtDataRepository sourceRepository, required HtDataRepository countryRepository, - }) : _headlinesRepository = headlinesRepository, - _categoryRepository = categoryRepository, - _sourceRepository = sourceRepository, - _countryRepository = countryRepository, - super(const HeadlinesSearchInitial()) { + }) : _headlinesRepository = headlinesRepository, + _categoryRepository = categoryRepository, + _sourceRepository = sourceRepository, + _countryRepository = countryRepository, + super(const HeadlinesSearchInitial()) { on(_onHeadlinesSearchModelTypeChanged); on( _onSearchFetchRequested, @@ -38,13 +38,15 @@ class HeadlinesSearchBloc Emitter emit, ) async { // If there's an active search term, re-trigger search with new model type - final currentSearchTerm = state is HeadlinesSearchLoading - ? (state as HeadlinesSearchLoading).lastSearchTerm - : state is HeadlinesSearchSuccess + // ignore: unused_local_variable + final currentSearchTerm = + state is HeadlinesSearchLoading + ? (state as HeadlinesSearchLoading).lastSearchTerm + : state is HeadlinesSearchSuccess ? (state as HeadlinesSearchSuccess).lastSearchTerm : state is HeadlinesSearchFailure - ? (state as HeadlinesSearchFailure).lastSearchTerm - : null; + ? (state as HeadlinesSearchFailure).lastSearchTerm + : null; emit(HeadlinesSearchInitial(selectedModelType: event.newModelType)); @@ -119,42 +121,44 @@ class HeadlinesSearchBloc emit(successState.copyWith(errorMessage: e.message)); } catch (e, st) { print('Search pagination error ($modelType): $e\n$st'); - emit(successState.copyWith( - errorMessage: 'Failed to load more results.', - )); + emit( + successState.copyWith(errorMessage: 'Failed to load more results.'), + ); } return; } } // New search - emit(HeadlinesSearchLoading( - lastSearchTerm: searchTerm, - selectedModelType: modelType, - )); + emit( + HeadlinesSearchLoading( + lastSearchTerm: searchTerm, + selectedModelType: modelType, + ), + ); try { PaginatedResponse response; switch (modelType) { case SearchModelType.headline: - response = await _headlinesRepository.readAllByQuery( - {'q': searchTerm, 'model': modelType.toJson()}, - limit: _limit, - ); + response = await _headlinesRepository.readAllByQuery({ + 'q': searchTerm, + 'model': modelType.toJson(), + }, limit: _limit,); case SearchModelType.category: - response = await _categoryRepository.readAllByQuery( - {'q': searchTerm, 'model': modelType.toJson()}, - limit: _limit, - ); + response = await _categoryRepository.readAllByQuery({ + 'q': searchTerm, + 'model': modelType.toJson(), + }, limit: _limit,); case SearchModelType.source: - response = await _sourceRepository.readAllByQuery( - {'q': searchTerm, 'model': modelType.toJson()}, - limit: _limit, - ); + response = await _sourceRepository.readAllByQuery({ + 'q': searchTerm, + 'model': modelType.toJson(), + }, limit: _limit,); case SearchModelType.country: - response = await _countryRepository.readAllByQuery( - {'q': searchTerm, 'model': modelType.toJson()}, - limit: _limit, - ); + response = await _countryRepository.readAllByQuery({ + 'q': searchTerm, + 'model': modelType.toJson(), + }, limit: _limit,); } emit( HeadlinesSearchSuccess( diff --git a/lib/headlines-search/view/headlines_search_page.dart b/lib/headlines-search/view/headlines_search_page.dart index fa07f622..9139bc13 100644 --- a/lib/headlines-search/view/headlines_search_page.dart +++ b/lib/headlines-search/view/headlines_search_page.dart @@ -4,18 +4,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ht_main/headlines-feed/widgets/headline_item_widget.dart'; +import 'package:ht_main/headlines-search/bloc/headlines_search_bloc.dart'; import 'package:ht_main/headlines-search/models/search_model_type.dart'; // Import SearchModelType -import 'package:ht_main/router/routes.dart'; // Import Routes -import 'package:ht_shared/ht_shared.dart'; // Import shared models // Import new item widgets import 'package:ht_main/headlines-search/widgets/category_item_widget.dart'; import 'package:ht_main/headlines-search/widgets/country_item_widget.dart'; import 'package:ht_main/headlines-search/widgets/source_item_widget.dart'; -import 'package:ht_main/headlines-search/bloc/headlines_search_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; +import 'package:ht_main/router/routes.dart'; // Import Routes import 'package:ht_main/shared/constants/app_spacing.dart'; // Import AppSpacing import 'package:ht_main/shared/widgets/failure_state_widget.dart'; import 'package:ht_main/shared/widgets/initial_state_widget.dart'; +import 'package:ht_shared/ht_shared.dart'; // Import shared models /// Page widget responsible for providing the BLoC for the headlines search feature. class HeadlinesSearchPage extends StatelessWidget { @@ -45,7 +45,8 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { final _scrollController = ScrollController(); final _textController = TextEditingController(); bool _showClearButton = false; - SearchModelType _selectedModelType = SearchModelType.headline; // Initial selection + SearchModelType _selectedModelType = + SearchModelType.headline; // Initial selection @override void initState() { @@ -58,9 +59,9 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { }); // Set initial model type in BLoC if not already set (e.g. on first load) // Though BLoC state now defaults, this ensures UI and BLoC are in sync. - context - .read() - .add(HeadlinesSearchModelTypeChanged(_selectedModelType)); + context.read().add( + HeadlinesSearchModelTypeChanged(_selectedModelType), + ); } @override @@ -75,8 +76,8 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { final state = context.read().state; if (_isBottom && state is HeadlinesSearchSuccess && state.hasMore) { context.read().add( - HeadlinesSearchFetchRequested(searchTerm: state.lastSearchTerm), - ); + HeadlinesSearchFetchRequested(searchTerm: state.lastSearchTerm), + ); } } @@ -89,8 +90,8 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { void _performSearch() { context.read().add( - HeadlinesSearchFetchRequested(searchTerm: _textController.text), - ); + HeadlinesSearchFetchRequested(searchTerm: _textController.text), + ); } @override @@ -105,13 +106,11 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { // Removed leading and leadingWidth titleSpacing: AppSpacing.paddingSmall, // Adjust title spacing if needed title: Row( - crossAxisAlignment: CrossAxisAlignment.center, // Center items vertically children: [ SizedBox( width: 140, // Constrain dropdown width child: DropdownButtonFormField( value: _selectedModelType, - isDense: true, // Makes the dropdown more compact decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.symmetric( @@ -120,52 +119,54 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ), ), style: theme.textTheme.titleMedium?.copyWith( - color: appBarTheme.titleTextStyle?.color ?? colorScheme.onSurface, + color: + appBarTheme.titleTextStyle?.color ?? + colorScheme.onSurface, ), dropdownColor: colorScheme.surfaceContainerHighest, icon: Icon( Icons.arrow_drop_down, color: appBarTheme.iconTheme?.color ?? colorScheme.onSurface, ), - items: SearchModelType.values.map((SearchModelType type) { - String displayLocalizedName; - switch (type) { - case SearchModelType.headline: - displayLocalizedName = l10n.searchModelTypeHeadline; - break; - case SearchModelType.category: - displayLocalizedName = l10n.searchModelTypeCategory; - break; - case SearchModelType.source: - displayLocalizedName = l10n.searchModelTypeSource; - break; - case SearchModelType.country: - displayLocalizedName = l10n.searchModelTypeCountry; - break; - } - return DropdownMenuItem( - value: type, - child: Text( - displayLocalizedName, - style: theme.textTheme.titleMedium?.copyWith( // Consistent style - color: colorScheme.onSurface, - ), - ), - ); - }).toList(), + items: + SearchModelType.values.map((SearchModelType type) { + String displayLocalizedName; + switch (type) { + case SearchModelType.headline: + displayLocalizedName = l10n.searchModelTypeHeadline; + case SearchModelType.category: + displayLocalizedName = l10n.searchModelTypeCategory; + case SearchModelType.source: + displayLocalizedName = l10n.searchModelTypeSource; + case SearchModelType.country: + displayLocalizedName = l10n.searchModelTypeCountry; + } + return DropdownMenuItem( + value: type, + child: Text( + displayLocalizedName, + style: theme.textTheme.titleMedium?.copyWith( + // Consistent style + color: colorScheme.onSurface, + ), + ), + ); + }).toList(), onChanged: (SearchModelType? newValue) { if (newValue != null) { setState(() { _selectedModelType = newValue; }); - context - .read() - .add(HeadlinesSearchModelTypeChanged(newValue)); + context.read().add( + HeadlinesSearchModelTypeChanged(newValue), + ); } }, ), ), - const SizedBox(width: AppSpacing.sm), // Spacing between dropdown and textfield + const SizedBox( + width: AppSpacing.sm, + ), // Spacing between dropdown and textfield Expanded( child: TextField( controller: _textController, @@ -182,20 +183,22 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { fillColor: colorScheme.surface.withAlpha(26), contentPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.paddingMedium, - vertical: AppSpacing.paddingSmall + 3, // Fine-tune vertical padding for alignment + vertical: + AppSpacing.paddingSmall + + 3, // Fine-tune vertical padding for alignment ), - suffixIcon: _showClearButton - ? IconButton( - icon: Icon( - Icons.clear, - color: appBarTheme.iconTheme?.color ?? - colorScheme.onSurface, - ), - onPressed: () { - _textController.clear(); - }, - ) - : null, + suffixIcon: + _showClearButton + ? IconButton( + icon: Icon( + Icons.clear, + color: + appBarTheme.iconTheme?.color ?? + colorScheme.onSurface, + ), + onPressed: _textController.clear, + ) + : null, ), onSubmitted: (_) => _performSearch(), ), @@ -214,17 +217,19 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { builder: (context, state) { return switch (state) { HeadlinesSearchInitial() => InitialStateWidget( - icon: Icons.search, // Changed icon - headline: l10n.searchPageInitialHeadline, // Use new generic key - subheadline: l10n.searchPageInitialSubheadline, // Use new generic key - ), + icon: Icons.search, // Changed icon + headline: l10n.searchPageInitialHeadline, // Use new generic key + subheadline: + l10n.searchPageInitialSubheadline, // Use new generic key + ), // Use more generic loading text or existing keys HeadlinesSearchLoading() => InitialStateWidget( - icon: Icons.manage_search, - headline: l10n.headlinesFeedLoadingHeadline, // Re-use feed loading - subheadline: - 'Searching ${state.selectedModelType.displayName.toLowerCase()}...', - ), + icon: Icons.manage_search, + headline: + l10n.headlinesFeedLoadingHeadline, // Re-use feed loading + subheadline: + 'Searching ${state.selectedModelType.displayName.toLowerCase()}...', + ), HeadlinesSearchSuccess( results: final results, hasMore: final hasMore, @@ -234,61 +239,64 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ) => errorMessage != null ? FailureStateWidget( - message: errorMessage, - onRetry: () => context.read().add( - HeadlinesSearchFetchRequested( - searchTerm: lastSearchTerm), + message: errorMessage, + onRetry: + () => context.read().add( + HeadlinesSearchFetchRequested( + searchTerm: lastSearchTerm, ), - ) - : results.isEmpty - ? FailureStateWidget( - message: - '${l10n.headlinesSearchNoResultsHeadline} for "${lastSearchTerm}" in ${resultsModelType.displayName.toLowerCase()}.\n${l10n.headlinesSearchNoResultsSubheadline}', - ) - : ListView.separated( - controller: _scrollController, - padding: const EdgeInsets.all(AppSpacing.paddingMedium), // Add overall padding - itemCount: - hasMore ? results.length + 1 : results.length, - separatorBuilder: (context, index) => - const SizedBox(height: AppSpacing.md), // Add separator - itemBuilder: (context, index) { - if (index >= results.length) { - return const Padding( - padding: - EdgeInsets.symmetric(vertical: AppSpacing.lg), // Adjusted padding for loader - child: - Center(child: CircularProgressIndicator()), - ); - } - final item = results[index]; - switch (resultsModelType) { - case SearchModelType.headline: - return HeadlineItemWidget( - headline: item as Headline, - targetRouteName: - Routes.searchArticleDetailsName, - ); - case SearchModelType.category: - return CategoryItemWidget( - category: item as Category); - case SearchModelType.source: - return SourceItemWidget(source: item as Source); - case SearchModelType.country: - return CountryItemWidget( - country: item as Country); - } - }, ), + ) + : results.isEmpty + ? FailureStateWidget( + message: + '${l10n.headlinesSearchNoResultsHeadline} for "$lastSearchTerm" in ${resultsModelType.displayName.toLowerCase()}.\n${l10n.headlinesSearchNoResultsSubheadline}', + ) + : ListView.separated( + controller: _scrollController, + padding: const EdgeInsets.all( + AppSpacing.paddingMedium, + ), // Add overall padding + itemCount: hasMore ? results.length + 1 : results.length, + separatorBuilder: + (context, index) => const SizedBox( + height: AppSpacing.md, + ), // Add separator + itemBuilder: (context, index) { + if (index >= results.length) { + return const Padding( + padding: EdgeInsets.symmetric( + vertical: AppSpacing.lg, + ), // Adjusted padding for loader + child: Center(child: CircularProgressIndicator()), + ); + } + final item = results[index]; + switch (resultsModelType) { + case SearchModelType.headline: + return HeadlineItemWidget( + headline: item as Headline, + targetRouteName: Routes.searchArticleDetailsName, + ); + case SearchModelType.category: + return CategoryItemWidget(category: item as Category); + case SearchModelType.source: + return SourceItemWidget(source: item as Source); + case SearchModelType.country: + return CountryItemWidget(country: item as Country); + } + }, + ), HeadlinesSearchFailure( errorMessage: final errorMessage, lastSearchTerm: final lastSearchTerm, - selectedModelType: final failedModelType + selectedModelType: final failedModelType, ) => FailureStateWidget( message: 'Failed to search $lastSearchTerm in ${failedModelType.displayName.toLowerCase()}:\n$errorMessage', - onRetry: () => context.read().add( + onRetry: + () => context.read().add( HeadlinesSearchFetchRequested(searchTerm: lastSearchTerm), ), ), @@ -300,7 +308,10 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ); } - String _getHintTextForModelType(SearchModelType modelType, AppLocalizations l10n) { + String _getHintTextForModelType( + SearchModelType modelType, + AppLocalizations l10n, + ) { switch (modelType) { case SearchModelType.headline: return l10n.searchHintTextHeadline; diff --git a/lib/headlines-search/widgets/category_item_widget.dart b/lib/headlines-search/widgets/category_item_widget.dart index 3708253c..23b1b083 100644 --- a/lib/headlines-search/widgets/category_item_widget.dart +++ b/lib/headlines-search/widgets/category_item_widget.dart @@ -11,13 +11,14 @@ class CategoryItemWidget extends StatelessWidget { Widget build(BuildContext context) { return ListTile( title: Text(category.name), - subtitle: category.description != null - ? Text( - category.description!, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ) - : null, + subtitle: + category.description != null + ? Text( + category.description!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ) + : null, // TODO(you): Implement onTap navigation if needed for categories onTap: () { // Example: Navigate to a filtered feed for this category diff --git a/lib/headlines-search/widgets/country_item_widget.dart b/lib/headlines-search/widgets/country_item_widget.dart index 52cddd3e..0e87f6ae 100644 --- a/lib/headlines-search/widgets/country_item_widget.dart +++ b/lib/headlines-search/widgets/country_item_widget.dart @@ -15,9 +15,12 @@ class CountryItemWidget extends StatelessWidget { onBackgroundImageError: (exception, stackTrace) { debugPrint('Error loading country flag: $exception'); }, - child: country.flagUrl.isEmpty - ? const Icon(Icons.public_off_outlined) // Placeholder if no flag - : null, + child: + country.flagUrl.isEmpty + ? const Icon( + Icons.public_off_outlined, + ) // Placeholder if no flag + : null, ), title: Text(country.name), // TODO(you): Implement onTap navigation if needed for countries diff --git a/lib/headlines-search/widgets/source_item_widget.dart b/lib/headlines-search/widgets/source_item_widget.dart index 079b60a4..35583293 100644 --- a/lib/headlines-search/widgets/source_item_widget.dart +++ b/lib/headlines-search/widgets/source_item_widget.dart @@ -11,13 +11,14 @@ class SourceItemWidget extends StatelessWidget { Widget build(BuildContext context) { return ListTile( title: Text(source.name), - subtitle: source.description != null - ? Text( - source.description!, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ) - : null, + subtitle: + source.description != null + ? Text( + source.description!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ) + : null, // TODO(you): Implement onTap navigation if needed for sources onTap: () { // Example: Navigate to a page showing headlines from this source diff --git a/lib/router/router.dart b/lib/router/router.dart index 613c8549..39099b66 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -293,14 +293,17 @@ GoRouter createRouter({ )..add(const HeadlinesFeedFetchRequested()), ), BlocProvider( - create: (context) => HeadlinesSearchBloc( - headlinesRepository: - context.read>(), - categoryRepository: - context.read>(), - sourceRepository: context.read>(), - countryRepository: context.read>(), - ), + create: + (context) => HeadlinesSearchBloc( + headlinesRepository: + context.read>(), + categoryRepository: + context.read>(), + sourceRepository: + context.read>(), + countryRepository: + context.read>(), + ), ), BlocProvider( create: