diff --git a/analysis_options.yaml b/analysis_options.yaml index 2f5f0fb0..47584577 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,9 +3,12 @@ analyzer: avoid_catches_without_on_clauses: ignore avoid_print: ignore avoid_redundant_argument_values: ignore + comment_references: ignore + deprecated_member_use: ignore document_ignores: ignore flutter_style_todos: ignore lines_longer_than_80_chars: ignore + prefer_asserts_with_message: ignore use_if_null_to_convert_nulls_to_bools: ignore include: package:very_good_analysis/analysis_options.7.0.0.yaml linter: diff --git a/lib/account/bloc/account_bloc.dart b/lib/account/bloc/account_bloc.dart index f895ae78..5444657f 100644 --- a/lib/account/bloc/account_bloc.dart +++ b/lib/account/bloc/account_bloc.dart @@ -4,6 +4,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:ht_auth_repository/ht_auth_repository.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_main/app/config/config.dart' as local_config; import 'package:ht_shared/ht_shared.dart'; part 'account_event.dart'; @@ -14,8 +15,10 @@ class AccountBloc extends Bloc { required HtAuthRepository authenticationRepository, required HtDataRepository userContentPreferencesRepository, + required local_config.AppEnvironment environment, }) : _authenticationRepository = authenticationRepository, _userContentPreferencesRepository = userContentPreferencesRepository, + _environment = environment, super(const AccountState()) { // Listen to user changes from HtAuthRepository _userSubscription = _authenticationRepository.authStateChanges.listen(( @@ -37,6 +40,7 @@ class AccountBloc extends Bloc { final HtAuthRepository _authenticationRepository; final HtDataRepository _userContentPreferencesRepository; + final local_config.AppEnvironment _environment; late StreamSubscription _userSubscription; Future _onAccountUserChanged( @@ -62,7 +66,7 @@ class AccountBloc extends Bloc { try { final preferences = await _userContentPreferencesRepository.read( id: event.userId, - userId: event.userId, // Scope to the current user + userId: event.userId, ); emit( state.copyWith( @@ -72,7 +76,39 @@ class AccountBloc extends Bloc { ), ); } on NotFoundException { - // If preferences not found, create a default one for the user + // In demo mode, a short delay is introduced here to mitigate a race + // condition during anonymous to authenticated data migration. + // This ensures that the DemoDataMigrationService has a chance to + // complete its migration of UserContentPreferences before AccountBloc + // attempts to create a new default preference for the authenticated user. + // This is a temporary stub for the demo environment only and is not + // needed in production/development where backend handles migration. + if (_environment == local_config.AppEnvironment.demo) { + // ignore: inference_failure_on_instance_creation + await Future.delayed(const Duration(seconds: 1)); + // After delay, re-attempt to read the preferences. + // This is crucial because migration might have completed during the delay. + try { + final migratedPreferences = await _userContentPreferencesRepository + .read(id: event.userId, userId: event.userId); + emit( + state.copyWith( + status: AccountStatus.success, + preferences: migratedPreferences, + clearErrorMessage: true, + ), + ); + return; // Exit if successfully read after migration + } on NotFoundException { + // Still not found after delay, proceed to create default. + print( + '[AccountBloc] UserContentPreferences still not found after ' + 'migration delay. Creating default preferences.', + ); + } + } + // If preferences not found (either initially or after re-attempt), + // create a default one for the user. final defaultPreferences = UserContentPreferences(id: event.userId); try { await _userContentPreferencesRepository.create( @@ -86,11 +122,29 @@ class AccountBloc extends Bloc { clearErrorMessage: true, ), ); + } on ConflictException { + // If a conflict occurs during creation (e.g., another process + // created it concurrently), attempt to read it again to get the + // existing one. This can happen if the migration service + // created it right after the second NotFoundException. + print( + '[AccountBloc] Conflict during creation of UserContentPreferences. ' + 'Attempting to re-read.', + ); + final existingPreferences = await _userContentPreferencesRepository + .read(id: event.userId, userId: event.userId); + emit( + state.copyWith( + status: AccountStatus.success, + preferences: existingPreferences, + clearErrorMessage: true, + ), + ); } catch (e) { emit( state.copyWith( status: AccountStatus.failure, - errorMessage: 'Failed to create default preferences.', + errorMessage: 'Failed to create default preferences: $e', ), ); } diff --git a/lib/account/bloc/available_sources_bloc.dart b/lib/account/bloc/available_sources_bloc.dart index 3db144f2..a5670bec 100644 --- a/lib/account/bloc/available_sources_bloc.dart +++ b/lib/account/bloc/available_sources_bloc.dart @@ -35,14 +35,14 @@ class AvailableSourcesBloc // Assuming readAll without parameters fetches all items. // Add pagination if necessary for very large datasets. final response = await _sourcesRepository.readAll( - // limit: _sourcesLimit, // Uncomment if pagination is needed + // limit: _sourcesLimit, ); emit( state.copyWith( status: AvailableSourcesStatus.success, availableSources: response.items, - // hasMore: response.hasMore, // Uncomment if pagination is needed - // cursor: response.cursor, // Uncomment if pagination is needed + // hasMore: response.hasMore, + // cursor: response.cursor, clearError: true, ), ); diff --git a/lib/account/bloc/available_sources_state.dart b/lib/account/bloc/available_sources_state.dart index 6fd451b8..ee830b46 100644 --- a/lib/account/bloc/available_sources_state.dart +++ b/lib/account/bloc/available_sources_state.dart @@ -41,7 +41,7 @@ class AvailableSourcesState extends Equatable { status, availableSources, error, - // hasMore, // Add if pagination is implemented - // cursor, // Add if pagination is implemented + // hasMore, + // cursor, ]; } diff --git a/lib/account/view/account_page.dart b/lib/account/view/account_page.dart index b4582faa..5fc04216 100644 --- a/lib/account/view/account_page.dart +++ b/lib/account/view/account_page.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ht_main/app/bloc/app_bloc.dart'; -import 'package:ht_main/authentication/bloc/authentication_bloc.dart'; // Import AuthenticationBloc +import 'package:ht_main/authentication/bloc/authentication_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_shared/ht_shared.dart'; // Import User and AppStatus +import 'package:ht_shared/ht_shared.dart'; /// {@template account_view} /// Displays the user's account information and actions. @@ -24,23 +24,18 @@ class AccountPage extends StatelessWidget { final user = appState.user; final status = appState.status; final isAnonymous = status == AppStatus.anonymous; - final theme = Theme.of(context); // Get theme for AppBar - final textTheme = theme.textTheme; // Get textTheme for AppBar + final theme = Theme.of(context); + final textTheme = theme.textTheme; return Scaffold( appBar: AppBar( - title: Text( - l10n.accountPageTitle, - style: textTheme.titleLarge, // Consistent AppBar title style - ), + title: Text(l10n.accountPageTitle, style: textTheme.titleLarge), ), body: ListView( - padding: const EdgeInsets.all( - AppSpacing.paddingMedium, - ), // Adjusted padding + padding: const EdgeInsets.all(AppSpacing.paddingMedium), children: [ _buildUserHeader(context, user, isAnonymous), - const SizedBox(height: AppSpacing.lg), // Adjusted spacing + const SizedBox(height: AppSpacing.lg), ListTile( leading: Icon( Icons.tune_outlined, @@ -91,11 +86,11 @@ class AccountPage extends StatelessWidget { final l10n = context.l10n; final theme = Theme.of(context); final textTheme = theme.textTheme; - final colorScheme = theme.colorScheme; // Get colorScheme + final colorScheme = theme.colorScheme; final avatarIcon = Icon( - Icons.person_outline, // Use outlined version - size: AppSpacing.xxl, // Standardized size + Icons.person_outline, + size: AppSpacing.xxl, color: colorScheme.onPrimaryContainer, ); @@ -105,7 +100,7 @@ class AccountPage extends StatelessWidget { if (isAnonymous) { displayName = l10n.accountAnonymousUser; statusWidget = Padding( - padding: const EdgeInsets.only(top: AppSpacing.md), // Increased padding + padding: const EdgeInsets.only(top: AppSpacing.md), child: ElevatedButton.icon( // Changed to ElevatedButton icon: const Icon(Icons.link_outlined), @@ -128,7 +123,7 @@ class AccountPage extends StatelessWidget { } else { displayName = user?.email ?? l10n.accountNoNameUser; statusWidget = Column( - mainAxisSize: MainAxisSize.min, // To keep column tight + mainAxisSize: MainAxisSize.min, children: [ // if (user?.role != null) ...[ // // Show role only if available @@ -141,7 +136,7 @@ class AccountPage extends StatelessWidget { // textAlign: TextAlign.center, // ), // ], - const SizedBox(height: AppSpacing.md), // Consistent spacing + const SizedBox(height: AppSpacing.md), OutlinedButton.icon( // Changed to OutlinedButton.icon icon: Icon(Icons.logout, color: colorScheme.error), @@ -168,14 +163,14 @@ class AccountPage extends StatelessWidget { return Column( children: [ CircleAvatar( - radius: AppSpacing.xxl - AppSpacing.sm, // Standardized radius (40) + radius: AppSpacing.xxl - AppSpacing.sm, backgroundColor: colorScheme.primaryContainer, child: avatarIcon, ), - const SizedBox(height: AppSpacing.md), // Adjusted spacing + const SizedBox(height: AppSpacing.md), Text( displayName, - style: textTheme.headlineSmall, // More prominent style + style: textTheme.headlineSmall, textAlign: TextAlign.center, ), statusWidget, diff --git a/lib/account/view/manage_followed_items/categories/add_category_to_follow_page.dart b/lib/account/view/manage_followed_items/categories/add_category_to_follow_page.dart index 24f9e25a..113caac0 100644 --- a/lib/account/view/manage_followed_items/categories/add_category_to_follow_page.dart +++ b/lib/account/view/manage_followed_items/categories/add_category_to_follow_page.dart @@ -18,8 +18,8 @@ class AddCategoryToFollowPage extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - final theme = Theme.of(context); // Get theme - final textTheme = theme.textTheme; // Get textTheme + final theme = Theme.of(context); + final textTheme = theme.textTheme; return BlocProvider( create: (context) => CategoriesFilterBloc( @@ -27,10 +27,7 @@ class AddCategoryToFollowPage extends StatelessWidget { )..add(CategoriesFilterRequested()), child: Scaffold( appBar: AppBar( - title: Text( - l10n.addCategoriesPageTitle, - style: textTheme.titleLarge, // Consistent AppBar title - ), + title: Text(l10n.addCategoriesPageTitle, style: textTheme.titleLarge), ), body: BlocBuilder( builder: (context, categoriesState) { @@ -86,14 +83,11 @@ class AddCategoryToFollowPage extends StatelessWidget { accountState.preferences?.followedCategories ?? []; return ListView.builder( - padding: - const EdgeInsets.symmetric( - // Consistent padding - horizontal: AppSpacing.paddingMedium, - vertical: AppSpacing.paddingSmall, - ).copyWith( - bottom: AppSpacing.xxl, - ), // Ensure bottom space for loader + padding: const EdgeInsets.symmetric( + // Consistent padding + horizontal: AppSpacing.paddingMedium, + vertical: AppSpacing.paddingSmall, + ).copyWith(bottom: AppSpacing.xxl), itemCount: categories.length + (isLoadingMore ? 1 : 0), itemBuilder: (context, index) { if (index == categories.length && isLoadingMore) { @@ -114,7 +108,7 @@ class AddCategoryToFollowPage extends StatelessWidget { return Card( margin: const EdgeInsets.only(bottom: AppSpacing.sm), - elevation: 0.5, // Subtle elevation + elevation: 0.5, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppSpacing.sm), side: BorderSide( @@ -124,7 +118,7 @@ class AddCategoryToFollowPage extends StatelessWidget { child: ListTile( leading: SizedBox( // Standardized leading icon/image size - width: AppSpacing.xl + AppSpacing.xs, // 36 + width: AppSpacing.xl + AppSpacing.xs, height: AppSpacing.xl + AppSpacing.xs, child: category.iconUrl != null && 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 e2e6da30..18bb44d5 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,7 +2,7 @@ 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'; // Import for Arguments +import 'package:ht_main/entity_details/view/entity_details_page.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; import 'package:ht_main/shared/widgets/widgets.dart'; @@ -23,11 +23,11 @@ class FollowedCategoriesListPage extends StatelessWidget { return Scaffold( appBar: AppBar( - title: const Text('Followed Categories'), // Placeholder + title: const Text('Followed Categories'), actions: [ IconButton( icon: const Icon(Icons.add_circle_outline), - tooltip: 'Add Category to Follow', // Placeholder + tooltip: 'Add Category to Follow', onPressed: () { context.goNamed(Routes.addCategoryToFollowName); }, @@ -40,8 +40,8 @@ class FollowedCategoriesListPage extends StatelessWidget { state.preferences == null) { return LoadingStateWidget( icon: Icons.category_outlined, - headline: 'Loading Followed Categories...', // Placeholder - subheadline: l10n.pleaseWait, // Assuming this exists + headline: 'Loading Followed Categories...', + subheadline: l10n.pleaseWait, ); } @@ -49,8 +49,7 @@ class FollowedCategoriesListPage extends StatelessWidget { state.preferences == null) { return FailureStateWidget( message: - state.errorMessage ?? - 'Could not load followed categories.', // Placeholder + state.errorMessage ?? 'Could not load followed categories.', onRetry: () { if (state.user?.id != null) { context.read().add( @@ -63,10 +62,9 @@ class FollowedCategoriesListPage extends StatelessWidget { 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 + icon: Icons.no_sim_outlined, + headline: 'No Followed Categories', + subheadline: 'Start following categories to see them here.', ); } @@ -99,7 +97,7 @@ class FollowedCategoriesListPage extends StatelessWidget { Icons.remove_circle_outline, color: Colors.red, ), - tooltip: 'Unfollow Category', // Placeholder + tooltip: 'Unfollow Category', onPressed: () { context.read().add( AccountFollowCategoryToggled(category: category), 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 6b627385..2e8ec39c 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 @@ -23,18 +23,16 @@ class ManageFollowedItemsPage extends StatelessWidget { appBar: AppBar( title: Text( l10n.accountContentPreferencesTile, - style: textTheme.titleLarge, // Consistent AppBar title style + style: textTheme.titleLarge, ), ), body: ListView( - padding: const EdgeInsets.symmetric( - vertical: AppSpacing.paddingSmall, - ), // Adjusted padding + padding: const EdgeInsets.symmetric(vertical: AppSpacing.paddingSmall), children: [ ListTile( leading: Icon(Icons.category_outlined, color: colorScheme.primary), title: Text( - l10n.headlinesFeedFilterCategoryLabel, // "Categories" + l10n.headlinesFeedFilterCategoryLabel, style: textTheme.titleMedium, ), trailing: const Icon(Icons.chevron_right), @@ -43,13 +41,13 @@ class ManageFollowedItemsPage extends StatelessWidget { }, ), const Divider( - indent: AppSpacing.paddingMedium, // Consistent indent + indent: AppSpacing.paddingMedium, endIndent: AppSpacing.paddingMedium, ), ListTile( leading: Icon(Icons.source_outlined, color: colorScheme.primary), title: Text( - l10n.headlinesFeedFilterSourceLabel, // "Sources" + l10n.headlinesFeedFilterSourceLabel, style: textTheme.titleMedium, ), trailing: const Icon(Icons.chevron_right), @@ -58,7 +56,7 @@ class ManageFollowedItemsPage extends StatelessWidget { }, ), const Divider( - indent: AppSpacing.paddingMedium, // Consistent indent + indent: AppSpacing.paddingMedium, endIndent: AppSpacing.paddingMedium, ), // 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 fa8f799f..611973d5 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,10 +2,10 @@ 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'; // Import for Arguments +import 'package:ht_main/entity_details/view/entity_details_page.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; -import 'package:ht_main/shared/widgets/widgets.dart'; // For common widgets +import 'package:ht_main/shared/widgets/widgets.dart'; /// {@template followed_sources_list_page} /// Page to display and manage sources followed by the user. @@ -22,11 +22,11 @@ class FollowedSourcesListPage extends StatelessWidget { return Scaffold( appBar: AppBar( - title: const Text('Followed Sources'), // Placeholder + title: const Text('Followed Sources'), actions: [ IconButton( icon: const Icon(Icons.add_circle_outline), - tooltip: 'Add Source to Follow', // Placeholder + tooltip: 'Add Source to Follow', onPressed: () { context.goNamed(Routes.addSourceToFollowName); }, @@ -39,17 +39,15 @@ class FollowedSourcesListPage extends StatelessWidget { state.preferences == null) { return LoadingStateWidget( icon: Icons.source_outlined, - headline: 'Loading Followed Sources...', // Placeholder - subheadline: l10n.pleaseWait, // Assuming this exists + headline: 'Loading Followed Sources...', + subheadline: l10n.pleaseWait, ); } if (state.status == AccountStatus.failure && state.preferences == null) { return FailureStateWidget( - message: - state.errorMessage ?? - 'Could not load followed sources.', // Placeholder + message: state.errorMessage ?? 'Could not load followed sources.', onRetry: () { if (state.user?.id != null) { context.read().add( @@ -65,10 +63,9 @@ class FollowedSourcesListPage extends StatelessWidget { 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 + icon: Icons.no_sim_outlined, + headline: 'No Followed Sources', + subheadline: 'Start following sources to see them here.', ); } @@ -77,7 +74,7 @@ class FollowedSourcesListPage extends StatelessWidget { itemBuilder: (context, index) { final source = followedSources[index]; return ListTile( - leading: const Icon(Icons.source_outlined), // Generic icon + leading: const Icon(Icons.source_outlined), title: Text(source.name), subtitle: source.description != null ? Text( @@ -91,7 +88,7 @@ class FollowedSourcesListPage extends StatelessWidget { Icons.remove_circle_outline, color: Colors.red, ), - tooltip: 'Unfollow Source', // Placeholder + tooltip: 'Unfollow Source', onPressed: () { context.read().add( AccountFollowSourceToggled(source: source), @@ -100,7 +97,7 @@ class FollowedSourcesListPage extends StatelessWidget { ), onTap: () { context.push( - Routes.sourceDetails, // Navigate to source details + Routes.sourceDetails, extra: EntityDetailsPageArguments(entity: source), ); }, diff --git a/lib/account/view/saved_headlines_page.dart b/lib/account/view/saved_headlines_page.dart index 18e5c6d7..c2f44fcd 100644 --- a/lib/account/view/saved_headlines_page.dart +++ b/lib/account/view/saved_headlines_page.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; // Added GoRouter +import 'package:go_router/go_router.dart'; import 'package:ht_main/account/bloc/account_bloc.dart'; -import 'package:ht_main/app/bloc/app_bloc.dart'; // Added AppBloc +import 'package:ht_main/app/bloc/app_bloc.dart'; // HeadlineItemWidget import removed import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; -import 'package:ht_main/shared/shared.dart'; // Imports new headline tiles -import 'package:ht_shared/ht_shared.dart' - show Headline, HeadlineImageStyle; // Added HeadlineImageStyle +import 'package:ht_main/shared/shared.dart'; +import 'package:ht_shared/ht_shared.dart'; /// {@template saved_headlines_page} /// Displays the list of headlines saved by the user. @@ -31,7 +30,7 @@ class SavedHeadlinesPage extends StatelessWidget { appBar: AppBar( title: Text( l10n.accountSavedHeadlinesTile, - style: textTheme.titleLarge, // Consistent AppBar title + style: textTheme.titleLarge, ), ), body: BlocBuilder( @@ -40,17 +39,15 @@ class SavedHeadlinesPage extends StatelessWidget { state.preferences == null) { return LoadingStateWidget( icon: Icons.bookmarks_outlined, - headline: l10n.savedHeadlinesLoadingHeadline, // Use l10n - subheadline: l10n.savedHeadlinesLoadingSubheadline, // Use l10n + headline: l10n.savedHeadlinesLoadingHeadline, + subheadline: l10n.savedHeadlinesLoadingSubheadline, ); } if (state.status == AccountStatus.failure && state.preferences == null) { return FailureStateWidget( - message: - state.errorMessage ?? - l10n.savedHeadlinesErrorHeadline, // Use l10n + message: state.errorMessage ?? l10n.savedHeadlinesErrorHeadline, onRetry: () { if (state.user?.id != null) { context.read().add( @@ -66,19 +63,19 @@ class SavedHeadlinesPage extends StatelessWidget { if (savedHeadlines.isEmpty) { return InitialStateWidget( icon: Icons.bookmark_add_outlined, - headline: l10n.savedHeadlinesEmptyHeadline, // Use l10n - subheadline: l10n.savedHeadlinesEmptySubheadline, // Use l10n + headline: l10n.savedHeadlinesEmptyHeadline, + subheadline: l10n.savedHeadlinesEmptySubheadline, ); } return ListView.separated( padding: const EdgeInsets.symmetric( vertical: AppSpacing.paddingSmall, - ), // Add padding + ), itemCount: savedHeadlines.length, separatorBuilder: (context, index) => const Divider( height: 1, - indent: AppSpacing.paddingMedium, // Indent divider + indent: AppSpacing.paddingMedium, endIndent: AppSpacing.paddingMedium, ), itemBuilder: (context, index) { @@ -91,10 +88,7 @@ class SavedHeadlinesPage extends StatelessWidget { .headlineImageStyle; final trailingButton = IconButton( - icon: Icon( - Icons.delete_outline, - color: colorScheme.error, - ), // Themed icon + icon: Icon(Icons.delete_outline, color: colorScheme.error), tooltip: l10n.headlineDetailsRemoveFromSavedTooltip, onPressed: () { context.read().add( diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index fcbe3830..d256a55b 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -7,7 +7,8 @@ import 'package:flutter/material.dart'; import 'package:ht_auth_repository/ht_auth_repository.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_main/app/config/config.dart' as local_config; -import 'package:ht_shared/ht_shared.dart'; // Import shared models and exceptions +import 'package:ht_main/shared/services/demo_data_migration_service.dart'; +import 'package:ht_shared/ht_shared.dart'; part 'app_event.dart'; part 'app_state.dart'; @@ -17,16 +18,18 @@ class AppBloc extends Bloc { required HtAuthRepository authenticationRepository, required HtDataRepository userAppSettingsRepository, required HtDataRepository appConfigRepository, - required local_config.AppEnvironment environment, // Added + required local_config.AppEnvironment environment, + this.demoDataMigrationService, }) : _authenticationRepository = authenticationRepository, _userAppSettingsRepository = userAppSettingsRepository, _appConfigRepository = appConfigRepository, + _environment = environment, super( AppState( settings: const UserAppSettings(id: 'default'), selectedBottomNavigationIndex: 0, appConfig: null, - environment: environment, // Pass environment to AppState + environment: environment, ), ) { on(_onAppUserChanged); @@ -41,13 +44,15 @@ class AppBloc extends Bloc { // Listen directly to the auth state changes stream _userSubscription = _authenticationRepository.authStateChanges.listen( - (User? user) => add(AppUserChanged(user)), // Handle nullable user + (User? user) => add(AppUserChanged(user)), ); } final HtAuthRepository _authenticationRepository; final HtDataRepository _userAppSettingsRepository; - final HtDataRepository _appConfigRepository; // Added + final HtDataRepository _appConfigRepository; + final local_config.AppEnvironment _environment; + final DemoDataMigrationService? demoDataMigrationService; late final StreamSubscription _userSubscription; /// Handles user changes and loads initial settings once user is available. @@ -57,13 +62,17 @@ class AppBloc extends Bloc { ) async { // Determine the AppStatus based on the user object and its role final AppStatus status; + final oldUser = state.user; + switch (event.user?.role) { - case null: // User is null (unauthenticated) + case null: status = AppStatus.unauthenticated; case UserRole.standardUser: status = AppStatus.authenticated; + case UserRole.guestUser: // Explicitly map guestUser to anonymous + status = AppStatus.anonymous; // ignore: no_default_cases - default: + default: // Fallback for any other roles not explicitly handled status = AppStatus.anonymous; } @@ -72,13 +81,46 @@ class AppBloc extends Bloc { if (event.user != null) { // User is present (authenticated or anonymous) - add(const AppSettingsRefreshed()); // Load user-specific settings - add(const AppConfigFetchRequested()); // Now attempt to fetch AppConfig + add(const AppSettingsRefreshed()); + add(const AppConfigFetchRequested()); + + // Check for anonymous to authenticated transition for data migration + if (oldUser != null && + oldUser.role == UserRole.guestUser && + event.user!.role == UserRole.standardUser) { + print( + '[AppBloc] Anonymous user ${oldUser.id} transitioned to ' + 'authenticated user ${event.user!.id}. Attempting data migration.', + ); + // This block handles data migration specifically for the demo environment. + // In production/development, this logic is typically handled by the backend. + if (demoDataMigrationService != null && + _environment == local_config.AppEnvironment.demo) { + print( + '[AppBloc] Demo mode: Awaiting data migration from anonymous ' + 'user ${oldUser.id} to authenticated user ${event.user!.id}.', + ); + // Await the migration to ensure it completes before refreshing settings. + await demoDataMigrationService!.migrateAnonymousData( + oldUserId: oldUser.id, + newUserId: event.user!.id, + ); + // After successful migration, explicitly refresh app settings + // to load the newly migrated data into the AppBloc's state. + add(const AppSettingsRefreshed()); + print( + '[AppBloc] Demo mode: Data migration completed and settings ' + 'refresh triggered for user ${event.user!.id}.', + ); + } else { + print( + '[AppBloc] DemoDataMigrationService not available or not in demo ' + 'environment. Skipping client-side data migration.', + ); + } + } } else { // User is null (unauthenticated or logged out) - // Clear appConfig if user is logged out, as it might be tied to auth context - // or simply to ensure fresh fetch on next login. - // Also ensure status is unauthenticated. emit( state.copyWith( appConfig: null, @@ -104,7 +146,7 @@ class AppBloc extends Bloc { // Use the current user's ID to fetch user-specific settings final userAppSettings = await _userAppSettingsRepository.read( id: state.user!.id, - userId: state.user!.id, // Scope to the current user + userId: state.user!.id, ); // Map settings from UserAppSettings to AppState properties @@ -139,8 +181,8 @@ class AppBloc extends Bloc { flexScheme: newFlexScheme, appTextScaleFactor: newAppTextScaleFactor, fontFamily: newFontFamily, - settings: userAppSettings, // Store the fetched settings - locale: newLocale, // Store the new locale + settings: userAppSettings, + locale: newLocale, ), ); } on NotFoundException { @@ -151,13 +193,9 @@ class AppBloc extends Bloc { state.copyWith( themeMode: ThemeMode.system, flexScheme: FlexScheme.material, - appTextScaleFactor: AppTextScaleFactor.medium, // Default enum value - locale: const Locale( - 'en', - ), // Default to English if settings not found - settings: UserAppSettings( - id: state.user!.id, - ), // Provide default settings + appTextScaleFactor: AppTextScaleFactor.medium, + locale: const Locale('en'), + settings: UserAppSettings(id: state.user!.id), ), ); } catch (e) { @@ -165,9 +203,7 @@ class AppBloc extends Bloc { // Optionally emit a failure state or log the error print('Error loading user app settings in AppBloc: $e'); // Keep the existing theme/font state on error, but ensure settings is not null - emit( - state.copyWith(settings: state.settings), - ); // Ensure settings is present + emit(state.copyWith(settings: state.settings)); } } @@ -203,8 +239,7 @@ class AppBloc extends Bloc { ? AppAccentTheme.defaultBlue : (event.flexScheme == FlexScheme.red ? AppAccentTheme.newsRed - : AppAccentTheme - .graphiteGray), // Mapping material to graphiteGray + : AppAccentTheme.graphiteGray), ), ); emit( @@ -221,8 +256,7 @@ class AppBloc extends Bloc { // Update settings and emit new state final updatedSettings = state.settings.copyWith( displaySettings: state.settings.displaySettings.copyWith( - fontFamily: - event.fontFamily ?? 'SystemDefault', // Map null to 'SystemDefault' + fontFamily: event.fontFamily ?? 'SystemDefault', ), ); emit( @@ -272,7 +306,7 @@ class AppBloc extends Bloc { case AppAccentTheme.newsRed: return FlexScheme.red; case AppAccentTheme.graphiteGray: - return FlexScheme.material; // Mapping graphiteGray to material for now + return FlexScheme.material; } } @@ -348,9 +382,7 @@ class AppBloc extends Bloc { ); try { - final appConfig = await _appConfigRepository.read( - id: 'app_config', - ); // API requires auth, so token will be used + final appConfig = await _appConfigRepository.read(id: 'app_config'); print( '[AppBloc] AppConfig fetched successfully. ID: ${appConfig.id} for user: ${state.user!.id}', ); @@ -411,7 +443,7 @@ class AppBloc extends Bloc { // } catch (e) { // // Handle error, potentially revert optimistic update or show an error. // print('Failed to update lastAccountActionShownAt on backend: $e'); - // // Optionally revert: emit(state.copyWith(user: state.user)); // Reverts to original + // // Optionally revert: emit(state.copyWith(user: state.user)); // } print( '[AppBloc] User ${event.userId} AccountAction shown. Last shown timestamp updated locally to $now. Backend update pending.', diff --git a/lib/app/bloc/app_event.dart b/lib/app/bloc/app_event.dart index d342882b..46997225 100644 --- a/lib/app/bloc/app_event.dart +++ b/lib/app/bloc/app_event.dart @@ -4,7 +4,7 @@ abstract class AppEvent extends Equatable { const AppEvent(); @override - List get props => []; // Allow nullable objects in props + List get props => []; } @Deprecated('Use SettingsBloc events instead') @@ -17,10 +17,10 @@ class AppThemeChanged extends AppEvent { class AppUserChanged extends AppEvent { const AppUserChanged(this.user); - final User? user; // Make user nullable + final User? user; @override - List get props => [user]; // Update props to handle nullable + List get props => [user]; } /// {@template app_settings_refreshed} diff --git a/lib/app/bloc/app_state.dart b/lib/app/bloc/app_state.dart index 9eca39bc..ed011a45 100644 --- a/lib/app/bloc/app_state.dart +++ b/lib/app/bloc/app_state.dart @@ -24,16 +24,15 @@ enum AppStatus { class AppState extends Equatable { /// {@macro app_state} const AppState({ - required this.settings, // Add settings property + required this.settings, required this.selectedBottomNavigationIndex, this.themeMode = ThemeMode.system, - this.appTextScaleFactor = - AppTextScaleFactor.medium, // Default text scale factor (enum) + this.appTextScaleFactor = AppTextScaleFactor.medium, this.flexScheme = FlexScheme.material, this.fontFamily, this.status = AppStatus.initial, - this.user, // User is now nullable and defaults to null - this.locale, // Added locale + this.user, + this.locale, this.appConfig, this.environment, }); @@ -45,7 +44,7 @@ class AppState extends Equatable { final ThemeMode themeMode; /// The text scale factor for the app's UI. - final AppTextScaleFactor appTextScaleFactor; // Change type to enum + final AppTextScaleFactor appTextScaleFactor; /// The active color scheme defined by FlexColorScheme. final FlexScheme flexScheme; @@ -61,10 +60,10 @@ class AppState extends Equatable { final User? user; /// User-specific application settings. - final UserAppSettings settings; // Add settings property + final UserAppSettings settings; /// The current application locale. - final Locale? locale; // Added locale + final Locale? locale; /// The global application configuration (remote config). final AppConfig? appConfig; @@ -78,17 +77,17 @@ class AppState extends Equatable { ThemeMode? themeMode, FlexScheme? flexScheme, String? fontFamily, - AppTextScaleFactor? appTextScaleFactor, // Change type to enum + AppTextScaleFactor? appTextScaleFactor, AppStatus? status, User? user, - UserAppSettings? settings, // Add settings to copyWith - Locale? locale, // Added locale + UserAppSettings? settings, + Locale? locale, AppConfig? appConfig, - local_config.AppEnvironment? environment, // Added AppEnvironment + local_config.AppEnvironment? environment, bool clearFontFamily = false, - bool clearLocale = false, // Added to allow clearing locale - bool clearAppConfig = false, // Added to allow clearing appConfig - bool clearEnvironment = false, // Added to allow clearing environment + bool clearLocale = false, + bool clearAppConfig = false, + bool clearEnvironment = false, }) { return AppState( selectedBottomNavigationIndex: @@ -99,12 +98,10 @@ class AppState extends Equatable { appTextScaleFactor: appTextScaleFactor ?? this.appTextScaleFactor, status: status ?? this.status, user: user ?? this.user, - settings: settings ?? this.settings, // Copy settings - locale: clearLocale ? null : locale ?? this.locale, // Added locale + settings: settings ?? this.settings, + locale: clearLocale ? null : locale ?? this.locale, appConfig: clearAppConfig ? null : appConfig ?? this.appConfig, - environment: clearEnvironment - ? null - : environment ?? this.environment, // Added + environment: clearEnvironment ? null : environment ?? this.environment, ); } @@ -117,8 +114,8 @@ class AppState extends Equatable { appTextScaleFactor, status, user, - settings, // Include settings in props - locale, // Added locale to props + settings, + locale, appConfig, environment, ]; diff --git a/lib/app/config/app_config.dart b/lib/app/config/app_config.dart index 0254571d..9310dc63 100644 --- a/lib/app/config/app_config.dart +++ b/lib/app/config/app_config.dart @@ -10,8 +10,8 @@ class AppConfig { // Factory constructors for different environments factory AppConfig.production() => const AppConfig( environment: AppEnvironment.production, - baseUrl: - 'http://api.yourproductiondomain.com', // Replace with actual production URL + // Todo(you): Replace with actual production URL + baseUrl: 'http://api.yourproductiondomain.com', ); factory AppConfig.demo() => const AppConfig( diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 913aa59d..d3adeb7e 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -1,23 +1,24 @@ // // ignore_for_file: deprecated_member_use -import 'package:flex_color_scheme/flex_color_scheme.dart'; // Added +import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_auth_repository/ht_auth_repository.dart'; // Auth Repository -import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository -import 'package:ht_kv_storage_service/ht_kv_storage_service.dart'; // KV Storage Interface +import 'package:ht_auth_repository/ht_auth_repository.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_kv_storage_service/ht_kv_storage_service.dart'; import 'package:ht_main/app/bloc/app_bloc.dart'; import 'package:ht_main/app/config/app_environment.dart'; import 'package:ht_main/authentication/bloc/authentication_bloc.dart'; import 'package:ht_main/l10n/app_localizations.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/router.dart'; +import 'package:ht_main/shared/services/demo_data_migration_service.dart'; import 'package:ht_main/shared/theme/app_theme.dart'; -import 'package:ht_main/shared/widgets/failure_state_widget.dart'; // Added -import 'package:ht_main/shared/widgets/loading_state_widget.dart'; // Added -import 'package:ht_shared/ht_shared.dart'; // Shared models, FromJson, ToJson, etc. +import 'package:ht_main/shared/widgets/failure_state_widget.dart'; +import 'package:ht_main/shared/widgets/loading_state_widget.dart'; +import 'package:ht_shared/ht_shared.dart'; class App extends StatelessWidget { const App({ @@ -31,7 +32,8 @@ class App extends StatelessWidget { htUserContentPreferencesRepository, required HtDataRepository htAppConfigRepository, required HtKVStorageService kvStorageService, - required AppEnvironment environment, // Added + required AppEnvironment environment, + this.demoDataMigrationService, super.key, }) : _htAuthenticationRepository = htAuthenticationRepository, _htHeadlinesRepository = htHeadlinesRepository, @@ -42,7 +44,7 @@ class App extends StatelessWidget { _htUserContentPreferencesRepository = htUserContentPreferencesRepository, _htAppConfigRepository = htAppConfigRepository, _kvStorageService = kvStorageService, - _environment = environment; // Added + _environment = environment; final HtAuthRepository _htAuthenticationRepository; final HtDataRepository _htHeadlinesRepository; @@ -54,7 +56,8 @@ class App extends StatelessWidget { _htUserContentPreferencesRepository; final HtDataRepository _htAppConfigRepository; final HtKVStorageService _kvStorageService; - final AppEnvironment _environment; // Added + final AppEnvironment _environment; + final DemoDataMigrationService? demoDataMigrationService; @override Widget build(BuildContext context) { @@ -70,17 +73,16 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _htAppConfigRepository), RepositoryProvider.value(value: _kvStorageService), ], - // Use MultiBlocProvider to provide global BLoCs child: MultiBlocProvider( providers: [ BlocProvider( - // AppBloc constructor needs refactoring in Step 4 create: (context) => AppBloc( authenticationRepository: context.read(), userAppSettingsRepository: context .read>(), appConfigRepository: context.read>(), - environment: _environment, // Pass environment + environment: _environment, + demoDataMigrationService: demoDataMigrationService, ), ), BlocProvider( @@ -99,7 +101,7 @@ class App extends StatelessWidget { htUserContentPreferencesRepository: _htUserContentPreferencesRepository, htAppConfigRepository: _htAppConfigRepository, - environment: _environment, // Pass environment + environment: _environment, ), ), ); @@ -116,7 +118,7 @@ class _AppView extends StatefulWidget { required this.htUserAppSettingsRepository, required this.htUserContentPreferencesRepository, required this.htAppConfigRepository, - required this.environment, // Added + required this.environment, }); final HtAuthRepository htAuthenticationRepository; @@ -128,7 +130,7 @@ class _AppView extends StatefulWidget { final HtDataRepository htUserContentPreferencesRepository; final HtDataRepository htAppConfigRepository; - final AppEnvironment environment; // Added + final AppEnvironment environment; @override State<_AppView> createState() => _AppViewState(); @@ -157,6 +159,7 @@ class _AppViewState extends State<_AppView> { htUserContentPreferencesRepository: widget.htUserContentPreferencesRepository, htAppConfigRepository: widget.htAppConfigRepository, + environment: widget.environment, ); // Removed Dynamic Link Initialization @@ -164,7 +167,7 @@ class _AppViewState extends State<_AppView> { @override void dispose() { - _statusNotifier.dispose(); // Dispose the correct notifier + _statusNotifier.dispose(); // Removed Dynamic Links subscription cancellation super.dispose(); } @@ -192,15 +195,15 @@ class _AppViewState extends State<_AppView> { return MaterialApp( debugShowCheckedModeBanner: false, theme: lightTheme( - scheme: FlexScheme.material, // Default scheme - appTextScaleFactor: AppTextScaleFactor.medium, // Default - appFontWeight: AppFontWeight.regular, // Default - fontFamily: null, // System default font + scheme: FlexScheme.material, + appTextScaleFactor: AppTextScaleFactor.medium, + appFontWeight: AppFontWeight.regular, + fontFamily: null, ), darkTheme: darkTheme( - scheme: FlexScheme.material, // Default scheme - appTextScaleFactor: AppTextScaleFactor.medium, // Default - appFontWeight: AppFontWeight.regular, // Default + scheme: FlexScheme.material, + appTextScaleFactor: AppTextScaleFactor.medium, + appFontWeight: AppFontWeight.regular, fontFamily: null, // System default font ), themeMode: state @@ -214,9 +217,8 @@ class _AppViewState extends State<_AppView> { final l10n = innerContext.l10n; return LoadingStateWidget( icon: Icons.settings_applications_outlined, - headline: - l10n.headlinesFeedLoadingHeadline, // "Loading..." - subheadline: l10n.pleaseWait, // "Please wait..." + headline: l10n.headlinesFeedLoadingHeadline, + subheadline: l10n.pleaseWait, ); }, ), @@ -228,16 +230,16 @@ class _AppViewState extends State<_AppView> { return MaterialApp( debugShowCheckedModeBanner: false, theme: lightTheme( - scheme: FlexScheme.material, // Default scheme - appTextScaleFactor: AppTextScaleFactor.medium, // Default - appFontWeight: AppFontWeight.regular, // Default - fontFamily: null, // System default font + scheme: FlexScheme.material, + appTextScaleFactor: AppTextScaleFactor.medium, + appFontWeight: AppFontWeight.regular, + fontFamily: null, ), darkTheme: darkTheme( - scheme: FlexScheme.material, // Default scheme - appTextScaleFactor: AppTextScaleFactor.medium, // Default - appFontWeight: AppFontWeight.regular, // Default - fontFamily: null, // System default font + scheme: FlexScheme.material, + appTextScaleFactor: AppTextScaleFactor.medium, + appFontWeight: AppFontWeight.regular, + fontFamily: null, ), themeMode: state.themeMode, localizationsDelegates: AppLocalizations.localizationsDelegates, @@ -248,9 +250,8 @@ class _AppViewState extends State<_AppView> { builder: (innerContext) { final l10n = innerContext.l10n; return FailureStateWidget( - message: - l10n.unknownError, // "An unknown error occurred." - retryButtonText: 'Retry', // Hardcoded for now + message: l10n.unknownError, + retryButtonText: 'Retry', onRetry: () { // Use outer context for BLoC access context.read().add( diff --git a/lib/authentication/bloc/authentication_bloc.dart b/lib/authentication/bloc/authentication_bloc.dart index fddddd02..95656807 100644 --- a/lib/authentication/bloc/authentication_bloc.dart +++ b/lib/authentication/bloc/authentication_bloc.dart @@ -66,14 +66,10 @@ class AuthenticationBloc emit(const AuthenticationFailure('Please enter a valid email address.')); return; } - emit( - AuthenticationRequestCodeLoading(), - ); // Indicate code request is sending + emit(AuthenticationRequestCodeLoading()); try { await _authenticationRepository.requestSignInCode(event.email); - emit( - AuthenticationCodeSentSuccess(email: event.email), - ); // Confirm code requested and include email + emit(AuthenticationCodeSentSuccess(email: event.email)); } on InvalidInputException catch (e) { emit(AuthenticationFailure('Invalid input: ${e.message}')); } on NetworkException catch (_) { @@ -100,15 +96,15 @@ class AuthenticationBloc AuthenticationVerifyCodeRequested event, Emitter emit, ) async { - emit(AuthenticationLoading()); // Indicate code verification is loading + emit(AuthenticationLoading()); try { await _authenticationRepository.verifySignInCode(event.email, event.code); // On success, the _AuthenticationUserChanged listener will handle // emitting AuthenticationAuthenticated. } on InvalidInputException catch (e) { - emit(AuthenticationFailure(e.message)); // Use specific error message + emit(AuthenticationFailure(e.message)); } on AuthenticationException catch (e) { - emit(AuthenticationFailure(e.message)); // Use specific error message + emit(AuthenticationFailure(e.message)); } on NetworkException catch (_) { emit(const AuthenticationFailure('Network error occurred.')); } on ServerException catch (e) { @@ -130,7 +126,7 @@ class AuthenticationBloc AuthenticationAnonymousSignInRequested event, Emitter emit, ) async { - emit(AuthenticationLoading()); // Indicate anonymous sign-in is loading + emit(AuthenticationLoading()); try { await _authenticationRepository.signInAnonymously(); // On success, the _AuthenticationUserChanged listener will handle @@ -154,7 +150,7 @@ class AuthenticationBloc AuthenticationSignOutRequested event, Emitter emit, ) async { - emit(AuthenticationLoading()); // Indicate sign-out is loading + emit(AuthenticationLoading()); try { await _authenticationRepository.signOut(); // On success, the _AuthenticationUserChanged listener will handle diff --git a/lib/authentication/view/authentication_page.dart b/lib/authentication/view/authentication_page.dart index d092c519..ce1dcc51 100644 --- a/lib/authentication/view/authentication_page.dart +++ b/lib/authentication/view/authentication_page.dart @@ -51,15 +51,13 @@ class AuthenticationPage extends StatelessWidget { leading: isLinkingContext ? IconButton( icon: const Icon(Icons.close), - tooltip: MaterialLocalizations.of( - context, - ).closeButtonTooltip, // Accessibility + tooltip: MaterialLocalizations.of(context).closeButtonTooltip, onPressed: () { // Navigate back to the account page when close is pressed context.goNamed(Routes.accountName); }, ) - : null, // No leading button if not linking (relies on system back if pushed) + : null, ), body: SafeArea( child: BlocConsumer( @@ -72,7 +70,7 @@ class AuthenticationPage extends StatelessWidget { SnackBar( content: Text( // Provide a more user-friendly error message if possible - state.errorMessage, // Or map specific errors + state.errorMessage, ), backgroundColor: colorScheme.error, ), @@ -83,16 +81,14 @@ class AuthenticationPage extends StatelessWidget { // Email link success is handled in the dedicated email flow pages. }, builder: (context, state) { - final isLoading = - state is AuthenticationLoading; // Simplified loading check + final isLoading = state is AuthenticationLoading; return Padding( padding: const EdgeInsets.all(AppSpacing.paddingLarge), child: Center( child: SingleChildScrollView( child: Column( - mainAxisAlignment: - MainAxisAlignment.center, // Center vertically + mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // --- Icon --- @@ -100,26 +96,24 @@ class AuthenticationPage extends StatelessWidget { padding: const EdgeInsets.only(bottom: AppSpacing.xl), child: Icon( isLinkingContext ? Icons.sync : Icons.newspaper, - size: AppSpacing.xxl * 2, // Standardized large icon + size: AppSpacing.xxl * 2, color: colorScheme.primary, ), ), - // const SizedBox(height: AppSpacing.lg), // Removed, padding above handles it + // const SizedBox(height: AppSpacing.lg), // --- Headline and Subheadline --- Text( headline, style: textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, // Ensure prominence + fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), - const SizedBox( - height: AppSpacing.md, - ), // Increased spacing + const SizedBox(height: AppSpacing.md), Text( subHeadline, style: textTheme.bodyLarge?.copyWith( - color: colorScheme.onSurfaceVariant, // Softer color + color: colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), diff --git a/lib/authentication/view/email_code_verification_page.dart b/lib/authentication/view/email_code_verification_page.dart index a3a04585..3fc8a59a 100644 --- a/lib/authentication/view/email_code_verification_page.dart +++ b/lib/authentication/view/email_code_verification_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:ht_main/app/bloc/app_bloc.dart'; // Added -import 'package:ht_main/app/config/config.dart'; // Added for AppEnvironment +import 'package:ht_main/app/bloc/app_bloc.dart'; +import 'package:ht_main/app/config/config.dart'; import 'package:ht_main/authentication/bloc/authentication_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/shared/constants/app_spacing.dart'; @@ -50,28 +50,27 @@ class EmailCodeVerificationPage extends StatelessWidget { child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.stretch, // Stretch buttons + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Icon( Icons.mark_email_read_outlined, - size: AppSpacing.xxl * 2, // Standardized large icon + size: AppSpacing.xxl * 2, color: colorScheme.primary, ), const SizedBox(height: AppSpacing.xl), Text( l10n.emailCodeSentConfirmation(email), style: textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, // Ensure prominence + fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), - const SizedBox(height: AppSpacing.lg), // Adjusted spacing + const SizedBox(height: AppSpacing.lg), Text( l10n.emailCodeSentInstructions, style: textTheme.bodyLarge?.copyWith( color: colorScheme.onSurfaceVariant, - ), // Softer color + ), textAlign: TextAlign.center, ), // Display demo code if in demo environment @@ -83,9 +82,7 @@ class EmailCodeVerificationPage extends StatelessWidget { children: [ const SizedBox(height: AppSpacing.md), Text( - l10n.demoVerificationCodeMessage( - '123456', - ), // Demo code + l10n.demoVerificationCodeMessage('123456'), style: textTheme.bodyMedium?.copyWith( color: colorScheme.secondary, fontWeight: FontWeight.bold, @@ -98,9 +95,7 @@ class EmailCodeVerificationPage extends StatelessWidget { return const SizedBox.shrink(); }, ), - const SizedBox( - height: AppSpacing.xl, - ), // Increased spacing + const SizedBox(height: AppSpacing.xl), _EmailCodeVerificationForm( email: email, isLoading: isLoading, @@ -156,7 +151,7 @@ class _EmailCodeVerificationFormState @override Widget build(BuildContext context) { final l10n = context.l10n; - final textTheme = Theme.of(context).textTheme; // Added missing textTheme + final textTheme = Theme.of(context).textTheme; return Form( key: _formKey, @@ -170,15 +165,15 @@ class _EmailCodeVerificationFormState child: TextFormField( controller: _codeController, decoration: InputDecoration( - labelText: l10n.emailCodeVerificationHint, // Use labelText - // border: const OutlineInputBorder(), // Uses theme default - counterText: '', // Hide the counter + labelText: l10n.emailCodeVerificationHint, + // border: const OutlineInputBorder(), + counterText: '', ), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], maxLength: 6, textAlign: TextAlign.center, - style: textTheme.headlineSmall, // Make input text larger + style: textTheme.headlineSmall, enabled: !widget.isLoading, validator: (value) { if (value == null || value.isEmpty) { @@ -192,24 +187,23 @@ class _EmailCodeVerificationFormState onFieldSubmitted: widget.isLoading ? null : (_) => _submitForm(), ), ), - const SizedBox(height: AppSpacing.xxl), // Increased spacing + const SizedBox(height: AppSpacing.xxl), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric( vertical: AppSpacing.md, - horizontal: AppSpacing.lg, // Added horizontal padding + horizontal: AppSpacing.lg, ), textStyle: textTheme.labelLarge, ), onPressed: widget.isLoading ? null : _submitForm, child: widget.isLoading ? const SizedBox( - height: AppSpacing.xl, // Consistent size with text + height: AppSpacing.xl, width: AppSpacing.xl, child: CircularProgressIndicator( strokeWidth: 2, - color: - Colors.white, // Explicit color for loader on button + color: Colors.white, ), ) : Text(l10n.emailCodeVerificationButtonLabel), diff --git a/lib/authentication/view/request_code_page.dart b/lib/authentication/view/request_code_page.dart index 7bdb852b..c19973b2 100644 --- a/lib/authentication/view/request_code_page.dart +++ b/lib/authentication/view/request_code_page.dart @@ -16,10 +16,7 @@ import 'package:ht_main/shared/constants/app_spacing.dart'; /// {@endtemplate} class RequestCodePage extends StatelessWidget { /// {@macro request_code_page} - const RequestCodePage({ - required this.isLinkingContext, // Accept the flag - super.key, - }); + const RequestCodePage({required this.isLinkingContext, super.key}); /// Whether this page is being shown in the account linking context. final bool isLinkingContext; @@ -42,7 +39,7 @@ class _RequestCodeView extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; // Added textTheme + final textTheme = Theme.of(context).textTheme; return Scaffold( appBar: AppBar( @@ -50,9 +47,7 @@ class _RequestCodeView extends StatelessWidget { // Add a custom leading back button to control navigation based on context. leading: IconButton( icon: const Icon(Icons.arrow_back), - tooltip: MaterialLocalizations.of( - context, - ).backButtonTooltip, // Accessibility + tooltip: MaterialLocalizations.of(context).backButtonTooltip, onPressed: () { // Navigate back differently based on the context. if (isLinkingContext) { @@ -96,8 +91,7 @@ class _RequestCodeView extends StatelessWidget { buildWhen: (previous, current) => current is AuthenticationInitial || current is AuthenticationRequestCodeLoading || - current - is AuthenticationFailure, // Rebuild on failure to re-enable form + current is AuthenticationFailure, builder: (context, state) { final isLoading = state is AuthenticationRequestCodeLoading; @@ -114,14 +108,14 @@ class _RequestCodeView extends StatelessWidget { padding: const EdgeInsets.only(bottom: AppSpacing.xl), child: Icon( Icons.email_outlined, - size: AppSpacing.xxl * 2, // Standardized large icon + size: AppSpacing.xxl * 2, color: colorScheme.primary, ), ), - // const SizedBox(height: AppSpacing.lg), // Removed + // const SizedBox(height: AppSpacing.lg), // --- Explanation Text --- Text( - l10n.requestCodePageHeadline, // Using a more title-like l10n key + l10n.requestCodePageHeadline, style: textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), @@ -129,7 +123,7 @@ class _RequestCodeView extends StatelessWidget { ), const SizedBox(height: AppSpacing.md), Text( - l10n.requestCodePageSubheadline, // Using a more descriptive subheadline + l10n.requestCodePageSubheadline, style: textTheme.bodyLarge?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -183,8 +177,8 @@ class _EmailLinkFormState extends State<_EmailLinkForm> { @override Widget build(BuildContext context) { final l10n = context.l10n; - final textTheme = Theme.of(context).textTheme; // Added textTheme - final colorScheme = Theme.of(context).colorScheme; // Added colorScheme + final textTheme = Theme.of(context).textTheme; + final colorScheme = Theme.of(context).colorScheme; return Form( key: _formKey, @@ -194,9 +188,9 @@ class _EmailLinkFormState extends State<_EmailLinkForm> { TextFormField( controller: _emailController, decoration: InputDecoration( - labelText: l10n.requestCodeEmailLabel, // More specific label - hintText: l10n.requestCodeEmailHint, // Added hint text - // border: const OutlineInputBorder(), // Uses theme default + labelText: l10n.requestCodeEmailLabel, + hintText: l10n.requestCodeEmailHint, + // border: const OutlineInputBorder(), ), keyboardType: TextInputType.emailAddress, autocorrect: false, @@ -208,8 +202,7 @@ class _EmailLinkFormState extends State<_EmailLinkForm> { } return null; }, - onFieldSubmitted: (_) => - _submitForm(), // Allow submitting from keyboard + onFieldSubmitted: (_) => _submitForm(), ), const SizedBox(height: AppSpacing.lg), ElevatedButton( @@ -220,17 +213,14 @@ class _EmailLinkFormState extends State<_EmailLinkForm> { ), child: widget.isLoading ? SizedBox( - height: AppSpacing.xl, // Consistent size with text + height: AppSpacing.xl, width: AppSpacing.xl, child: CircularProgressIndicator( strokeWidth: 2, - color: - colorScheme.onPrimary, // Color for loader on button + color: colorScheme.onPrimary, ), ) - : Text( - l10n.requestCodeSendCodeButton, - ), // More specific button text + : Text(l10n.requestCodeSendCodeButton), ), ], ), diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 422ff804..1e12a35c 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -16,12 +16,13 @@ import 'package:ht_main/app/config/config.dart' as app_config; import 'package:ht_main/bloc_observer.dart'; import 'package:ht_main/shared/localization/ar_timeago_messages.dart'; import 'package:ht_main/shared/localization/en_timeago_messages.dart'; +import 'package:ht_main/shared/services/demo_data_migration_service.dart'; import 'package:ht_shared/ht_shared.dart'; import 'package:timeago/timeago.dart' as timeago; Future bootstrap( app_config.AppConfig appConfig, - app_config.AppEnvironment environment, // Added + app_config.AppEnvironment environment, ) async { WidgetsFlutterBinding.ensureInitialized(); Bloc.observer = const AppBlocObserver(); @@ -208,6 +209,15 @@ Future bootstrap( dataClient: appConfigClient, ); + // Conditionally instantiate DemoDataMigrationService + final demoDataMigrationService = + appConfig.environment == app_config.AppEnvironment.demo + ? DemoDataMigrationService( + userAppSettingsRepository: userAppSettingsRepository, + userContentPreferencesRepository: userContentPreferencesRepository, + ) + : null; + return App( htAuthenticationRepository: authenticationRepository, htHeadlinesRepository: headlinesRepository, @@ -218,6 +228,7 @@ Future bootstrap( htUserContentPreferencesRepository: userContentPreferencesRepository, htAppConfigRepository: appConfigRepository, kvStorageService: kvStorage, - environment: environment, // Pass environment to App + environment: environment, + demoDataMigrationService: demoDataMigrationService, ); } diff --git a/lib/entity_details/bloc/entity_details_bloc.dart b/lib/entity_details/bloc/entity_details_bloc.dart index e9ea1461..d259dfd1 100644 --- a/lib/entity_details/bloc/entity_details_bloc.dart +++ b/lib/entity_details/bloc/entity_details_bloc.dart @@ -1,13 +1,15 @@ +// ignore_for_file: avoid_dynamic_calls + 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_main/account/bloc/account_bloc.dart'; -import 'package:ht_main/app/bloc/app_bloc.dart'; // Added +import 'package:ht_main/app/bloc/app_bloc.dart'; import 'package:ht_main/entity_details/models/entity_type.dart'; -import 'package:ht_main/shared/services/feed_injector_service.dart'; // Added -import 'package:ht_shared/ht_shared.dart'; // Ensures FeedItem, AppConfig, User are available +import 'package:ht_main/shared/services/feed_injector_service.dart'; +import 'package:ht_shared/ht_shared.dart'; part 'entity_details_event.dart'; part 'entity_details_state.dart'; @@ -18,14 +20,14 @@ class EntityDetailsBloc extends Bloc { required HtDataRepository categoryRepository, required HtDataRepository sourceRepository, required AccountBloc accountBloc, - required AppBloc appBloc, // Added - required FeedInjectorService feedInjectorService, // Added + required AppBloc appBloc, + required FeedInjectorService feedInjectorService, }) : _headlinesRepository = headlinesRepository, _categoryRepository = categoryRepository, _sourceRepository = sourceRepository, _accountBloc = accountBloc, - _appBloc = appBloc, // Added - _feedInjectorService = feedInjectorService, // Added + _appBloc = appBloc, + _feedInjectorService = feedInjectorService, super(const EntityDetailsState()) { on(_onEntityDetailsLoadRequested); on( @@ -50,11 +52,11 @@ class EntityDetailsBloc extends Bloc { final HtDataRepository _categoryRepository; final HtDataRepository _sourceRepository; final AccountBloc _accountBloc; - final AppBloc _appBloc; // Added - final FeedInjectorService _feedInjectorService; // Added + final AppBloc _appBloc; + final FeedInjectorService _feedInjectorService; late final StreamSubscription _accountBlocSubscription; - static const _headlinesLimit = 10; // For fetching original headlines + static const _headlinesLimit = 10; Future _onEntityDetailsLoadRequested( EntityDetailsLoadRequested event, @@ -72,7 +74,7 @@ class EntityDetailsBloc extends Bloc { if (entityToLoad == null && event.entityId != null && event.entityType != null) { - entityTypeToLoad = event.entityType; // Ensure type is set + entityTypeToLoad = event.entityType; if (event.entityType == EntityType.category) { entityToLoad = await _categoryRepository.read(id: event.entityId!); } else if (event.entityType == EntityType.source) { @@ -133,7 +135,7 @@ class EntityDetailsBloc extends Bloc { headlines: headlineResponse.items, user: currentUser, appConfig: appConfig, - currentFeedItemCount: 0, // Initial load for this entity's feed + currentFeedItemCount: 0, ); // 3. Determine isFollowing status @@ -161,10 +163,9 @@ class EntityDetailsBloc extends Bloc { entityType: entityTypeToLoad, entity: entityToLoad, isFollowing: isCurrentlyFollowing, - feedItems: processedFeedItems, // Changed + feedItems: processedFeedItems, headlinesStatus: EntityHeadlinesStatus.success, - hasMoreHeadlines: - headlineResponse.hasMore, // Based on original headlines + hasMoreHeadlines: headlineResponse.hasMore, headlinesCursor: headlineResponse.cursor, clearErrorMessage: true, ), @@ -182,7 +183,7 @@ class EntityDetailsBloc extends Bloc { state.copyWith( status: EntityDetailsStatus.failure, errorMessage: e.message, - entityType: entityTypeToLoad, // Keep type if known + entityType: entityTypeToLoad, ), ); } catch (e) { @@ -190,7 +191,7 @@ class EntityDetailsBloc extends Bloc { state.copyWith( status: EntityDetailsStatus.failure, errorMessage: 'An unexpected error occurred: $e', - entityType: entityTypeToLoad, // Keep type if known + entityType: entityTypeToLoad, ), ); } @@ -205,7 +206,7 @@ class EntityDetailsBloc extends Bloc { emit( state.copyWith( errorMessage: 'No entity loaded to follow/unfollow.', - clearErrorMessage: false, // Keep existing error if any, or set new + clearErrorMessage: false, ), ); return; @@ -270,7 +271,7 @@ class EntityDetailsBloc extends Bloc { final headlineResponse = await _headlinesRepository.readAllByQuery( queryParams, limit: _headlinesLimit, - startAfterId: state.headlinesCursor, // Cursor for original headlines + startAfterId: state.headlinesCursor, ); final currentUser = _appBloc.state.user; @@ -290,16 +291,14 @@ class EntityDetailsBloc extends Bloc { headlines: headlineResponse.items, user: currentUser, appConfig: appConfig, - currentFeedItemCount: state.feedItems.length, // Pass current total + currentFeedItemCount: state.feedItems.length, ); emit( state.copyWith( - feedItems: List.of(state.feedItems) - ..addAll(newProcessedFeedItems), // Changed + feedItems: List.of(state.feedItems)..addAll(newProcessedFeedItems), headlinesStatus: EntityHeadlinesStatus.success, - hasMoreHeadlines: - headlineResponse.hasMore, // Based on original headlines + hasMoreHeadlines: headlineResponse.hasMore, headlinesCursor: headlineResponse.cursor, clearHeadlinesCursor: !headlineResponse.hasMore, ), diff --git a/lib/entity_details/bloc/entity_details_event.dart b/lib/entity_details/bloc/entity_details_event.dart index 1bdc0953..36438d7d 100644 --- a/lib/entity_details/bloc/entity_details_event.dart +++ b/lib/entity_details/bloc/entity_details_event.dart @@ -21,7 +21,7 @@ class EntityDetailsLoadRequested extends EntityDetailsEvent { final String? entityId; final EntityType? entityType; - final dynamic entity; // Category or Source + final dynamic entity; @override List get props => [entityId, entityType, entity]; diff --git a/lib/entity_details/bloc/entity_details_state.dart b/lib/entity_details/bloc/entity_details_state.dart index 1529a281..517e2347 100644 --- a/lib/entity_details/bloc/entity_details_state.dart +++ b/lib/entity_details/bloc/entity_details_state.dart @@ -12,21 +12,21 @@ class EntityDetailsState extends Equatable { this.entityType, this.entity, this.isFollowing = false, - this.feedItems = const [], // Changed from headlines + this.feedItems = const [], this.headlinesStatus = EntityHeadlinesStatus.initial, - this.hasMoreHeadlines = true, // This refers to original headlines + this.hasMoreHeadlines = true, this.headlinesCursor, this.errorMessage, }); final EntityDetailsStatus status; final EntityType? entityType; - final dynamic entity; // Will be Category or Source + final dynamic entity; final bool isFollowing; - final List feedItems; // Changed from List + final List feedItems; final EntityHeadlinesStatus headlinesStatus; - final bool hasMoreHeadlines; // This refers to original headlines - final String? headlinesCursor; // Cursor for fetching original headlines + final bool hasMoreHeadlines; + final String? headlinesCursor; final String? errorMessage; EntityDetailsState copyWith({ @@ -34,7 +34,7 @@ class EntityDetailsState extends Equatable { EntityType? entityType, dynamic entity, bool? isFollowing, - List? feedItems, // Changed + List? feedItems, EntityHeadlinesStatus? headlinesStatus, bool? hasMoreHeadlines, String? headlinesCursor, @@ -48,7 +48,7 @@ class EntityDetailsState extends Equatable { entityType: entityType ?? this.entityType, entity: clearEntity ? null : entity ?? this.entity, isFollowing: isFollowing ?? this.isFollowing, - feedItems: feedItems ?? this.feedItems, // Changed + feedItems: feedItems ?? this.feedItems, headlinesStatus: headlinesStatus ?? this.headlinesStatus, hasMoreHeadlines: hasMoreHeadlines ?? this.hasMoreHeadlines, headlinesCursor: // This cursor is for fetching original headlines @@ -67,7 +67,7 @@ class EntityDetailsState extends Equatable { entityType, entity, isFollowing, - feedItems, // Changed + feedItems, headlinesStatus, hasMoreHeadlines, headlinesCursor, diff --git a/lib/entity_details/view/entity_details_page.dart b/lib/entity_details/view/entity_details_page.dart index 504f2c98..7d790768 100644 --- a/lib/entity_details/view/entity_details_page.dart +++ b/lib/entity_details/view/entity_details_page.dart @@ -1,16 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; // Added -import 'package:ht_data_repository/ht_data_repository.dart'; // For repository provider +import 'package:go_router/go_router.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_main/account/bloc/account_bloc.dart'; -import 'package:ht_main/app/bloc/app_bloc.dart'; // For accessing settings +import 'package:ht_main/app/bloc/app_bloc.dart'; import 'package:ht_main/entity_details/bloc/entity_details_bloc.dart'; import 'package:ht_main/entity_details/models/entity_type.dart'; import 'package:ht_main/l10n/app_localizations.dart'; import 'package:ht_main/l10n/l10n.dart'; -import 'package:ht_main/router/routes.dart'; // Added +import 'package:ht_main/router/routes.dart'; import 'package:ht_main/shared/constants/app_spacing.dart'; -import 'package:ht_main/shared/services/feed_injector_service.dart'; // Added +import 'package:ht_main/shared/services/feed_injector_service.dart'; import 'package:ht_main/shared/widgets/widgets.dart'; import 'package:ht_shared/ht_shared.dart'; @@ -26,7 +26,7 @@ class EntityDetailsPageArguments { final String? entityId; final EntityType? entityType; - final dynamic entity; // Category or Source + final dynamic entity; } class EntityDetailsPage extends StatelessWidget { @@ -46,21 +46,21 @@ class EntityDetailsPage extends StatelessWidget { // Explicitly type BlocProvider create: (context) { final feedInjectorService = FeedInjectorService(); - final entityDetailsBloc = EntityDetailsBloc( - headlinesRepository: context.read>(), - categoryRepository: context.read>(), - sourceRepository: context.read>(), - accountBloc: context.read(), - appBloc: context.read(), - feedInjectorService: feedInjectorService, - ); - entityDetailsBloc.add( - EntityDetailsLoadRequested( - entityId: args.entityId, - entityType: args.entityType, - entity: args.entity, - ), - ); + final entityDetailsBloc = + EntityDetailsBloc( + headlinesRepository: context.read>(), + categoryRepository: context.read>(), + sourceRepository: context.read>(), + accountBloc: context.read(), + appBloc: context.read(), + feedInjectorService: feedInjectorService, + )..add( + EntityDetailsLoadRequested( + entityId: args.entityId, + entityType: args.entityType, + entity: args.entity, + ), + ); return entityDetailsBloc; }, child: EntityDetailsView(args: args), @@ -69,9 +69,9 @@ class EntityDetailsPage extends StatelessWidget { } class EntityDetailsView extends StatefulWidget { - const EntityDetailsView({required this.args, super.key}); // Accept args + const EntityDetailsView({required this.args, super.key}); - final EntityDetailsPageArguments args; // Store args + final EntityDetailsPageArguments args; @override State createState() => _EntityDetailsViewState(); @@ -111,16 +111,13 @@ class _EntityDetailsViewState extends State { } String _getEntityTypeDisplayName(EntityType? type, AppLocalizations l10n) { - if (type == null) return l10n.detailsPageTitle; // Fallback + if (type == null) return l10n.detailsPageTitle; String name; switch (type) { case EntityType.category: - name = l10n.entityDetailsCategoryTitle; // Use direct l10n string + name = l10n.entityDetailsCategoryTitle; case EntityType.source: - name = l10n.entityDetailsSourceTitle; // Use direct l10n string - // EntityType.country does not exist, remove or map if added later - default: - name = l10n.detailsPageTitle; // Fallback + name = l10n.entityDetailsSourceTitle; } // Manual capitalization return name.isNotEmpty @@ -148,8 +145,7 @@ class _EntityDetailsViewState extends State { state.entity == null)) { return LoadingStateWidget( icon: Icons.info_outline, - headline: - entityTypeDisplayNameForTitle, // Use the display name directly + headline: entityTypeDisplayNameForTitle, subheadline: l10n.pleaseWait, ); } @@ -172,7 +168,7 @@ class _EntityDetailsViewState extends State { final String appBarTitleText; IconData? appBarIconData; - // String? entityImageHeroTag; // Not currently used + // String? entityImageHeroTag; if (state.entity is Category) { final cat = state.entity as Category; @@ -184,7 +180,7 @@ class _EntityDetailsViewState extends State { appBarTitleText = src.name; appBarIconData = Icons.source_outlined; } else { - appBarTitleText = l10n.detailsPageTitle; // Fallback + appBarTitleText = l10n.detailsPageTitle; } final description = state.entity is Category diff --git a/lib/headline-details/view/headline_details_page.dart b/lib/headline-details/view/headline_details_page.dart index 9ff508ad..45a90f6a 100644 --- a/lib/headline-details/view/headline_details_page.dart +++ b/lib/headline-details/view/headline_details_page.dart @@ -1,26 +1,21 @@ // // ignore_for_file: avoid_redundant_argument_values -import 'package:flutter/foundation.dart' show kIsWeb; // Import kIsWeb +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; // Import GoRouter +import 'package:go_router/go_router.dart'; import 'package:ht_main/account/bloc/account_bloc.dart'; -import 'package:ht_main/app/bloc/app_bloc.dart'; // Added AppBloc -import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Added for Page Arguments +import 'package:ht_main/app/bloc/app_bloc.dart'; +import 'package:ht_main/entity_details/view/entity_details_page.dart'; import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart'; import 'package:ht_main/headline-details/bloc/similar_headlines_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; -import 'package:ht_main/shared/shared.dart'; // Imports AppSpacing -import 'package:ht_shared/ht_shared.dart' - show - Category, - Headline, - HeadlineImageStyle, - Source; // Added Category, Source +import 'package:ht_main/shared/shared.dart'; +import 'package:ht_shared/ht_shared.dart'; import 'package:intl/intl.dart'; -import 'package:share_plus/share_plus.dart'; // Import share_plus +import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; class HeadlineDetailsPage extends StatefulWidget { @@ -136,7 +131,7 @@ class _HeadlineDetailsPageState extends State { return switch (state) { HeadlineDetailsInitial() || HeadlineDetailsLoading() => LoadingStateWidget( - icon: Icons.article_outlined, // Themed icon + icon: Icons.article_outlined, headline: l10n.headlineDetailsLoadingHeadline, subheadline: l10n.headlineDetailsLoadingSubheadline, ), @@ -188,7 +183,7 @@ class _HeadlineDetailsPageState extends State { final bookmarkButton = IconButton( icon: Icon( isSaved ? Icons.bookmark : Icons.bookmark_border_outlined, - color: colorScheme.primary, // Ensure icon color from theme + color: colorScheme.primary, ), tooltip: isSaved ? l10n.headlineDetailsRemoveFromSavedTooltip @@ -203,10 +198,7 @@ class _HeadlineDetailsPageState extends State { final Widget shareButtonWidget = Builder( builder: (BuildContext buttonContext) { return IconButton( - icon: Icon( - Icons.share_outlined, - color: colorScheme.primary, // Ensure icon color from theme - ), + icon: Icon(Icons.share_outlined, color: colorScheme.primary), tooltip: l10n.shareActionTooltip, onPressed: () async { final box = buttonContext.findRenderObject() as RenderBox?; @@ -254,7 +246,7 @@ class _HeadlineDetailsPageState extends State { icon: const Icon(Icons.arrow_back_ios_new), tooltip: MaterialLocalizations.of(context).backButtonTooltip, onPressed: () => context.pop(), - color: colorScheme.onSurface, // Ensure icon color from theme + color: colorScheme.onSurface, ), actions: [ bookmarkButton, @@ -269,13 +261,13 @@ class _HeadlineDetailsPageState extends State { foregroundColor: colorScheme.onSurface, ), SliverPadding( - padding: horizontalPadding.copyWith(top: AppSpacing.sm), // Adjusted + padding: horizontalPadding.copyWith(top: AppSpacing.sm), sliver: SliverToBoxAdapter( child: Text( headline.title, style: textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, - ), // Adjusted style + ), ), ), ), @@ -288,9 +280,7 @@ class _HeadlineDetailsPageState extends State { ), sliver: SliverToBoxAdapter( child: ClipRRect( - borderRadius: BorderRadius.circular( - AppSpacing.md, - ), // Consistent radius + borderRadius: BorderRadius.circular(AppSpacing.md), child: AspectRatio( aspectRatio: 16 / 9, child: Image.network( @@ -310,7 +300,7 @@ class _HeadlineDetailsPageState extends State { child: Icon( Icons.broken_image_outlined, color: colorScheme.onSurfaceVariant, - size: AppSpacing.xxl * 1.5, // Larger placeholder + size: AppSpacing.xxl * 1.5, ), ), ), @@ -336,35 +326,31 @@ class _HeadlineDetailsPageState extends State { child: Icon( Icons.image_not_supported_outlined, color: colorScheme.onSurfaceVariant, - size: AppSpacing.xxl * 1.5, // Larger placeholder + size: AppSpacing.xxl * 1.5, ), ), ), ), ), SliverPadding( - padding: horizontalPadding.copyWith( - top: AppSpacing.lg, - ), // Increased spacing + padding: horizontalPadding.copyWith(top: AppSpacing.lg), sliver: SliverToBoxAdapter( child: Wrap( - spacing: AppSpacing.md, // Increased spacing - runSpacing: AppSpacing.sm, // Adjusted runSpacing + spacing: AppSpacing.md, + runSpacing: AppSpacing.sm, children: _buildMetadataChips(context, headline), ), ), ), if (headline.description != null && headline.description!.isNotEmpty) SliverPadding( - padding: horizontalPadding.copyWith( - top: AppSpacing.lg, - ), // Increased + padding: horizontalPadding.copyWith(top: AppSpacing.lg), sliver: SliverToBoxAdapter( child: Text( headline.description!, style: textTheme.bodyLarge?.copyWith( color: colorScheme.onSurfaceVariant, - height: 1.6, // Improved line height + height: 1.6, ), ), ), @@ -373,7 +359,7 @@ class _HeadlineDetailsPageState extends State { SliverPadding( padding: horizontalPadding.copyWith( top: AppSpacing.xl, - bottom: AppSpacing.xl, // Consistent padding + bottom: AppSpacing.xl, ), sliver: SliverToBoxAdapter( child: ElevatedButton.icon( @@ -429,7 +415,7 @@ class _HeadlineDetailsPageState extends State { final textTheme = theme.textTheme; final colorScheme = theme.colorScheme; final chipLabelStyle = textTheme.labelMedium?.copyWith( - color: colorScheme.onSecondaryContainer, // Ensure text is visible + color: colorScheme.onSecondaryContainer, ); final chipBackgroundColor = colorScheme.secondaryContainer; final chipAvatarColor = colorScheme.onSecondaryContainer; @@ -477,9 +463,7 @@ class _HeadlineDetailsPageState extends State { extra: EntityDetailsPageArguments(entity: headline.source), ); }, - borderRadius: BorderRadius.circular( - AppSpacing.sm, - ), // Match chip shape + borderRadius: BorderRadius.circular(AppSpacing.sm), child: Chip( avatar: Icon( Icons.source_outlined, @@ -508,9 +492,7 @@ class _HeadlineDetailsPageState extends State { extra: EntityDetailsPageArguments(entity: headline.category), ); }, - borderRadius: BorderRadius.circular( - AppSpacing.sm, - ), // Match chip shape + borderRadius: BorderRadius.circular(AppSpacing.sm), child: Chip( avatar: Icon( Icons.category_outlined, @@ -581,9 +563,8 @@ class _HeadlineDetailsPageState extends State { final SimilarHeadlinesLoaded loadedState => SliverPadding( padding: hPadding.copyWith(bottom: AppSpacing.xxl), sliver: SliverList.separated( - separatorBuilder: (context, index) => const SizedBox( - height: AppSpacing.sm, - ), // Spacing between items + separatorBuilder: (context, index) => + const SizedBox(height: AppSpacing.sm), itemCount: loadedState.similarHeadlines.length, itemBuilder: (context, index) { // Corrected: SliverList.separated uses itemBuilder diff --git a/lib/headlines-feed/bloc/categories_filter_bloc.dart b/lib/headlines-feed/bloc/categories_filter_bloc.dart index 47c9d033..63e9887b 100644 --- a/lib/headlines-feed/bloc/categories_filter_bloc.dart +++ b/lib/headlines-feed/bloc/categories_filter_bloc.dart @@ -3,11 +3,8 @@ import 'dart:async'; 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_shared/ht_shared.dart' - show - Category, - HtHttpException; // Shared models, including Category and standardized exceptions +import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_shared/ht_shared.dart' show Category, HtHttpException; part 'categories_filter_event.dart'; part 'categories_filter_state.dart'; @@ -29,11 +26,11 @@ class CategoriesFilterBloc super(const CategoriesFilterState()) { on( _onCategoriesFilterRequested, - transformer: restartable(), // Only process the latest request + transformer: restartable(), ); on( _onCategoriesFilterLoadMoreRequested, - transformer: droppable(), // Ignore new requests while one is processing + transformer: droppable(), ); } @@ -66,7 +63,7 @@ class CategoriesFilterBloc categories: response.items, hasMore: response.hasMore, cursor: response.cursor, - clearError: true, // Clear any previous error + clearError: true, ), ); } on HtHttpException catch (e) { @@ -92,7 +89,7 @@ class CategoriesFilterBloc try { final response = await _categoriesRepository.readAll( limit: _categoriesLimit, - startAfterId: state.cursor, // Use the cursor from the current state + startAfterId: state.cursor, ); emit( state.copyWith( @@ -105,13 +102,7 @@ class CategoriesFilterBloc ); } on HtHttpException catch (e) { // Keep existing data but indicate failure - emit( - state.copyWith( - status: CategoriesFilterStatus - .failure, // Or a specific 'loadMoreFailure' status? - error: e, - ), - ); + emit(state.copyWith(status: CategoriesFilterStatus.failure, error: e)); } catch (e) { // Catch unexpected errors emit(state.copyWith(status: CategoriesFilterStatus.failure, error: e)); diff --git a/lib/headlines-feed/bloc/categories_filter_state.dart b/lib/headlines-feed/bloc/categories_filter_state.dart index 1f2e8172..e0158da7 100644 --- a/lib/headlines-feed/bloc/categories_filter_state.dart +++ b/lib/headlines-feed/bloc/categories_filter_state.dart @@ -57,8 +57,8 @@ final class CategoriesFilterState extends Equatable { bool? hasMore, String? cursor, Object? error, - bool clearError = false, // Flag to explicitly clear the error - bool clearCursor = false, // Flag to explicitly clear the cursor + bool clearError = false, + bool clearCursor = false, }) { return CategoriesFilterState( status: status ?? this.status, diff --git a/lib/headlines-feed/bloc/countries_filter_bloc.dart b/lib/headlines-feed/bloc/countries_filter_bloc.dart index dc65bd4f..a3978338 100644 --- a/lib/headlines-feed/bloc/countries_filter_bloc.dart +++ b/lib/headlines-feed/bloc/countries_filter_bloc.dart @@ -1,13 +1,10 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; -import 'package:bloc_concurrency/bloc_concurrency.dart'; // For transformers +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_shared/ht_shared.dart' - show - Country, - HtHttpException; // Shared models, including Country and standardized exceptions +import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_shared/ht_shared.dart' show Country, HtHttpException; part 'countries_filter_event.dart'; part 'countries_filter_state.dart'; @@ -28,11 +25,11 @@ class CountriesFilterBloc super(const CountriesFilterState()) { on( _onCountriesFilterRequested, - transformer: restartable(), // Only process the latest request + transformer: restartable(), ); on( _onCountriesFilterLoadMoreRequested, - transformer: droppable(), // Ignore new requests while one is processing + transformer: droppable(), ); } @@ -64,7 +61,7 @@ class CountriesFilterBloc countries: response.items, hasMore: response.hasMore, cursor: response.cursor, - clearError: true, // Clear any previous error + clearError: true, ), ); } on HtHttpException catch (e) { @@ -90,7 +87,7 @@ class CountriesFilterBloc try { final response = await _countriesRepository.readAll( limit: _countriesLimit, - startAfterId: state.cursor, // Use the cursor from the current state + startAfterId: state.cursor, ); emit( state.copyWith( diff --git a/lib/headlines-feed/bloc/countries_filter_state.dart b/lib/headlines-feed/bloc/countries_filter_state.dart index 53f02d4e..2be4546d 100644 --- a/lib/headlines-feed/bloc/countries_filter_state.dart +++ b/lib/headlines-feed/bloc/countries_filter_state.dart @@ -59,8 +59,8 @@ final class CountriesFilterState extends Equatable { bool? hasMore, String? cursor, Object? error, - bool clearError = false, // Flag to explicitly clear the error - bool clearCursor = false, // Flag to explicitly clear the cursor + bool clearError = false, + bool clearCursor = false, }) { return CountriesFilterState( status: status ?? this.status, diff --git a/lib/headlines-feed/bloc/headlines_feed_bloc.dart b/lib/headlines-feed/bloc/headlines_feed_bloc.dart index df143741..e0a6bbb9 100644 --- a/lib/headlines-feed/bloc/headlines_feed_bloc.dart +++ b/lib/headlines-feed/bloc/headlines_feed_bloc.dart @@ -3,11 +3,11 @@ import 'dart:async'; 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/app/bloc/app_bloc.dart'; // Added +import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_main/app/bloc/app_bloc.dart'; import 'package:ht_main/headlines-feed/models/headline_filter.dart'; -import 'package:ht_main/shared/services/feed_injector_service.dart'; // Added -import 'package:ht_shared/ht_shared.dart'; // Updated for FeedItem, AppConfig, User +import 'package:ht_main/shared/services/feed_injector_service.dart'; +import 'package:ht_shared/ht_shared.dart'; part 'headlines_feed_event.dart'; part 'headlines_feed_state.dart'; @@ -25,11 +25,11 @@ class HeadlinesFeedBloc extends Bloc { /// Requires repositories and services for its operations. HeadlinesFeedBloc({ required HtDataRepository headlinesRepository, - required FeedInjectorService feedInjectorService, // Added - required AppBloc appBloc, // Added + required FeedInjectorService feedInjectorService, + required AppBloc appBloc, }) : _headlinesRepository = headlinesRepository, - _feedInjectorService = feedInjectorService, // Added - _appBloc = appBloc, // Added + _feedInjectorService = feedInjectorService, + _appBloc = appBloc, super(HeadlinesFeedInitial()) { on( _onHeadlinesFeedFetchRequested, @@ -46,8 +46,8 @@ class HeadlinesFeedBloc extends Bloc { } final HtDataRepository _headlinesRepository; - final FeedInjectorService _feedInjectorService; // Added - final AppBloc _appBloc; // Added + final FeedInjectorService _feedInjectorService; + final AppBloc _appBloc; /// The number of headlines to fetch per page during pagination or initial load. static const _headlinesFetchLimit = 10; @@ -62,7 +62,7 @@ class HeadlinesFeedBloc extends Bloc { HeadlinesFeedFiltersApplied event, Emitter emit, ) async { - emit(HeadlinesFeedLoading()); // Show loading for filter application + emit(HeadlinesFeedLoading()); try { final queryParams = {}; if (event.filter.categories?.isNotEmpty ?? false) { @@ -96,7 +96,7 @@ class HeadlinesFeedBloc extends Bloc { headlines: headlineResponse.items, user: currentUser, appConfig: appConfig, - currentFeedItemCount: 0, // Initial load for filters + currentFeedItemCount: 0, ); emit( @@ -132,7 +132,7 @@ class HeadlinesFeedBloc extends Bloc { HeadlinesFeedFiltersCleared event, Emitter emit, ) async { - emit(HeadlinesFeedLoading()); // Show loading indicator + emit(HeadlinesFeedLoading()); try { // Fetch the first page with no filters final headlineResponse = await _headlinesRepository.readAll( @@ -161,7 +161,7 @@ class HeadlinesFeedBloc extends Bloc { feedItems: processedFeedItems, hasMore: headlineResponse.hasMore, cursor: headlineResponse.cursor, - filter: const HeadlineFilter(), // Ensure filter is reset + filter: const HeadlineFilter(), ), ); @@ -207,18 +207,17 @@ class HeadlinesFeedBloc extends Bloc { if (event.cursor != null) { // Explicit pagination request - if (!loadedState.hasMore) return; // No more items to fetch + if (!loadedState.hasMore) return; isPaginating = true; - currentCursorForFetch = - loadedState.cursor; // Use BLoC's cursor for safety + currentCursorForFetch = loadedState.cursor; } else { // Initial fetch or refresh (event.cursor is null) - currentFeedItems = []; // Reset for non-pagination + currentFeedItems = []; currentFeedItemCountForInjector = 0; } } else if (state is HeadlinesFeedLoading || state is HeadlinesFeedLoadingSilently) { - if (event.cursor == null) return; // Avoid concurrent initial fetches + if (event.cursor == null) return; } // For initial load or if event.cursor is null, currentCursorForFetch remains null. @@ -298,7 +297,7 @@ class HeadlinesFeedBloc extends Bloc { HeadlinesFeedRefreshRequested event, Emitter emit, ) async { - emit(HeadlinesFeedLoading()); // Show loading indicator for refresh + emit(HeadlinesFeedLoading()); var currentFilter = const HeadlineFilter(); if (state is HeadlinesFeedLoaded) { diff --git a/lib/headlines-feed/bloc/headlines_feed_state.dart b/lib/headlines-feed/bloc/headlines_feed_state.dart index 628a49b7..a9a9df22 100644 --- a/lib/headlines-feed/bloc/headlines_feed_state.dart +++ b/lib/headlines-feed/bloc/headlines_feed_state.dart @@ -41,14 +41,14 @@ final class HeadlinesFeedLoadingSilently extends HeadlinesFeedState {} final class HeadlinesFeedLoaded extends HeadlinesFeedState { /// {@macro headlines_feed_loaded} const HeadlinesFeedLoaded({ - this.feedItems = const [], // Changed from headlines + this.feedItems = const [], this.hasMore = true, this.cursor, this.filter = const HeadlineFilter(), }); /// The list of [FeedItem] objects currently loaded. - final List feedItems; // Changed from List + final List feedItems; /// Flag indicating if there are more headlines available to fetch /// via pagination. `true` if more might exist, `false` otherwise. @@ -65,13 +65,13 @@ final class HeadlinesFeedLoaded extends HeadlinesFeedState { /// Creates a copy of this [HeadlinesFeedLoaded] state with the given fields /// replaced with new values. HeadlinesFeedLoaded copyWith({ - List? feedItems, // Changed from List + List? feedItems, bool? hasMore, String? cursor, HeadlineFilter? filter, }) { return HeadlinesFeedLoaded( - feedItems: feedItems ?? this.feedItems, // Changed + feedItems: feedItems ?? this.feedItems, hasMore: hasMore ?? this.hasMore, cursor: cursor ?? this.cursor, filter: filter ?? this.filter, @@ -79,7 +79,7 @@ final class HeadlinesFeedLoaded extends HeadlinesFeedState { } @override - List get props => [feedItems, hasMore, cursor, filter]; // Changed + List get props => [feedItems, hasMore, cursor, filter]; } /// {@template headlines_feed_error} diff --git a/lib/headlines-feed/bloc/sources_filter_bloc.dart b/lib/headlines-feed/bloc/sources_filter_bloc.dart index 2413277b..9b127923 100644 --- a/lib/headlines-feed/bloc/sources_filter_bloc.dart +++ b/lib/headlines-feed/bloc/sources_filter_bloc.dart @@ -53,20 +53,18 @@ class SourcesFilterBloc extends Bloc { // However, if initial capsule filters ARE provided, we should respect them for the initial display. final displayableSources = _getFilteredSources( allSources: allAvailableSources, - selectedCountries: initialSelectedCountryIsoCodes, // Use event's data - selectedTypes: initialSelectedSourceTypes, // Use event's data + selectedCountries: initialSelectedCountryIsoCodes, + selectedTypes: initialSelectedSourceTypes, ); emit( state.copyWith( availableCountries: availableCountries.items, allAvailableSources: allAvailableSources, - displayableSources: - displayableSources, // Now correctly filtered if initial capsules were set + displayableSources: displayableSources, finallySelectedSourceIds: initialSelectedSourceIds, - selectedCountryIsoCodes: - initialSelectedCountryIsoCodes, // Use event's data - selectedSourceTypes: initialSelectedSourceTypes, // Use event's data + selectedCountryIsoCodes: initialSelectedCountryIsoCodes, + selectedSourceTypes: initialSelectedSourceTypes, dataLoadingStatus: SourceFilterDataLoadingStatus.success, clearErrorMessage: true, ), @@ -120,7 +118,7 @@ class SourcesFilterBloc extends Bloc { final newDisplayableSources = _getFilteredSources( allSources: state.allAvailableSources, selectedCountries: state.selectedCountryIsoCodes, - selectedTypes: {}, // Cleared source types + selectedTypes: {}, ); emit( state.copyWith( @@ -175,7 +173,7 @@ class SourcesFilterBloc extends Bloc { selectedCountryIsoCodes: {}, selectedSourceTypes: {}, finallySelectedSourceIds: {}, - displayableSources: List.from(state.allAvailableSources), // Reset + displayableSources: List.from(state.allAvailableSources), dataLoadingStatus: SourceFilterDataLoadingStatus.success, clearErrorMessage: true, ), @@ -189,7 +187,7 @@ class SourcesFilterBloc extends Bloc { required Set selectedTypes, }) { if (selectedCountries.isEmpty && selectedTypes.isEmpty) { - return List.from(allSources); // Return all if no filters + return List.from(allSources); } return allSources.where((source) { diff --git a/lib/headlines-feed/bloc/sources_filter_state.dart b/lib/headlines-feed/bloc/sources_filter_state.dart index 9e4c16bb..73232da3 100644 --- a/lib/headlines-feed/bloc/sources_filter_state.dart +++ b/lib/headlines-feed/bloc/sources_filter_state.dart @@ -10,7 +10,7 @@ class SourcesFilterState extends Equatable { this.selectedCountryIsoCodes = const {}, this.availableSourceTypes = SourceType.values, this.selectedSourceTypes = const {}, - this.allAvailableSources = const [], // Added new property + this.allAvailableSources = const [], this.displayableSources = const [], this.finallySelectedSourceIds = const {}, this.dataLoadingStatus = SourceFilterDataLoadingStatus.initial, @@ -21,7 +21,7 @@ class SourcesFilterState extends Equatable { final Set selectedCountryIsoCodes; final List availableSourceTypes; final Set selectedSourceTypes; - final List allAvailableSources; // Added new property + final List allAvailableSources; final List displayableSources; final Set finallySelectedSourceIds; final SourceFilterDataLoadingStatus dataLoadingStatus; @@ -32,7 +32,7 @@ class SourcesFilterState extends Equatable { Set? selectedCountryIsoCodes, List? availableSourceTypes, Set? selectedSourceTypes, - List? allAvailableSources, // Added new property + List? allAvailableSources, List? displayableSources, Set? finallySelectedSourceIds, SourceFilterDataLoadingStatus? dataLoadingStatus, @@ -45,8 +45,7 @@ class SourcesFilterState extends Equatable { selectedCountryIsoCodes ?? this.selectedCountryIsoCodes, availableSourceTypes: availableSourceTypes ?? this.availableSourceTypes, selectedSourceTypes: selectedSourceTypes ?? this.selectedSourceTypes, - allAvailableSources: - allAvailableSources ?? this.allAvailableSources, // Added + allAvailableSources: allAvailableSources ?? this.allAvailableSources, displayableSources: displayableSources ?? this.displayableSources, finallySelectedSourceIds: finallySelectedSourceIds ?? this.finallySelectedSourceIds, @@ -63,7 +62,7 @@ class SourcesFilterState extends Equatable { selectedCountryIsoCodes, availableSourceTypes, selectedSourceTypes, - allAvailableSources, // Added new property + allAvailableSources, displayableSources, finallySelectedSourceIds, dataLoadingStatus, diff --git a/lib/headlines-feed/models/headline_filter.dart b/lib/headlines-feed/models/headline_filter.dart index 87f898e9..6e8f4cb5 100644 --- a/lib/headlines-feed/models/headline_filter.dart +++ b/lib/headlines-feed/models/headline_filter.dart @@ -11,7 +11,7 @@ class HeadlineFilter extends Equatable { this.sources, this.selectedSourceCountryIsoCodes, this.selectedSourceSourceTypes, - this.isFromFollowedItems = false, // Added new field with default + this.isFromFollowedItems = false, }); /// The list of selected category filters. @@ -37,7 +37,7 @@ class HeadlineFilter extends Equatable { sources, selectedSourceCountryIsoCodes, selectedSourceSourceTypes, - isFromFollowedItems, // Added to props + isFromFollowedItems, ]; /// Creates a copy of this [HeadlineFilter] with the given fields @@ -47,7 +47,7 @@ class HeadlineFilter extends Equatable { List? sources, Set? selectedSourceCountryIsoCodes, Set? selectedSourceSourceTypes, - bool? isFromFollowedItems, // Added to copyWith + bool? isFromFollowedItems, }) { return HeadlineFilter( categories: categories ?? this.categories, @@ -56,8 +56,7 @@ class HeadlineFilter extends Equatable { selectedSourceCountryIsoCodes ?? this.selectedSourceCountryIsoCodes, selectedSourceSourceTypes: selectedSourceSourceTypes ?? this.selectedSourceSourceTypes, - isFromFollowedItems: - isFromFollowedItems ?? this.isFromFollowedItems, // Added + isFromFollowedItems: isFromFollowedItems ?? this.isFromFollowedItems, ); } } diff --git a/lib/headlines-feed/view/category_filter_page.dart b/lib/headlines-feed/view/category_filter_page.dart index 8d0c0b6a..03142506 100644 --- a/lib/headlines-feed/view/category_filter_page.dart +++ b/lib/headlines-feed/view/category_filter_page.dart @@ -4,12 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_main/headlines-feed/bloc/categories_filter_bloc.dart'; // Import the BLoC +import 'package:ht_main/headlines-feed/bloc/categories_filter_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/shared/constants/constants.dart'; -import 'package:ht_main/shared/widgets/widgets.dart'; // For loading/error widgets -import 'package:ht_shared/ht_shared.dart' - show Category; // Import Category model +import 'package:ht_main/shared/widgets/widgets.dart'; +import 'package:ht_shared/ht_shared.dart' show Category; /// {@template category_filter_page} /// A page dedicated to selecting news categories for filtering headlines. @@ -100,15 +99,14 @@ class _CategoryFilterPageState extends State { Widget build(BuildContext context) { final l10n = context.l10n; - final theme = Theme.of(context); // Get theme - final textTheme = theme.textTheme; // Get textTheme - final colorScheme = theme.colorScheme; // Get colorScheme + final theme = Theme.of(context); + final textTheme = theme.textTheme; return Scaffold( appBar: AppBar( title: Text( l10n.headlinesFeedFilterCategoryLabel, - style: textTheme.titleLarge, // Apply consistent title style + style: textTheme.titleLarge, ), actions: [ IconButton( @@ -134,9 +132,9 @@ class _CategoryFilterPageState extends State { /// Builds the main content body based on the current [CategoriesFilterState]. Widget _buildBody(BuildContext context, CategoriesFilterState state) { final l10n = context.l10n; - final theme = Theme.of(context); // Get theme - final textTheme = theme.textTheme; // Get textTheme - final colorScheme = theme.colorScheme; // Get colorScheme + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; // Handle initial loading state if (state.status == CategoriesFilterStatus.loading) { @@ -162,7 +160,7 @@ class _CategoryFilterPageState extends State { if (state.status == CategoriesFilterStatus.success && state.categories.isEmpty) { return InitialStateWidget( - icon: Icons.search_off_outlined, // Use outlined version + icon: Icons.search_off_outlined, headline: l10n.categoryFilterEmptyHeadline, subheadline: l10n.categoryFilterEmptySubheadline, ); @@ -173,7 +171,7 @@ class _CategoryFilterPageState extends State { controller: _scrollController, padding: const EdgeInsets.symmetric( vertical: AppSpacing.paddingSmall, - ).copyWith(bottom: AppSpacing.xxl), // Consistent vertical padding + ).copyWith(bottom: AppSpacing.xxl), itemCount: state.categories.length + ((state.status == CategoriesFilterStatus.loadingMore || @@ -214,7 +212,7 @@ class _CategoryFilterPageState extends State { title: Text(category.name, style: textTheme.titleMedium), secondary: category.iconUrl != null ? SizedBox( - width: AppSpacing.xl + AppSpacing.sm, // 40 -> 32 + width: AppSpacing.xl + AppSpacing.sm, height: AppSpacing.xl + AppSpacing.sm, child: ClipRRect( borderRadius: BorderRadius.circular(AppSpacing.xs), @@ -222,8 +220,8 @@ class _CategoryFilterPageState extends State { category.iconUrl!, fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) => Icon( - Icons.category_outlined, // Use outlined - color: colorScheme.onSurfaceVariant, // Theme color + Icons.category_outlined, + color: colorScheme.onSurfaceVariant, size: AppSpacing.xl, ), loadingBuilder: (context, child, loadingProgress) { @@ -254,7 +252,7 @@ class _CategoryFilterPageState extends State { }, controlAffinity: ListTileControlAffinity.leading, contentPadding: const EdgeInsets.symmetric( - horizontal: AppSpacing.paddingMedium, // Standard padding + horizontal: AppSpacing.paddingMedium, ), ); }, diff --git a/lib/headlines-feed/view/country_filter_page.dart b/lib/headlines-feed/view/country_filter_page.dart index 8c14e26e..ef575ff4 100644 --- a/lib/headlines-feed/view/country_filter_page.dart +++ b/lib/headlines-feed/view/country_filter_page.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_main/headlines-feed/bloc/countries_filter_bloc.dart'; // Import the BLoC +import 'package:ht_main/headlines-feed/bloc/countries_filter_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/shared/constants/constants.dart'; -import 'package:ht_main/shared/widgets/widgets.dart'; // For loading/error widgets -import 'package:ht_shared/ht_shared.dart' show Country; // Import Country model +import 'package:ht_main/shared/widgets/widgets.dart'; +import 'package:ht_shared/ht_shared.dart' show Country; /// {@template country_filter_page} /// A page dedicated to selecting event countries for filtering headlines. @@ -98,14 +98,14 @@ class _CountryFilterPageState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; - final theme = Theme.of(context); // Get theme - final textTheme = theme.textTheme; // Get textTheme + final theme = Theme.of(context); + final textTheme = theme.textTheme; return Scaffold( appBar: AppBar( title: Text( l10n.headlinesFeedFilterEventCountryLabel, - style: textTheme.titleLarge, // Apply consistent title style + style: textTheme.titleLarge, ), actions: [ IconButton( @@ -131,9 +131,9 @@ class _CountryFilterPageState extends State { /// Builds the main content body based on the current [CountriesFilterState]. Widget _buildBody(BuildContext context, CountriesFilterState state) { final l10n = context.l10n; - final theme = Theme.of(context); // Get theme - final textTheme = theme.textTheme; // Get textTheme - final colorScheme = theme.colorScheme; // Get colorScheme + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; // Handle initial loading state if (state.status == CountriesFilterStatus.loading) { @@ -158,7 +158,7 @@ class _CountryFilterPageState extends State { if (state.status == CountriesFilterStatus.success && state.countries.isEmpty) { return InitialStateWidget( - icon: Icons.flag_circle_outlined, // More relevant icon + icon: Icons.flag_circle_outlined, headline: l10n.countryFilterEmptyHeadline, subheadline: l10n.countryFilterEmptySubheadline, ); @@ -169,7 +169,7 @@ class _CountryFilterPageState extends State { controller: _scrollController, padding: const EdgeInsets.symmetric( vertical: AppSpacing.paddingSmall, - ).copyWith(bottom: AppSpacing.xxl), // Consistent vertical padding + ).copyWith(bottom: AppSpacing.xxl), itemCount: state.countries.length + ((state.status == CountriesFilterStatus.loadingMore || @@ -209,18 +209,18 @@ class _CountryFilterPageState extends State { return CheckboxListTile( title: Text(country.name, style: textTheme.titleMedium), secondary: SizedBox( - width: AppSpacing.xl + AppSpacing.xs, // Standardized width (36) - height: AppSpacing.lg + AppSpacing.sm, // Standardized height (24) + width: AppSpacing.xl + AppSpacing.xs, + height: AppSpacing.lg + AppSpacing.sm, child: ClipRRect( // Clip the image for rounded corners if desired borderRadius: BorderRadius.circular(AppSpacing.xs / 2), child: Image.network( country.flagUrl, - fit: BoxFit.cover, // Use cover for better filling + fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Icon( Icons.flag_outlined, color: colorScheme.onSurfaceVariant, - size: AppSpacing.lg, // Adjust size as needed + size: AppSpacing.lg, ), loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; diff --git a/lib/headlines-feed/view/headlines_feed_page.dart b/lib/headlines-feed/view/headlines_feed_page.dart index 064ad3ca..c4a64f1a 100644 --- a/lib/headlines-feed/view/headlines_feed_page.dart +++ b/lib/headlines-feed/view/headlines_feed_page.dart @@ -4,13 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_main/app/bloc/app_bloc.dart'; // Added to access settings +import 'package:ht_main/app/bloc/app_bloc.dart'; import 'package:ht_main/headlines-feed/bloc/headlines_feed_bloc.dart'; // HeadlineItemWidget import removed import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; import 'package:ht_main/shared/shared.dart'; -import 'package:ht_shared/ht_shared.dart'; // Import all of ht_shared +import 'package:ht_shared/ht_shared.dart'; /// {@template headlines_feed_view} /// The core view widget for the headlines feed. @@ -59,7 +59,7 @@ class _HeadlinesFeedPageState extends State { if (state.hasMore) { // Request the next page of headlines context.read().add( - HeadlinesFeedFetchRequested(cursor: state.cursor), // Pass cursor + HeadlinesFeedFetchRequested(cursor: state.cursor), ); } } @@ -92,11 +92,11 @@ class _HeadlinesFeedPageState extends State { actions: [ // IconButton( // icon: const Icon(Icons.notifications_outlined), - // tooltip: l10n.notificationsTooltip, // Add tooltip for accessibility + // tooltip: l10n.notificationsTooltip, // onPressed: () { // context.goNamed( // Routes.notificationsName, - // ); // Ensure correct route name + // ); // }, // ), BlocBuilder( @@ -107,7 +107,7 @@ class _HeadlinesFeedPageState extends State { isFilterApplied = (state.filter.categories?.isNotEmpty ?? false) || (state.filter.sources?.isNotEmpty ?? false); - // (state.filter.eventCountries?.isNotEmpty ?? false); // Removed + // (state.filter.eventCountries?.isNotEmpty ?? false); } return Stack( children: [ @@ -152,7 +152,7 @@ class _HeadlinesFeedPageState extends State { case HeadlinesFeedLoading(): // Display full-screen loading indicator return LoadingStateWidget( - icon: Icons.newspaper, // Use a relevant icon + icon: Icons.newspaper, headline: l10n.headlinesFeedLoadingHeadline, subheadline: l10n.headlinesFeedLoadingSubheadline, ); @@ -191,7 +191,7 @@ class _HeadlinesFeedPageState extends State { itemCount: state.hasMore ? state.feedItems.length + 1 // Changed - : state.feedItems.length, // Changed + : state.feedItems.length, separatorBuilder: (context, index) { // Add a bit more space if the next item is an Ad or AccountAction if (index < state.feedItems.length - 1) { @@ -215,7 +215,7 @@ class _HeadlinesFeedPageState extends State { child: Center(child: CircularProgressIndicator()), ); } - final item = state.feedItems[index]; // Changed + final item = state.feedItems[index]; if (item is Headline) { final imageStyle = context @@ -346,7 +346,7 @@ class _HeadlinesFeedPageState extends State { ), ); } - return const SizedBox.shrink(); // Should not happen + return const SizedBox.shrink(); }, ), ); diff --git a/lib/headlines-feed/view/headlines_filter_page.dart b/lib/headlines-feed/view/headlines_filter_page.dart index 24498da7..9a1e3f73 100644 --- a/lib/headlines-feed/view/headlines_filter_page.dart +++ b/lib/headlines-feed/view/headlines_filter_page.dart @@ -1,25 +1,17 @@ // -// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs, unused_field import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_data_repository/ht_data_repository.dart'; // Added -import 'package:ht_main/app/bloc/app_bloc.dart'; // Added +import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_main/app/bloc/app_bloc.dart'; import 'package:ht_main/headlines-feed/bloc/headlines_feed_bloc.dart'; import 'package:ht_main/headlines-feed/models/headline_filter.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; import 'package:ht_main/shared/constants/constants.dart'; -import 'package:ht_shared/ht_shared.dart' - show - Category, - HtHttpException, - NotFoundException, - Source, - SourceType, - User, // Added - UserContentPreferences; // Added +import 'package:ht_shared/ht_shared.dart'; // Keys for passing data to/from SourceFilterPage const String keySelectedSources = 'selectedSources'; @@ -117,7 +109,7 @@ class _HeadlinesFilterPageState extends State { if (currentUser == null) { setState(() { _isLoadingFollowedFilters = false; - _useFollowedFilters = false; // Uncheck the box + _useFollowedFilters = false; _loadFollowedFiltersError = context.l10n.mustBeLoggedInToUseFeatureError; }); @@ -130,15 +122,15 @@ class _HeadlinesFilterPageState extends State { final preferences = await preferencesRepo.read( id: currentUser.id, userId: currentUser.id, - ); // Assuming read by user ID + ); // NEW: Check if followed items are empty if (preferences.followedCategories.isEmpty && preferences.followedSources.isEmpty) { setState(() { _isLoadingFollowedFilters = false; - _useFollowedFilters = false; // Uncheck the box - _tempSelectedCategories = []; // Ensure lists are cleared + _useFollowedFilters = false; + _tempSelectedCategories = []; _tempSelectedSources = []; }); if (mounted) { @@ -151,7 +143,7 @@ class _HeadlinesFilterPageState extends State { ), ); } - return; // Exit the function as no filters to apply + return; } else { setState(() { _currentUserPreferences = preferences; @@ -164,14 +156,11 @@ class _HeadlinesFilterPageState extends State { } } on NotFoundException { setState(() { - _currentUserPreferences = UserContentPreferences( - id: currentUser.id, - ); // Empty prefs + _currentUserPreferences = UserContentPreferences(id: currentUser.id); _tempSelectedCategories = []; _tempSelectedSources = []; _isLoadingFollowedFilters = false; - _useFollowedFilters = - false; // Uncheck as no prefs found (implies no followed) + _useFollowedFilters = false; }); if (mounted) { ScaffoldMessenger.of(context) @@ -186,13 +175,13 @@ class _HeadlinesFilterPageState extends State { } on HtHttpException catch (e) { setState(() { _isLoadingFollowedFilters = false; - _useFollowedFilters = false; // Uncheck the box - _loadFollowedFiltersError = e.message; // Or a generic "Failed to load" + _useFollowedFilters = false; + _loadFollowedFiltersError = e.message; }); } catch (e) { setState(() { _isLoadingFollowedFilters = false; - _useFollowedFilters = false; // Uncheck the box + _useFollowedFilters = false; _loadFollowedFiltersError = context.l10n.unknownError; }); } @@ -226,7 +215,7 @@ class _HeadlinesFilterPageState extends State { required String routeName, // For sources, currentSelection will be a Map required dynamic currentSelectionData, - required void Function(dynamic)? onResult, // Result can also be a Map + required void Function(dynamic)? onResult, bool enabled = true, }) { final l10n = context.l10n; @@ -241,13 +230,13 @@ class _HeadlinesFilterPageState extends State { title: Text(title), subtitle: Text(subtitle), trailing: const Icon(Icons.chevron_right), - enabled: enabled, // Use the enabled parameter + enabled: enabled, onTap: enabled // Only allow tap if enabled ? () async { final result = await context.pushNamed( routeName, - extra: currentSelectionData, // Pass the map or list + extra: currentSelectionData, ); if (result != null && onResult != null) { onResult(result); @@ -267,7 +256,7 @@ class _HeadlinesFilterPageState extends State { leading: IconButton( icon: const Icon(Icons.close), tooltip: MaterialLocalizations.of(context).closeButtonTooltip, - onPressed: () => context.pop(), // Discard changes + onPressed: () => context.pop(), ), title: Text(l10n.headlinesFeedFilterTitle), actions: [ @@ -307,7 +296,7 @@ class _HeadlinesFilterPageState extends State { _tempSelectedSourceSourceTypes.isNotEmpty ? _tempSelectedSourceSourceTypes : null, - isFromFollowedItems: _useFollowedFilters, // Set the new flag + isFromFollowedItems: _useFollowedFilters, ); context.read().add( HeadlinesFeedFiltersApplied(filter: newFilter), @@ -322,7 +311,7 @@ class _HeadlinesFilterPageState extends State { children: [ Padding( padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.paddingSmall, // Consistent with ListTiles + horizontal: AppSpacing.paddingSmall, ), child: CheckboxListTile( title: Text(l10n.headlinesFeedFilterApplyFollowedLabel), @@ -335,7 +324,7 @@ class _HeadlinesFilterPageState extends State { } else { _isLoadingFollowedFilters = false; _loadFollowedFiltersError = null; - _clearTemporaryFilters(); // Clear auto-applied filters + _clearTemporaryFilters(); } }); }, diff --git a/lib/headlines-feed/view/source_filter_page.dart b/lib/headlines-feed/view/source_filter_page.dart index 0a1da55f..d68b9950 100644 --- a/lib/headlines-feed/view/source_filter_page.dart +++ b/lib/headlines-feed/view/source_filter_page.dart @@ -14,9 +14,9 @@ import 'package:ht_main/shared/widgets/loading_state_widget.dart'; import 'package:ht_shared/ht_shared.dart' show Country, Source, SourceType; // Keys are defined in headlines_filter_page.dart and imported by router.dart -// const String keySelectedSources = 'selectedSources'; // REMOVED -// const String keySelectedCountryIsoCodes = 'selectedCountryIsoCodes'; // REMOVED -// const String keySelectedSourceTypes = 'selectedSourceTypes'; // REMOVED +// const String keySelectedSources = 'selectedSources'; +// const String keySelectedCountryIsoCodes = 'selectedCountryIsoCodes'; +// const String keySelectedSourceTypes = 'selectedSourceTypes'; class SourceFilterPage extends StatelessWidget { const SourceFilterPage({ @@ -55,19 +55,19 @@ class _SourceFilterView extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - final theme = Theme.of(context); // Get theme - final textTheme = theme.textTheme; // Get textTheme + final theme = Theme.of(context); + final textTheme = theme.textTheme; final state = context.watch().state; return Scaffold( appBar: AppBar( title: Text( l10n.headlinesFeedFilterSourceLabel, - style: textTheme.titleLarge, // Apply consistent title style + style: textTheme.titleLarge, ), actions: [ IconButton( - icon: const Icon(Icons.clear_all_outlined), // Use outlined + icon: const Icon(Icons.clear_all_outlined), tooltip: l10n.headlinesFeedFilterResetButton, onPressed: () { context.read().add( @@ -108,9 +108,9 @@ class _SourceFilterView extends StatelessWidget { state.allAvailableSources.isEmpty) { // Check allAvailableSources return LoadingStateWidget( - icon: Icons.source_outlined, // More relevant icon - headline: l10n.sourceFilterLoadingHeadline, // Specific l10n - subheadline: l10n.sourceFilterLoadingSubheadline, // Specific l10n + icon: Icons.source_outlined, + headline: l10n.sourceFilterLoadingHeadline, + subheadline: l10n.sourceFilterLoadingSubheadline, ); } if (state.dataLoadingStatus == SourceFilterDataLoadingStatus.failure && @@ -129,15 +129,15 @@ class _SourceFilterView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildCountryCapsules(context, state, l10n, textTheme), - const SizedBox(height: AppSpacing.md), // Adjusted spacing + const SizedBox(height: AppSpacing.md), _buildSourceTypeCapsules(context, state, l10n, textTheme), - const SizedBox(height: AppSpacing.md), // Adjusted spacing + const SizedBox(height: AppSpacing.md), Padding( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.paddingMedium, ), child: Text( - l10n.headlinesFeedFilterSourceLabel, // "Sources" title + l10n.headlinesFeedFilterSourceLabel, style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ), @@ -151,23 +151,23 @@ class _SourceFilterView extends StatelessWidget { BuildContext context, SourcesFilterState state, AppLocalizations l10n, - TextTheme textTheme, // Added textTheme + TextTheme textTheme, ) { return Padding( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.paddingMedium, - ).copyWith(top: AppSpacing.md), // Add top padding + ).copyWith(top: AppSpacing.md), child: Column( // Use Column for label and then list crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - l10n.headlinesFeedFilterCountryLabel, // "Countries" label + l10n.headlinesFeedFilterCountryLabel, style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: AppSpacing.sm), SizedBox( - height: AppSpacing.xl + AppSpacing.md, // Standardized height + height: AppSpacing.xl + AppSpacing.md, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: state.availableCountries.length + 1, @@ -217,7 +217,7 @@ class _SourceFilterView extends StatelessWidget { BuildContext context, SourcesFilterState state, AppLocalizations l10n, - TextTheme textTheme, // Added textTheme + TextTheme textTheme, ) { return Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.paddingMedium), @@ -231,7 +231,7 @@ class _SourceFilterView extends StatelessWidget { ), const SizedBox(height: AppSpacing.sm), SizedBox( - height: AppSpacing.xl + AppSpacing.md, // Standardized height + height: AppSpacing.xl + AppSpacing.md, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: state.availableSourceTypes.length + 1, @@ -252,9 +252,7 @@ class _SourceFilterView extends StatelessWidget { } final sourceType = state.availableSourceTypes[index - 1]; return ChoiceChip( - label: Text( - sourceType.name, - ), // Assuming SourceType.name is user-friendly + label: Text(sourceType.name), labelStyle: textTheme.labelLarge, selected: state.selectedSourceTypes.contains(sourceType), onSelected: (_) { @@ -275,7 +273,7 @@ class _SourceFilterView extends StatelessWidget { BuildContext context, SourcesFilterState state, AppLocalizations l10n, - TextTheme textTheme, // Added textTheme + TextTheme textTheme, ) { if (state.dataLoadingStatus == SourceFilterDataLoadingStatus.loading && state.displayableSources.isEmpty) { diff --git a/lib/headlines-search/bloc/headlines_search_bloc.dart b/lib/headlines-search/bloc/headlines_search_bloc.dart index 99875b4e..6d816aa3 100644 --- a/lib/headlines-search/bloc/headlines_search_bloc.dart +++ b/lib/headlines-search/bloc/headlines_search_bloc.dart @@ -2,10 +2,10 @@ 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'; -import 'package:ht_main/app/bloc/app_bloc.dart'; // Added +import 'package:ht_main/app/bloc/app_bloc.dart'; import 'package:ht_main/headlines-search/models/search_model_type.dart'; -import 'package:ht_main/shared/services/feed_injector_service.dart'; // Added -import 'package:ht_shared/ht_shared.dart'; // Updated for FeedItem, AppConfig, User etc. +import 'package:ht_main/shared/services/feed_injector_service.dart'; +import 'package:ht_shared/ht_shared.dart'; part 'headlines_search_event.dart'; part 'headlines_search_state.dart'; @@ -16,26 +16,26 @@ class HeadlinesSearchBloc required HtDataRepository headlinesRepository, required HtDataRepository categoryRepository, required HtDataRepository sourceRepository, - required AppBloc appBloc, // Added - required FeedInjectorService feedInjectorService, // Added + required AppBloc appBloc, + required FeedInjectorService feedInjectorService, }) : _headlinesRepository = headlinesRepository, _categoryRepository = categoryRepository, _sourceRepository = sourceRepository, - _appBloc = appBloc, // Added - _feedInjectorService = feedInjectorService, // Added + _appBloc = appBloc, + _feedInjectorService = feedInjectorService, super(const HeadlinesSearchInitial()) { on(_onHeadlinesSearchModelTypeChanged); on( _onSearchFetchRequested, - transformer: restartable(), // Process only the latest search + transformer: restartable(), ); } final HtDataRepository _headlinesRepository; final HtDataRepository _categoryRepository; final HtDataRepository _sourceRepository; - final AppBloc _appBloc; // Added - final FeedInjectorService _feedInjectorService; // Added + final AppBloc _appBloc; + final FeedInjectorService _feedInjectorService; static const _limit = 10; Future _onHeadlinesSearchModelTypeChanged( @@ -65,7 +65,7 @@ class HeadlinesSearchBloc if (searchTerm.isEmpty) { emit( HeadlinesSearchSuccess( - items: const [], // Changed + items: const [], hasMore: false, lastSearchTerm: '', selectedModelType: modelType, @@ -112,8 +112,7 @@ class HeadlinesSearchBloc emit( successState.copyWith( items: List.of(successState.items)..addAll(injectedItems), - hasMore: - response.hasMore, // hasMore from original headline fetch + hasMore: response.hasMore, cursor: response.cursor, ), ); diff --git a/lib/headlines-search/bloc/headlines_search_state.dart b/lib/headlines-search/bloc/headlines_search_state.dart index 81e08b76..c97a53de 100644 --- a/lib/headlines-search/bloc/headlines_search_state.dart +++ b/lib/headlines-search/bloc/headlines_search_state.dart @@ -22,7 +22,7 @@ class HeadlinesSearchLoading extends HeadlinesSearchState { required this.lastSearchTerm, required super.selectedModelType, }); - final String lastSearchTerm; // Term being loaded + final String lastSearchTerm; @override List get props => [...super.props, lastSearchTerm]; @@ -31,22 +31,22 @@ class HeadlinesSearchLoading extends HeadlinesSearchState { /// State when a search has successfully returned results. class HeadlinesSearchSuccess extends HeadlinesSearchState { const HeadlinesSearchSuccess({ - required this.items, // Changed from results + required this.items, required this.hasMore, required this.lastSearchTerm, - required super.selectedModelType, // The model type for these results + required super.selectedModelType, this.cursor, - this.errorMessage, // For non-critical errors like pagination failure + this.errorMessage, }); - final List items; // Changed from List to List + final List items; final bool hasMore; final String? cursor; - final String? errorMessage; // e.g., for pagination errors - final String lastSearchTerm; // The term that yielded these results + final String? errorMessage; + final String lastSearchTerm; HeadlinesSearchSuccess copyWith({ - List? items, // Changed + List? items, bool? hasMore, String? cursor, String? errorMessage, @@ -55,7 +55,7 @@ class HeadlinesSearchSuccess extends HeadlinesSearchState { bool clearErrorMessage = false, }) { return HeadlinesSearchSuccess( - items: items ?? this.items, // Changed + items: items ?? this.items, hasMore: hasMore ?? this.hasMore, cursor: cursor ?? this.cursor, errorMessage: clearErrorMessage @@ -69,7 +69,7 @@ class HeadlinesSearchSuccess extends HeadlinesSearchState { @override List get props => [ ...super.props, - items, // Changed + items, hasMore, cursor, errorMessage, @@ -86,7 +86,7 @@ class HeadlinesSearchFailure extends HeadlinesSearchState { }); final String errorMessage; - final String lastSearchTerm; // The term that failed + final String lastSearchTerm; @override List get props => [...super.props, errorMessage, lastSearchTerm]; diff --git a/lib/headlines-search/models/search_model_type.dart b/lib/headlines-search/models/search_model_type.dart index 386cb1de..d2f7208b 100644 --- a/lib/headlines-search/models/search_model_type.dart +++ b/lib/headlines-search/models/search_model_type.dart @@ -2,14 +2,14 @@ enum SearchModelType { headline, category, - // country, // Removed + // 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. + /// TODO(fulleni): Localize these display names. String get displayName { switch (this) { case SearchModelType.headline: @@ -17,7 +17,7 @@ enum SearchModelType { case SearchModelType.category: return 'Categories'; // case SearchModelType.country: // Removed - // return 'Countries'; // Removed + // return 'Countries'; 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 cc07a27f..c023a0ca 100644 --- a/lib/headlines-search/view/headlines_search_page.dart +++ b/lib/headlines-search/view/headlines_search_page.dart @@ -3,20 +3,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; // Import GoRouter for navigation -import 'package:ht_main/app/bloc/app_bloc.dart'; // Import AppBloc for settings +import 'package:go_router/go_router.dart'; +import 'package:ht_main/app/bloc/app_bloc.dart'; // HeadlineItemWidget import removed 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'; // Removed +// 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/l10n/app_localizations.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; -import 'package:ht_main/shared/shared.dart'; // Imports new headline tiles -import 'package:ht_shared/ht_shared.dart'; // Changed to general import +import 'package:ht_main/shared/shared.dart'; +import 'package:ht_shared/ht_shared.dart'; /// Page widget responsible for providing the BLoC for the headlines search feature. class HeadlinesSearchPage extends StatelessWidget { @@ -36,7 +36,7 @@ class HeadlinesSearchPage extends StatelessWidget { /// Private View widget that builds the UI for the headlines search page. /// It listens to the HeadlinesSearchBloc state and displays the appropriate UI. class _HeadlinesSearchView extends StatefulWidget { - const _HeadlinesSearchView(); // Private constructor + const _HeadlinesSearchView(); @override State<_HeadlinesSearchView> createState() => _HeadlinesSearchViewState(); @@ -46,8 +46,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { final _scrollController = ScrollController(); final _textController = TextEditingController(); bool _showClearButton = false; - SearchModelType _selectedModelType = - SearchModelType.headline; // Initial selection + SearchModelType _selectedModelType = SearchModelType.headline; @override void initState() { @@ -69,8 +68,9 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { @override void dispose() { - _scrollController.removeListener(_onScroll); - _scrollController.dispose(); + _scrollController + ..removeListener(_onScroll) + ..dispose(); _textController.dispose(); super.dispose(); } @@ -102,7 +102,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { final l10n = context.l10n; final theme = Theme.of(context); final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; // Defined for use + final textTheme = theme.textTheme; final appBarTheme = theme.appBarTheme; final availableSearchModelTypes = SearchModelType.values.toList(); @@ -121,21 +121,21 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { return Scaffold( appBar: AppBar( titleSpacing: AppSpacing.paddingSmall, - // backgroundColor: appBarTheme.backgroundColor ?? colorScheme.surface, // Use theme - elevation: appBarTheme.elevation ?? 0, // Use theme elevation + // backgroundColor: appBarTheme.backgroundColor ?? colorScheme.surface, + elevation: appBarTheme.elevation ?? 0, title: Row( children: [ SizedBox( - width: 150, // Adjusted width for potentially longer translations + width: 150, child: DropdownButtonFormField( value: _selectedModelType, decoration: const InputDecoration( - border: InputBorder.none, // Clean look + border: InputBorder.none, contentPadding: EdgeInsets.symmetric( - horizontal: AppSpacing.sm, // Adjusted padding + horizontal: AppSpacing.sm, vertical: AppSpacing.xs, ), - isDense: true, // Make it more compact + isDense: true, ), style: textTheme.titleMedium?.copyWith( color: @@ -144,7 +144,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ), dropdownColor: colorScheme.surfaceContainerHighest, icon: Icon( - Icons.arrow_drop_down_rounded, // Rounded icon + Icons.arrow_drop_down_rounded, color: appBarTheme.iconTheme?.color ?? colorScheme.onSurfaceVariant, @@ -161,9 +161,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { } return DropdownMenuItem( value: type, - child: Text( - displayLocalizedName, - ), // Style applied by DropdownButtonFormField + child: Text(displayLocalizedName), ); }).toList(), onChanged: (SearchModelType? newValue) { @@ -176,7 +174,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ); // Optionally trigger search or clear text when type changes // _textController.clear(); - // _performSearch(); // Or wait for user to tap search + // _performSearch(); } }, ), @@ -185,28 +183,26 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { Expanded( child: TextField( controller: _textController, - style: - appBarTheme.titleTextStyle ?? - textTheme.titleMedium, // Consistent style + style: appBarTheme.titleTextStyle ?? textTheme.titleMedium, decoration: InputDecoration( hintText: _getHintTextForModelType(_selectedModelType, l10n), hintStyle: textTheme.bodyMedium?.copyWith( color: (appBarTheme.titleTextStyle?.color ?? colorScheme.onSurface) - .withOpacity(0.6), // Adjusted opacity + .withOpacity(0.6), ), - border: InputBorder.none, // Clean look - filled: false, // Use theme's inputDecoratorIsFilled - // fillColor: colorScheme.surface.withAlpha(26), // Use theme + border: InputBorder.none, + filled: false, + // fillColor: colorScheme.surface.withAlpha(26), contentPadding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, // Standard padding - vertical: AppSpacing.sm, // Adjusted + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, ), suffixIcon: _showClearButton ? IconButton( icon: Icon( - Icons.clear_rounded, // Rounded icon + Icons.clear_rounded, color: appBarTheme.iconTheme?.color ?? colorScheme.onSurfaceVariant, @@ -222,12 +218,12 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ), actions: [ IconButton( - icon: const Icon(Icons.search_outlined), // Use outlined + icon: const Icon(Icons.search_outlined), tooltip: l10n.headlinesSearchActionTooltip, onPressed: _performSearch, - // color: appBarTheme.actionsIconTheme?.color, // Use theme + // color: appBarTheme.actionsIconTheme?.color, ), - const SizedBox(width: AppSpacing.xs), // Add a bit of padding + const SizedBox(width: AppSpacing.xs), ], ), body: BlocBuilder( @@ -238,14 +234,14 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { return switch (state) { HeadlinesSearchInitial() => InitialStateWidget( - icon: Icons.search_outlined, // Themed icon + icon: Icons.search_outlined, headline: l10n.searchPageInitialHeadline, subheadline: l10n.searchPageInitialSubheadline, ), HeadlinesSearchLoading() => LoadingStateWidget( // Use LoadingStateWidget - icon: Icons.search_outlined, // Themed icon - headline: l10n.headlinesFeedLoadingHeadline, // Re-use existing + icon: Icons.search_outlined, + headline: l10n.headlinesFeedLoadingHeadline, subheadline: 'Searching ${state.selectedModelType.displayName.toLowerCase()}...', ), @@ -278,11 +274,10 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { // Consistent padding horizontal: AppSpacing.paddingMedium, vertical: AppSpacing.paddingSmall, - ).copyWith(bottom: AppSpacing.xxl), // Ensure bottom space + ).copyWith(bottom: AppSpacing.xxl), itemCount: hasMore ? items.length + 1 : items.length, - separatorBuilder: (context, index) => const SizedBox( - height: AppSpacing.sm, - ), // Consistent spacing + separatorBuilder: (context, index) => + const SizedBox(height: AppSpacing.sm), itemBuilder: (context, index) { if (index >= items.length) { return const Padding( @@ -354,7 +349,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ), child: Image.network( feedItem.imageUrl, - height: 100, // Consistent height + height: 100, fit: BoxFit.cover, errorBuilder: (ctx, err, st) => Icon( Icons.broken_image_outlined, @@ -389,7 +384,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { AccountActionType.linkAccount ? Icons .link_outlined // Outlined - : Icons.upgrade_outlined, // Outlined + : Icons.upgrade_outlined, color: currentColorScheme.onSecondaryContainer, ), title: Text( @@ -407,9 +402,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ?.copyWith( color: currentColorScheme .onSecondaryContainer - .withOpacity( - 0.85, - ), // Adjusted opacity + .withOpacity(0.85), ), ) : null, @@ -458,12 +451,12 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ) => FailureStateWidget( message: - 'Failed to search "$lastSearchTerm" in ${failedModelType.displayName.toLowerCase()}:\n$errorMessage', // Improved message + 'Failed to search "$lastSearchTerm" in ${failedModelType.displayName.toLowerCase()}:\n$errorMessage', onRetry: () => context.read().add( HeadlinesSearchFetchRequested(searchTerm: lastSearchTerm), ), ), - _ => const SizedBox.shrink(), // Fallback for any other state + _ => const SizedBox.shrink(), }; }, ), diff --git a/lib/headlines-search/widgets/category_item_widget.dart b/lib/headlines-search/widgets/category_item_widget.dart index 3c57f296..6ac2267d 100644 --- a/lib/headlines-search/widgets/category_item_widget.dart +++ b/lib/headlines-search/widgets/category_item_widget.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; // Added -import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Added -import 'package:ht_main/router/routes.dart'; // Added -import 'package:ht_shared/ht_shared.dart'; // Import Category model +import 'package:go_router/go_router.dart'; +import 'package:ht_main/entity_details/view/entity_details_page.dart'; +import 'package:ht_main/router/routes.dart'; +import 'package:ht_shared/ht_shared.dart'; /// A simple widget to display a Category search result. class CategoryItemWidget extends StatelessWidget { diff --git a/lib/headlines-search/widgets/source_item_widget.dart b/lib/headlines-search/widgets/source_item_widget.dart index 02eef431..08e00fd2 100644 --- a/lib/headlines-search/widgets/source_item_widget.dart +++ b/lib/headlines-search/widgets/source_item_widget.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; // Added -import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Added -import 'package:ht_main/router/routes.dart'; // Added -import 'package:ht_shared/ht_shared.dart'; // Import Source model +import 'package:go_router/go_router.dart'; +import 'package:ht_main/entity_details/view/entity_details_page.dart'; +import 'package:ht_main/router/routes.dart'; +import 'package:ht_shared/ht_shared.dart'; /// A simple widget to display a Source search result. class SourceItemWidget extends StatelessWidget { diff --git a/lib/main.dart b/lib/main.dart index 3bbebf89..12701f96 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,19 +9,19 @@ import 'package:ht_main/bootstrap.dart'; // Define the current application environment here. // Change this value to switch between environments for local development. // production/development/demo -const currentEnvironment = AppEnvironment.demo; +const appEnvironment = AppEnvironment.demo; @JS('removeSplashFromWeb') external void removeSplashFromWeb(); void main() async { - final appConfig = switch (currentEnvironment) { + final appConfig = switch (appEnvironment) { AppEnvironment.production => AppConfig.production(), AppEnvironment.development => AppConfig.development(), AppEnvironment.demo => AppConfig.demo(), }; - final appWidget = await bootstrap(appConfig, currentEnvironment); + final appWidget = await bootstrap(appConfig, appEnvironment); // Only remove the splash screen on web after the app is ready. if (kIsWeb) { diff --git a/lib/router/router.dart b/lib/router/router.dart index e9ec8165..aa8aae63 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -1,34 +1,35 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_auth_repository/ht_auth_repository.dart'; // Auth Repository -import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository +import 'package:ht_auth_repository/ht_auth_repository.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; 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'; // 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 -import 'package:ht_main/account/view/saved_headlines_page.dart'; // Import SavedHeadlinesPage +import 'package:ht_main/account/view/manage_followed_items/categories/add_category_to_follow_page.dart'; +import 'package:ht_main/account/view/manage_followed_items/categories/followed_categories_list_page.dart'; +// import 'package:ht_main/account/view/manage_followed_items/countries/add_country_to_follow_page.dart'; +// import 'package:ht_main/account/view/manage_followed_items/countries/followed_countries_list_page.dart'; +import 'package:ht_main/account/view/manage_followed_items/manage_followed_items_page.dart'; +import 'package:ht_main/account/view/manage_followed_items/sources/add_source_to_follow_page.dart'; +import 'package:ht_main/account/view/manage_followed_items/sources/followed_sources_list_page.dart'; +import 'package:ht_main/account/view/saved_headlines_page.dart'; import 'package:ht_main/app/bloc/app_bloc.dart'; +import 'package:ht_main/app/config/config.dart' as local_config; import 'package:ht_main/app/view/app_shell.dart'; import 'package:ht_main/authentication/bloc/authentication_bloc.dart'; import 'package:ht_main/authentication/view/authentication_page.dart'; import 'package:ht_main/authentication/view/email_code_verification_page.dart'; import 'package:ht_main/authentication/view/request_code_page.dart'; -import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Added -import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart'; // Re-added -import 'package:ht_main/headline-details/bloc/similar_headlines_bloc.dart'; // Import SimilarHeadlinesBloc +import 'package:ht_main/entity_details/view/entity_details_page.dart'; +import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart'; +import 'package:ht_main/headline-details/bloc/similar_headlines_bloc.dart'; import 'package:ht_main/headline-details/view/headline_details_page.dart'; -import 'package:ht_main/headlines-feed/bloc/categories_filter_bloc.dart'; // Import new BLoC -// import 'package:ht_main/headlines-feed/bloc/countries_filter_bloc.dart'; // Import new BLoC - REMOVED +import 'package:ht_main/headlines-feed/bloc/categories_filter_bloc.dart'; +// import 'package:ht_main/headlines-feed/bloc/countries_filter_bloc.dart'; import 'package:ht_main/headlines-feed/bloc/headlines_feed_bloc.dart'; -import 'package:ht_main/headlines-feed/bloc/sources_filter_bloc.dart'; // Import new BLoC +import 'package:ht_main/headlines-feed/bloc/sources_filter_bloc.dart'; import 'package:ht_main/headlines-feed/view/category_filter_page.dart'; -// import 'package:ht_main/headlines-feed/view/country_filter_page.dart'; // REMOVED +// import 'package:ht_main/headlines-feed/view/country_filter_page.dart'; import 'package:ht_main/headlines-feed/view/headlines_feed_page.dart'; import 'package:ht_main/headlines-feed/view/headlines_filter_page.dart'; import 'package:ht_main/headlines-feed/view/source_filter_page.dart'; @@ -36,16 +37,16 @@ import 'package:ht_main/headlines-search/bloc/headlines_search_bloc.dart'; import 'package:ht_main/headlines-search/view/headlines_search_page.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; -import 'package:ht_main/settings/bloc/settings_bloc.dart'; // Added -import 'package:ht_main/settings/view/appearance_settings_page.dart'; // Added -import 'package:ht_main/settings/view/feed_settings_page.dart'; // Added -import 'package:ht_main/settings/view/font_settings_page.dart'; // Added for new page -import 'package:ht_main/settings/view/language_settings_page.dart'; // Added for new page -import 'package:ht_main/settings/view/notification_settings_page.dart'; // Added -import 'package:ht_main/settings/view/settings_page.dart'; // Added -import 'package:ht_main/settings/view/theme_settings_page.dart'; // Added for new page -import 'package:ht_main/shared/services/feed_injector_service.dart'; // Added -import 'package:ht_shared/ht_shared.dart'; // Shared models, FromJson, ToJson, etc. +import 'package:ht_main/settings/bloc/settings_bloc.dart'; +import 'package:ht_main/settings/view/appearance_settings_page.dart'; +import 'package:ht_main/settings/view/feed_settings_page.dart'; +import 'package:ht_main/settings/view/font_settings_page.dart'; +import 'package:ht_main/settings/view/language_settings_page.dart'; +import 'package:ht_main/settings/view/notification_settings_page.dart'; +import 'package:ht_main/settings/view/settings_page.dart'; +import 'package:ht_main/settings/view/theme_settings_page.dart'; +import 'package:ht_main/shared/services/feed_injector_service.dart'; +import 'package:ht_shared/ht_shared.dart'; /// Creates and configures the GoRouter instance for the application. /// @@ -62,24 +63,23 @@ GoRouter createRouter({ required HtDataRepository htUserContentPreferencesRepository, required HtDataRepository htAppConfigRepository, + required local_config.AppEnvironment environment, }) { // Instantiate AccountBloc once to be shared final accountBloc = AccountBloc( authenticationRepository: htAuthenticationRepository, userContentPreferencesRepository: htUserContentPreferencesRepository, + environment: environment, ); return GoRouter( refreshListenable: authStatusNotifier, initialLocation: Routes.feed, - debugLogDiagnostics: true, // Enable verbose logging for debugging redirects + debugLogDiagnostics: true, // --- Redirect Logic --- redirect: (BuildContext context, GoRouterState state) { final appStatus = context.read().state.status; - final appConfig = context - .read() - .state - .appConfig; // Get appConfig + final appConfig = context.read().state.appConfig; final currentLocation = state.matchedLocation; final currentUri = state.uri; @@ -219,11 +219,11 @@ GoRouter createRouter({ if (isLinkingContext) { headline = l10n.authenticationLinkingHeadline; subHeadline = l10n.authenticationLinkingSubheadline; - showAnonymousButton = false; // Don't show anon button when linking + showAnonymousButton = false; } else { headline = l10n.authenticationSignInHeadline; subHeadline = l10n.authenticationSignInSubheadline; - showAnonymousButton = true; // Show anon button for initial sign-in + showAnonymousButton = true; } return BlocProvider( @@ -241,20 +241,18 @@ GoRouter createRouter({ routes: [ // Nested route for account linking flow (defined first for priority) GoRoute( - path: Routes.accountLinking, // This is 'linking' - name: Routes.accountLinkingName, // Name for the linking segment - builder: (context, state) => const SizedBox.shrink(), // Placeholder + path: Routes.accountLinking, + name: Routes.accountLinkingName, + builder: (context, state) => const SizedBox.shrink(), routes: [ GoRoute( - path: Routes - .requestCode, // Path: /authentication/linking/request-code + path: Routes.requestCode, name: Routes.linkingRequestCodeName, builder: (context, state) => const RequestCodePage(isLinkingContext: true), ), GoRoute( - path: - '${Routes.verifyCode}/:email', // Path: /authentication/linking/verify-code/:email + path: '${Routes.verifyCode}/:email', name: Routes.linkingVerifyCodeName, builder: (context, state) { final email = state.pathParameters['email']!; @@ -342,8 +340,8 @@ GoRouter createRouter({ // preserving the shell's UI (like the bottom navigation bar). // This global route, being top-level, will typically cover the entire screen. GoRoute( - path: Routes.globalArticleDetails, // Use new path: '/article/:id' - name: Routes.globalArticleDetailsName, // Use new name + path: Routes.globalArticleDetails, + name: Routes.globalArticleDetailsName, builder: (context, state) { final headlineFromExtra = state.extra as Headline?; final headlineIdFromPath = state.pathParameters['id']; @@ -383,7 +381,7 @@ GoRouter createRouter({ // Return the shell widget which contains the AdaptiveScaffold return MultiBlocProvider( providers: [ - BlocProvider.value(value: accountBloc), // Use the shared instance + BlocProvider.value(value: accountBloc), BlocProvider( create: (context) { // Instantiate FeedInjectorService here as it's stateless for now @@ -391,23 +389,22 @@ GoRouter createRouter({ return HeadlinesFeedBloc( headlinesRepository: context .read>(), - feedInjectorService: feedInjectorService, // Pass instance - appBloc: context.read(), // Pass AppBloc instance + feedInjectorService: feedInjectorService, + appBloc: context.read(), )..add(const HeadlinesFeedFetchRequested()); }, ), BlocProvider( create: (context) { - final feedInjectorService = - FeedInjectorService(); // Instantiate + final feedInjectorService = FeedInjectorService(); return HeadlinesSearchBloc( headlinesRepository: context .read>(), categoryRepository: context .read>(), sourceRepository: context.read>(), - appBloc: context.read(), // Provide AppBloc - feedInjectorService: feedInjectorService, // Provide Service + appBloc: context.read(), + feedInjectorService: feedInjectorService, ); }, ), @@ -421,13 +418,13 @@ GoRouter createRouter({ StatefulShellBranch( routes: [ GoRoute( - path: Routes.feed, // '/feed' + path: Routes.feed, name: Routes.feedName, builder: (context, state) => const HeadlinesFeedPage(), routes: [ // Sub-route for article details GoRoute( - path: 'article/:id', // Relative path + path: 'article/:id', name: Routes.articleDetailsName, builder: (context, state) { final headlineFromExtra = state.extra as Headline?; @@ -435,7 +432,7 @@ GoRouter createRouter({ return MultiBlocProvider( providers: [ - BlocProvider.value(value: accountBloc), // Added + BlocProvider.value(value: accountBloc), BlocProvider( create: (context) => HeadlineDetailsBloc( headlinesRepository: context @@ -460,7 +457,7 @@ GoRouter createRouter({ ), // Sub-route for notifications (placeholder) - MOVED HERE GoRoute( - path: Routes.notifications, // Relative path 'notifications' + path: Routes.notifications, name: Routes.notificationsName, builder: (context, state) { // TODO(fulleni): Replace with actual NotificationsPage @@ -472,7 +469,7 @@ GoRouter createRouter({ // --- Filter Routes (Nested under Feed) --- GoRoute( - path: Routes.feedFilter, // Relative path: 'filter' + path: Routes.feedFilter, name: Routes.feedFilterName, // Use MaterialPage with fullscreenDialog for modal presentation pageBuilder: (context, state) { @@ -480,14 +477,13 @@ GoRouter createRouter({ BlocProvider.of(context); return const MaterialPage( fullscreenDialog: true, - child: HeadlinesFilterPage(), // Pass the BLoC instance + child: HeadlinesFilterPage(), ); }, routes: [ // Sub-route for category selection GoRoute( - path: Routes - .feedFilterCategories, // Relative path: 'categories' + path: Routes.feedFilterCategories, name: Routes.feedFilterCategoriesName, // Wrap with BlocProvider builder: (context, state) => BlocProvider( @@ -500,8 +496,7 @@ GoRouter createRouter({ ), // Sub-route for source selection GoRoute( - path: Routes - .feedFilterSources, // Relative path: 'sources' + path: Routes.feedFilterSources, name: Routes.feedFilterSourcesName, // Wrap with BlocProvider builder: (context, state) => BlocProvider( @@ -552,20 +547,20 @@ GoRouter createRouter({ StatefulShellBranch( routes: [ GoRoute( - path: Routes.search, // '/search' + path: Routes.search, name: Routes.searchName, builder: (context, state) => const HeadlinesSearchPage(), routes: [ // Sub-route for article details from search GoRoute( - path: 'article/:id', // Relative path - name: Routes.searchArticleDetailsName, // New route name + path: 'article/:id', + name: Routes.searchArticleDetailsName, builder: (context, state) { final headlineFromExtra = state.extra as Headline?; final headlineIdFromPath = state.pathParameters['id']; return MultiBlocProvider( providers: [ - BlocProvider.value(value: accountBloc), // Added + BlocProvider.value(value: accountBloc), BlocProvider( create: (context) => HeadlineDetailsBloc( headlinesRepository: context @@ -595,7 +590,7 @@ GoRouter createRouter({ StatefulShellBranch( routes: [ GoRoute( - path: Routes.account, // '/account' + path: Routes.account, name: Routes.accountName, builder: (context, state) => const AccountPage(), routes: [ @@ -632,30 +627,26 @@ GoRouter createRouter({ }, routes: [ GoRoute( - path: Routes - .settings, // Relative path 'settings' from /account + path: Routes.settings, name: Routes.settingsName, builder: (context, state) => const SettingsPage(), // --- Settings Sub-Routes --- routes: [ GoRoute( - path: Routes - .settingsAppearance, // 'appearance' relative to /account/settings + path: Routes.settingsAppearance, name: Routes.settingsAppearanceName, builder: (context, state) => const AppearanceSettingsPage(), routes: [ // Children of AppearanceSettingsPage GoRoute( - path: Routes - .settingsAppearanceTheme, // 'theme' relative to /account/settings/appearance + path: Routes.settingsAppearanceTheme, name: Routes.settingsAppearanceThemeName, builder: (context, state) => const ThemeSettingsPage(), ), GoRoute( - path: Routes - .settingsAppearanceFont, // 'font' relative to /account/settings/appearance + path: Routes.settingsAppearanceFont, name: Routes.settingsAppearanceFontName, builder: (context, state) => const FontSettingsPage(), @@ -663,22 +654,19 @@ GoRouter createRouter({ ], ), GoRoute( - path: Routes - .settingsFeed, // 'feed' relative to /account/settings + path: Routes.settingsFeed, name: Routes.settingsFeedName, builder: (context, state) => const FeedSettingsPage(), ), GoRoute( - path: Routes - .settingsNotifications, // 'notifications' relative to /account/settings + path: Routes.settingsNotifications, name: Routes.settingsNotificationsName, builder: (context, state) => const NotificationSettingsPage(), ), GoRoute( - path: Routes - .settingsLanguage, // 'language' relative to /account/settings + path: Routes.settingsLanguage, name: Routes.settingsLanguageName, builder: (context, state) => const LanguageSettingsPage(), @@ -689,10 +677,10 @@ GoRouter createRouter({ ), // New routes for Account sub-pages GoRoute( - path: Routes.manageFollowedItems, // Updated path - name: Routes.manageFollowedItemsName, // Updated name + path: Routes.manageFollowedItems, + name: Routes.manageFollowedItemsName, builder: (context, state) => - const ManageFollowedItemsPage(), // Use the new page + const ManageFollowedItemsPage(), routes: [ GoRoute( path: Routes.followedCategoriesList, @@ -729,18 +717,18 @@ GoRouter createRouter({ path: Routes.accountSavedHeadlines, name: Routes.accountSavedHeadlinesName, builder: (context, state) { - return const SavedHeadlinesPage(); // Use the actual page + return const SavedHeadlinesPage(); }, routes: [ GoRoute( - path: Routes.accountArticleDetails, // 'article/:id' + path: Routes.accountArticleDetails, name: Routes.accountArticleDetailsName, builder: (context, state) { final headlineFromExtra = state.extra as Headline?; final headlineIdFromPath = state.pathParameters['id']; return MultiBlocProvider( providers: [ - BlocProvider.value(value: accountBloc), // Added + BlocProvider.value(value: accountBloc), BlocProvider( create: (context) => HeadlineDetailsBloc( headlinesRepository: context diff --git a/lib/router/routes.dart b/lib/router/routes.dart index 9164f1bc..10ae94ac 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -7,14 +7,13 @@ abstract final class Routes { static const feedName = 'feed'; // --- Filter Sub-Routes (relative to /feed) --- - static const feedFilter = 'filter'; // Path: /feed/filter + static const feedFilter = 'filter'; static const feedFilterName = 'feedFilter'; - static const feedFilterCategories = - 'categories'; // Path: /feed/filter/categories + static const feedFilterCategories = 'categories'; static const feedFilterCategoriesName = 'feedFilterCategories'; - static const feedFilterSources = 'sources'; // Path: /feed/filter/sources + static const feedFilterSources = 'sources'; static const feedFilterSourcesName = 'feedFilterSources'; static const search = '/search'; @@ -28,17 +27,17 @@ abstract final class Routes { // Add a new name for article details when accessed from search static const searchArticleDetailsName = 'searchArticleDetails'; // Settings is now relative to account - static const settings = 'settings'; // Relative path + static const settings = 'settings'; static const settingsName = 'settings'; // Notifications is now relative to account - static const notifications = 'notifications'; // Relative path + static const notifications = 'notifications'; static const notificationsName = 'notifications'; // --- Entity Details Routes (can be accessed from multiple places) --- - static const categoryDetails = '/category-details'; // New - static const categoryDetailsName = 'categoryDetails'; // New - static const sourceDetails = '/source-details'; // New - static const sourceDetailsName = 'sourceDetails'; // New + static const categoryDetails = '/category-details'; + static const categoryDetailsName = 'categoryDetails'; + static const sourceDetails = '/source-details'; + static const sourceDetailsName = 'sourceDetails'; // --- Authentication Routes --- static const authentication = '/authentication'; @@ -49,8 +48,8 @@ abstract final class Routes { static const resetPasswordName = 'resetPassword'; static const confirmEmail = 'confirm-email'; static const confirmEmailName = 'confirmEmail'; - static const accountLinking = 'linking'; // Query param context, not a path - static const accountLinkingName = 'accountLinking'; // Name for context + static const accountLinking = 'linking'; + static const accountLinkingName = 'accountLinking'; // routes for email code verification flow static const requestCode = 'request-code'; @@ -69,11 +68,9 @@ abstract final class Routes { static const settingsAppearanceName = 'settingsAppearance'; // --- Appearance Sub-Routes (relative to /account/settings/appearance) --- - static const settingsAppearanceTheme = - 'theme'; // Path: /account/settings/appearance/theme + static const settingsAppearanceTheme = 'theme'; static const settingsAppearanceThemeName = 'settingsAppearanceTheme'; - static const settingsAppearanceFont = - 'font'; // Path: /account/settings/appearance/font + static const settingsAppearanceFont = 'font'; static const settingsAppearanceFontName = 'settingsAppearanceFont'; static const settingsFeed = 'feed'; @@ -84,8 +81,7 @@ abstract final class Routes { static const settingsNotificationsName = 'settingsNotifications'; // --- Language Settings Sub-Route (relative to /account/settings) --- - static const settingsLanguage = - 'language'; // Path: /account/settings/language + static const settingsLanguage = 'language'; static const settingsLanguageName = 'settingsLanguage'; // Add names for notification sub-selection routes if needed later @@ -93,19 +89,18 @@ abstract final class Routes { // static const settingsNotificationCategoriesName = 'settingsNotificationCategories'; // --- Account Sub-Routes (relative to /account) --- - static const manageFollowedItems = 'manage-followed-items'; // Renamed - static const manageFollowedItemsName = 'manageFollowedItems'; // Renamed + static const manageFollowedItems = 'manage-followed-items'; + static const manageFollowedItemsName = 'manageFollowedItems'; static const accountSavedHeadlines = 'saved-headlines'; static const accountSavedHeadlinesName = 'accountSavedHeadlines'; // New route for article details from saved headlines - static const String accountArticleDetails = - 'article/:id'; // Relative to accountSavedHeadlines + static const String accountArticleDetails = 'article/:id'; static const String accountArticleDetailsName = 'accountArticleDetails'; // --- Global Article Details --- // This route is intended for accessing article details from contexts // outside the main bottom navigation shell (e.g., from entity detail pages). - static const globalArticleDetails = '/article/:id'; // Top-level path + static const globalArticleDetails = '/article/:id'; static const globalArticleDetailsName = 'globalArticleDetails'; // --- Manage Followed Items Sub-Routes (relative to /account/manage-followed-items) --- @@ -119,8 +114,8 @@ abstract final class Routes { static const addSourceToFollow = 'add-source'; static const addSourceToFollowName = 'addSourceToFollow'; - // static const followedCountriesList = 'countries'; // Removed - // static const followedCountriesListName = 'followedCountriesList'; // Removed - // static const addCountryToFollow = 'add-country'; // Removed - // static const addCountryToFollowName = 'addCountryToFollow'; // Removed + // static const followedCountriesList = 'countries'; + // static const followedCountriesListName = 'followedCountriesList'; + // static const addCountryToFollow = 'add-country'; + // static const addCountryToFollowName = 'addCountryToFollow'; } diff --git a/lib/settings/bloc/settings_bloc.dart b/lib/settings/bloc/settings_bloc.dart index 2a66a98e..1d2e6b6f 100644 --- a/lib/settings/bloc/settings_bloc.dart +++ b/lib/settings/bloc/settings_bloc.dart @@ -3,10 +3,10 @@ import 'dart:async'; 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_shared/ht_shared.dart'; // Shared models, including UserAppSettings and UserContentPreferences +import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_shared/ht_shared.dart'; -part 'settings_event.dart'; // Contains event definitions +part 'settings_event.dart'; part 'settings_state.dart'; /// {@template settings_bloc} @@ -119,7 +119,7 @@ class SettingsBloc extends Bloc { SettingsAppThemeModeChanged event, Emitter emit, ) async { - if (state.userAppSettings == null) return; // Guard against null settings + if (state.userAppSettings == null) return; final updatedSettings = state.userAppSettings!.copyWith( displaySettings: state.userAppSettings!.displaySettings.copyWith( diff --git a/lib/settings/bloc/settings_event.dart b/lib/settings/bloc/settings_event.dart index b553fc9d..f0716610 100644 --- a/lib/settings/bloc/settings_event.dart +++ b/lib/settings/bloc/settings_event.dart @@ -40,7 +40,7 @@ class SettingsAppThemeModeChanged extends SettingsEvent { const SettingsAppThemeModeChanged(this.themeMode); /// The newly selected theme mode. - final AppBaseTheme themeMode; // Use AppBaseTheme from ht_shared + final AppBaseTheme themeMode; @override List get props => [themeMode]; @@ -54,7 +54,7 @@ class SettingsAppThemeNameChanged extends SettingsEvent { const SettingsAppThemeNameChanged(this.themeName); /// The newly selected theme name. - final AppAccentTheme themeName; // Use AppAccentTheme from ht_shared + final AppAccentTheme themeName; @override List get props => [themeName]; @@ -68,7 +68,7 @@ class SettingsAppFontSizeChanged extends SettingsEvent { const SettingsAppFontSizeChanged(this.fontSize); /// The newly selected font size. - final AppTextScaleFactor fontSize; // Use AppTextScaleFactor from ht_shared + final AppTextScaleFactor fontSize; @override List get props => [fontSize]; @@ -82,7 +82,7 @@ class SettingsAppFontTypeChanged extends SettingsEvent { const SettingsAppFontTypeChanged(this.fontType); /// The newly selected font type. - final String fontType; // Use String for fontFamily + final String fontType; @override List get props => [fontType]; @@ -96,7 +96,7 @@ class SettingsAppFontWeightChanged extends SettingsEvent { const SettingsAppFontWeightChanged(this.fontWeight); /// The newly selected font weight. - final AppFontWeight fontWeight; // Use AppFontWeight from ht_shared + final AppFontWeight fontWeight; @override List get props => [fontWeight]; @@ -113,7 +113,7 @@ class SettingsFeedTileTypeChanged extends SettingsEvent { /// The newly selected feed list tile type. // Note: This event might need to be split into density and image style changes. - final HeadlineImageStyle tileType; // Use HeadlineImageStyle from ht_shared + final HeadlineImageStyle tileType; @override List get props => [tileType]; @@ -127,7 +127,7 @@ class SettingsLanguageChanged extends SettingsEvent { const SettingsLanguageChanged(this.languageCode); /// The newly selected language code (e.g., 'en', 'ar'). - final AppLanguage languageCode; // Use AppLanguage typedef from ht_shared + final AppLanguage languageCode; @override List get props => [languageCode]; @@ -137,6 +137,6 @@ class SettingsLanguageChanged extends SettingsEvent { // SettingsNotificationsEnabledChanged event removed as UserAppSettings // does not currently support a general notifications enabled flag. -// TODO(cline): Add events for changing followed categories/sources/countries +// TODO(fulleni): Add events for changing followed categories/sources/countries // for notifications if needed later. Example: // class SettingsNotificationCategoriesChanged extends SettingsEvent { ... } diff --git a/lib/settings/bloc/settings_state.dart b/lib/settings/bloc/settings_state.dart index eb42a879..f4c24671 100644 --- a/lib/settings/bloc/settings_state.dart +++ b/lib/settings/bloc/settings_state.dart @@ -23,7 +23,7 @@ class SettingsState extends Equatable { /// {@macro settings_state} const SettingsState({ this.status = SettingsStatus.initial, - this.userAppSettings, // Nullable, populated after successful load + this.userAppSettings, this.error, }); @@ -43,8 +43,8 @@ class SettingsState extends Equatable { SettingsStatus? status, UserAppSettings? userAppSettings, Object? error, - bool clearError = false, // Flag to explicitly clear error - bool clearUserAppSettings = false, // Flag to explicitly clear settings + bool clearError = false, + bool clearUserAppSettings = false, }) { return SettingsState( status: status ?? this.status, diff --git a/lib/settings/view/appearance_settings_page.dart b/lib/settings/view/appearance_settings_page.dart index d493ae88..b3ab3f51 100644 --- a/lib/settings/view/appearance_settings_page.dart +++ b/lib/settings/view/appearance_settings_page.dart @@ -34,9 +34,7 @@ class AppearanceSettingsPage extends StatelessWidget { children: [ ListTile( leading: const Icon(Icons.color_lens_outlined), - title: Text( - l10n.settingsAppearanceThemeSubPageTitle, - ), // Use new l10n key + title: Text(l10n.settingsAppearanceThemeSubPageTitle), trailing: const Icon(Icons.chevron_right), onTap: () { context.goNamed(Routes.settingsAppearanceThemeName); @@ -45,9 +43,7 @@ class AppearanceSettingsPage extends StatelessWidget { const Divider(indent: AppSpacing.lg, endIndent: AppSpacing.lg), ListTile( leading: const Icon(Icons.font_download_outlined), - title: Text( - l10n.settingsAppearanceFontSubPageTitle, - ), // Use new l10n key + title: Text(l10n.settingsAppearanceFontSubPageTitle), trailing: const Icon(Icons.chevron_right), onTap: () { context.goNamed(Routes.settingsAppearanceFontName); diff --git a/lib/settings/view/feed_settings_page.dart b/lib/settings/view/feed_settings_page.dart index a1a06e02..d103da89 100644 --- a/lib/settings/view/feed_settings_page.dart +++ b/lib/settings/view/feed_settings_page.dart @@ -5,8 +5,7 @@ import 'package:ht_main/l10n/app_localizations.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/settings/bloc/settings_bloc.dart'; import 'package:ht_main/shared/constants/constants.dart'; -import 'package:ht_shared/ht_shared.dart' - show HeadlineImageStyle; // Import new enum +import 'package:ht_shared/ht_shared.dart' show HeadlineImageStyle; /// {@template feed_settings_page} /// A page for configuring feed display settings. @@ -19,11 +18,11 @@ class FeedSettingsPage extends StatelessWidget { String _imageStyleToString(HeadlineImageStyle style, AppLocalizations l10n) { switch (style) { case HeadlineImageStyle.hidden: - return l10n.settingsFeedTileTypeTextOnly; // Closest match + return l10n.settingsFeedTileTypeTextOnly; case HeadlineImageStyle.smallThumbnail: - return l10n.settingsFeedTileTypeImageStart; // Closest match + return l10n.settingsFeedTileTypeImageStart; case HeadlineImageStyle.largeThumbnail: - return l10n.settingsFeedTileTypeImageTop; // Closest match + return l10n.settingsFeedTileTypeImageTop; } } @@ -36,9 +35,7 @@ class FeedSettingsPage extends StatelessWidget { // Ensure we have loaded state before building controls if (state.status != SettingsStatus.success) { return Scaffold( - appBar: AppBar( - title: Text(l10n.settingsFeedDisplayTitle), - ), // Reuse title + appBar: AppBar(title: Text(l10n.settingsFeedDisplayTitle)), body: const Center(child: CircularProgressIndicator()), ); } @@ -50,27 +47,21 @@ class FeedSettingsPage extends StatelessWidget { } }, child: Scaffold( - appBar: AppBar( - title: Text(l10n.settingsFeedDisplayTitle), // Reuse title - ), + appBar: AppBar(title: Text(l10n.settingsFeedDisplayTitle)), body: ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ // --- Feed Tile Type --- _buildDropdownSetting( context: context, - title: l10n.settingsFeedTileTypeLabel, // Add l10n key - currentValue: state - .userAppSettings! - .feedPreferences - .headlineImageStyle, // Use new model field + title: l10n.settingsFeedTileTypeLabel, + currentValue: + state.userAppSettings!.feedPreferences.headlineImageStyle, items: HeadlineImageStyle.values, itemToString: (style) => _imageStyleToString(style, l10n), onChanged: (value) { if (value != null) { - settingsBloc.add( - SettingsFeedTileTypeChanged(value), - ); // Use new event + settingsBloc.add(SettingsFeedTileTypeChanged(value)); } }, ), diff --git a/lib/settings/view/font_settings_page.dart b/lib/settings/view/font_settings_page.dart index 6b41020a..7f807963 100644 --- a/lib/settings/view/font_settings_page.dart +++ b/lib/settings/view/font_settings_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:ht_main/app/bloc/app_bloc.dart'; // Import AppBloc and events +import 'package:ht_main/app/bloc/app_bloc.dart'; import 'package:ht_main/l10n/app_localizations.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/settings/bloc/settings_bloc.dart'; @@ -44,11 +44,11 @@ class FontSettingsPage extends StatelessWidget { // Using direct strings as placeholders until specific l10n keys are confirmed switch (weight) { case AppFontWeight.light: - return 'Light'; // Placeholder + return 'Light'; case AppFontWeight.regular: - return 'Regular'; // Placeholder + return 'Regular'; case AppFontWeight.bold: - return 'Bold'; // Placeholder + return 'Bold'; } } @@ -61,9 +61,7 @@ class FontSettingsPage extends StatelessWidget { if (state.status != SettingsStatus.success || state.userAppSettings == null) { return Scaffold( - appBar: AppBar( - title: Text(l10n.settingsAppearanceTitle), - ), // Use existing key + appBar: AppBar(title: Text(l10n.settingsAppearanceTitle)), body: const Center(child: CircularProgressIndicator()), ); } @@ -76,9 +74,7 @@ class FontSettingsPage extends StatelessWidget { } }, child: Scaffold( - appBar: AppBar( - title: Text(l10n.settingsAppearanceTitle), - ), // Use existing key + appBar: AppBar(title: Text(l10n.settingsAppearanceTitle)), body: ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ @@ -110,7 +106,7 @@ class FontSettingsPage extends StatelessWidget { 'Lato', 'Montserrat', 'Merriweather', - ], // Updated font list + ], itemToString: (fontFamily) => _fontFamilyToString(fontFamily, l10n), onChanged: (value) { @@ -136,7 +132,7 @@ class FontSettingsPage extends StatelessWidget { ), ], ), - ), // Correctly close BlocListener's child Scaffold + ), ); } diff --git a/lib/settings/view/language_settings_page.dart b/lib/settings/view/language_settings_page.dart index 4ea43a12..8f1e5099 100644 --- a/lib/settings/view/language_settings_page.dart +++ b/lib/settings/view/language_settings_page.dart @@ -28,7 +28,7 @@ class LanguageSettingsPage extends StatelessWidget { if (settingsState.status != SettingsStatus.success || settingsState.userAppSettings == null) { return Scaffold( - appBar: AppBar(title: Text(l10n.settingsTitle)), // Placeholder l10n key + appBar: AppBar(title: Text(l10n.settingsTitle)), body: const Center(child: CircularProgressIndicator()), ); } @@ -42,7 +42,7 @@ class LanguageSettingsPage extends StatelessWidget { } }, child: Scaffold( - appBar: AppBar(title: Text(l10n.settingsTitle)), // Placeholder l10n key + appBar: AppBar(title: Text(l10n.settingsTitle)), body: ListView.separated( padding: const EdgeInsets.symmetric(vertical: AppSpacing.md), itemCount: _supportedLanguages.length, diff --git a/lib/settings/view/notification_settings_page.dart b/lib/settings/view/notification_settings_page.dart index d4fea364..dc781974 100644 --- a/lib/settings/view/notification_settings_page.dart +++ b/lib/settings/view/notification_settings_page.dart @@ -20,31 +20,27 @@ class NotificationSettingsPage extends StatelessWidget { // Ensure we have loaded state before building controls if (state.status != SettingsStatus.success) { return Scaffold( - appBar: AppBar( - title: Text(l10n.settingsNotificationsTitle), - ), // Reuse title + appBar: AppBar(title: Text(l10n.settingsNotificationsTitle)), body: const Center(child: CircularProgressIndicator()), ); } - // TODO(cline): Full implementation of Notification Settings UI and BLoC logic + // TODO(fulleni): Full implementation of Notification Settings UI and BLoC logic // is pending backend and shared model development (specifically, adding // a 'notificationsEnabled' field to UserAppSettings or a similar model). // This UI is temporarily disabled. - const notificationsEnabled = false; // Placeholder value + const notificationsEnabled = false; return Scaffold( - appBar: AppBar( - title: Text(l10n.settingsNotificationsTitle), // Reuse title - ), + appBar: AppBar(title: Text(l10n.settingsNotificationsTitle)), body: ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ // --- Enable/Disable Notifications --- SwitchListTile( - title: Text(l10n.settingsNotificationsEnableLabel), // Add l10n key + title: Text(l10n.settingsNotificationsEnableLabel), value: notificationsEnabled, - onChanged: null, // Disable the switch + onChanged: null, secondary: const Icon(Icons.notifications_active_outlined), ), const Divider(), @@ -58,10 +54,10 @@ class NotificationSettingsPage extends StatelessWidget { leading: const Icon(Icons.category_outlined), title: Text( l10n.settingsNotificationsCategoriesLabel, - ), // Add l10n key + ), trailing: const Icon(Icons.chevron_right), onTap: () { - // TODO(cline): Implement navigation to category selection page + // TODO(fulleni): Implement navigation to category selection page // Example: context.goNamed(Routes.settingsNotificationCategoriesName); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Category selection TBD')), @@ -72,10 +68,10 @@ class NotificationSettingsPage extends StatelessWidget { leading: const Icon(Icons.source_outlined), title: Text( l10n.settingsNotificationsSourcesLabel, - ), // Add l10n key + ), trailing: const Icon(Icons.chevron_right), onTap: () { - // TODO(cline): Implement navigation to source selection page + // TODO(fulleni): Implement navigation to source selection page // Example: context.goNamed(Routes.settingsNotificationSourcesName); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Source selection TBD')), @@ -86,10 +82,10 @@ class NotificationSettingsPage extends StatelessWidget { leading: const Icon(Icons.public_outlined), title: Text( l10n.settingsNotificationsCountriesLabel, - ), // Add l10n key + ), trailing: const Icon(Icons.chevron_right), onTap: () { - // TODO(cline): Implement navigation to country selection page + // TODO(fulleni): Implement navigation to country selection page // Example: context.goNamed(Routes.settingsNotificationCountriesName); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Country selection TBD')), diff --git a/lib/settings/view/settings_page.dart b/lib/settings/view/settings_page.dart index 453c824c..32f223a5 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_main/app/bloc/app_bloc.dart'; // Import AppBloc +import 'package:ht_main/app/bloc/app_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; -import 'package:ht_main/router/routes.dart'; // Assuming sub-routes will be added here +import 'package:ht_main/router/routes.dart'; import 'package:ht_main/settings/bloc/settings_bloc.dart'; import 'package:ht_main/shared/constants/constants.dart'; -import 'package:ht_main/shared/widgets/widgets.dart'; // For loading/error +import 'package:ht_main/shared/widgets/widgets.dart'; /// {@template settings_page} /// The main page for accessing different application settings categories. @@ -35,7 +35,7 @@ class SettingsPage extends StatelessWidget { } }, ), - title: Text(l10n.settingsTitle), // Add l10n key: settingsTitle + title: Text(l10n.settingsTitle), ), // Use BlocBuilder to react to loading/error states if needed, // though the main list is often static. @@ -46,8 +46,8 @@ class SettingsPage extends StatelessWidget { if (state.status == SettingsStatus.loading) { return LoadingStateWidget( icon: Icons.settings_outlined, - headline: l10n.settingsLoadingHeadline, // Add l10n key - subheadline: l10n.settingsLoadingSubheadline, // Add l10n key + headline: l10n.settingsLoadingHeadline, + subheadline: l10n.settingsLoadingSubheadline, ); } @@ -66,11 +66,9 @@ class SettingsPage extends StatelessWidget { } else { // Handle case where user is null on retry, though unlikely // if router guards are effective. - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.unknownError), // Or a specific error - ), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.unknownError))); } }, ); @@ -83,21 +81,21 @@ class SettingsPage extends StatelessWidget { _buildSettingsTile( context: context, icon: Icons.language_outlined, - title: l10n.settingsLanguageTitle, // Add l10n key + title: l10n.settingsLanguageTitle, onTap: () => context.goNamed(Routes.settingsLanguageName), ), const Divider(indent: AppSpacing.lg, endIndent: AppSpacing.lg), _buildSettingsTile( context: context, icon: Icons.palette_outlined, - title: l10n.settingsAppearanceTitle, // Add l10n key + title: l10n.settingsAppearanceTitle, onTap: () => context.goNamed(Routes.settingsAppearanceName), ), const Divider(indent: AppSpacing.lg, endIndent: AppSpacing.lg), _buildSettingsTile( context: context, icon: Icons.feed_outlined, - title: l10n.settingsFeedDisplayTitle, // Add l10n key + title: l10n.settingsFeedDisplayTitle, onTap: () => context.goNamed(Routes.settingsFeedName), ), const Divider(indent: AppSpacing.lg, endIndent: AppSpacing.lg), diff --git a/lib/settings/view/theme_settings_page.dart b/lib/settings/view/theme_settings_page.dart index 25d61177..43a14937 100644 --- a/lib/settings/view/theme_settings_page.dart +++ b/lib/settings/view/theme_settings_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:ht_main/app/bloc/app_bloc.dart'; // Import AppBloc and events +import 'package:ht_main/app/bloc/app_bloc.dart'; import 'package:ht_main/l10n/app_localizations.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/settings/bloc/settings_bloc.dart'; @@ -107,7 +107,7 @@ class ThemeSettingsPage extends StatelessWidget { ), ], ), - ), // Correctly close BlocListener's child Scaffold + ), ); } diff --git a/lib/shared/localization/ar_timeago_messages.dart b/lib/shared/localization/ar_timeago_messages.dart index 617b01fa..a4de4608 100644 --- a/lib/shared/localization/ar_timeago_messages.dart +++ b/lib/shared/localization/ar_timeago_messages.dart @@ -3,11 +3,11 @@ import 'package:timeago/timeago.dart' as timeago; /// Custom Arabic lookup messages for the timeago package. class ArTimeagoMessages implements timeago.LookupMessages { @override - String prefixAgo() => ''; // No prefix, will include in string + String prefixAgo() => ''; @override - String prefixFromNow() => 'بعد '; // Prefix for future + String prefixFromNow() => 'بعد '; @override - String suffixAgo() => ''; // No suffix + String suffixAgo() => ''; @override String suffixFromNow() => ''; @@ -24,7 +24,7 @@ class ArTimeagoMessages implements timeago.LookupMessages { String hours(int hours) => 'منذ $hoursس'; @override - String aDay(int hours) => 'منذ 1ي'; // Or 'أمس' if preferred for exactly 1 day + String aDay(int hours) => 'منذ 1ي'; @override String days(int days) => 'منذ $daysي'; @@ -34,9 +34,9 @@ class ArTimeagoMessages implements timeago.LookupMessages { String months(int months) => 'منذ $monthsش'; @override - String aboutAYear(int year) => 'منذ 1سنة'; // Using سنة for year + String aboutAYear(int year) => 'منذ 1سنة'; @override - String years(int years) => 'منذ $yearsسنوات'; // Standard plural + String years(int years) => 'منذ $yearsسنوات'; @override String wordSeparator() => ' '; diff --git a/lib/shared/localization/en_timeago_messages.dart b/lib/shared/localization/en_timeago_messages.dart index e42cc5fa..f3f284aa 100644 --- a/lib/shared/localization/en_timeago_messages.dart +++ b/lib/shared/localization/en_timeago_messages.dart @@ -3,13 +3,13 @@ import 'package:timeago/timeago.dart' as timeago; /// Custom English lookup messages for the timeago package (concise). class EnTimeagoMessages implements timeago.LookupMessages { @override - String prefixAgo() => ''; // No prefix + String prefixAgo() => ''; @override - String prefixFromNow() => ''; // No prefix + String prefixFromNow() => ''; @override - String suffixAgo() => ' ago'; // Suffix instead + String suffixAgo() => ' ago'; @override - String suffixFromNow() => ' from now'; // Suffix instead + String suffixFromNow() => ' from now'; @override String lessThanOneMinute(int seconds) => 'now'; diff --git a/lib/shared/services/demo_data_migration_service.dart b/lib/shared/services/demo_data_migration_service.dart new file mode 100644 index 00000000..760cbfde --- /dev/null +++ b/lib/shared/services/demo_data_migration_service.dart @@ -0,0 +1,142 @@ +import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_shared/ht_shared.dart'; + +/// {@template demo_data_migration_service} +/// A service responsible for migrating user data (settings and preferences) +/// from an anonymous user ID to a new authenticated user ID in demo mode. +/// +/// This service is specifically designed for the in-memory data clients +/// used in the demo environment, as backend APIs typically handle this +/// migration automatically. +/// {@endtemplate} +class DemoDataMigrationService { + /// {@macro demo_data_migration_service} + const DemoDataMigrationService({ + required HtDataRepository userAppSettingsRepository, + required HtDataRepository + userContentPreferencesRepository, + }) : _userAppSettingsRepository = userAppSettingsRepository, + _userContentPreferencesRepository = userContentPreferencesRepository; + + final HtDataRepository _userAppSettingsRepository; + final HtDataRepository + _userContentPreferencesRepository; + + /// Migrates user settings and content preferences from an old anonymous + /// user ID to a new authenticated user ID. + /// + /// This operation is designed to be idempotent and resilient to missing + /// data for the old user ID. + Future migrateAnonymousData({ + required String oldUserId, + required String newUserId, + }) async { + print( + '[DemoDataMigrationService] Attempting to migrate data from ' + 'anonymous user ID: $oldUserId to authenticated user ID: $newUserId', + ); + + // Migrate UserAppSettings + try { + final oldSettings = await _userAppSettingsRepository.read( + id: oldUserId, + userId: oldUserId, + ); + final newSettings = oldSettings.copyWith(id: newUserId); + + try { + // Attempt to update first (if a default entry already exists) + await _userAppSettingsRepository.update( + id: newUserId, + item: newSettings, + userId: newUserId, + ); + } on NotFoundException { + // If update fails because item not found, try to create + try { + await _userAppSettingsRepository.create( + item: newSettings, + userId: newUserId, + ); + } on ConflictException { + // If create fails due to conflict (item was created concurrently), + // re-attempt update. This handles a race condition. + await _userAppSettingsRepository.update( + id: newUserId, + item: newSettings, + userId: newUserId, + ); + } + } + + await _userAppSettingsRepository.delete(id: oldUserId, userId: oldUserId); + print( + '[DemoDataMigrationService] UserAppSettings migrated successfully ' + 'from $oldUserId to $newUserId.', + ); + } on NotFoundException { + print( + '[DemoDataMigrationService] No UserAppSettings found for old user ID: ' + '$oldUserId. Skipping migration for settings.', + ); + } catch (e, s) { + print( + '[DemoDataMigrationService] Error migrating UserAppSettings from ' + '$oldUserId to $newUserId: $e\n$s', + ); + } + + // Migrate UserContentPreferences + try { + final oldPreferences = await _userContentPreferencesRepository.read( + id: oldUserId, + userId: oldUserId, + ); + final newPreferences = oldPreferences.copyWith(id: newUserId); + + try { + // Attempt to update first (if a default entry already exists) + await _userContentPreferencesRepository.update( + id: newUserId, + item: newPreferences, + userId: newUserId, + ); + } on NotFoundException { + // If update fails because item not found, try to create + try { + await _userContentPreferencesRepository.create( + item: newPreferences, + userId: newUserId, + ); + } on ConflictException { + // If create fails due to conflict (item was created concurrently), + // re-attempt update. This handles a race condition. + await _userContentPreferencesRepository.update( + id: newUserId, + item: newPreferences, + userId: newUserId, + ); + } + } + + await _userContentPreferencesRepository.delete( + id: oldUserId, + userId: oldUserId, + ); + print( + '[DemoDataMigrationService] UserContentPreferences migrated ' + 'successfully from $oldUserId to $newUserId.', + ); + } on NotFoundException { + print( + '[DemoDataMigrationService] No UserContentPreferences found for old ' + 'user ID: $oldUserId. Skipping migration for preferences.', + ); + } catch (e, s) { + print( + '[DemoDataMigrationService] Error migrating UserContentPreferences ' + 'from $oldUserId to $newUserId: $e\n$s', + ); + } + } +} diff --git a/lib/shared/services/feed_injector_service.dart b/lib/shared/services/feed_injector_service.dart index fffd8196..2c1d5614 100644 --- a/lib/shared/services/feed_injector_service.dart +++ b/lib/shared/services/feed_injector_service.dart @@ -49,8 +49,9 @@ class FeedInjectorService { case UserRole.premiumUser: adFrequency = adConfig.premiumAdFrequency; adPlacementInterval = adConfig.premiumAdPlacementInterval; + // ignore: no_default_cases default: // For any other roles, or if UserRole enum expands - adFrequency = adConfig.guestAdFrequency; // Default to guest ads + adFrequency = adConfig.guestAdFrequency; adPlacementInterval = adConfig.guestAdPlacementInterval; } @@ -96,8 +97,7 @@ class FeedInjectorService { required User? user, required AppConfig appConfig, }) { - final userRole = - user?.role ?? UserRole.guestUser; // Default to guest if user is null + final userRole = user?.role ?? UserRole.guestUser; final now = DateTime.now(); final lastActionShown = user?.lastAccountActionShownAt; final daysBetweenActionsConfig = appConfig.accountActionConfig; @@ -108,14 +108,14 @@ class FeedInjectorService { if (userRole == UserRole.guestUser) { daysThreshold = daysBetweenActionsConfig.guestDaysBetweenAccountActions; actionType = AccountActionType.linkAccount; - } else if (userRole == UserRole.standardUser) { + } else if (userRole == UserRole.standardUser) { daysThreshold = daysBetweenActionsConfig.standardUserDaysBetweenAccountActions; - - // todo(fulleni): once account upgrade feature is implemented, + + // todo(fulleni): once account upgrade feature is implemented, // uncomment the action type line // and remove teh null return line. - + // actionType = AccountActionType.upgrade; return null; } else { @@ -135,9 +135,8 @@ class FeedInjectorService { } AccountAction _buildLinkAccountActionVariant(AppConfig appConfig) { - final prefs = appConfig.userPreferenceLimits; - // final prefs = appConfig.userPreferenceLimits; // Not using specific numbers - // final ads = appConfig.adConfig; // Not using specific numbers + // final prefs = appConfig.userPreferenceLimits; + // final ads = appConfig.adConfig; final variant = _random.nextInt(3); String title; @@ -174,9 +173,8 @@ class FeedInjectorService { } AccountAction _buildUpgradeAccountActionVariant(AppConfig appConfig) { - final prefs = appConfig.userPreferenceLimits; - // final prefs = appConfig.userPreferenceLimits; // Not using specific numbers - // final ads = appConfig.adConfig; // Not using specific numbers + // final prefs = appConfig.userPreferenceLimits; + // final ads = appConfig.adConfig; final variant = _random.nextInt(3); String title; @@ -206,7 +204,7 @@ class FeedInjectorService { accountActionType: AccountActionType.upgrade, callToActionText: ctaText, // URL could point to a subscription page/flow - callToActionUrl: '/account/upgrade', // Placeholder route + callToActionUrl: '/account/upgrade', ); } @@ -214,14 +212,14 @@ class FeedInjectorService { Ad? _getAdToInject() { // For now, return a placeholder Ad, always native. // In a real scenario, this would fetch from an ad network or predefined list. - // final adPlacements = AdPlacement.values; // Can still use for variety if needed + // final adPlacements = AdPlacement.values; return Ad( // id is generated by model if not provided imageUrl: - 'https://via.placeholder.com/300x100.png/000000/FFFFFF?Text=Native+Placeholder+Ad', // Adjusted placeholder + 'https://via.placeholder.com/300x100.png/000000/FFFFFF?Text=Native+Placeholder+Ad', targetUrl: 'https://example.com/adtarget', - adType: AdType.native, // Always native + adType: AdType.native, // Default placement or random from native-compatible placements placement: AdPlacement.feedInlineNativeBanner, ); diff --git a/lib/shared/shared.dart b/lib/shared/shared.dart index d0ae1fee..a0f4b74e 100644 --- a/lib/shared/shared.dart +++ b/lib/shared/shared.dart @@ -6,5 +6,5 @@ library; export 'constants/constants.dart'; export 'theme/theme.dart'; -export 'utils/utils.dart'; // Added export for utils +export 'utils/utils.dart'; export 'widgets/widgets.dart'; diff --git a/lib/shared/theme/app_theme.dart b/lib/shared/theme/app_theme.dart index 397f62c0..96a701af 100644 --- a/lib/shared/theme/app_theme.dart +++ b/lib/shared/theme/app_theme.dart @@ -22,9 +22,9 @@ const FlexSubThemesData _commonSubThemesData = FlexSubThemesData( // --- Input Decorator (for Search TextField) --- // Example: Add a border radius - inputDecoratorRadius: 8, // Corrected parameter name + inputDecoratorRadius: 8, // Example: Use outline border (common modern style) - inputDecoratorIsFilled: false, // Set to false if using outline border + inputDecoratorIsFilled: false, inputDecoratorBorderType: FlexInputBorderType.outline, // Add other component themes as needed (Buttons, Dialogs, etc.) @@ -34,7 +34,7 @@ const FlexSubThemesData _commonSubThemesData = FlexSubThemesData( TextTheme _customizeTextTheme( TextTheme baseTextTheme, { required AppTextScaleFactor appTextScaleFactor, - required AppFontWeight appFontWeight, // Added parameter + required AppFontWeight appFontWeight, }) { print( '[_customizeTextTheme] Received appFontWeight: $appFontWeight, appTextScaleFactor: $appTextScaleFactor', @@ -49,7 +49,7 @@ TextTheme _customizeTextTheme( case AppTextScaleFactor.medium: factor = 1.0; case AppTextScaleFactor.extraLarge: - factor = 1.3; // Define factor for extraLarge + factor = 1.3; } // Helper to apply factor safely @@ -77,19 +77,19 @@ TextTheme _customizeTextTheme( // body text is the primary target for user-defined weight. headlineLarge: baseTextTheme.headlineLarge?.copyWith( fontSize: applyFactor(28), - fontWeight: FontWeight.bold, // Keeping titles bold by default + fontWeight: FontWeight.bold, ), headlineMedium: baseTextTheme.headlineMedium?.copyWith( fontSize: applyFactor(24), - fontWeight: FontWeight.bold, // Keeping titles bold by default + fontWeight: FontWeight.bold, ), titleLarge: baseTextTheme.titleLarge?.copyWith( fontSize: applyFactor(18), - fontWeight: FontWeight.w600, // Keeping titles semi-bold by default + fontWeight: FontWeight.w600, ), titleMedium: baseTextTheme.titleMedium?.copyWith( fontSize: applyFactor(16), - fontWeight: FontWeight.w600, // Keeping titles semi-bold by default + fontWeight: FontWeight.w600, ), // --- Body/Content Styles --- @@ -97,19 +97,19 @@ TextTheme _customizeTextTheme( bodyLarge: baseTextTheme.bodyLarge?.copyWith( fontSize: applyFactor(16), height: 1.5, - fontWeight: selectedFontWeight, // Apply selected weight + fontWeight: selectedFontWeight, ), bodyMedium: baseTextTheme.bodyMedium?.copyWith( fontSize: applyFactor(14), height: 1.4, - fontWeight: selectedFontWeight, // Apply selected weight + fontWeight: selectedFontWeight, ), // --- Metadata/Caption Styles --- // Captions might also benefit from user-defined weight or stay regular. labelSmall: baseTextTheme.labelSmall?.copyWith( fontSize: applyFactor(12), - fontWeight: selectedFontWeight, // Apply selected weight + fontWeight: selectedFontWeight, ), // --- Button Style (Usually default is fine) --- @@ -160,7 +160,7 @@ TextTheme Function([TextTheme?]) _getGoogleFontTextTheme(String? fontFamily) { ThemeData lightTheme({ required FlexScheme scheme, required AppTextScaleFactor appTextScaleFactor, - required AppFontWeight appFontWeight, // Added parameter + required AppFontWeight appFontWeight, String? fontFamily, }) { print( @@ -175,7 +175,7 @@ ThemeData lightTheme({ textTheme: _customizeTextTheme( baseTextTheme, appTextScaleFactor: appTextScaleFactor, - appFontWeight: appFontWeight, // Pass new parameter + appFontWeight: appFontWeight, ), subThemesData: _commonSubThemesData, ); @@ -187,7 +187,7 @@ ThemeData lightTheme({ ThemeData darkTheme({ required FlexScheme scheme, required AppTextScaleFactor appTextScaleFactor, - required AppFontWeight appFontWeight, // Added parameter + required AppFontWeight appFontWeight, String? fontFamily, }) { print( @@ -204,7 +204,7 @@ ThemeData darkTheme({ textTheme: _customizeTextTheme( baseTextTheme, appTextScaleFactor: appTextScaleFactor, - appFontWeight: appFontWeight, // Pass new parameter + appFontWeight: appFontWeight, ), subThemesData: _commonSubThemesData, ); diff --git a/lib/shared/theme/theme.dart b/lib/shared/theme/theme.dart index 3d183ad5..5d889ee2 100644 --- a/lib/shared/theme/theme.dart +++ b/lib/shared/theme/theme.dart @@ -2,4 +2,4 @@ /// Exports application-wide theme definitions like colors and theme data. library; -export 'app_theme.dart'; // Export the theme definitions +export 'app_theme.dart'; diff --git a/lib/shared/widgets/failure_state_widget.dart b/lib/shared/widgets/failure_state_widget.dart index 3c0ec1a2..4bea388a 100644 --- a/lib/shared/widgets/failure_state_widget.dart +++ b/lib/shared/widgets/failure_state_widget.dart @@ -12,7 +12,7 @@ class FailureStateWidget extends StatelessWidget { required this.message, super.key, this.onRetry, - this.retryButtonText, // Optional custom text for the retry button + this.retryButtonText, }); /// The error message to display. @@ -41,9 +41,7 @@ class FailureStateWidget extends StatelessWidget { padding: const EdgeInsets.only(top: 16), child: ElevatedButton( onPressed: onRetry, - child: Text( - retryButtonText ?? 'Retry', - ), // Use custom text or default + child: Text(retryButtonText ?? 'Retry'), ), ), ], diff --git a/lib/shared/widgets/headline_tile_image_start.dart b/lib/shared/widgets/headline_tile_image_start.dart index c368001f..01bc0a33 100644 --- a/lib/shared/widgets/headline_tile_image_start.dart +++ b/lib/shared/widgets/headline_tile_image_start.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; // Added +import 'package:go_router/go_router.dart'; import 'package:ht_main/entity_details/models/entity_type.dart'; -import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Added for Page Arguments +import 'package:ht_main/entity_details/view/entity_details_page.dart'; import 'package:ht_main/l10n/app_localizations.dart'; import 'package:ht_main/l10n/l10n.dart'; -import 'package:ht_main/router/routes.dart'; // Added +import 'package:ht_main/router/routes.dart'; import 'package:ht_main/shared/constants/app_spacing.dart'; -import 'package:ht_main/shared/utils/utils.dart'; // Import the new utility +import 'package:ht_main/shared/utils/utils.dart'; import 'package:ht_shared/ht_shared.dart' show Headline; // timeago import removed from here, handled by utility @@ -52,14 +52,14 @@ class HeadlineTileImageStart extends StatelessWidget { vertical: AppSpacing.xs, ), child: InkWell( - onTap: onHeadlineTap, // Main tap for image + title area + onTap: onHeadlineTap, child: Padding( padding: const EdgeInsets.all(AppSpacing.md), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - width: 72, // Standard small image size + width: 72, height: 72, child: ClipRRect( borderRadius: BorderRadius.circular(AppSpacing.xs), @@ -98,7 +98,7 @@ class HeadlineTileImageStart extends StatelessWidget { ), ), ), - const SizedBox(width: AppSpacing.md), // Always add spacing + const SizedBox(width: AppSpacing.md), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -117,10 +117,8 @@ class HeadlineTileImageStart extends StatelessWidget { l10n: l10n, colorScheme: colorScheme, textTheme: textTheme, - currentContextEntityType: - currentContextEntityType, // Pass down - currentContextEntityId: - currentContextEntityId, // Pass down + currentContextEntityType: currentContextEntityType, + currentContextEntityId: currentContextEntityId, ), ], ), @@ -165,10 +163,10 @@ class _HeadlineMetadataRow extends StatelessWidget { ); // Icon color to match the subtle text final iconColor = colorScheme.primary.withOpacity(0.7); - const iconSize = AppSpacing.sm; // Standard small icon size + const iconSize = AppSpacing.sm; return Wrap( - spacing: AppSpacing.sm, // Increased spacing for readability + spacing: AppSpacing.sm, runSpacing: AppSpacing.xs, crossAxisAlignment: WrapCrossAlignment.center, children: [ diff --git a/lib/shared/widgets/headline_tile_image_top.dart b/lib/shared/widgets/headline_tile_image_top.dart index 505ce238..3985acd8 100644 --- a/lib/shared/widgets/headline_tile_image_top.dart +++ b/lib/shared/widgets/headline_tile_image_top.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; // Added +import 'package:go_router/go_router.dart'; import 'package:ht_main/entity_details/models/entity_type.dart'; -import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Added for Page Arguments +import 'package:ht_main/entity_details/view/entity_details_page.dart'; import 'package:ht_main/l10n/app_localizations.dart'; import 'package:ht_main/l10n/l10n.dart'; -import 'package:ht_main/router/routes.dart'; // Added +import 'package:ht_main/router/routes.dart'; import 'package:ht_main/shared/constants/app_spacing.dart'; -import 'package:ht_main/shared/utils/utils.dart'; // Import the new utility +import 'package:ht_main/shared/utils/utils.dart'; import 'package:ht_shared/ht_shared.dart' show Headline; // timeago import removed from here, handled by utility @@ -55,7 +55,7 @@ class HeadlineTileImageTop extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( - onTap: onHeadlineTap, // Image area is part of the main tap area + onTap: onHeadlineTap, child: ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(AppSpacing.xs), @@ -65,7 +65,7 @@ class HeadlineTileImageTop extends StatelessWidget { ? Image.network( headline.imageUrl!, width: double.infinity, - height: 180, // Standard large image height + height: 180, fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; @@ -111,7 +111,7 @@ class HeadlineTileImageTop extends StatelessWidget { children: [ Expanded( child: InkWell( - onTap: onHeadlineTap, // Title is part of main tap area + onTap: onHeadlineTap, child: Text( headline.title, style: textTheme.titleMedium?.copyWith( @@ -134,9 +134,8 @@ class HeadlineTileImageTop extends StatelessWidget { l10n: l10n, colorScheme: colorScheme, textTheme: textTheme, - currentContextEntityType: - currentContextEntityType, // Pass down - currentContextEntityId: currentContextEntityId, // Pass down + currentContextEntityType: currentContextEntityType, + currentContextEntityId: currentContextEntityId, ), ], ), @@ -175,10 +174,10 @@ class _HeadlineMetadataRow extends StatelessWidget { ); // Icon color to match the subtle text final iconColor = colorScheme.primary.withOpacity(0.7); - const iconSize = AppSpacing.sm; // Standard small icon size + const iconSize = AppSpacing.sm; return Wrap( - spacing: AppSpacing.sm, // Increased spacing for readability + spacing: AppSpacing.sm, runSpacing: AppSpacing.xs, crossAxisAlignment: WrapCrossAlignment.center, children: [ diff --git a/lib/shared/widgets/headline_tile_text_only.dart b/lib/shared/widgets/headline_tile_text_only.dart index a8750b0f..a06948fc 100644 --- a/lib/shared/widgets/headline_tile_text_only.dart +++ b/lib/shared/widgets/headline_tile_text_only.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; // Added +import 'package:go_router/go_router.dart'; import 'package:ht_main/entity_details/models/entity_type.dart'; -import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Added for Page Arguments +import 'package:ht_main/entity_details/view/entity_details_page.dart'; import 'package:ht_main/l10n/app_localizations.dart'; import 'package:ht_main/l10n/l10n.dart'; -import 'package:ht_main/router/routes.dart'; // Added +import 'package:ht_main/router/routes.dart'; import 'package:ht_main/shared/constants/app_spacing.dart'; -import 'package:ht_main/shared/utils/utils.dart'; // Import the new utility +import 'package:ht_main/shared/utils/utils.dart'; import 'package:ht_shared/ht_shared.dart' show Headline; // timeago import removed from here, handled by utility @@ -69,7 +69,7 @@ class HeadlineTileTextOnly extends StatelessWidget { style: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w500, ), - maxLines: 3, // Allow more lines for text-only + maxLines: 3, overflow: TextOverflow.ellipsis, ), const SizedBox(height: AppSpacing.sm), @@ -78,10 +78,8 @@ class HeadlineTileTextOnly extends StatelessWidget { l10n: l10n, colorScheme: colorScheme, textTheme: textTheme, - currentContextEntityType: - currentContextEntityType, // Pass down - currentContextEntityId: - currentContextEntityId, // Pass down + currentContextEntityType: currentContextEntityType, + currentContextEntityId: currentContextEntityId, ), ], ), @@ -126,10 +124,10 @@ class _HeadlineMetadataRow extends StatelessWidget { ); // Icon color to match the subtle text final iconColor = colorScheme.primary.withOpacity(0.7); - const iconSize = AppSpacing.sm; // Standard small icon size + const iconSize = AppSpacing.sm; return Wrap( - spacing: AppSpacing.sm, // Increased spacing for readability + spacing: AppSpacing.sm, runSpacing: AppSpacing.xs, crossAxisAlignment: WrapCrossAlignment.center, children: [ diff --git a/web/index.html b/web/index.html index 8106dfe3..73d218ea 100644 --- a/web/index.html +++ b/web/index.html @@ -135,7 +135,7 @@ function removeSplashFromWeb() { document.getElementById("splash")?.remove(); document.getElementById("splash-branding")?.remove(); - document.getElementById("loading-indicator")?.remove(); // Remove the new loading indicator + document.getElementById("loading-indicator")?.remove(); document.body.style.background = "transparent"; }