diff --git a/lib/account/bloc/account_bloc.dart b/lib/account/bloc/account_bloc.dart index a5dd11b5..2100a401 100644 --- a/lib/account/bloc/account_bloc.dart +++ b/lib/account/bloc/account_bloc.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -// Hide Category import 'package:ht_auth_repository/ht_auth_repository.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; @@ -10,279 +9,291 @@ import 'package:ht_shared/ht_shared.dart'; part 'account_event.dart'; part 'account_state.dart'; -/// {@template account_bloc} -/// BLoC responsible for managing the state and logic for the Account feature. -/// {@endtemplate} class AccountBloc extends Bloc { - /// {@macro account_bloc} AccountBloc({ required HtAuthRepository authenticationRepository, required HtDataRepository - userContentPreferencesRepository, - }) : _authenticationRepository = authenticationRepository, - _userContentPreferencesRepository = userContentPreferencesRepository, - super(const AccountState()) { - // Listen to authentication state changes from the repository - _authenticationRepository.authStateChanges.listen( - (user) => add(_AccountUserChanged(user: user)), - ); + userContentPreferencesRepository, + }) : _authenticationRepository = authenticationRepository, + _userContentPreferencesRepository = userContentPreferencesRepository, + super(const AccountState()) { + // Listen to user changes from HtAuthRepository + _userSubscription = + _authenticationRepository.authStateChanges.listen((user) { + add(AccountUserChanged(user)); + }); - on<_AccountUserChanged>(_onAccountUserChanged); - on( - _onAccountLoadContentPreferencesRequested, - ); - on(_onFollowCategoryToggled); - on(_onFollowSourceToggled); - on(_onFollowCountryToggled); - on(_onSaveHeadlineToggled); - // Handlers for AccountSettingsNavigationRequested and - // AccountBackupNavigationRequested are typically handled in the UI layer - // (e.g., BlocListener navigating) or could emit specific states if needed. + // Register event handlers + on(_onAccountUserChanged); + on(_onAccountLoadUserPreferences); + on(_onAccountSaveHeadlineToggled); + on(_onAccountFollowCategoryToggled); + on(_onAccountFollowSourceToggled); + // AccountFollowCountryToggled handler removed + on(_onAccountClearUserPreferences); } final HtAuthRepository _authenticationRepository; final HtDataRepository - _userContentPreferencesRepository; + _userContentPreferencesRepository; + late StreamSubscription _userSubscription; - /// Handles [_AccountUserChanged] events. - /// - /// Updates the state with the current user and triggers loading - /// of user preferences if the user is authenticated. Future _onAccountUserChanged( - _AccountUserChanged event, + AccountUserChanged event, Emitter emit, ) async { emit(state.copyWith(user: event.user)); if (event.user != null) { - // User is authenticated, load preferences - add(AccountLoadContentPreferencesRequested(userId: event.user!.id)); + add(AccountLoadUserPreferences(userId: event.user!.id)); } else { - // User is unauthenticated, clear preferences - emit(state.copyWith()); + // Clear preferences if user is null (logged out) + emit(state.copyWith(clearPreferences: true, status: AccountStatus.initial)); } } - /// Handles [AccountLoadContentPreferencesRequested] events. - /// - /// Attempts to load the user's content preferences. - Future _onAccountLoadContentPreferencesRequested( - AccountLoadContentPreferencesRequested event, + Future _onAccountLoadUserPreferences( + AccountLoadUserPreferences event, Emitter emit, ) async { - emit(state.copyWith(status: AccountStatus.loading)); // Indicate loading + emit(state.copyWith(status: AccountStatus.loading)); try { final preferences = await _userContentPreferencesRepository.read( id: event.userId, - userId: event.userId, - ); - emit( - state.copyWith(status: AccountStatus.success, preferences: preferences), + userId: event.userId, // Scope to the current user ); - } on NotFoundException { - // Specifically handle NotFound emit( state.copyWith( - status: AccountStatus.success, // It's a success, just no data - preferences: UserContentPreferences( - id: event.userId, - ), // Provide default/empty + status: AccountStatus.success, + preferences: preferences, + clearErrorMessage: true, ), ); + } on NotFoundException { + // If preferences not found, create a default one for the user + final defaultPreferences = UserContentPreferences(id: event.userId); + try { + await _userContentPreferencesRepository.create( + item: defaultPreferences, + userId: event.userId, + ); + emit( + state.copyWith( + status: AccountStatus.success, + preferences: defaultPreferences, + clearErrorMessage: true, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: AccountStatus.failure, + errorMessage: 'Failed to create default preferences.', + ), + ); + } } on HtHttpException catch (e) { - // Handle other HTTP errors emit( - state.copyWith( - status: AccountStatus.failure, - errorMessage: 'Failed to load preferences: ${e.message}', - preferences: UserContentPreferences( - id: event.userId, - ), // Provide default - ), + state.copyWith(status: AccountStatus.failure, errorMessage: e.message), ); } catch (e) { - // Catch-all for other unexpected errors emit( state.copyWith( status: AccountStatus.failure, - errorMessage: 'An unexpected error occurred: $e', - preferences: UserContentPreferences( - id: event.userId, - ), // Provide default + errorMessage: 'An unexpected error occurred.', ), ); } } - Future _persistPreferences( - UserContentPreferences preferences, + Future _onAccountSaveHeadlineToggled( + AccountSaveHeadlineToggled event, Emitter emit, ) async { - if (state.user == null) { - emit( - state.copyWith( - status: AccountStatus.failure, - errorMessage: 'User not authenticated to save preferences.', - ), - ); - return; + if (state.user == null || state.preferences == null) return; + emit(state.copyWith(status: AccountStatus.loading)); + + final currentPrefs = state.preferences!; + final isCurrentlySaved = + currentPrefs.savedHeadlines.any((h) => h.id == event.headline.id); + final List updatedSavedHeadlines; + + if (isCurrentlySaved) { + updatedSavedHeadlines = List.from(currentPrefs.savedHeadlines) + ..removeWhere((h) => h.id == event.headline.id); + } else { + updatedSavedHeadlines = List.from(currentPrefs.savedHeadlines) + ..add(event.headline); } - print( - '[AccountBloc._persistPreferences] Attempting to persist preferences for user ${state.user!.id}', - ); - print( - '[AccountBloc._persistPreferences] Preferences to save: ${preferences.toJson()}', - ); + + final updatedPrefs = + currentPrefs.copyWith(savedHeadlines: updatedSavedHeadlines); + try { await _userContentPreferencesRepository.update( - id: state.user!.id, // ID of the preferences object is the user's ID - item: preferences, + id: state.user!.id, + item: updatedPrefs, userId: state.user!.id, ); - print( - '[AccountBloc._persistPreferences] Successfully persisted preferences for user ${state.user!.id}', - ); - // Optimistic update already done, emit success if needed for UI feedback - // emit(state.copyWith(status: AccountStatus.success)); - } on HtHttpException catch (e) { - print( - '[AccountBloc._persistPreferences] HtHttpException while persisting: ${e.message}', - ); emit( state.copyWith( - status: AccountStatus.failure, - errorMessage: 'Failed to save preferences: ${e.message}', + status: AccountStatus.success, + preferences: updatedPrefs, + clearErrorMessage: true, ), ); - } catch (e) { - print( - '[AccountBloc._persistPreferences] Unknown error while persisting: $e', + } on HtHttpException catch (e) { + emit( + state.copyWith(status: AccountStatus.failure, errorMessage: e.message), ); + } catch (e) { emit( state.copyWith( status: AccountStatus.failure, - errorMessage: 'An unexpected error occurred while saving: $e', + errorMessage: 'Failed to update saved headlines.', ), ); } } - Future _onFollowCategoryToggled( + Future _onAccountFollowCategoryToggled( AccountFollowCategoryToggled event, Emitter emit, ) async { - if (state.preferences == null || state.user == null) return; + if (state.user == null || state.preferences == null) return; + emit(state.copyWith(status: AccountStatus.loading)); final currentPrefs = state.preferences!; - final updatedFollowedCategories = List.from( - currentPrefs.followedCategories, - ); - - final isCurrentlyFollowing = updatedFollowedCategories.any( - (category) => category.id == event.category.id, - ); + final isCurrentlyFollowed = currentPrefs.followedCategories + .any((c) => c.id == event.category.id); + final List updatedFollowedCategories; - if (isCurrentlyFollowing) { - updatedFollowedCategories.removeWhere( - (category) => category.id == event.category.id, - ); + if (isCurrentlyFollowed) { + updatedFollowedCategories = List.from(currentPrefs.followedCategories) + ..removeWhere((c) => c.id == event.category.id); } else { - updatedFollowedCategories.add(event.category); + updatedFollowedCategories = List.from(currentPrefs.followedCategories) + ..add(event.category); } - final newPreferences = currentPrefs.copyWith( - followedCategories: updatedFollowedCategories, - ); - emit(state.copyWith(preferences: newPreferences)); - await _persistPreferences(newPreferences, emit); + final updatedPrefs = + currentPrefs.copyWith(followedCategories: updatedFollowedCategories); + + try { + await _userContentPreferencesRepository.update( + id: state.user!.id, + item: updatedPrefs, + userId: state.user!.id, + ); + emit( + state.copyWith( + status: AccountStatus.success, + preferences: updatedPrefs, + clearErrorMessage: true, + ), + ); + } on HtHttpException catch (e) { + emit( + state.copyWith(status: AccountStatus.failure, errorMessage: e.message), + ); + } catch (e) { + emit( + state.copyWith( + status: AccountStatus.failure, + errorMessage: 'Failed to update followed categories.', + ), + ); + } } - Future _onFollowSourceToggled( + Future _onAccountFollowSourceToggled( AccountFollowSourceToggled event, Emitter emit, ) async { - if (state.preferences == null || state.user == null) return; + if (state.user == null || state.preferences == null) return; + emit(state.copyWith(status: AccountStatus.loading)); final currentPrefs = state.preferences!; - final updatedFollowedSources = List.from( - currentPrefs.followedSources, - ); - - final isCurrentlyFollowing = updatedFollowedSources.any( - (source) => source.id == event.source.id, - ); + final isCurrentlyFollowed = + currentPrefs.followedSources.any((s) => s.id == event.source.id); + final List updatedFollowedSources; - if (isCurrentlyFollowing) { - updatedFollowedSources.removeWhere( - (source) => source.id == event.source.id, - ); + if (isCurrentlyFollowed) { + updatedFollowedSources = List.from(currentPrefs.followedSources) + ..removeWhere((s) => s.id == event.source.id); } else { - updatedFollowedSources.add(event.source); + updatedFollowedSources = List.from(currentPrefs.followedSources) + ..add(event.source); } - final newPreferences = currentPrefs.copyWith( - followedSources: updatedFollowedSources, - ); - emit(state.copyWith(preferences: newPreferences)); - await _persistPreferences(newPreferences, emit); - } - - Future _onFollowCountryToggled( - AccountFollowCountryToggled event, - Emitter emit, - ) async { - if (state.preferences == null || state.user == null) return; - - final currentPrefs = state.preferences!; - final updatedFollowedCountries = List.from( - currentPrefs.followedCountries, - ); - - final isCurrentlyFollowing = updatedFollowedCountries.any( - (country) => country.id == event.country.id, - ); + final updatedPrefs = + currentPrefs.copyWith(followedSources: updatedFollowedSources); - if (isCurrentlyFollowing) { - updatedFollowedCountries.removeWhere( - (country) => country.id == event.country.id, + try { + await _userContentPreferencesRepository.update( + id: state.user!.id, + item: updatedPrefs, + userId: state.user!.id, + ); + emit( + state.copyWith( + status: AccountStatus.success, + preferences: updatedPrefs, + clearErrorMessage: true, + ), + ); + } on HtHttpException catch (e) { + emit( + state.copyWith(status: AccountStatus.failure, errorMessage: e.message), + ); + } catch (e) { + emit( + state.copyWith( + status: AccountStatus.failure, + errorMessage: 'Failed to update followed sources.', + ), ); - } else { - updatedFollowedCountries.add(event.country); } - - final newPreferences = currentPrefs.copyWith( - followedCountries: updatedFollowedCountries, - ); - emit(state.copyWith(preferences: newPreferences)); - await _persistPreferences(newPreferences, emit); } - Future _onSaveHeadlineToggled( - AccountSaveHeadlineToggled event, + // _onAccountFollowCountryToggled method removed + + Future _onAccountClearUserPreferences( + AccountClearUserPreferences event, Emitter emit, ) async { - if (state.preferences == null || state.user == null) return; - - final currentPrefs = state.preferences!; - final updatedSavedHeadlines = List.from( - currentPrefs.savedHeadlines, - ); - - final isCurrentlySaved = updatedSavedHeadlines.any( - (headline) => headline.id == event.headline.id, - ); - - if (isCurrentlySaved) { - updatedSavedHeadlines.removeWhere( - (headline) => headline.id == event.headline.id, + emit(state.copyWith(status: AccountStatus.loading)); + try { + // Create a new default preferences object to "clear" existing ones + final defaultPreferences = UserContentPreferences(id: event.userId); + await _userContentPreferencesRepository.update( + id: event.userId, + item: defaultPreferences, + userId: event.userId, + ); + emit( + state.copyWith( + status: AccountStatus.success, + preferences: defaultPreferences, + clearErrorMessage: true, + ), + ); + } on HtHttpException catch (e) { + emit( + state.copyWith(status: AccountStatus.failure, errorMessage: e.message), + ); + } catch (e) { + emit( + state.copyWith( + status: AccountStatus.failure, + errorMessage: 'Failed to clear user preferences.', + ), ); - } else { - updatedSavedHeadlines.add(event.headline); } + } - final newPreferences = currentPrefs.copyWith( - savedHeadlines: updatedSavedHeadlines, - ); - emit(state.copyWith(preferences: newPreferences)); - await _persistPreferences(newPreferences, emit); + @override + Future close() { + _userSubscription.cancel(); + return super.close(); } } diff --git a/lib/account/bloc/account_event.dart b/lib/account/bloc/account_event.dart index 2f8ac8b1..435ea886 100644 --- a/lib/account/bloc/account_event.dart +++ b/lib/account/bloc/account_event.dart @@ -1,92 +1,58 @@ part of 'account_bloc.dart'; -/// {@template account_event} -/// Base class for Account events. -/// {@endtemplate} -sealed class AccountEvent extends Equatable { - /// {@macro account_event} +abstract class AccountEvent extends Equatable { const AccountEvent(); @override List get props => []; } -/// {@template _account_user_changed} -/// Internal event triggered when the authenticated user changes. -/// {@endtemplate} -final class _AccountUserChanged extends AccountEvent { - /// {@macro _account_user_changed} - const _AccountUserChanged({required this.user}); - - /// The current authenticated user, or null if unauthenticated. +class AccountUserChanged extends AccountEvent { // Corrected name + const AccountUserChanged(this.user); final User? user; @override List get props => [user]; } -/// {@template account_load_content_preferences_requested} -/// Event triggered when the user's content preferences need to be loaded. -/// {@endtemplate} -final class AccountLoadContentPreferencesRequested extends AccountEvent { - /// {@macro account_load_content_preferences_requested} - const AccountLoadContentPreferencesRequested({required this.userId}); - - /// The ID of the user whose content preferences should be loaded. +class AccountLoadUserPreferences extends AccountEvent { // Corrected name + const AccountLoadUserPreferences({required this.userId}); final String userId; @override List get props => [userId]; } -/// {@template account_follow_category_toggled} -/// Event triggered when a user toggles following a category. -/// {@endtemplate} -final class AccountFollowCategoryToggled extends AccountEvent { - /// {@macro account_follow_category_toggled} - const AccountFollowCategoryToggled({required this.category}); +class AccountSaveHeadlineToggled extends AccountEvent { + const AccountSaveHeadlineToggled({required this.headline}); + final Headline headline; + @override + List get props => [headline]; +} + +class AccountFollowCategoryToggled extends AccountEvent { + const AccountFollowCategoryToggled({required this.category}); final Category category; @override List get props => [category]; } -/// {@template account_follow_source_toggled} -/// Event triggered when a user toggles following a source. -/// {@endtemplate} -final class AccountFollowSourceToggled extends AccountEvent { - /// {@macro account_follow_source_toggled} +class AccountFollowSourceToggled extends AccountEvent { const AccountFollowSourceToggled({required this.source}); - final Source source; @override List get props => [source]; } -/// {@template account_follow_country_toggled} -/// Event triggered when a user toggles following a country. -/// {@endtemplate} -final class AccountFollowCountryToggled extends AccountEvent { - /// {@macro account_follow_country_toggled} - const AccountFollowCountryToggled({required this.country}); +// AccountFollowCountryToggled event correctly removed previously - final Country country; - - @override - List get props => [country]; -} - -/// {@template account_save_headline_toggled} -/// Event triggered when a user toggles saving a headline. -/// {@endtemplate} -final class AccountSaveHeadlineToggled extends AccountEvent { - /// {@macro account_save_headline_toggled} - const AccountSaveHeadlineToggled({required this.headline}); - - final Headline headline; +class AccountClearUserPreferences extends AccountEvent { + const AccountClearUserPreferences({required this.userId}); + final String userId; @override - List get props => [headline]; + List get props => [userId]; } diff --git a/lib/account/bloc/account_state.dart b/lib/account/bloc/account_state.dart index bd7d0e03..ea81052f 100644 --- a/lib/account/bloc/account_state.dart +++ b/lib/account/bloc/account_state.dart @@ -1,25 +1,8 @@ part of 'account_bloc.dart'; -/// Defines the status of the account state. -enum AccountStatus { - /// The initial state. - initial, +enum AccountStatus { initial, loading, success, failure } - /// An operation is in progress. - loading, - - /// An operation was successful. - success, - - /// An operation failed. - failure, -} - -/// {@template account_state} -/// State for the Account feature. -/// {@endtemplate} -final class AccountState extends Equatable { - /// {@macro account_state} +class AccountState extends Equatable { const AccountState({ this.status = AccountStatus.initial, this.user, @@ -27,30 +10,27 @@ final class AccountState extends Equatable { this.errorMessage, }); - /// The current status of the account state. final AccountStatus status; - - /// The currently authenticated user. final User? user; - - /// The user's content preferences. final UserContentPreferences? preferences; - - /// An error message if an operation failed. final String? errorMessage; - /// Creates a copy of this [AccountState] with the given fields replaced. AccountState copyWith({ AccountStatus? status, User? user, UserContentPreferences? preferences, String? errorMessage, + bool clearUser = false, + bool clearPreferences = false, + bool clearErrorMessage = false, }) { return AccountState( status: status ?? this.status, - user: user ?? this.user, - preferences: preferences ?? this.preferences, - errorMessage: errorMessage ?? this.errorMessage, + user: clearUser ? null : user ?? this.user, + preferences: + clearPreferences ? null : preferences ?? this.preferences, + errorMessage: + clearErrorMessage ? null : errorMessage ?? this.errorMessage, ); } diff --git a/lib/account/bloc/available_countries_bloc.dart b/lib/account/bloc/available_countries_bloc.dart deleted file mode 100644 index 39eea4c7..00000000 --- a/lib/account/bloc/available_countries_bloc.dart +++ /dev/null @@ -1,56 +0,0 @@ -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; - -part 'available_countries_event.dart'; -part 'available_countries_state.dart'; - -class AvailableCountriesBloc - extends Bloc { - AvailableCountriesBloc({ - required HtDataRepository countriesRepository, - }) : _countriesRepository = countriesRepository, - super(const AvailableCountriesState()) { - on(_onFetchAvailableCountries); - } - - final HtDataRepository _countriesRepository; - - Future _onFetchAvailableCountries( - FetchAvailableCountries event, - Emitter emit, - ) async { - if (state.status == AvailableCountriesStatus.loading || - state.status == AvailableCountriesStatus.success) { - return; - } - emit(state.copyWith(status: AvailableCountriesStatus.loading)); - try { - final response = await _countriesRepository.readAll(); - emit( - state.copyWith( - status: AvailableCountriesStatus.success, - availableCountries: response.items, - clearError: true, - ), - ); - } on HtHttpException catch (e) { - emit( - state.copyWith( - status: AvailableCountriesStatus.failure, - error: e.message, - ), - ); - } catch (e) { - emit( - state.copyWith( - status: AvailableCountriesStatus.failure, - error: 'An unexpected error occurred while fetching countries.', - ), - ); - } - } -} diff --git a/lib/account/bloc/available_countries_event.dart b/lib/account/bloc/available_countries_event.dart deleted file mode 100644 index ecad40bd..00000000 --- a/lib/account/bloc/available_countries_event.dart +++ /dev/null @@ -1,12 +0,0 @@ -part of 'available_countries_bloc.dart'; - -abstract class AvailableCountriesEvent extends Equatable { - const AvailableCountriesEvent(); - - @override - List get props => []; -} - -class FetchAvailableCountries extends AvailableCountriesEvent { - const FetchAvailableCountries(); -} diff --git a/lib/account/bloc/available_countries_state.dart b/lib/account/bloc/available_countries_state.dart deleted file mode 100644 index 68341aed..00000000 --- a/lib/account/bloc/available_countries_state.dart +++ /dev/null @@ -1,47 +0,0 @@ -part of 'available_countries_bloc.dart'; - -enum AvailableCountriesStatus { initial, loading, success, failure } - -class AvailableCountriesState extends Equatable { - const AvailableCountriesState({ - this.status = AvailableCountriesStatus.initial, - this.availableCountries = const [], - this.error, - // Properties for pagination if added later - // this.hasMore = true, - // this.cursor, - }); - - final AvailableCountriesStatus status; - final List availableCountries; - final String? error; - // final bool hasMore; - // final String? cursor; - - AvailableCountriesState copyWith({ - AvailableCountriesStatus? status, - List? availableCountries, - String? error, - bool clearError = false, - // bool? hasMore, - // String? cursor, - // bool clearCursor = false, - }) { - return AvailableCountriesState( - status: status ?? this.status, - availableCountries: availableCountries ?? this.availableCountries, - error: clearError ? null : error ?? this.error, - // hasMore: hasMore ?? this.hasMore, - // cursor: clearCursor ? null : (cursor ?? this.cursor), - ); - } - - @override - List get props => [ - status, - availableCountries, - error, - // hasMore, // Add if pagination is implemented - // cursor, // Add if pagination is implemented - ]; -} diff --git a/lib/account/view/manage_followed_items/categories/followed_categories_list_page.dart b/lib/account/view/manage_followed_items/categories/followed_categories_list_page.dart index 030a0fcf..03f433af 100644 --- a/lib/account/view/manage_followed_items/categories/followed_categories_list_page.dart +++ b/lib/account/view/manage_followed_items/categories/followed_categories_list_page.dart @@ -2,15 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ht_main/account/bloc/account_bloc.dart'; -import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Added +import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Import for Arguments import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; -import 'package:ht_main/shared/constants/app_spacing.dart'; import 'package:ht_main/shared/widgets/widgets.dart'; +import 'package:ht_shared/ht_shared.dart'; /// {@template followed_categories_list_page} -/// Displays a list of categories the user is currently following. -/// Allows unfollowing and navigating to add more categories. +/// Page to display and manage categories followed by the user. /// {@endtemplate} class FollowedCategoriesListPage extends StatelessWidget { /// {@macro followed_categories_list_page} @@ -19,14 +18,17 @@ class FollowedCategoriesListPage extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; + final followedCategories = + context.watch().state.preferences?.followedCategories ?? + []; return Scaffold( appBar: AppBar( - title: Text(l10n.followedCategoriesPageTitle), + title: const Text('Followed Categories'), // Placeholder actions: [ IconButton( icon: const Icon(Icons.add_circle_outline), - tooltip: l10n.addCategoriesTooltip, + tooltip: 'Add Category to Follow', // Placeholder onPressed: () { context.goNamed(Routes.addCategoryToFollowName); }, @@ -35,103 +37,78 @@ class FollowedCategoriesListPage extends StatelessWidget { ), body: BlocBuilder( builder: (context, state) { - if (state.status == AccountStatus.initial || - (state.status == AccountStatus.loading && - state.preferences == null)) { - return const Center(child: CircularProgressIndicator()); + if (state.status == AccountStatus.loading && + state.preferences == null) { + return LoadingStateWidget( + icon: Icons.category_outlined, + headline: 'Loading Followed Categories...', // Placeholder + subheadline: l10n.pleaseWait, // Assuming this exists + ); } if (state.status == AccountStatus.failure && state.preferences == null) { return FailureStateWidget( - message: state.errorMessage ?? l10n.unknownError, + message: state.errorMessage ?? 'Could not load followed categories.', // Placeholder onRetry: () { if (state.user?.id != null) { context.read().add( - AccountLoadContentPreferencesRequested( - userId: state.user!.id, - ), - ); + AccountLoadUserPreferences( + userId: state.user!.id, + ), + ); } }, ); } - final followedCategories = state.preferences?.followedCategories; - - if (followedCategories == null || followedCategories.isEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.category_outlined, size: 48), - const SizedBox(height: AppSpacing.md), - Text( - l10n.noFollowedCategoriesMessage, - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: AppSpacing.lg), - ElevatedButton.icon( - icon: const Icon(Icons.add_circle_outline), - label: Text(l10n.addCategoriesButtonLabel), - onPressed: () { - context.goNamed(Routes.addCategoryToFollowName); - }, - ), - ], - ), - ), + if (followedCategories.isEmpty) { + return const InitialStateWidget( + icon: Icons.no_sim_outlined, // Placeholder icon + headline: 'No Followed Categories', // Placeholder + subheadline: 'Start following categories to see them here.', // Placeholder ); } return ListView.builder( - padding: const EdgeInsets.all(AppSpacing.md), itemCount: followedCategories.length, itemBuilder: (context, index) { final category = followedCategories[index]; - return Card( - margin: const EdgeInsets.only(bottom: AppSpacing.sm), - child: ListTile( - leading: - category.iconUrl != null && - Uri.tryParse(category.iconUrl!)?.isAbsolute == - true - ? SizedBox( - width: 36, - height: 36, - child: Image.network( - category.iconUrl!, - fit: BoxFit.contain, - errorBuilder: - (context, error, stackTrace) => - const Icon(Icons.category_outlined), - ), - ) - : const Icon(Icons.category_outlined), - title: Text(category.name), - onTap: () { - // Added onTap for navigation - context.push( - Routes.categoryDetails, - extra: EntityDetailsPageArguments(entity: category), - ); + return ListTile( + leading: category.iconUrl != null + ? SizedBox( + width: 40, + height: 40, + child: Image.network( + category.iconUrl!, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.category_outlined), + ), + ) + : const Icon(Icons.category_outlined), + title: Text(category.name), + subtitle: category.description != null + ? Text( + category.description!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + : null, + trailing: IconButton( + icon: const Icon(Icons.remove_circle_outline, color: Colors.red), + tooltip: 'Unfollow Category', // Placeholder + onPressed: () { + context.read().add( + AccountFollowCategoryToggled(category: category), + ); }, - trailing: IconButton( - icon: Icon( - Icons.remove_circle_outline, - color: Theme.of(context).colorScheme.error, - ), - tooltip: l10n.unfollowCategoryTooltip(category.name), - onPressed: () { - context.read().add( - AccountFollowCategoryToggled(category: category), - ); - }, - ), ), + onTap: () { + context.push( + Routes.categoryDetails, + extra: EntityDetailsPageArguments(entity: category), + ); + }, ); }, ); diff --git a/lib/account/view/manage_followed_items/countries/add_country_to_follow_page.dart b/lib/account/view/manage_followed_items/countries/add_country_to_follow_page.dart deleted file mode 100644 index c4c2e11c..00000000 --- a/lib/account/view/manage_followed_items/countries/add_country_to_follow_page.dart +++ /dev/null @@ -1,145 +0,0 @@ -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/account/bloc/account_bloc.dart'; -import 'package:ht_main/l10n/l10n.dart'; -import 'package:ht_main/shared/constants/app_spacing.dart'; -import 'package:ht_main/shared/widgets/widgets.dart'; -import 'package:ht_shared/ht_shared.dart'; - -/// {@template add_country_to_follow_page} -/// A page that allows users to browse and select countries to follow. -/// {@endtemplate} -class AddCountryToFollowPage extends StatefulWidget { - /// {@macro add_country_to_follow_page} - const AddCountryToFollowPage({super.key}); - - @override - State createState() => _AddCountryToFollowPageState(); -} - -class _AddCountryToFollowPageState extends State { - List _allCountries = []; - bool _isLoading = true; - String? _errorMessage; - - @override - void initState() { - super.initState(); - _fetchCountries(); - } - - Future _fetchCountries() async { - setState(() { - _isLoading = true; - _errorMessage = null; - }); - try { - final countryRepository = context.read>(); - final paginatedResponse = await countryRepository.readAll(); - setState(() { - _allCountries = paginatedResponse.items; - _isLoading = false; - }); - } on HtHttpException catch (e) { - setState(() { - _isLoading = false; - _errorMessage = e.message; - }); - } catch (e) { - setState(() { - _isLoading = false; - _errorMessage = context.l10n.unknownError; - }); - } - } - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return Scaffold( - appBar: AppBar(title: Text(l10n.addCountriesPageTitle)), - body: Builder( - builder: (context) { - if (_isLoading) { - return const Center(child: CircularProgressIndicator()); - } - if (_errorMessage != null) { - return FailureStateWidget( - message: _errorMessage!, - onRetry: _fetchCountries, - ); - } - if (_allCountries.isEmpty) { - return FailureStateWidget(message: l10n.countryFilterEmptyHeadline); - } - - return BlocBuilder( - buildWhen: - (previous, current) => - previous.preferences?.followedCountries != - current.preferences?.followedCountries || - previous.status != current.status, - builder: (context, accountState) { - final followedCountries = - accountState.preferences?.followedCountries ?? []; - - return ListView.builder( - padding: const EdgeInsets.all(AppSpacing.md), - itemCount: _allCountries.length, - itemBuilder: (context, index) { - final country = _allCountries[index]; - final isFollowed = followedCountries.any( - (fc) => fc.id == country.id, - ); - - return Card( - margin: const EdgeInsets.only(bottom: AppSpacing.sm), - child: ListTile( - leading: - country.flagUrl.isNotEmpty && - Uri.tryParse(country.flagUrl)?.isAbsolute == - true - ? SizedBox( - width: 36, - height: 24, - child: Image.network( - country.flagUrl, - fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) => - const Icon(Icons.public_outlined), - ), - ) - : const Icon(Icons.public_outlined), - title: Text(country.name), - trailing: IconButton( - icon: - isFollowed - ? Icon( - Icons.check_circle, - color: Theme.of(context).colorScheme.primary, - ) - : const Icon(Icons.add_circle_outline), - tooltip: - isFollowed - ? l10n.unfollowCountryTooltip(country.name) - : l10n.followCountryTooltip(country.name), - onPressed: () { - context.read().add( - AccountFollowCountryToggled(country: country), - ); - }, - ), - ), - ); - }, - ); - }, - ); - }, - ), - ); - } -} diff --git a/lib/account/view/manage_followed_items/countries/followed_countries_list_page.dart b/lib/account/view/manage_followed_items/countries/followed_countries_list_page.dart deleted file mode 100644 index 6cf4808d..00000000 --- a/lib/account/view/manage_followed_items/countries/followed_countries_list_page.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; -import 'package:ht_main/account/bloc/account_bloc.dart'; -import 'package:ht_main/l10n/l10n.dart'; -import 'package:ht_main/router/routes.dart'; -import 'package:ht_main/shared/constants/app_spacing.dart'; -import 'package:ht_main/shared/widgets/widgets.dart'; - -/// {@template followed_countries_list_page} -/// Displays a list of countries the user is currently following. -/// Allows unfollowing and navigating to add more countries. -/// {@endtemplate} -class FollowedCountriesListPage extends StatelessWidget { - /// {@macro followed_countries_list_page} - const FollowedCountriesListPage({super.key}); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return Scaffold( - appBar: AppBar( - title: Text(l10n.followedCountriesPageTitle), - actions: [ - IconButton( - icon: const Icon(Icons.add_circle_outline), - tooltip: l10n.addCountriesTooltip, - onPressed: () { - context.goNamed(Routes.addCountryToFollowName); - }, - ), - ], - ), - body: BlocBuilder( - builder: (context, state) { - if (state.status == AccountStatus.initial || - (state.status == AccountStatus.loading && - state.preferences == null)) { - return const Center(child: CircularProgressIndicator()); - } - - if (state.status == AccountStatus.failure && - state.preferences == null) { - return FailureStateWidget( - message: state.errorMessage ?? l10n.unknownError, - onRetry: () { - if (state.user?.id != null) { - context.read().add( - AccountLoadContentPreferencesRequested( - userId: state.user!.id, - ), - ); - } - }, - ); - } - - final followedCountries = state.preferences?.followedCountries; - - if (followedCountries == null || followedCountries.isEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.public_outlined, size: 48), - const SizedBox(height: AppSpacing.md), - Text( - l10n.noFollowedCountriesMessage, - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: AppSpacing.lg), - ElevatedButton.icon( - icon: const Icon(Icons.add_circle_outline), - label: Text(l10n.addCountriesButtonLabel), - onPressed: () { - context.goNamed(Routes.addCountryToFollowName); - }, - ), - ], - ), - ), - ); - } - - return ListView.builder( - padding: const EdgeInsets.all(AppSpacing.md), - itemCount: followedCountries.length, - itemBuilder: (context, index) { - final country = followedCountries[index]; - return Card( - margin: const EdgeInsets.only(bottom: AppSpacing.sm), - child: ListTile( - leading: - country.flagUrl.isNotEmpty && - Uri.tryParse(country.flagUrl)?.isAbsolute == true - ? SizedBox( - width: 36, - height: 24, - child: Image.network( - country.flagUrl, - fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) => - const Icon(Icons.public_outlined), - ), - ) - : const Icon(Icons.public_outlined), - title: Text(country.name), - trailing: IconButton( - icon: Icon( - Icons.remove_circle_outline, - color: Theme.of(context).colorScheme.error, - ), - tooltip: l10n.unfollowCountryTooltip(country.name), - onPressed: () { - context.read().add( - AccountFollowCountryToggled(country: country), - ); - }, - ), - ), - ); - }, - ); - }, - ), - ); - } -} diff --git a/lib/account/view/manage_followed_items/manage_followed_items_page.dart b/lib/account/view/manage_followed_items/manage_followed_items_page.dart index c1cd765d..d77075db 100644 --- a/lib/account/view/manage_followed_items/manage_followed_items_page.dart +++ b/lib/account/view/manage_followed_items/manage_followed_items_page.dart @@ -43,17 +43,7 @@ class ManageFollowedItemsPage extends StatelessWidget { }, ), const Divider(indent: AppSpacing.lg, endIndent: AppSpacing.lg), - ListTile( - leading: const Icon(Icons.public_outlined), - title: Text( - l10n.headlinesFeedFilterEventCountryLabel, - ), // "Countries" - trailing: const Icon(Icons.chevron_right), - onTap: () { - context.goNamed(Routes.followedCountriesListName); - }, - ), - const Divider(indent: AppSpacing.lg, endIndent: AppSpacing.lg), + // ListTile for Followed Countries removed ], ), ); diff --git a/lib/account/view/manage_followed_items/sources/followed_sources_list_page.dart b/lib/account/view/manage_followed_items/sources/followed_sources_list_page.dart index 5d2f744c..2abbf780 100644 --- a/lib/account/view/manage_followed_items/sources/followed_sources_list_page.dart +++ b/lib/account/view/manage_followed_items/sources/followed_sources_list_page.dart @@ -2,15 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ht_main/account/bloc/account_bloc.dart'; -import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Added +import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Import for Arguments import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; -import 'package:ht_main/shared/constants/app_spacing.dart'; -import 'package:ht_main/shared/widgets/widgets.dart'; +import 'package:ht_main/shared/widgets/widgets.dart'; // For common widgets +import 'package:ht_shared/ht_shared.dart'; /// {@template followed_sources_list_page} -/// Displays a list of sources the user is currently following. -/// Allows unfollowing and navigating to add more sources. +/// Page to display and manage sources followed by the user. /// {@endtemplate} class FollowedSourcesListPage extends StatelessWidget { /// {@macro followed_sources_list_page} @@ -19,14 +18,16 @@ class FollowedSourcesListPage extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; + final followedSources = + context.watch().state.preferences?.followedSources ?? []; return Scaffold( appBar: AppBar( - title: Text(l10n.followedSourcesPageTitle), + title: const Text('Followed Sources'), // Placeholder actions: [ IconButton( icon: const Icon(Icons.add_circle_outline), - tooltip: l10n.addSourcesTooltip, + tooltip: 'Add Source to Follow', // Placeholder onPressed: () { context.goNamed(Routes.addSourceToFollowName); }, @@ -35,87 +36,68 @@ class FollowedSourcesListPage extends StatelessWidget { ), body: BlocBuilder( builder: (context, state) { - if (state.status == AccountStatus.initial || - (state.status == AccountStatus.loading && - state.preferences == null)) { - return const Center(child: CircularProgressIndicator()); + if (state.status == AccountStatus.loading && + state.preferences == null) { + return LoadingStateWidget( + icon: Icons.source_outlined, + headline: 'Loading Followed Sources...', // Placeholder + subheadline: l10n.pleaseWait, // Assuming this exists + ); } if (state.status == AccountStatus.failure && state.preferences == null) { return FailureStateWidget( - message: state.errorMessage ?? l10n.unknownError, + message: state.errorMessage ?? 'Could not load followed sources.', // Placeholder onRetry: () { if (state.user?.id != null) { context.read().add( - AccountLoadContentPreferencesRequested( - userId: state.user!.id, - ), - ); + AccountLoadUserPreferences( // Corrected event name + userId: state.user!.id, + ), + ); } }, ); } - final followedSources = state.preferences?.followedSources; - - if (followedSources == null || followedSources.isEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.source_outlined, size: 48), - const SizedBox(height: AppSpacing.md), - Text( - l10n.noFollowedSourcesMessage, - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: AppSpacing.lg), - ElevatedButton.icon( - icon: const Icon(Icons.add_circle_outline), - label: Text(l10n.addSourcesButtonLabel), - onPressed: () { - context.goNamed(Routes.addSourceToFollowName); - }, - ), - ], - ), - ), + if (followedSources.isEmpty) { + return const InitialStateWidget( + icon: Icons.no_sim_outlined, // Placeholder icon + headline: 'No Followed Sources', // Placeholder + subheadline: 'Start following sources to see them here.', // Placeholder ); } return ListView.builder( - padding: const EdgeInsets.all(AppSpacing.md), itemCount: followedSources.length, itemBuilder: (context, index) { final source = followedSources[index]; - return Card( - margin: const EdgeInsets.only(bottom: AppSpacing.sm), - child: ListTile( - title: Text(source.name), - onTap: () { - // Added onTap for navigation - context.push( - Routes.sourceDetails, - extra: EntityDetailsPageArguments(entity: source), - ); + return ListTile( + leading: const Icon(Icons.source_outlined), // Generic icon + title: Text(source.name), + subtitle: source.description != null + ? Text( + source.description!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + : null, + trailing: IconButton( + icon: const Icon(Icons.remove_circle_outline, color: Colors.red), + tooltip: 'Unfollow Source', // Placeholder + onPressed: () { + context.read().add( + AccountFollowSourceToggled(source: source), + ); }, - trailing: IconButton( - icon: Icon( - Icons.remove_circle_outline, - color: Theme.of(context).colorScheme.error, - ), - tooltip: l10n.unfollowSourceTooltip(source.name), - onPressed: () { - context.read().add( - AccountFollowSourceToggled(source: source), - ); - }, - ), ), + onTap: () { + context.push( + Routes.sourceDetails, // Navigate to source details + extra: EntityDetailsPageArguments(entity: source), + ); + }, ); }, ); diff --git a/lib/account/view/saved_headlines_page.dart b/lib/account/view/saved_headlines_page.dart index 66e2af2a..a1db6822 100644 --- a/lib/account/view/saved_headlines_page.dart +++ b/lib/account/view/saved_headlines_page.dart @@ -28,8 +28,8 @@ class SavedHeadlinesPage extends StatelessWidget { appBar: AppBar(title: Text(l10n.accountSavedHeadlinesTile)), body: BlocBuilder( builder: (context, state) { - if (state.status == AccountStatus.loading && - state.preferences == null) { + // Initial load or loading state for preferences + if (state.status == AccountStatus.loading && state.preferences == null) { return const LoadingStateWidget( icon: Icons.bookmarks_outlined, headline: 'Loading Saved Headlines...', // Placeholder @@ -38,8 +38,8 @@ class SavedHeadlinesPage extends StatelessWidget { ); } - if (state.status == AccountStatus.failure && - state.preferences == null) { + // Failure to load preferences + if (state.status == AccountStatus.failure && state.preferences == null) { return FailureStateWidget( message: state.errorMessage ?? @@ -47,10 +47,10 @@ class SavedHeadlinesPage extends StatelessWidget { onRetry: () { if (state.user?.id != null) { context.read().add( - AccountLoadContentPreferencesRequested( - userId: state.user!.id, - ), - ); + AccountLoadUserPreferences( // Corrected event name + userId: state.user!.id, + ), + ); } }, ); @@ -59,11 +59,11 @@ class SavedHeadlinesPage extends StatelessWidget { final savedHeadlines = state.preferences?.savedHeadlines ?? []; if (savedHeadlines.isEmpty) { - return const InitialStateWidget( + return const InitialStateWidget( icon: Icons.bookmark_add_outlined, - headline: 'No Saved Headlines', // Placeholder + headline: 'No Saved Headlines', // Placeholder - Reverted subheadline: - "You haven't saved any articles yet. Start exploring!", // Placeholder + "You haven't saved any articles yet. Start exploring!", // Placeholder - Reverted ); } @@ -103,6 +103,7 @@ class SavedHeadlinesPage extends StatelessWidget { ), trailing: trailingButton, ); + break; case HeadlineImageStyle.smallThumbnail: tile = HeadlineTileImageStart( headline: headline, @@ -114,6 +115,7 @@ class SavedHeadlinesPage extends StatelessWidget { ), trailing: trailingButton, ); + break; case HeadlineImageStyle.largeThumbnail: tile = HeadlineTileImageTop( headline: headline, @@ -125,6 +127,7 @@ class SavedHeadlinesPage extends StatelessWidget { ), trailing: trailingButton, ); + break; } return tile; }, diff --git a/lib/headline-details/view/headline_details_page.dart b/lib/headline-details/view/headline_details_page.dart index ef5b2826..62401526 100644 --- a/lib/headline-details/view/headline_details_page.dart +++ b/lib/headline-details/view/headline_details_page.dart @@ -400,11 +400,10 @@ class _HeadlineDetailsPageState extends State { if (headline.source != null) { chips.add( GestureDetector( - // Added GestureDetector onTap: () { context.push( Routes.sourceDetails, - extra: EntityDetailsPageArguments(entity: headline.source), + extra: EntityDetailsPageArguments(entity: headline.source!), ); }, child: Chip( @@ -445,35 +444,15 @@ class _HeadlineDetailsPageState extends State { ); } - if (headline.source?.headquarters != null) { - final country = headline.source!.headquarters!; - chips.add( - Chip( - // Country chip is usually not tappable to a details page in this context - avatar: CircleAvatar( - radius: chipAvatarSize / 2, - backgroundColor: Colors.transparent, - backgroundImage: NetworkImage(country.flagUrl), - onBackgroundImageError: (exception, stackTrace) {}, - ), - label: Text(country.name), - labelStyle: chipLabelStyle, - backgroundColor: chipBackgroundColor, - padding: chipPadding, - visualDensity: chipVisualDensity, - materialTapTargetSize: chipMaterialTapTargetSize, - ), - ); - } + // Country chip for headline.source.headquarters removed. if (headline.category != null) { chips.add( GestureDetector( - // Added GestureDetector onTap: () { context.push( Routes.categoryDetails, - extra: EntityDetailsPageArguments(entity: headline.category), + extra: EntityDetailsPageArguments(entity: headline.category!), ); }, child: Chip( @@ -551,6 +530,7 @@ class _HeadlineDetailsPageState extends State { extra: similarHeadline, ), ); + break; case HeadlineImageStyle.smallThumbnail: tile = HeadlineTileImageStart( headline: similarHeadline, @@ -561,6 +541,7 @@ class _HeadlineDetailsPageState extends State { extra: similarHeadline, ), ); + break; case HeadlineImageStyle.largeThumbnail: tile = HeadlineTileImageTop( headline: similarHeadline, @@ -571,12 +552,13 @@ class _HeadlineDetailsPageState extends State { extra: similarHeadline, ), ); + break; } return tile; }, ), ); - }, childCount: loadedState.similarHeadlines.length,), + }, childCount: loadedState.similarHeadlines.length), ), _ => const SliverToBoxAdapter(child: SizedBox.shrink()), }; diff --git a/lib/headlines-search/bloc/headlines_search_bloc.dart b/lib/headlines-search/bloc/headlines_search_bloc.dart index 14d6c39f..d14a52e8 100644 --- a/lib/headlines-search/bloc/headlines_search_bloc.dart +++ b/lib/headlines-search/bloc/headlines_search_bloc.dart @@ -3,7 +3,7 @@ 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 +import 'package:ht_shared/ht_shared.dart' show Headline, Category, Source, HtHttpException, PaginatedResponse; // Shared models part 'headlines_search_event.dart'; part 'headlines_search_state.dart'; @@ -14,11 +14,11 @@ class HeadlinesSearchBloc required HtDataRepository headlinesRepository, required HtDataRepository categoryRepository, required HtDataRepository sourceRepository, - required HtDataRepository countryRepository, + // required HtDataRepository countryRepository, // Removed }) : _headlinesRepository = headlinesRepository, _categoryRepository = categoryRepository, _sourceRepository = sourceRepository, - _countryRepository = countryRepository, + // _countryRepository = countryRepository, // Removed super(const HeadlinesSearchInitial()) { on(_onHeadlinesSearchModelTypeChanged); on( @@ -30,7 +30,7 @@ class HeadlinesSearchBloc final HtDataRepository _headlinesRepository; final HtDataRepository _categoryRepository; final HtDataRepository _sourceRepository; - final HtDataRepository _countryRepository; + // final HtDataRepository _countryRepository; // Removed static const _limit = 10; Future _onHeadlinesSearchModelTypeChanged( @@ -49,11 +49,6 @@ class HeadlinesSearchBloc : null; emit(HeadlinesSearchInitial(selectedModelType: event.newModelType)); - - // Removed automatic re-search: - // if (currentSearchTerm != null && currentSearchTerm.isNotEmpty) { - // add(HeadlinesSearchFetchRequested(searchTerm: currentSearchTerm)); - // } } Future _onSearchFetchRequested( @@ -103,12 +98,12 @@ class HeadlinesSearchBloc limit: _limit, startAfterId: successState.cursor, ); - case SearchModelType.country: - response = await _countryRepository.readAllByQuery( - {'q': searchTerm, 'model': modelType.toJson()}, - limit: _limit, - startAfterId: successState.cursor, - ); + // case SearchModelType.country: // Removed + // response = await _countryRepository.readAllByQuery( + // {'q': searchTerm, 'model': modelType.toJson()}, + // limit: _limit, + // startAfterId: successState.cursor, + // ); } emit( successState.copyWith( @@ -154,11 +149,11 @@ class HeadlinesSearchBloc 'q': searchTerm, 'model': modelType.toJson(), }, limit: _limit,); - case SearchModelType.country: - response = await _countryRepository.readAllByQuery({ - 'q': searchTerm, - 'model': modelType.toJson(), - }, limit: _limit,); + // case SearchModelType.country: // Removed + // response = await _countryRepository.readAllByQuery({ + // 'q': searchTerm, + // 'model': modelType.toJson(), + // }, limit: _limit,); } emit( HeadlinesSearchSuccess( diff --git a/lib/headlines-search/models/search_model_type.dart b/lib/headlines-search/models/search_model_type.dart index de508d5d..386cb1de 100644 --- a/lib/headlines-search/models/search_model_type.dart +++ b/lib/headlines-search/models/search_model_type.dart @@ -2,7 +2,7 @@ enum SearchModelType { headline, category, - country, + // country, // Removed source; /// Returns a user-friendly display name for the enum value. @@ -16,8 +16,8 @@ enum SearchModelType { return 'Headlines'; case SearchModelType.category: return 'Categories'; - case SearchModelType.country: - return 'Countries'; + // case SearchModelType.country: // Removed + // return 'Countries'; // Removed case SearchModelType.source: return 'Sources'; } diff --git a/lib/headlines-search/view/headlines_search_page.dart b/lib/headlines-search/view/headlines_search_page.dart index 93a39d7f..aa418d9a 100644 --- a/lib/headlines-search/view/headlines_search_page.dart +++ b/lib/headlines-search/view/headlines_search_page.dart @@ -10,13 +10,14 @@ import 'package:ht_main/headlines-search/bloc/headlines_search_bloc.dart'; import 'package:ht_main/headlines-search/models/search_model_type.dart'; // 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/country_item_widget.dart'; // Removed import 'package:ht_main/headlines-search/widgets/source_item_widget.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; import 'package:ht_main/shared/constants/app_spacing.dart'; import 'package:ht_main/shared/shared.dart'; // Imports new headline tiles -import 'package:ht_shared/ht_shared.dart'; +// Adjusted imports to only include what's necessary after country removal +import 'package:ht_shared/ht_shared.dart' show Category, Headline, Source, HeadlineImageStyle, SearchModelType; /// Page widget responsible for providing the BLoC for the headlines search feature. class HeadlinesSearchPage extends StatelessWidget { @@ -58,8 +59,10 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { _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. + // Ensure _selectedModelType is valid (it should be, as .country is removed from enum) + if (!SearchModelType.values.contains(_selectedModelType)) { + _selectedModelType = SearchModelType.headline; + } context.read().add( HeadlinesSearchModelTypeChanged(_selectedModelType), ); @@ -102,21 +105,36 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { final colorScheme = theme.colorScheme; final appBarTheme = theme.appBarTheme; + // Use all values from SearchModelType as .country is already removed from the enum itself + final availableSearchModelTypes = SearchModelType.values.toList(); + + // Ensure _selectedModelType is still valid if it somehow was .country + // (though this shouldn't happen if initState logic is correct and enum is updated) + if (!availableSearchModelTypes.contains(_selectedModelType)) { + _selectedModelType = SearchModelType.headline; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + context.read().add( + HeadlinesSearchModelTypeChanged(_selectedModelType), + ); + } + }); + } + return Scaffold( appBar: AppBar( - // Removed leading and leadingWidth - titleSpacing: AppSpacing.paddingSmall, // Adjust title spacing if needed + titleSpacing: AppSpacing.paddingSmall, title: Row( children: [ SizedBox( - width: 140, // Constrain dropdown width + width: 140, child: DropdownButtonFormField( value: _selectedModelType, decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.symmetric( - horizontal: AppSpacing.xs, // Minimal horizontal padding - vertical: AppSpacing.xs, // Reduce vertical padding + horizontal: AppSpacing.xs, + vertical: AppSpacing.xs, ), ), style: theme.textTheme.titleMedium?.copyWith( @@ -129,30 +147,30 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { 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; - 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(), + items: availableSearchModelTypes.map((SearchModelType type) { + String displayLocalizedName; + // The switch is now exhaustive as SearchModelType.country is removed from the enum + switch (type) { + case SearchModelType.headline: + displayLocalizedName = l10n.searchModelTypeHeadline; + break; + case SearchModelType.category: + displayLocalizedName = l10n.searchModelTypeCategory; + break; + case SearchModelType.source: + displayLocalizedName = l10n.searchModelTypeSource; + break; + } + return DropdownMenuItem( + value: type, + child: Text( + displayLocalizedName, + style: theme.textTheme.titleMedium?.copyWith( + color: colorScheme.onSurface, + ), + ), + ); + }).toList(), onChanged: (SearchModelType? newValue) { if (newValue != null) { setState(() { @@ -165,9 +183,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { }, ), ), - const SizedBox( - width: AppSpacing.sm, - ), // Spacing between dropdown and textfield + const SizedBox(width: AppSpacing.sm), Expanded( child: TextField( controller: _textController, @@ -184,9 +200,7 @@ 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, ), suffixIcon: _showClearButton @@ -218,16 +232,13 @@ 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, + headline: l10n.searchPageInitialHeadline, + subheadline: l10n.searchPageInitialSubheadline, ), - // Use more generic loading text or existing keys HeadlinesSearchLoading() => InitialStateWidget( icon: Icons.manage_search, - headline: - l10n.headlinesFeedLoadingHeadline, // Re-use feed loading + headline: l10n.headlinesFeedLoadingHeadline, subheadline: 'Searching ${state.selectedModelType.displayName.toLowerCase()}...', ), @@ -255,24 +266,19 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ) : ListView.separated( controller: _scrollController, - padding: const EdgeInsets.all( - AppSpacing.paddingMedium, - ), // Add overall padding + padding: const EdgeInsets.all(AppSpacing.paddingMedium), itemCount: hasMore ? results.length + 1 : results.length, separatorBuilder: - (context, index) => const SizedBox( - height: AppSpacing.md, - ), // Add separator + (context, index) => const SizedBox(height: AppSpacing.md), itemBuilder: (context, index) { if (index >= results.length) { return const Padding( - padding: EdgeInsets.symmetric( - vertical: AppSpacing.lg, - ), // Adjusted padding for loader + padding: EdgeInsets.symmetric(vertical: AppSpacing.lg), child: Center(child: CircularProgressIndicator()), ); } final item = results[index]; + // The switch is now exhaustive for the remaining SearchModelType values switch (resultsModelType) { case SearchModelType.headline: final headline = item as Headline; @@ -321,8 +327,6 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { return CategoryItemWidget(category: item as Category); case SearchModelType.source: return SourceItemWidget(source: item as Source); - case SearchModelType.country: - return CountryItemWidget(country: item as Country); } }, ), @@ -339,7 +343,6 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { HeadlinesSearchFetchRequested(searchTerm: lastSearchTerm), ), ), - // Add default case for exhaustiveness _ => const SizedBox.shrink(), }; }, @@ -351,6 +354,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { SearchModelType modelType, AppLocalizations l10n, ) { + // The switch is now exhaustive for the remaining SearchModelType values switch (modelType) { case SearchModelType.headline: return l10n.searchHintTextHeadline; @@ -358,8 +362,6 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { return l10n.searchHintTextCategory; case SearchModelType.source: return l10n.searchHintTextSource; - case SearchModelType.country: - return l10n.searchHintTextCountry; } } } diff --git a/lib/headlines-search/widgets/country_item_widget.dart b/lib/headlines-search/widgets/country_item_widget.dart deleted file mode 100644 index 0e87f6ae..00000000 --- a/lib/headlines-search/widgets/country_item_widget.dart +++ /dev/null @@ -1,36 +0,0 @@ -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}')), - ); - }, - ); - } -} diff --git a/lib/router/router.dart b/lib/router/router.dart index 324f5eee..a7402ba7 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -7,8 +7,8 @@ import 'package:ht_main/account/bloc/account_bloc.dart'; import 'package:ht_main/account/view/account_page.dart'; import 'package:ht_main/account/view/manage_followed_items/categories/add_category_to_follow_page.dart'; // New import 'package:ht_main/account/view/manage_followed_items/categories/followed_categories_list_page.dart'; // New -import 'package:ht_main/account/view/manage_followed_items/countries/add_country_to_follow_page.dart'; // New -import 'package:ht_main/account/view/manage_followed_items/countries/followed_countries_list_page.dart'; // New +// import 'package:ht_main/account/view/manage_followed_items/countries/add_country_to_follow_page.dart'; // Removed +// import 'package:ht_main/account/view/manage_followed_items/countries/followed_countries_list_page.dart'; // Removed import 'package:ht_main/account/view/manage_followed_items/manage_followed_items_page.dart'; // New import 'package:ht_main/account/view/manage_followed_items/sources/add_source_to_follow_page.dart'; // New import 'package:ht_main/account/view/manage_followed_items/sources/followed_sources_list_page.dart'; // New @@ -435,8 +435,8 @@ GoRouter createRouter({ context.read>(), sourceRepository: context.read>(), - countryRepository: - context.read>(), + // countryRepository: // Removed + // context.read>(), // Removed ), ), // Removed separate AccountBloc creation here @@ -788,22 +788,7 @@ GoRouter createRouter({ ), ], ), - GoRoute( - path: Routes.followedCountriesList, - name: Routes.followedCountriesListName, - builder: - (context, state) => - const FollowedCountriesListPage(), - routes: [ - GoRoute( - path: Routes.addCountryToFollow, - name: Routes.addCountryToFollowName, - builder: - (context, state) => - const AddCountryToFollowPage(), - ), - ], - ), + // GoRoute for followedCountriesList removed ], ), GoRoute( diff --git a/lib/router/routes.dart b/lib/router/routes.dart index c667b1d7..39a2d27e 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -113,8 +113,8 @@ abstract final class Routes { static const addSourceToFollow = 'add-source'; static const addSourceToFollowName = 'addSourceToFollow'; - static const followedCountriesList = 'countries'; - static const followedCountriesListName = 'followedCountriesList'; - static const addCountryToFollow = 'add-country'; - static const addCountryToFollowName = 'addCountryToFollow'; + // static const followedCountriesList = 'countries'; // Removed + // static const followedCountriesListName = 'followedCountriesList'; // Removed + // static const addCountryToFollow = 'add-country'; // Removed + // static const addCountryToFollowName = 'addCountryToFollow'; // Removed }