diff --git a/lib/account/bloc/account_state.dart b/lib/account/bloc/account_state.dart index 08f2b649..8521b5c0 100644 --- a/lib/account/bloc/account_state.dart +++ b/lib/account/bloc/account_state.dart @@ -28,8 +28,9 @@ class AccountState extends Equatable { status: status ?? this.status, user: clearUser ? null : user ?? this.user, preferences: clearPreferences ? null : preferences ?? this.preferences, - errorMessage: - clearErrorMessage ? null : errorMessage ?? this.errorMessage, + errorMessage: clearErrorMessage + ? null + : errorMessage ?? this.errorMessage, ); } 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 e5bbbcdf..24f9e25a 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 @@ -22,10 +22,9 @@ class AddCategoryToFollowPage extends StatelessWidget { final textTheme = theme.textTheme; // Get textTheme return BlocProvider( - create: - (context) => CategoriesFilterBloc( - categoriesRepository: context.read>(), - )..add(CategoriesFilterRequested()), + create: (context) => CategoriesFilterBloc( + categoriesRepository: context.read>(), + )..add(CategoriesFilterRequested()), child: Scaffold( appBar: AppBar( title: Text( @@ -56,10 +55,9 @@ class AddCategoryToFollowPage extends StatelessWidget { } return FailureStateWidget( message: errorMessage, - onRetry: - () => context.read().add( - CategoriesFilterRequested(), - ), + onRetry: () => context.read().add( + CategoriesFilterRequested(), + ), ); } if (categoriesState.categories.isEmpty && @@ -79,23 +77,23 @@ class AddCategoryToFollowPage extends StatelessWidget { categoriesState.status == CategoriesFilterStatus.loadingMore; return BlocBuilder( - buildWhen: - (previous, current) => - previous.preferences?.followedCategories != - current.preferences?.followedCategories || - previous.status != current.status, + buildWhen: (previous, current) => + previous.preferences?.followedCategories != + current.preferences?.followedCategories || + previous.status != current.status, builder: (context, accountState) { final followedCategories = 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, + ), // Ensure bottom space for loader itemCount: categories.length + (isLoadingMore ? 1 : 0), itemBuilder: (context, index) { if (index == categories.length && isLoadingMore) { @@ -130,73 +128,66 @@ class AddCategoryToFollowPage extends StatelessWidget { height: AppSpacing.xl + AppSpacing.xs, child: category.iconUrl != null && - Uri.tryParse( - category.iconUrl!, - )?.isAbsolute == - true - ? ClipRRect( - borderRadius: BorderRadius.circular( - AppSpacing.xs, - ), - child: Image.network( - category.iconUrl!, - fit: BoxFit.contain, - errorBuilder: - (context, error, stackTrace) => Icon( - Icons.category_outlined, - color: colorScheme.onSurfaceVariant, - size: AppSpacing.lg, - ), - loadingBuilder: ( - context, - child, - loadingProgress, - ) { - if (loadingProgress == null) { - return child; - } - return Center( - child: CircularProgressIndicator( - strokeWidth: 2, - value: - loadingProgress - .expectedTotalBytes != - null - ? loadingProgress + Uri.tryParse(category.iconUrl!)?.isAbsolute == + true + ? ClipRRect( + borderRadius: BorderRadius.circular( + AppSpacing.xs, + ), + child: Image.network( + category.iconUrl!, + fit: BoxFit.contain, + errorBuilder: + (context, error, stackTrace) => Icon( + Icons.category_outlined, + color: colorScheme.onSurfaceVariant, + size: AppSpacing.lg, + ), + loadingBuilder: + (context, child, loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Center( + child: CircularProgressIndicator( + strokeWidth: 2, + value: + loadingProgress + .expectedTotalBytes != + null + ? loadingProgress .cumulativeBytesLoaded / loadingProgress .expectedTotalBytes! - : null, - ), - ); - }, - ), - ) - : Icon( - Icons.category_outlined, - color: colorScheme.onSurfaceVariant, - size: AppSpacing.lg, + : null, + ), + ); + }, ), + ) + : Icon( + Icons.category_outlined, + color: colorScheme.onSurfaceVariant, + size: AppSpacing.lg, + ), ), title: Text( category.name, style: textTheme.titleMedium, ), trailing: IconButton( - icon: - isFollowed - ? Icon( - Icons.check_circle, - color: colorScheme.primary, - ) - : Icon( - Icons.add_circle_outline, - color: colorScheme.onSurfaceVariant, - ), - tooltip: - isFollowed - ? l10n.unfollowCategoryTooltip(category.name) - : l10n.followCategoryTooltip(category.name), + icon: isFollowed + ? Icon( + Icons.check_circle, + color: colorScheme.primary, + ) + : Icon( + Icons.add_circle_outline, + color: colorScheme.onSurfaceVariant, + ), + tooltip: isFollowed + ? l10n.unfollowCategoryTooltip(category.name) + : l10n.followCategoryTooltip(category.name), onPressed: () { context.read().add( AccountFollowCategoryToggled(category: category), 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 f6958044..e2e6da30 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 @@ -75,28 +75,25 @@ class FollowedCategoriesListPage extends StatelessWidget { itemBuilder: (context, index) { final category = followedCategories[index]; return ListTile( - leading: - category.iconUrl != null - ? SizedBox( - width: 40, - height: 40, - child: Image.network( - category.iconUrl!, - errorBuilder: - (context, error, stackTrace) => - const Icon(Icons.category_outlined), - ), - ) - : const Icon(Icons.category_outlined), + leading: category.iconUrl != null + ? SizedBox( + width: 40, + height: 40, + child: Image.network( + category.iconUrl!, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.category_outlined), + ), + ) + : const Icon(Icons.category_outlined), title: Text(category.name), - subtitle: - category.description != null - ? Text( - category.description!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ) - : null, + subtitle: category.description != null + ? Text( + category.description!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + : null, trailing: IconButton( icon: const Icon( Icons.remove_circle_outline, diff --git a/lib/account/view/manage_followed_items/sources/add_source_to_follow_page.dart b/lib/account/view/manage_followed_items/sources/add_source_to_follow_page.dart index ae5c8dbf..b533ed94 100644 --- a/lib/account/view/manage_followed_items/sources/add_source_to_follow_page.dart +++ b/lib/account/view/manage_followed_items/sources/add_source_to_follow_page.dart @@ -19,11 +19,10 @@ class AddSourceToFollowPage extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; return BlocProvider( - create: - (context) => SourcesFilterBloc( - sourcesRepository: context.read>(), - countriesRepository: context.read>(), - )..add(const LoadSourceFilterData()), + create: (context) => SourcesFilterBloc( + sourcesRepository: context.read>(), + countriesRepository: context.read>(), + )..add(const LoadSourceFilterData()), child: Scaffold( appBar: AppBar(title: Text(l10n.addSourcesPageTitle)), body: BlocBuilder( @@ -38,10 +37,9 @@ class AddSourceToFollowPage extends StatelessWidget { SourceFilterDataLoadingStatus.failure) { return FailureStateWidget( message: sourcesState.errorMessage ?? l10n.sourceFilterError, - onRetry: - () => context.read().add( - const LoadSourceFilterData(), - ), + onRetry: () => context.read().add( + const LoadSourceFilterData(), + ), ); } if (sourcesState.allAvailableSources.isEmpty) { @@ -51,11 +49,10 @@ class AddSourceToFollowPage extends StatelessWidget { } return BlocBuilder( - buildWhen: - (previous, current) => - previous.preferences?.followedSources != - current.preferences?.followedSources || - previous.status != current.status, + buildWhen: (previous, current) => + previous.preferences?.followedSources != + current.preferences?.followedSources || + previous.status != current.status, builder: (context, accountState) { final followedSources = accountState.preferences?.followedSources ?? []; @@ -74,18 +71,15 @@ class AddSourceToFollowPage extends StatelessWidget { child: ListTile( title: Text(source.name), trailing: IconButton( - icon: - isFollowed - ? Icon( - Icons.check_circle, - color: - Theme.of(context).colorScheme.primary, - ) - : const Icon(Icons.add_circle_outline), - tooltip: - isFollowed - ? l10n.unfollowSourceTooltip(source.name) - : l10n.followSourceTooltip(source.name), + icon: isFollowed + ? Icon( + Icons.check_circle, + color: Theme.of(context).colorScheme.primary, + ) + : const Icon(Icons.add_circle_outline), + tooltip: isFollowed + ? l10n.unfollowSourceTooltip(source.name) + : l10n.followSourceTooltip(source.name), onPressed: () { context.read().add( AccountFollowSourceToggled(source: source), 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 3078edf1..fa8f799f 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 @@ -79,14 +79,13 @@ class FollowedSourcesListPage extends StatelessWidget { return ListTile( leading: const Icon(Icons.source_outlined), // Generic icon title: Text(source.name), - subtitle: - source.description != null - ? Text( - source.description!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ) - : null, + subtitle: source.description != null + ? Text( + source.description!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + : null, trailing: IconButton( icon: const Icon( Icons.remove_circle_outline, diff --git a/lib/account/view/saved_headlines_page.dart b/lib/account/view/saved_headlines_page.dart index 873e7ebb..18e5c6d7 100644 --- a/lib/account/view/saved_headlines_page.dart +++ b/lib/account/view/saved_headlines_page.dart @@ -76,21 +76,19 @@ class SavedHeadlinesPage extends StatelessWidget { vertical: AppSpacing.paddingSmall, ), // Add padding itemCount: savedHeadlines.length, - separatorBuilder: - (context, index) => const Divider( - height: 1, - indent: AppSpacing.paddingMedium, // Indent divider - endIndent: AppSpacing.paddingMedium, - ), + separatorBuilder: (context, index) => const Divider( + height: 1, + indent: AppSpacing.paddingMedium, // Indent divider + endIndent: AppSpacing.paddingMedium, + ), itemBuilder: (context, index) { final headline = savedHeadlines[index]; - final imageStyle = - context - .watch() - .state - .settings - .feedPreferences - .headlineImageStyle; + final imageStyle = context + .watch() + .state + .settings + .feedPreferences + .headlineImageStyle; final trailingButton = IconButton( icon: Icon( @@ -110,34 +108,31 @@ class SavedHeadlinesPage extends StatelessWidget { case HeadlineImageStyle.hidden: tile = HeadlineTileTextOnly( headline: headline, - onHeadlineTap: - () => context.goNamed( - Routes.accountArticleDetailsName, - pathParameters: {'id': headline.id}, - extra: headline, - ), + onHeadlineTap: () => context.goNamed( + Routes.accountArticleDetailsName, + pathParameters: {'id': headline.id}, + extra: headline, + ), trailing: trailingButton, ); case HeadlineImageStyle.smallThumbnail: tile = HeadlineTileImageStart( headline: headline, - onHeadlineTap: - () => context.goNamed( - Routes.accountArticleDetailsName, - pathParameters: {'id': headline.id}, - extra: headline, - ), + onHeadlineTap: () => context.goNamed( + Routes.accountArticleDetailsName, + pathParameters: {'id': headline.id}, + extra: headline, + ), trailing: trailingButton, ); case HeadlineImageStyle.largeThumbnail: tile = HeadlineTileImageTop( headline: headline, - onHeadlineTap: - () => context.goNamed( - Routes.accountArticleDetailsName, - pathParameters: {'id': headline.id}, - extra: headline, - ), + onHeadlineTap: () => context.goNamed( + Routes.accountArticleDetailsName, + pathParameters: {'id': headline.id}, + extra: headline, + ), trailing: trailingButton, ); } diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index 5b2a02a2..fcbe3830 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -18,17 +18,17 @@ class AppBloc extends Bloc { required HtDataRepository userAppSettingsRepository, required HtDataRepository appConfigRepository, required local_config.AppEnvironment environment, // Added - }) : _authenticationRepository = authenticationRepository, - _userAppSettingsRepository = userAppSettingsRepository, - _appConfigRepository = appConfigRepository, - super( - AppState( - settings: const UserAppSettings(id: 'default'), - selectedBottomNavigationIndex: 0, - appConfig: null, - environment: environment, // Pass environment to AppState - ), - ) { + }) : _authenticationRepository = authenticationRepository, + _userAppSettingsRepository = userAppSettingsRepository, + _appConfigRepository = appConfigRepository, + super( + AppState( + settings: const UserAppSettings(id: 'default'), + selectedBottomNavigationIndex: 0, + appConfig: null, + environment: environment, // Pass environment to AppState + ), + ) { on(_onAppUserChanged); on(_onAppSettingsRefreshed); on(_onAppConfigFetchRequested); @@ -180,12 +180,11 @@ class AppBloc extends Bloc { // Update settings and emit new state final updatedSettings = state.settings.copyWith( displaySettings: state.settings.displaySettings.copyWith( - baseTheme: - event.themeMode == ThemeMode.light - ? AppBaseTheme.light - : (event.themeMode == ThemeMode.dark - ? AppBaseTheme.dark - : AppBaseTheme.system), + baseTheme: event.themeMode == ThemeMode.light + ? AppBaseTheme.light + : (event.themeMode == ThemeMode.dark + ? AppBaseTheme.dark + : AppBaseTheme.system), ), ); emit(state.copyWith(settings: updatedSettings, themeMode: event.themeMode)); @@ -200,12 +199,11 @@ class AppBloc extends Bloc { // Update settings and emit new state final updatedSettings = state.settings.copyWith( displaySettings: state.settings.displaySettings.copyWith( - accentTheme: - event.flexScheme == FlexScheme.blue - ? AppAccentTheme.defaultBlue - : (event.flexScheme == FlexScheme.red - ? AppAccentTheme.newsRed - : AppAccentTheme + accentTheme: event.flexScheme == FlexScheme.blue + ? AppAccentTheme.defaultBlue + : (event.flexScheme == FlexScheme.red + ? AppAccentTheme.newsRed + : AppAccentTheme .graphiteGray), // Mapping material to graphiteGray ), ); @@ -359,10 +357,9 @@ class AppBloc extends Bloc { // Determine the correct status based on the existing user's role. // This ensures that successfully fetching config doesn't revert auth status to 'initial'. - final newStatusBasedOnUser = - state.user!.role == UserRole.standardUser - ? AppStatus.authenticated - : AppStatus.anonymous; + final newStatusBasedOnUser = state.user!.role == UserRole.standardUser + ? AppStatus.authenticated + : AppStatus.anonymous; emit(state.copyWith(appConfig: appConfig, status: newStatusBasedOnUser)); } on HtHttpException catch (e) { print( diff --git a/lib/app/bloc/app_state.dart b/lib/app/bloc/app_state.dart index a95175cb..9eca39bc 100644 --- a/lib/app/bloc/app_state.dart +++ b/lib/app/bloc/app_state.dart @@ -102,8 +102,9 @@ class AppState extends Equatable { settings: settings ?? this.settings, // Copy settings locale: clearLocale ? null : locale ?? this.locale, // Added locale appConfig: clearAppConfig ? null : appConfig ?? this.appConfig, - environment: - clearEnvironment ? null : environment ?? this.environment, // Added + environment: clearEnvironment + ? null + : environment ?? this.environment, // Added ); } diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 6e0b5584..913aa59d 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -75,21 +75,18 @@ class App extends StatelessWidget { 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 - ), + create: (context) => AppBloc( + authenticationRepository: context.read(), + userAppSettingsRepository: context + .read>(), + appConfigRepository: context.read>(), + environment: _environment, // Pass environment + ), ), BlocProvider( - create: - (context) => AuthenticationBloc( - authenticationRepository: context.read(), - ), + create: (context) => AuthenticationBloc( + authenticationRepository: context.read(), + ), ), ], child: _AppView( @@ -206,9 +203,8 @@ class _AppViewState extends State<_AppView> { appFontWeight: AppFontWeight.regular, // Default fontFamily: null, // System default font ), - themeMode: - state - .themeMode, // Still respect light/dark if available from system + themeMode: state + .themeMode, // Still respect light/dark if available from system localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: Scaffold( diff --git a/lib/authentication/bloc/authentication_bloc.dart b/lib/authentication/bloc/authentication_bloc.dart index 531b2cf8..fddddd02 100644 --- a/lib/authentication/bloc/authentication_bloc.dart +++ b/lib/authentication/bloc/authentication_bloc.dart @@ -84,10 +84,9 @@ class AuthenticationBloc emit(AuthenticationFailure('Operation failed: ${e.message}')); } on HtHttpException catch (e) { // Catch any other HtHttpException subtypes - final message = - e.message.isNotEmpty - ? e.message - : 'An unspecified HTTP error occurred.'; + final message = e.message.isNotEmpty + ? e.message + : 'An unspecified HTTP error occurred.'; emit(AuthenticationFailure('HTTP error: $message')); } catch (e) { // Catch any other unexpected errors diff --git a/lib/authentication/view/email_code_verification_page.dart b/lib/authentication/view/email_code_verification_page.dart index 8d08aa40..a3a04585 100644 --- a/lib/authentication/view/email_code_verification_page.dart +++ b/lib/authentication/view/email_code_verification_page.dart @@ -202,18 +202,17 @@ class _EmailCodeVerificationFormState textStyle: textTheme.labelLarge, ), onPressed: widget.isLoading ? null : _submitForm, - child: - widget.isLoading - ? const SizedBox( - height: AppSpacing.xl, // Consistent size with text - width: AppSpacing.xl, - child: CircularProgressIndicator( - strokeWidth: 2, - color: - Colors.white, // Explicit color for loader on button - ), - ) - : Text(l10n.emailCodeVerificationButtonLabel), + child: widget.isLoading + ? const SizedBox( + height: AppSpacing.xl, // Consistent size with text + width: AppSpacing.xl, + child: CircularProgressIndicator( + strokeWidth: 2, + color: + Colors.white, // Explicit color for loader on button + ), + ) + : Text(l10n.emailCodeVerificationButtonLabel), ), ], ), diff --git a/lib/authentication/view/request_code_page.dart b/lib/authentication/view/request_code_page.dart index f4f95fec..7bdb852b 100644 --- a/lib/authentication/view/request_code_page.dart +++ b/lib/authentication/view/request_code_page.dart @@ -50,18 +50,18 @@ 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, // Accessibility onPressed: () { // Navigate back differently based on the context. if (isLinkingContext) { // If linking, go back to Auth page preserving the linking query param. context.goNamed( Routes.authenticationName, - queryParameters: - isLinkingContext ? {'context': 'linking'} : const {}, + queryParameters: isLinkingContext + ? {'context': 'linking'} + : const {}, ); } else { // If normal sign-in, just go back to the Auth page. @@ -93,12 +93,11 @@ class _RequestCodeView extends StatelessWidget { } }, // BuildWhen prevents unnecessary rebuilds if only listening - buildWhen: - (previous, current) => - current is AuthenticationInitial || - current is AuthenticationRequestCodeLoading || - current - is AuthenticationFailure, // Rebuild on failure to re-enable form + buildWhen: (previous, current) => + current is AuthenticationInitial || + current is AuthenticationRequestCodeLoading || + current + is AuthenticationFailure, // Rebuild on failure to re-enable form builder: (context, state) { final isLoading = state is AuthenticationRequestCodeLoading; @@ -209,8 +208,8 @@ class _EmailLinkFormState extends State<_EmailLinkForm> { } return null; }, - onFieldSubmitted: - (_) => _submitForm(), // Allow submitting from keyboard + onFieldSubmitted: (_) => + _submitForm(), // Allow submitting from keyboard ), const SizedBox(height: AppSpacing.lg), ElevatedButton( @@ -219,20 +218,19 @@ class _EmailLinkFormState extends State<_EmailLinkForm> { padding: const EdgeInsets.symmetric(vertical: AppSpacing.md), textStyle: textTheme.labelLarge, ), - child: - widget.isLoading - ? SizedBox( - height: AppSpacing.xl, // Consistent size with text - width: AppSpacing.xl, - child: CircularProgressIndicator( - strokeWidth: 2, - color: - colorScheme.onPrimary, // Color for loader on button - ), - ) - : Text( - l10n.requestCodeSendCodeButton, - ), // More specific button text + child: widget.isLoading + ? SizedBox( + height: AppSpacing.xl, // Consistent size with text + width: AppSpacing.xl, + child: CircularProgressIndicator( + strokeWidth: 2, + color: + colorScheme.onPrimary, // Color for loader on button + ), + ) + : Text( + l10n.requestCodeSendCodeButton, + ), // More specific button text ), ], ), diff --git a/lib/entity_details/bloc/entity_details_state.dart b/lib/entity_details/bloc/entity_details_state.dart index 250d17ac..1529a281 100644 --- a/lib/entity_details/bloc/entity_details_state.dart +++ b/lib/entity_details/bloc/entity_details_state.dart @@ -52,9 +52,12 @@ class EntityDetailsState extends Equatable { headlinesStatus: headlinesStatus ?? this.headlinesStatus, hasMoreHeadlines: hasMoreHeadlines ?? this.hasMoreHeadlines, headlinesCursor: // This cursor is for fetching original headlines - clearHeadlinesCursor ? null : headlinesCursor ?? this.headlinesCursor, - errorMessage: - clearErrorMessage ? null : errorMessage ?? this.errorMessage, + clearHeadlinesCursor + ? null + : headlinesCursor ?? this.headlinesCursor, + errorMessage: clearErrorMessage + ? null + : errorMessage ?? this.errorMessage, ); } diff --git a/lib/entity_details/view/entity_details_page.dart b/lib/entity_details/view/entity_details_page.dart index dbcd64f1..504f2c98 100644 --- a/lib/entity_details/view/entity_details_page.dart +++ b/lib/entity_details/view/entity_details_page.dart @@ -160,14 +160,13 @@ class _EntityDetailsViewState extends State { //TODO(fulleni): add entityDetailsErrorLoadingto l10n // message: state.errorMessage ?? l10n.entityDetailsErrorLoading(entityType: entityTypeDisplayNameForTitle), message: state.errorMessage ?? '...', - onRetry: - () => context.read().add( - EntityDetailsLoadRequested( - entityId: widget.args.entityId, - entityType: widget.args.entityType, - entity: widget.args.entity, - ), - ), + onRetry: () => context.read().add( + EntityDetailsLoadRequested( + entityId: widget.args.entityId, + entityType: widget.args.entityType, + entity: widget.args.entity, + ), + ), ); } @@ -188,28 +187,26 @@ class _EntityDetailsViewState extends State { appBarTitleText = l10n.detailsPageTitle; // Fallback } - final description = - state.entity is Category - ? (state.entity as Category).description - : state.entity is Source - ? (state.entity as Source).description - : null; + final description = state.entity is Category + ? (state.entity as Category).description + : state.entity is Source + ? (state.entity as Source).description + : null; final entityIconUrl = (state.entity is Category && - (state.entity as Category).iconUrl != null) - ? (state.entity as Category).iconUrl - : null; + (state.entity as Category).iconUrl != null) + ? (state.entity as Category).iconUrl + : null; final followButton = IconButton( icon: Icon( state.isFollowing ? Icons.check_circle : Icons.add_circle_outline, color: colorScheme.primary, ), - tooltip: - state.isFollowing - ? l10n.unfollowButtonLabel - : l10n.followButtonLabel, + tooltip: state.isFollowing + ? l10n.unfollowButtonLabel + : l10n.followButtonLabel, onPressed: () { context.read().add( const EntityDetailsToggleFollowRequested(), @@ -230,12 +227,11 @@ class _EntityDetailsViewState extends State { width: kToolbarHeight - AppSpacing.lg, height: kToolbarHeight - AppSpacing.lg, fit: BoxFit.contain, - errorBuilder: - (context, error, stackTrace) => Icon( - appBarIconData ?? Icons.info_outline, - size: kToolbarHeight - AppSpacing.xl, - color: colorScheme.onSurfaceVariant, - ), + errorBuilder: (context, error, stackTrace) => Icon( + appBarIconData ?? Icons.info_outline, + size: kToolbarHeight - AppSpacing.xl, + color: colorScheme.onSurfaceVariant, + ), ), ), ) @@ -266,7 +262,10 @@ class _EntityDetailsViewState extends State { pinned: true, floating: false, snap: false, - actions: [followButton, const SizedBox(width: AppSpacing.sm)], + actions: [ + followButton, + const SizedBox(width: AppSpacing.sm), + ], ), SliverPadding( padding: const EdgeInsets.all(AppSpacing.paddingMedium), @@ -328,9 +327,8 @@ class _EntityDetailsViewState extends State { EntityHeadlinesStatus.loadingMore ? 1 : 0), - separatorBuilder: - (context, index) => - const SizedBox(height: AppSpacing.sm), + separatorBuilder: (context, index) => + const SizedBox(height: AppSpacing.sm), itemBuilder: (context, index) { if (index >= state.feedItems.length) { return const Center( @@ -345,65 +343,58 @@ class _EntityDetailsViewState extends State { final item = state.feedItems[index]; if (item is Headline) { - final imageStyle = - context - .watch() - .state - .settings - .feedPreferences - .headlineImageStyle; + final imageStyle = context + .watch() + .state + .settings + .feedPreferences + .headlineImageStyle; Widget tile; switch (imageStyle) { case HeadlineImageStyle.hidden: tile = HeadlineTileTextOnly( headline: item, - onHeadlineTap: - () => context.pushNamed( - Routes.globalArticleDetailsName, - pathParameters: {'id': item.id}, - extra: item, - ), + onHeadlineTap: () => context.pushNamed( + Routes.globalArticleDetailsName, + pathParameters: {'id': item.id}, + extra: item, + ), currentContextEntityType: state.entityType, - currentContextEntityId: - state.entity is Category - ? (state.entity as Category).id - : state.entity is Source - ? (state.entity as Source).id - : null, + currentContextEntityId: state.entity is Category + ? (state.entity as Category).id + : state.entity is Source + ? (state.entity as Source).id + : null, ); case HeadlineImageStyle.smallThumbnail: tile = HeadlineTileImageStart( headline: item, - onHeadlineTap: - () => context.pushNamed( - Routes.globalArticleDetailsName, - pathParameters: {'id': item.id}, - extra: item, - ), + onHeadlineTap: () => context.pushNamed( + Routes.globalArticleDetailsName, + pathParameters: {'id': item.id}, + extra: item, + ), currentContextEntityType: state.entityType, - currentContextEntityId: - state.entity is Category - ? (state.entity as Category).id - : state.entity is Source - ? (state.entity as Source).id - : null, + currentContextEntityId: state.entity is Category + ? (state.entity as Category).id + : state.entity is Source + ? (state.entity as Source).id + : null, ); case HeadlineImageStyle.largeThumbnail: tile = HeadlineTileImageTop( headline: item, - onHeadlineTap: - () => context.pushNamed( - Routes.globalArticleDetailsName, - pathParameters: {'id': item.id}, - extra: item, - ), + onHeadlineTap: () => context.pushNamed( + Routes.globalArticleDetailsName, + pathParameters: {'id': item.id}, + extra: item, + ), currentContextEntityType: state.entityType, - currentContextEntityId: - state.entity is Category - ? (state.entity as Category).id - : state.entity is Source - ? (state.entity as Source).id - : null, + currentContextEntityId: state.entity is Category + ? (state.entity as Category).id + : state.entity is Source + ? (state.entity as Source).id + : null, ); } return tile; diff --git a/lib/headline-details/bloc/similar_headlines_bloc.dart b/lib/headline-details/bloc/similar_headlines_bloc.dart index 909f9e7c..3dda6351 100644 --- a/lib/headline-details/bloc/similar_headlines_bloc.dart +++ b/lib/headline-details/bloc/similar_headlines_bloc.dart @@ -43,14 +43,14 @@ class SimilarHeadlinesBloc ); // Filter out the current headline from the results - final similarHeadlines = - response.items - .where((headline) => headline.id != currentHeadline.id) - .toList(); + final similarHeadlines = response.items + .where((headline) => headline.id != currentHeadline.id) + .toList(); // Take only the required limit after filtering - final finalSimilarHeadlines = - similarHeadlines.take(_similarHeadlinesLimit).toList(); + final finalSimilarHeadlines = similarHeadlines + .take(_similarHeadlinesLimit) + .toList(); if (finalSimilarHeadlines.isEmpty) { emit(SimilarHeadlinesEmpty()); diff --git a/lib/headline-details/view/headline_details_page.dart b/lib/headline-details/view/headline_details_page.dart index 784e5327..9ff508ad 100644 --- a/lib/headline-details/view/headline_details_page.dart +++ b/lib/headline-details/view/headline_details_page.dart @@ -190,10 +190,9 @@ class _HeadlineDetailsPageState extends State { isSaved ? Icons.bookmark : Icons.bookmark_border_outlined, color: colorScheme.primary, // Ensure icon color from theme ), - tooltip: - isSaved - ? l10n.headlineDetailsRemoveFromSavedTooltip - : l10n.headlineDetailsSaveTooltip, + tooltip: isSaved + ? l10n.headlineDetailsRemoveFromSavedTooltip + : l10n.headlineDetailsSaveTooltip, onPressed: () { context.read().add( AccountSaveHeadlineToggled(headline: headline), @@ -306,15 +305,14 @@ class _HeadlineDetailsPageState extends State { ), ); }, - errorBuilder: - (context, error, stackTrace) => ColoredBox( - color: colorScheme.surfaceContainerHighest, - child: Icon( - Icons.broken_image_outlined, - color: colorScheme.onSurfaceVariant, - size: AppSpacing.xxl * 1.5, // Larger placeholder - ), - ), + errorBuilder: (context, error, stackTrace) => ColoredBox( + color: colorScheme.surfaceContainerHighest, + child: Icon( + Icons.broken_image_outlined, + color: colorScheme.onSurfaceVariant, + size: AppSpacing.xxl * 1.5, // Larger placeholder + ), + ), ), ), ), @@ -407,10 +405,9 @@ class _HeadlineDetailsPageState extends State { sliver: SliverToBoxAdapter( child: Padding( padding: EdgeInsets.only( - top: - (headline.url != null && headline.url!.isNotEmpty) - ? AppSpacing.sm - : AppSpacing.xl, + top: (headline.url != null && headline.url!.isNotEmpty) + ? AppSpacing.sm + : AppSpacing.xl, bottom: AppSpacing.md, ), child: Text( @@ -584,54 +581,49 @@ 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, + ), // Spacing between items itemCount: loadedState.similarHeadlines.length, itemBuilder: (context, index) { // Corrected: SliverList.separated uses itemBuilder final similarHeadline = loadedState.similarHeadlines[index]; return Builder( builder: (context) { - final imageStyle = - context - .watch() - .state - .settings - .feedPreferences - .headlineImageStyle; + final imageStyle = context + .watch() + .state + .settings + .feedPreferences + .headlineImageStyle; Widget tile; switch (imageStyle) { case HeadlineImageStyle.hidden: tile = HeadlineTileTextOnly( headline: similarHeadline, - onHeadlineTap: - () => context.pushNamed( - Routes.globalArticleDetailsName, - pathParameters: {'id': similarHeadline.id}, - extra: similarHeadline, - ), + onHeadlineTap: () => context.pushNamed( + Routes.globalArticleDetailsName, + pathParameters: {'id': similarHeadline.id}, + extra: similarHeadline, + ), ); case HeadlineImageStyle.smallThumbnail: tile = HeadlineTileImageStart( headline: similarHeadline, - onHeadlineTap: - () => context.pushNamed( - Routes.globalArticleDetailsName, - pathParameters: {'id': similarHeadline.id}, - extra: similarHeadline, - ), + onHeadlineTap: () => context.pushNamed( + Routes.globalArticleDetailsName, + pathParameters: {'id': similarHeadline.id}, + extra: similarHeadline, + ), ); case HeadlineImageStyle.largeThumbnail: tile = HeadlineTileImageTop( headline: similarHeadline, - onHeadlineTap: - () => context.pushNamed( - Routes.globalArticleDetailsName, - pathParameters: {'id': similarHeadline.id}, - extra: similarHeadline, - ), + onHeadlineTap: () => context.pushNamed( + Routes.globalArticleDetailsName, + pathParameters: {'id': similarHeadline.id}, + extra: similarHeadline, + ), ); } return tile; diff --git a/lib/headlines-feed/bloc/categories_filter_bloc.dart b/lib/headlines-feed/bloc/categories_filter_bloc.dart index cfaaf53c..47c9d033 100644 --- a/lib/headlines-feed/bloc/categories_filter_bloc.dart +++ b/lib/headlines-feed/bloc/categories_filter_bloc.dart @@ -107,9 +107,8 @@ class CategoriesFilterBloc // Keep existing data but indicate failure emit( state.copyWith( - status: - CategoriesFilterStatus - .failure, // Or a specific 'loadMoreFailure' status? + status: CategoriesFilterStatus + .failure, // Or a specific 'loadMoreFailure' status? error: e, ), ); diff --git a/lib/headlines-feed/bloc/headlines_feed_bloc.dart b/lib/headlines-feed/bloc/headlines_feed_bloc.dart index 89234957..df143741 100644 --- a/lib/headlines-feed/bloc/headlines_feed_bloc.dart +++ b/lib/headlines-feed/bloc/headlines_feed_bloc.dart @@ -262,10 +262,9 @@ class HeadlinesFeedBloc extends Bloc { currentFeedItemCount: currentFeedItemCountForInjector, ); - final resultingFeedItems = - isPaginating - ? (List.of(currentFeedItems)..addAll(newProcessedFeedItems)) - : newProcessedFeedItems; + final resultingFeedItems = isPaginating + ? (List.of(currentFeedItems)..addAll(newProcessedFeedItems)) + : newProcessedFeedItems; emit( HeadlinesFeedLoaded( diff --git a/lib/headlines-feed/bloc/sources_filter_bloc.dart b/lib/headlines-feed/bloc/sources_filter_bloc.dart index 272aea1d..2413277b 100644 --- a/lib/headlines-feed/bloc/sources_filter_bloc.dart +++ b/lib/headlines-feed/bloc/sources_filter_bloc.dart @@ -36,8 +36,9 @@ class SourcesFilterBloc extends Bloc { ); try { final availableCountries = await _countriesRepository.readAll(); - final initialSelectedSourceIds = - event.initialSelectedSources.map((s) => s.id).toSet(); + final initialSelectedSourceIds = event.initialSelectedSources + .map((s) => s.id) + .toSet(); // Use the passed-in initial capsule selections directly final initialSelectedCountryIsoCodes = diff --git a/lib/headlines-feed/bloc/sources_filter_state.dart b/lib/headlines-feed/bloc/sources_filter_state.dart index 818b10da..9e4c16bb 100644 --- a/lib/headlines-feed/bloc/sources_filter_state.dart +++ b/lib/headlines-feed/bloc/sources_filter_state.dart @@ -51,8 +51,9 @@ class SourcesFilterState extends Equatable { finallySelectedSourceIds: finallySelectedSourceIds ?? this.finallySelectedSourceIds, dataLoadingStatus: dataLoadingStatus ?? this.dataLoadingStatus, - errorMessage: - clearErrorMessage ? null : errorMessage ?? this.errorMessage, + errorMessage: clearErrorMessage + ? null + : errorMessage ?? this.errorMessage, ); } diff --git a/lib/headlines-feed/view/category_filter_page.dart b/lib/headlines-feed/view/category_filter_page.dart index b949c808..8d0c0b6a 100644 --- a/lib/headlines-feed/view/category_filter_page.dart +++ b/lib/headlines-feed/view/category_filter_page.dart @@ -152,10 +152,9 @@ class _CategoryFilterPageState extends State { state.categories.isEmpty) { return FailureStateWidget( message: state.error?.toString() ?? l10n.unknownError, - onRetry: - () => context.read().add( - CategoriesFilterRequested(), - ), + onRetry: () => context.read().add( + CategoriesFilterRequested(), + ), ); } @@ -213,40 +212,36 @@ class _CategoryFilterPageState extends State { return CheckboxListTile( title: Text(category.name, style: textTheme.titleMedium), - secondary: - category.iconUrl != null - ? SizedBox( - width: AppSpacing.xl + AppSpacing.sm, // 40 -> 32 - height: AppSpacing.xl + AppSpacing.sm, - child: ClipRRect( - borderRadius: BorderRadius.circular(AppSpacing.xs), - child: Image.network( - category.iconUrl!, - fit: BoxFit.contain, - errorBuilder: - (context, error, stackTrace) => Icon( - Icons.category_outlined, // Use outlined - color: - colorScheme.onSurfaceVariant, // Theme color - size: AppSpacing.xl, - ), - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Center( - child: CircularProgressIndicator( - strokeWidth: 2, - value: - loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), - ); - }, + secondary: category.iconUrl != null + ? SizedBox( + width: AppSpacing.xl + AppSpacing.sm, // 40 -> 32 + height: AppSpacing.xl + AppSpacing.sm, + child: ClipRRect( + borderRadius: BorderRadius.circular(AppSpacing.xs), + child: Image.network( + category.iconUrl!, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) => Icon( + Icons.category_outlined, // Use outlined + color: colorScheme.onSurfaceVariant, // Theme color + size: AppSpacing.xl, ), + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + strokeWidth: 2, + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, ), - ) - : null, + ), + ) + : null, value: isSelected, onChanged: (bool? value) { setState(() { diff --git a/lib/headlines-feed/view/country_filter_page.dart b/lib/headlines-feed/view/country_filter_page.dart index 3917945e..8c14e26e 100644 --- a/lib/headlines-feed/view/country_filter_page.dart +++ b/lib/headlines-feed/view/country_filter_page.dart @@ -149,10 +149,8 @@ class _CountryFilterPageState extends State { state.countries.isEmpty) { return FailureStateWidget( message: state.error?.toString() ?? l10n.unknownError, - onRetry: - () => context.read().add( - CountriesFilterRequested(), - ), + onRetry: () => + context.read().add(CountriesFilterRequested()), ); } @@ -219,22 +217,20 @@ class _CountryFilterPageState extends State { child: Image.network( country.flagUrl, fit: BoxFit.cover, // Use cover for better filling - errorBuilder: - (context, error, stackTrace) => Icon( - Icons.flag_outlined, - color: colorScheme.onSurfaceVariant, - size: AppSpacing.lg, // Adjust size as needed - ), + errorBuilder: (context, error, stackTrace) => Icon( + Icons.flag_outlined, + color: colorScheme.onSurfaceVariant, + size: AppSpacing.lg, // Adjust size as needed + ), loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Center( child: CircularProgressIndicator( strokeWidth: 2, - value: - loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, ), ); }, diff --git a/lib/headlines-feed/view/headlines_feed_page.dart b/lib/headlines-feed/view/headlines_feed_page.dart index e838e266..2435da20 100644 --- a/lib/headlines-feed/view/headlines_feed_page.dart +++ b/lib/headlines-feed/view/headlines_feed_page.dart @@ -116,8 +116,8 @@ class _HeadlinesFeedPageState extends State { tooltip: l10n.headlinesFeedFilterTooltip, onPressed: () { // Navigate to the filter page route - final headlinesFeedBloc = - context.read(); + final headlinesFeedBloc = context + .read(); context.goNamed( Routes.feedFilterName, extra: headlinesFeedBloc, @@ -144,8 +144,8 @@ class _HeadlinesFeedPageState extends State { ], ), body: BlocBuilder( - buildWhen: - (previous, current) => current is! HeadlinesFeedLoadingSilently, + buildWhen: (previous, current) => + current is! HeadlinesFeedLoadingSilently, builder: (context, state) { switch (state) { case HeadlinesFeedInitial(): // Handle initial state @@ -188,11 +188,10 @@ class _HeadlinesFeedPageState extends State { top: AppSpacing.md, bottom: AppSpacing.xxl, ), - itemCount: - state.hasMore - ? state.feedItems.length + - 1 // Changed - : state.feedItems.length, // Changed + itemCount: state.hasMore + ? state.feedItems.length + + 1 // Changed + : state.feedItems.length, // Changed separatorBuilder: (context, index) { // Add a bit more space if the next item is an Ad or AccountAction if (index < state.feedItems.length - 1) { @@ -219,44 +218,40 @@ class _HeadlinesFeedPageState extends State { final item = state.feedItems[index]; // Changed if (item is Headline) { - final imageStyle = - context - .watch() - .state - .settings - .feedPreferences - .headlineImageStyle; + final imageStyle = context + .watch() + .state + .settings + .feedPreferences + .headlineImageStyle; Widget tile; switch (imageStyle) { case HeadlineImageStyle.hidden: tile = HeadlineTileTextOnly( headline: item, - onHeadlineTap: - () => context.goNamed( - Routes.articleDetailsName, - pathParameters: {'id': item.id}, - extra: item, - ), + onHeadlineTap: () => context.goNamed( + Routes.articleDetailsName, + pathParameters: {'id': item.id}, + extra: item, + ), ); case HeadlineImageStyle.smallThumbnail: tile = HeadlineTileImageStart( headline: item, - onHeadlineTap: - () => context.goNamed( - Routes.articleDetailsName, - pathParameters: {'id': item.id}, - extra: item, - ), + onHeadlineTap: () => context.goNamed( + Routes.articleDetailsName, + pathParameters: {'id': item.id}, + extra: item, + ), ); case HeadlineImageStyle.largeThumbnail: tile = HeadlineTileImageTop( headline: item, - onHeadlineTap: - () => context.goNamed( - Routes.articleDetailsName, - pathParameters: {'id': item.id}, - extra: item, - ), + onHeadlineTap: () => context.goNamed( + Routes.articleDetailsName, + pathParameters: {'id': item.id}, + extra: item, + ), ); } return tile; @@ -276,11 +271,8 @@ class _HeadlinesFeedPageState extends State { Image.network( item.imageUrl, height: 100, - errorBuilder: - (ctx, err, st) => const Icon( - Icons.broken_image, - size: 50, - ), + errorBuilder: (ctx, err, st) => + const Icon(Icons.broken_image, size: 50), ), const SizedBox(height: AppSpacing.sm), Text( @@ -325,31 +317,29 @@ class _HeadlinesFeedPageState extends State { fontWeight: FontWeight.bold, ), ), - subtitle: - item.description != null - ? Text( - item.description!, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSecondaryContainer - .withOpacity(0.8), - ), - ) - : null, - trailing: - item.callToActionText != null - ? ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.secondary, - foregroundColor: colorScheme.onSecondary, - ), - onPressed: () { - if (item.callToActionUrl != null) { - context.push(item.callToActionUrl!); - } - }, - child: Text(item.callToActionText!), - ) - : null, + subtitle: item.description != null + ? Text( + item.description!, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSecondaryContainer + .withOpacity(0.8), + ), + ) + : null, + trailing: item.callToActionText != null + ? ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.secondary, + foregroundColor: colorScheme.onSecondary, + ), + onPressed: () { + if (item.callToActionUrl != null) { + context.push(item.callToActionUrl!); + } + }, + child: Text(item.callToActionText!), + ) + : null, isThreeLine: item.description != null && item.description!.length > 50, diff --git a/lib/headlines-feed/view/headlines_filter_page.dart b/lib/headlines-feed/view/headlines_filter_page.dart index e116b536..8cbfef29 100644 --- a/lib/headlines-feed/view/headlines_filter_page.dart +++ b/lib/headlines-feed/view/headlines_filter_page.dart @@ -61,8 +61,9 @@ class _HeadlinesFilterPageState extends State { @override void initState() { super.initState(); - final headlinesFeedState = - BlocProvider.of(context).state; + final headlinesFeedState = BlocProvider.of( + context, + ).state; var initialUseFollowedFilters = false; @@ -124,8 +125,8 @@ class _HeadlinesFilterPageState extends State { } try { - final preferencesRepo = - context.read>(); + final preferencesRepo = context + .read>(); final preferences = await preferencesRepo.read( id: currentUser.id, userId: currentUser.id, @@ -243,16 +244,16 @@ class _HeadlinesFilterPageState extends State { enabled: enabled, // Use the enabled parameter onTap: enabled // Only allow tap if enabled - ? () async { - final result = await context.pushNamed( - routeName, - extra: currentSelectionData, // Pass the map or list - ); - if (result != null && onResult != null) { - onResult(result); - } + ? () async { + final result = await context.pushNamed( + routeName, + extra: currentSelectionData, // Pass the map or list + ); + if (result != null && onResult != null) { + onResult(result); } - : null, + } + : null, ); } @@ -292,22 +293,20 @@ class _HeadlinesFilterPageState extends State { tooltip: l10n.headlinesFeedFilterApplyButton, onPressed: () { final newFilter = HeadlineFilter( - categories: - _tempSelectedCategories.isNotEmpty - ? _tempSelectedCategories - : null, - sources: - _tempSelectedSources.isNotEmpty - ? _tempSelectedSources - : null, + categories: _tempSelectedCategories.isNotEmpty + ? _tempSelectedCategories + : null, + sources: _tempSelectedSources.isNotEmpty + ? _tempSelectedSources + : null, selectedSourceCountryIsoCodes: _tempSelectedSourceCountryIsoCodes.isNotEmpty - ? _tempSelectedSourceCountryIsoCodes - : null, + ? _tempSelectedSourceCountryIsoCodes + : null, selectedSourceSourceTypes: _tempSelectedSourceSourceTypes.isNotEmpty - ? _tempSelectedSourceSourceTypes - : null, + ? _tempSelectedSourceSourceTypes + : null, isFromFollowedItems: _useFollowedFilters, // Set the new flag ); context.read().add( @@ -340,14 +339,13 @@ class _HeadlinesFilterPageState extends State { } }); }, - secondary: - _isLoadingFollowedFilters - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : null, + secondary: _isLoadingFollowedFilters + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : null, controlAffinity: ListTileControlAffinity.leading, ), ), diff --git a/lib/headlines-feed/view/source_filter_page.dart b/lib/headlines-feed/view/source_filter_page.dart index 5f351878..0a1da55f 100644 --- a/lib/headlines-feed/view/source_filter_page.dart +++ b/lib/headlines-feed/view/source_filter_page.dart @@ -33,8 +33,8 @@ class SourceFilterPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: - (context) => SourcesFilterBloc( + create: (context) => + SourcesFilterBloc( sourcesRepository: context.read>(), countriesRepository: context.read>(), )..add( @@ -79,12 +79,9 @@ class _SourceFilterView extends StatelessWidget { icon: const Icon(Icons.check), tooltip: l10n.headlinesFeedFilterApplyButton, onPressed: () { - final selectedSources = - state.displayableSources - .where( - (s) => state.finallySelectedSourceIds.contains(s.id), - ) - .toList(); + final selectedSources = state.displayableSources + .where((s) => state.finallySelectedSourceIds.contains(s.id)) + .toList(); // Pop with a map containing all relevant filter state Navigator.of(context).pop({ keySelectedSources: selectedSources, @@ -174,8 +171,8 @@ class _SourceFilterView extends StatelessWidget { child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: state.availableCountries.length + 1, - separatorBuilder: - (context, index) => const SizedBox(width: AppSpacing.sm), + separatorBuilder: (context, index) => + const SizedBox(width: AppSpacing.sm), itemBuilder: (context, index) { if (index == 0) { return ChoiceChip( @@ -191,13 +188,12 @@ class _SourceFilterView extends StatelessWidget { } final country = state.availableCountries[index - 1]; return ChoiceChip( - avatar: - country.flagUrl.isNotEmpty - ? CircleAvatar( - backgroundImage: NetworkImage(country.flagUrl), - radius: AppSpacing.sm + AppSpacing.xs, - ) - : null, + avatar: country.flagUrl.isNotEmpty + ? CircleAvatar( + backgroundImage: NetworkImage(country.flagUrl), + radius: AppSpacing.sm + AppSpacing.xs, + ) + : null, label: Text(country.name), labelStyle: textTheme.labelLarge, selected: state.selectedCountryIsoCodes.contains( @@ -239,8 +235,8 @@ class _SourceFilterView extends StatelessWidget { child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: state.availableSourceTypes.length + 1, - separatorBuilder: - (context, index) => const SizedBox(width: AppSpacing.sm), + separatorBuilder: (context, index) => + const SizedBox(width: AppSpacing.sm), itemBuilder: (context, index) { if (index == 0) { return ChoiceChip( diff --git a/lib/headlines-search/bloc/headlines_search_bloc.dart b/lib/headlines-search/bloc/headlines_search_bloc.dart index 25550f2a..99875b4e 100644 --- a/lib/headlines-search/bloc/headlines_search_bloc.dart +++ b/lib/headlines-search/bloc/headlines_search_bloc.dart @@ -44,14 +44,13 @@ class HeadlinesSearchBloc ) async { // If there's an active search term, re-trigger search with new model type // ignore: unused_local_variable - final currentSearchTerm = - state is HeadlinesSearchLoading - ? (state as HeadlinesSearchLoading).lastSearchTerm - : state is HeadlinesSearchSuccess - ? (state as HeadlinesSearchSuccess).lastSearchTerm - : state is HeadlinesSearchFailure - ? (state as HeadlinesSearchFailure).lastSearchTerm - : null; + final currentSearchTerm = state is HeadlinesSearchLoading + ? (state as HeadlinesSearchLoading).lastSearchTerm + : state is HeadlinesSearchSuccess + ? (state as HeadlinesSearchSuccess).lastSearchTerm + : state is HeadlinesSearchFailure + ? (state as HeadlinesSearchFailure).lastSearchTerm + : null; emit(HeadlinesSearchInitial(selectedModelType: event.newModelType)); } diff --git a/lib/headlines-search/bloc/headlines_search_state.dart b/lib/headlines-search/bloc/headlines_search_state.dart index c7d091ea..81e08b76 100644 --- a/lib/headlines-search/bloc/headlines_search_state.dart +++ b/lib/headlines-search/bloc/headlines_search_state.dart @@ -58,8 +58,9 @@ class HeadlinesSearchSuccess extends HeadlinesSearchState { items: items ?? this.items, // Changed hasMore: hasMore ?? this.hasMore, cursor: cursor ?? this.cursor, - errorMessage: - clearErrorMessage ? null : errorMessage ?? this.errorMessage, + errorMessage: clearErrorMessage + ? null + : errorMessage ?? this.errorMessage, lastSearchTerm: lastSearchTerm ?? this.lastSearchTerm, selectedModelType: selectedModelType ?? this.selectedModelType, ); diff --git a/lib/headlines-search/view/headlines_search_page.dart b/lib/headlines-search/view/headlines_search_page.dart index 045dafc4..cc07a27f 100644 --- a/lib/headlines-search/view/headlines_search_page.dart +++ b/lib/headlines-search/view/headlines_search_page.dart @@ -149,24 +149,23 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { appBarTheme.iconTheme?.color ?? colorScheme.onSurfaceVariant, ), - items: - availableSearchModelTypes.map((SearchModelType type) { - String displayLocalizedName; - switch (type) { - case SearchModelType.headline: - displayLocalizedName = l10n.searchModelTypeHeadline; - case SearchModelType.category: - displayLocalizedName = l10n.searchModelTypeCategory; - case SearchModelType.source: - displayLocalizedName = l10n.searchModelTypeSource; - } - return DropdownMenuItem( - value: type, - child: Text( - displayLocalizedName, - ), // Style applied by DropdownButtonFormField - ); - }).toList(), + items: availableSearchModelTypes.map((SearchModelType type) { + String displayLocalizedName; + switch (type) { + case SearchModelType.headline: + displayLocalizedName = l10n.searchModelTypeHeadline; + case SearchModelType.category: + displayLocalizedName = l10n.searchModelTypeCategory; + case SearchModelType.source: + displayLocalizedName = l10n.searchModelTypeSource; + } + return DropdownMenuItem( + value: type, + child: Text( + displayLocalizedName, + ), // Style applied by DropdownButtonFormField + ); + }).toList(), onChanged: (SearchModelType? newValue) { if (newValue != null) { setState(() { @@ -192,9 +191,10 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { decoration: InputDecoration( hintText: _getHintTextForModelType(_selectedModelType, l10n), hintStyle: textTheme.bodyMedium?.copyWith( - color: (appBarTheme.titleTextStyle?.color ?? - colorScheme.onSurface) - .withOpacity(0.6), // Adjusted opacity + color: + (appBarTheme.titleTextStyle?.color ?? + colorScheme.onSurface) + .withOpacity(0.6), // Adjusted opacity ), border: InputBorder.none, // Clean look filled: false, // Use theme's inputDecoratorIsFilled @@ -203,18 +203,17 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { horizontal: AppSpacing.md, // Standard padding vertical: AppSpacing.sm, // Adjusted ), - suffixIcon: - _showClearButton - ? IconButton( - icon: Icon( - Icons.clear_rounded, // Rounded icon - color: - appBarTheme.iconTheme?.color ?? - colorScheme.onSurfaceVariant, - ), - onPressed: _textController.clear, - ) - : null, + suffixIcon: _showClearButton + ? IconButton( + icon: Icon( + Icons.clear_rounded, // Rounded icon + color: + appBarTheme.iconTheme?.color ?? + colorScheme.onSurfaceVariant, + ), + onPressed: _textController.clear, + ) + : null, ), onSubmitted: (_) => _performSearch(), ), @@ -259,158 +258,150 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ) => errorMessage != null ? FailureStateWidget( - message: errorMessage, - onRetry: - () => context.read().add( - HeadlinesSearchFetchRequested( - searchTerm: lastSearchTerm, - ), + message: errorMessage, + onRetry: () => context.read().add( + HeadlinesSearchFetchRequested( + searchTerm: lastSearchTerm, ), - ) + ), + ) : items.isEmpty ? FailureStateWidget( - // Use FailureStateWidget for no results - message: - '${l10n.headlinesSearchNoResultsHeadline} for "$lastSearchTerm" in ${resultsModelType.displayName.toLowerCase()}.\n${l10n.headlinesSearchNoResultsSubheadline}', - // No retry button for "no results" - ) + // Use FailureStateWidget for no results + message: + '${l10n.headlinesSearchNoResultsHeadline} for "$lastSearchTerm" in ${resultsModelType.displayName.toLowerCase()}.\n${l10n.headlinesSearchNoResultsSubheadline}', + // No retry button for "no results" + ) : ListView.separated( - controller: _scrollController, - padding: const EdgeInsets.symmetric( - // Consistent padding - horizontal: AppSpacing.paddingMedium, - vertical: AppSpacing.paddingSmall, - ).copyWith(bottom: AppSpacing.xxl), // Ensure bottom space - itemCount: hasMore ? items.length + 1 : items.length, - separatorBuilder: - (context, index) => const SizedBox( - height: AppSpacing.sm, - ), // Consistent spacing - itemBuilder: (context, index) { - if (index >= items.length) { - return const Padding( - padding: EdgeInsets.symmetric( - vertical: AppSpacing.lg, - ), - child: Center(child: CircularProgressIndicator()), - ); - } - final feedItem = items[index]; - - if (feedItem is Headline) { - final imageStyle = - context - .watch() - .state - .settings - .feedPreferences - .headlineImageStyle; - Widget tile; - switch (imageStyle) { - case HeadlineImageStyle.hidden: - tile = HeadlineTileTextOnly( - headline: feedItem, - onHeadlineTap: - () => context.goNamed( - Routes.searchArticleDetailsName, - pathParameters: {'id': feedItem.id}, - extra: feedItem, - ), - ); - case HeadlineImageStyle.smallThumbnail: - tile = HeadlineTileImageStart( - headline: feedItem, - onHeadlineTap: - () => context.goNamed( - Routes.searchArticleDetailsName, - pathParameters: {'id': feedItem.id}, - extra: feedItem, - ), - ); - case HeadlineImageStyle.largeThumbnail: - tile = HeadlineTileImageTop( - headline: feedItem, - onHeadlineTap: - () => context.goNamed( - Routes.searchArticleDetailsName, - pathParameters: {'id': feedItem.id}, - extra: feedItem, - ), - ); + controller: _scrollController, + padding: const EdgeInsets.symmetric( + // Consistent padding + horizontal: AppSpacing.paddingMedium, + vertical: AppSpacing.paddingSmall, + ).copyWith(bottom: AppSpacing.xxl), // Ensure bottom space + itemCount: hasMore ? items.length + 1 : items.length, + separatorBuilder: (context, index) => const SizedBox( + height: AppSpacing.sm, + ), // Consistent spacing + itemBuilder: (context, index) { + if (index >= items.length) { + return const Padding( + padding: EdgeInsets.symmetric( + vertical: AppSpacing.lg, + ), + child: Center(child: CircularProgressIndicator()), + ); } - return tile; - } else if (feedItem is Category) { - return CategoryItemWidget(category: feedItem); - } else if (feedItem is Source) { - return SourceItemWidget(source: feedItem); - } else if (feedItem is Ad) { - return Card( - margin: const EdgeInsets.symmetric( - vertical: AppSpacing.xs, - ), - color: currentColorScheme.surfaceContainerHighest, - child: Padding( - padding: const EdgeInsets.all(AppSpacing.md), - child: Column( - children: [ - if (feedItem.imageUrl.isNotEmpty) - ClipRRect( - // Add ClipRRect for consistency - borderRadius: BorderRadius.circular( - AppSpacing.xs, - ), - child: Image.network( - feedItem.imageUrl, - height: 100, // Consistent height - fit: BoxFit.cover, - errorBuilder: - (ctx, err, st) => Icon( - Icons.broken_image_outlined, - size: AppSpacing.xxl, - color: - currentColorScheme - .onSurfaceVariant, - ), - ), - ), - const SizedBox(height: AppSpacing.sm), - Text( - 'Placeholder Ad: ${feedItem.adType?.name ?? 'Generic'}', - style: currentTextTheme.titleSmall, + final feedItem = items[index]; + + if (feedItem is Headline) { + final imageStyle = context + .watch() + .state + .settings + .feedPreferences + .headlineImageStyle; + Widget tile; + switch (imageStyle) { + case HeadlineImageStyle.hidden: + tile = HeadlineTileTextOnly( + headline: feedItem, + onHeadlineTap: () => context.goNamed( + Routes.searchArticleDetailsName, + pathParameters: {'id': feedItem.id}, + extra: feedItem, + ), + ); + case HeadlineImageStyle.smallThumbnail: + tile = HeadlineTileImageStart( + headline: feedItem, + onHeadlineTap: () => context.goNamed( + Routes.searchArticleDetailsName, + pathParameters: {'id': feedItem.id}, + extra: feedItem, ), - Text( - 'Placement: ${feedItem.placement?.name ?? 'Default'}', - style: currentTextTheme.bodySmall, + ); + case HeadlineImageStyle.largeThumbnail: + tile = HeadlineTileImageTop( + headline: feedItem, + onHeadlineTap: () => context.goNamed( + Routes.searchArticleDetailsName, + pathParameters: {'id': feedItem.id}, + extra: feedItem, ), - ], + ); + } + return tile; + } else if (feedItem is Category) { + return CategoryItemWidget(category: feedItem); + } else if (feedItem is Source) { + return SourceItemWidget(source: feedItem); + } else if (feedItem is Ad) { + return Card( + margin: const EdgeInsets.symmetric( + vertical: AppSpacing.xs, ), - ), - ); - } else if (feedItem is AccountAction) { - return Card( - margin: const EdgeInsets.symmetric( - vertical: AppSpacing.xs, - ), - color: currentColorScheme.secondaryContainer, - child: ListTile( - leading: Icon( - feedItem.accountActionType == - AccountActionType.linkAccount - ? Icons - .link_outlined // Outlined - : Icons.upgrade_outlined, // Outlined - color: currentColorScheme.onSecondaryContainer, + color: currentColorScheme.surfaceContainerHighest, + child: Padding( + padding: const EdgeInsets.all(AppSpacing.md), + child: Column( + children: [ + if (feedItem.imageUrl.isNotEmpty) + ClipRRect( + // Add ClipRRect for consistency + borderRadius: BorderRadius.circular( + AppSpacing.xs, + ), + child: Image.network( + feedItem.imageUrl, + height: 100, // Consistent height + fit: BoxFit.cover, + errorBuilder: (ctx, err, st) => Icon( + Icons.broken_image_outlined, + size: AppSpacing.xxl, + color: currentColorScheme + .onSurfaceVariant, + ), + ), + ), + const SizedBox(height: AppSpacing.sm), + Text( + 'Placeholder Ad: ${feedItem.adType?.name ?? 'Generic'}', + style: currentTextTheme.titleSmall, + ), + Text( + 'Placement: ${feedItem.placement?.name ?? 'Default'}', + style: currentTextTheme.bodySmall, + ), + ], + ), ), - title: Text( - feedItem.title, - style: currentTextTheme.titleMedium?.copyWith( + ); + } else if (feedItem is AccountAction) { + return Card( + margin: const EdgeInsets.symmetric( + vertical: AppSpacing.xs, + ), + color: currentColorScheme.secondaryContainer, + child: ListTile( + leading: Icon( + feedItem.accountActionType == + AccountActionType.linkAccount + ? Icons + .link_outlined // Outlined + : Icons.upgrade_outlined, // Outlined color: currentColorScheme.onSecondaryContainer, - fontWeight: FontWeight.bold, ), - ), - subtitle: - feedItem.description != null - ? Text( + title: Text( + feedItem.title, + style: currentTextTheme.titleMedium?.copyWith( + color: + currentColorScheme.onSecondaryContainer, + fontWeight: FontWeight.bold, + ), + ), + subtitle: feedItem.description != null + ? Text( feedItem.description!, style: currentTextTheme.bodySmall ?.copyWith( @@ -421,10 +412,9 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { ), // Adjusted opacity ), ) - : null, - trailing: - feedItem.callToActionText != null - ? ElevatedButton( + : null, + trailing: feedItem.callToActionText != null + ? ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: currentColorScheme.secondary, @@ -446,21 +436,21 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { }, child: Text(feedItem.callToActionText!), ) - : null, - isThreeLine: - feedItem.description != null && - feedItem.description!.length > 50, - contentPadding: const EdgeInsets.symmetric( - // Consistent padding - horizontal: AppSpacing.paddingMedium, - vertical: AppSpacing.paddingSmall, + : null, + isThreeLine: + feedItem.description != null && + feedItem.description!.length > 50, + contentPadding: const EdgeInsets.symmetric( + // Consistent padding + horizontal: AppSpacing.paddingMedium, + vertical: AppSpacing.paddingSmall, + ), ), - ), - ); - } - return const SizedBox.shrink(); - }, - ), + ); + } + return const SizedBox.shrink(); + }, + ), HeadlinesSearchFailure( errorMessage: final errorMessage, lastSearchTerm: final lastSearchTerm, @@ -469,10 +459,9 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> { FailureStateWidget( message: 'Failed to search "$lastSearchTerm" in ${failedModelType.displayName.toLowerCase()}:\n$errorMessage', // Improved message - onRetry: - () => context.read().add( - HeadlinesSearchFetchRequested(searchTerm: lastSearchTerm), - ), + onRetry: () => context.read().add( + HeadlinesSearchFetchRequested(searchTerm: lastSearchTerm), + ), ), _ => const SizedBox.shrink(), // Fallback for any other state }; diff --git a/lib/headlines-search/widgets/category_item_widget.dart b/lib/headlines-search/widgets/category_item_widget.dart index 3d9b46d3..3c57f296 100644 --- a/lib/headlines-search/widgets/category_item_widget.dart +++ b/lib/headlines-search/widgets/category_item_widget.dart @@ -14,14 +14,13 @@ class CategoryItemWidget extends StatelessWidget { Widget build(BuildContext context) { return ListTile( title: Text(category.name), - subtitle: - category.description != null - ? Text( - category.description!, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ) - : null, + subtitle: category.description != null + ? Text( + category.description!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ) + : null, onTap: () { context.push( Routes.categoryDetails, diff --git a/lib/headlines-search/widgets/source_item_widget.dart b/lib/headlines-search/widgets/source_item_widget.dart index 9c67c15a..02eef431 100644 --- a/lib/headlines-search/widgets/source_item_widget.dart +++ b/lib/headlines-search/widgets/source_item_widget.dart @@ -14,14 +14,13 @@ class SourceItemWidget extends StatelessWidget { Widget build(BuildContext context) { return ListTile( title: Text(source.name), - subtitle: - source.description != null - ? Text( - source.description!, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ) - : null, + subtitle: source.description != null + ? Text( + source.description!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ) + : null, onTap: () { context.push( Routes.sourceDetails, diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index c82ea613..9c3128df 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:ht_main/l10n/app_localizations.dart'; - extension AppLocalizationsX on BuildContext { AppLocalizations get l10n => AppLocalizations.of(this); } diff --git a/lib/router/router.dart b/lib/router/router.dart index 237809fe..e9ec8165 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -76,8 +76,10 @@ GoRouter createRouter({ // --- 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; // Get appConfig final currentLocation = state.matchedLocation; final currentUri = state.uri; @@ -135,47 +137,61 @@ GoRouter createRouter({ appStatus == AppStatus.authenticated) { print(' Redirect Decision: User is $appStatus.'); - final isLinkingContextQueryPresent = state.uri.queryParameters['context'] == 'linking'; - final isLinkingPathSegmentPresent = currentLocation.contains('/linking/'); + final isLinkingContextQueryPresent = + state.uri.queryParameters['context'] == 'linking'; + final isLinkingPathSegmentPresent = currentLocation.contains( + '/linking/', + ); - // Determine if the current location is part of any linking flow (either via query or path segment) - final isAnyLinkingContext = isLinkingContextQueryPresent || isLinkingPathSegmentPresent; + // Determine if the current location is part of any linking flow (either via query or path segment) + final isAnyLinkingContext = + isLinkingContextQueryPresent || isLinkingPathSegmentPresent; - // If an authenticated/anonymous user is on any authentication-related path: - if (currentLocation.startsWith(authenticationPath)) { - print(' Debug: Auth path detected. Current Location: $currentLocation'); - print(' Debug: URI Query Parameters: ${state.uri.queryParameters}'); - print(' Debug: isLinkingContextQueryPresent: $isLinkingContextQueryPresent'); - print(' Debug: isLinkingPathSegmentPresent: $isLinkingPathSegmentPresent'); - print(' Debug: isAnyLinkingContext evaluated to: $isAnyLinkingContext'); + // If an authenticated/anonymous user is on any authentication-related path: + if (currentLocation.startsWith(authenticationPath)) { + print( + ' Debug: Auth path detected. Current Location: $currentLocation', + ); + print( + ' Debug: URI Query Parameters: ${state.uri.queryParameters}', + ); + print( + ' Debug: isLinkingContextQueryPresent: $isLinkingContextQueryPresent', + ); + print( + ' Debug: isLinkingPathSegmentPresent: $isLinkingPathSegmentPresent', + ); + print( + ' Debug: isAnyLinkingContext evaluated to: $isAnyLinkingContext', + ); - // If the user is authenticated, always redirect away from auth paths. - if (appStatus == AppStatus.authenticated) { - print( - ' Action: Authenticated user on auth path ($currentLocation). Redirecting to $feedPath', - ); - return feedPath; - } + // If the user is authenticated, always redirect away from auth paths. + if (appStatus == AppStatus.authenticated) { + print( + ' Action: Authenticated user on auth path ($currentLocation). Redirecting to $feedPath', + ); + return feedPath; + } - // If the user is anonymous, allow navigation within auth paths if in a linking context. - // Otherwise, redirect anonymous users trying to access non-linking auth paths to feed. - if (isAnyLinkingContext) { - print( - ' Action: Anonymous user on auth linking path ($currentLocation). Allowing navigation.', - ); - return null; - } else { - print( - ' Action: Anonymous user trying to access non-linking auth path ($currentLocation). Redirecting to $feedPath', - ); - return feedPath; - } + // If the user is anonymous, allow navigation within auth paths if in a linking context. + // Otherwise, redirect anonymous users trying to access non-linking auth paths to feed. + if (isAnyLinkingContext) { + print( + ' Action: Anonymous user on auth linking path ($currentLocation). Allowing navigation.', + ); + return null; + } else { + print( + ' Action: Anonymous user trying to access non-linking auth path ($currentLocation). Redirecting to $feedPath', + ); + return feedPath; } - // Allow access to other routes (non-auth paths) - print( - ' Action: Allowing navigation to $currentLocation for $appStatus user (non-auth path).', - ); - return null; + } + // Allow access to other routes (non-auth paths) + print( + ' Action: Allowing navigation to $currentLocation for $appStatus user (non-auth path).', + ); + return null; } // Fallback (should ideally not be reached if all statuses are handled) @@ -211,10 +227,9 @@ GoRouter createRouter({ } return BlocProvider( - create: - (context) => AuthenticationBloc( - authenticationRepository: context.read(), - ), + create: (context) => AuthenticationBloc( + authenticationRepository: context.read(), + ), child: AuthenticationPage( headline: headline, subHeadline: subHeadline, @@ -231,13 +246,15 @@ GoRouter createRouter({ builder: (context, state) => const SizedBox.shrink(), // Placeholder routes: [ GoRoute( - path: Routes.requestCode, // Path: /authentication/linking/request-code + path: Routes + .requestCode, // Path: /authentication/linking/request-code 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', // Path: /authentication/linking/verify-code/:email name: Routes.linkingVerifyCodeName, builder: (context, state) { final email = state.pathParameters['email']!; @@ -341,18 +358,16 @@ GoRouter createRouter({ providers: [ BlocProvider.value(value: accountBloc), BlocProvider( - create: - (context) => HeadlineDetailsBloc( - headlinesRepository: - context.read>(), - ), + create: (context) => HeadlineDetailsBloc( + headlinesRepository: context + .read>(), + ), ), BlocProvider( - create: - (context) => SimilarHeadlinesBloc( - headlinesRepository: - context.read>(), - ), + create: (context) => SimilarHeadlinesBloc( + headlinesRepository: context + .read>(), + ), ), ], child: HeadlineDetailsPage( @@ -374,8 +389,8 @@ GoRouter createRouter({ // Instantiate FeedInjectorService here as it's stateless for now final feedInjectorService = FeedInjectorService(); return HeadlinesFeedBloc( - headlinesRepository: - context.read>(), + headlinesRepository: context + .read>(), feedInjectorService: feedInjectorService, // Pass instance appBloc: context.read(), // Pass AppBloc instance )..add(const HeadlinesFeedFetchRequested()); @@ -386,10 +401,10 @@ GoRouter createRouter({ final feedInjectorService = FeedInjectorService(); // Instantiate return HeadlinesSearchBloc( - headlinesRepository: - context.read>(), - categoryRepository: - context.read>(), + headlinesRepository: context + .read>(), + categoryRepository: context + .read>(), sourceRepository: context.read>(), appBloc: context.read(), // Provide AppBloc feedInjectorService: feedInjectorService, // Provide Service @@ -422,20 +437,16 @@ GoRouter createRouter({ providers: [ BlocProvider.value(value: accountBloc), // Added BlocProvider( - create: - (context) => HeadlineDetailsBloc( - headlinesRepository: - context - .read>(), - ), + create: (context) => HeadlineDetailsBloc( + headlinesRepository: context + .read>(), + ), ), BlocProvider( - create: - (context) => SimilarHeadlinesBloc( - headlinesRepository: - context - .read>(), - ), + create: (context) => SimilarHeadlinesBloc( + headlinesRepository: context + .read>(), + ), ), ], child: HeadlineDetailsPage( @@ -475,69 +486,60 @@ GoRouter createRouter({ routes: [ // Sub-route for category selection GoRoute( - path: - Routes - .feedFilterCategories, // Relative path: 'categories' + path: Routes + .feedFilterCategories, // Relative path: 'categories' name: Routes.feedFilterCategoriesName, // Wrap with BlocProvider - builder: - (context, state) => BlocProvider( - create: - (context) => CategoriesFilterBloc( - categoriesRepository: - context - .read>(), - ), - child: const CategoryFilterPage(), - ), + builder: (context, state) => BlocProvider( + create: (context) => CategoriesFilterBloc( + categoriesRepository: context + .read>(), + ), + child: const CategoryFilterPage(), + ), ), // Sub-route for source selection GoRoute( - path: - Routes - .feedFilterSources, // Relative path: 'sources' + path: Routes + .feedFilterSources, // Relative path: 'sources' name: Routes.feedFilterSourcesName, // Wrap with BlocProvider - builder: - (context, state) => BlocProvider( - create: - (context) => SourcesFilterBloc( - sourcesRepository: - context - .read>(), - countriesRepository: // Added missing repository - context - .read>(), - ), - // Pass initialSelectedSources, country ISO codes, and source types from state.extra - child: Builder( - builder: (context) { - final extraData = - state.extra as Map? ?? - const {}; - final initialSources = - extraData[keySelectedSources] - as List? ?? - const []; - final initialCountryIsoCodes = - extraData[keySelectedCountryIsoCodes] - as Set? ?? - const {}; - final initialSourceTypes = - extraData[keySelectedSourceTypes] - as Set? ?? - const {}; + builder: (context, state) => BlocProvider( + create: (context) => SourcesFilterBloc( + sourcesRepository: context + .read>(), + countriesRepository: // Added missing repository + context + .read>(), + ), + // Pass initialSelectedSources, country ISO codes, and source types from state.extra + child: Builder( + builder: (context) { + final extraData = + state.extra as Map? ?? + const {}; + final initialSources = + extraData[keySelectedSources] + as List? ?? + const []; + final initialCountryIsoCodes = + extraData[keySelectedCountryIsoCodes] + as Set? ?? + const {}; + final initialSourceTypes = + extraData[keySelectedSourceTypes] + as Set? ?? + const {}; - return SourceFilterPage( - initialSelectedSources: initialSources, - initialSelectedCountryIsoCodes: - initialCountryIsoCodes, - initialSelectedSourceTypes: - initialSourceTypes, - ); - }, - ), - ), + return SourceFilterPage( + initialSelectedSources: initialSources, + initialSelectedCountryIsoCodes: + initialCountryIsoCodes, + initialSelectedSourceTypes: initialSourceTypes, + ); + }, + ), + ), ), // Sub-route for country selection REMOVED ], @@ -565,20 +567,16 @@ GoRouter createRouter({ providers: [ BlocProvider.value(value: accountBloc), // Added BlocProvider( - create: - (context) => HeadlineDetailsBloc( - headlinesRepository: - context - .read>(), - ), + create: (context) => HeadlineDetailsBloc( + headlinesRepository: context + .read>(), + ), ), BlocProvider( - create: - (context) => SimilarHeadlinesBloc( - headlinesRepository: - context - .read>(), - ), + create: (context) => SimilarHeadlinesBloc( + headlinesRepository: context + .read>(), + ), ), ], child: HeadlineDetailsPage( @@ -603,11 +601,7 @@ GoRouter createRouter({ routes: [ // ShellRoute for settings to provide SettingsBloc to children ShellRoute( - builder: ( - BuildContext context, - GoRouterState state, - Widget child, - ) { + builder: (BuildContext context, GoRouterState state, Widget child) { // This builder provides SettingsBloc to all routes within this ShellRoute. // 'child' will be SettingsPage, AppearanceSettingsPage, etc. final appBloc = context.read(); @@ -616,9 +610,8 @@ GoRouter createRouter({ return BlocProvider( create: (context) { final settingsBloc = SettingsBloc( - userAppSettingsRepository: - context - .read>(), + userAppSettingsRepository: context + .read>(), ); // Only load settings if a userId is available if (userId != null) { @@ -639,68 +632,56 @@ GoRouter createRouter({ }, routes: [ GoRoute( - path: - Routes - .settings, // Relative path 'settings' from /account + path: Routes + .settings, // Relative path 'settings' from /account name: Routes.settingsName, builder: (context, state) => const SettingsPage(), // --- Settings Sub-Routes --- routes: [ GoRoute( - path: - Routes - .settingsAppearance, // 'appearance' relative to /account/settings + path: Routes + .settingsAppearance, // 'appearance' relative to /account/settings name: Routes.settingsAppearanceName, - builder: - (context, state) => - const AppearanceSettingsPage(), + builder: (context, state) => + const AppearanceSettingsPage(), routes: [ // Children of AppearanceSettingsPage GoRoute( - path: - Routes - .settingsAppearanceTheme, // 'theme' relative to /account/settings/appearance + path: Routes + .settingsAppearanceTheme, // 'theme' relative to /account/settings/appearance name: Routes.settingsAppearanceThemeName, - builder: - (context, state) => - const ThemeSettingsPage(), + builder: (context, state) => + const ThemeSettingsPage(), ), GoRoute( - path: - Routes - .settingsAppearanceFont, // 'font' relative to /account/settings/appearance + path: Routes + .settingsAppearanceFont, // 'font' relative to /account/settings/appearance name: Routes.settingsAppearanceFontName, - builder: - (context, state) => - const FontSettingsPage(), + builder: (context, state) => + const FontSettingsPage(), ), ], ), GoRoute( - path: - Routes - .settingsFeed, // 'feed' relative to /account/settings + path: Routes + .settingsFeed, // 'feed' relative to /account/settings name: Routes.settingsFeedName, - builder: - (context, state) => const FeedSettingsPage(), + builder: (context, state) => + const FeedSettingsPage(), ), GoRoute( - path: - Routes - .settingsNotifications, // 'notifications' relative to /account/settings + path: Routes + .settingsNotifications, // 'notifications' relative to /account/settings name: Routes.settingsNotificationsName, - builder: - (context, state) => - const NotificationSettingsPage(), + builder: (context, state) => + const NotificationSettingsPage(), ), GoRoute( - path: - Routes - .settingsLanguage, // 'language' relative to /account/settings + path: Routes + .settingsLanguage, // 'language' relative to /account/settings name: Routes.settingsLanguageName, - builder: - (context, state) => - const LanguageSettingsPage(), + builder: (context, state) => + const LanguageSettingsPage(), ), ], ), @@ -710,38 +691,34 @@ GoRouter createRouter({ GoRoute( path: Routes.manageFollowedItems, // Updated path name: Routes.manageFollowedItemsName, // Updated name - builder: - (context, state) => - const ManageFollowedItemsPage(), // Use the new page + builder: (context, state) => + const ManageFollowedItemsPage(), // Use the new page routes: [ GoRoute( path: Routes.followedCategoriesList, name: Routes.followedCategoriesListName, - builder: - (context, state) => - const FollowedCategoriesListPage(), + builder: (context, state) => + const FollowedCategoriesListPage(), routes: [ GoRoute( path: Routes.addCategoryToFollow, name: Routes.addCategoryToFollowName, - builder: - (context, state) => - const AddCategoryToFollowPage(), + builder: (context, state) => + const AddCategoryToFollowPage(), ), ], ), GoRoute( path: Routes.followedSourcesList, name: Routes.followedSourcesListName, - builder: - (context, state) => const FollowedSourcesListPage(), + builder: (context, state) => + const FollowedSourcesListPage(), routes: [ GoRoute( path: Routes.addSourceToFollow, name: Routes.addSourceToFollowName, - builder: - (context, state) => - const AddSourceToFollowPage(), + builder: (context, state) => + const AddSourceToFollowPage(), ), ], ), @@ -765,24 +742,16 @@ GoRouter createRouter({ providers: [ BlocProvider.value(value: accountBloc), // Added BlocProvider( - create: - (context) => HeadlineDetailsBloc( - headlinesRepository: - context - .read< - HtDataRepository - >(), - ), + create: (context) => HeadlineDetailsBloc( + headlinesRepository: context + .read>(), + ), ), BlocProvider( - create: - (context) => SimilarHeadlinesBloc( - headlinesRepository: - context - .read< - HtDataRepository - >(), - ), + create: (context) => SimilarHeadlinesBloc( + headlinesRepository: context + .read>(), + ), ), ], child: HeadlineDetailsPage( diff --git a/lib/settings/bloc/settings_bloc.dart b/lib/settings/bloc/settings_bloc.dart index 3066e608..2a66a98e 100644 --- a/lib/settings/bloc/settings_bloc.dart +++ b/lib/settings/bloc/settings_bloc.dart @@ -68,7 +68,7 @@ class SettingsBloc extends Bloc { // If settings not found, create them // needed speciically for teh demo mode // that uses the ht data in memory impl - // as for the api impl, teh backend handle + // as for the api impl, teh backend handle // this use case. await _userAppSettingsRepository.create( item: settingsToSave, diff --git a/lib/settings/bloc/settings_state.dart b/lib/settings/bloc/settings_state.dart index 9c2365cb..eb42a879 100644 --- a/lib/settings/bloc/settings_state.dart +++ b/lib/settings/bloc/settings_state.dart @@ -48,8 +48,9 @@ class SettingsState extends Equatable { }) { return SettingsState( status: status ?? this.status, - userAppSettings: - clearUserAppSettings ? null : userAppSettings ?? this.userAppSettings, + userAppSettings: clearUserAppSettings + ? null + : userAppSettings ?? this.userAppSettings, error: clearError ? null : error ?? this.error, ); } diff --git a/lib/settings/view/feed_settings_page.dart b/lib/settings/view/feed_settings_page.dart index fc259d1d..a1a06e02 100644 --- a/lib/settings/view/feed_settings_page.dart +++ b/lib/settings/view/feed_settings_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +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'; @@ -42,33 +43,39 @@ class FeedSettingsPage extends StatelessWidget { ); } - return Scaffold( - appBar: AppBar( - title: Text(l10n.settingsFeedDisplayTitle), // Reuse title - ), - 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 - items: HeadlineImageStyle.values, - itemToString: (style) => _imageStyleToString(style, l10n), - onChanged: (value) { - if (value != null) { - settingsBloc.add( - SettingsFeedTileTypeChanged(value), - ); // Use new event - } - }, - ), - ], + return BlocListener( + listener: (context, settingsState) { + if (settingsState.status == SettingsStatus.success) { + context.read().add(const AppSettingsRefreshed()); + } + }, + child: Scaffold( + appBar: AppBar( + title: Text(l10n.settingsFeedDisplayTitle), // Reuse title + ), + 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 + items: HeadlineImageStyle.values, + itemToString: (style) => _imageStyleToString(style, l10n), + onChanged: (value) { + if (value != null) { + settingsBloc.add( + SettingsFeedTileTypeChanged(value), + ); // Use new event + } + }, + ), + ], + ), ), ); } @@ -90,13 +97,12 @@ class FeedSettingsPage extends StatelessWidget { const SizedBox(height: AppSpacing.sm), DropdownButtonFormField( value: currentValue, - items: - items.map((T value) { - return DropdownMenuItem( - value: value, - child: Text(itemToString(value)), - ); - }).toList(), + items: items.map((T value) { + return DropdownMenuItem( + value: value, + child: Text(itemToString(value)), + ); + }).toList(), onChanged: onChanged, decoration: const InputDecoration( border: OutlineInputBorder(), diff --git a/lib/settings/view/font_settings_page.dart b/lib/settings/view/font_settings_page.dart index ac925bd6..6b41020a 100644 --- a/lib/settings/view/font_settings_page.dart +++ b/lib/settings/view/font_settings_page.dart @@ -111,8 +111,8 @@ class FontSettingsPage extends StatelessWidget { 'Montserrat', 'Merriweather', ], // Updated font list - itemToString: - (fontFamily) => _fontFamilyToString(fontFamily, l10n), + itemToString: (fontFamily) => + _fontFamilyToString(fontFamily, l10n), onChanged: (value) { if (value != null) { settingsBloc.add(SettingsAppFontTypeChanged(value)); @@ -156,13 +156,12 @@ class FontSettingsPage extends StatelessWidget { const SizedBox(height: AppSpacing.sm), DropdownButtonFormField( value: currentValue, - items: - items.map((T value) { - return DropdownMenuItem( - value: value, - child: Text(itemToString(value)), - ); - }).toList(), + items: items.map((T value) { + return DropdownMenuItem( + value: value, + child: Text(itemToString(value)), + ); + }).toList(), onChanged: onChanged, decoration: const InputDecoration( border: OutlineInputBorder(), diff --git a/lib/settings/view/language_settings_page.dart b/lib/settings/view/language_settings_page.dart index 4565cbfa..4ea43a12 100644 --- a/lib/settings/view/language_settings_page.dart +++ b/lib/settings/view/language_settings_page.dart @@ -46,11 +46,8 @@ class LanguageSettingsPage extends StatelessWidget { body: ListView.separated( padding: const EdgeInsets.symmetric(vertical: AppSpacing.md), itemCount: _supportedLanguages.length, - separatorBuilder: - (context, index) => const Divider( - indent: AppSpacing.lg, - endIndent: AppSpacing.lg, - ), + separatorBuilder: (context, index) => + const Divider(indent: AppSpacing.lg, endIndent: AppSpacing.lg), itemBuilder: (context, index) { final languageCode = _supportedLanguages.keys.elementAt(index); final languageName = _supportedLanguages.values.elementAt(index); @@ -58,10 +55,9 @@ class LanguageSettingsPage extends StatelessWidget { return ListTile( title: Text(languageName), - trailing: - isSelected - ? Icon(Icons.check, color: Theme.of(context).primaryColor) - : null, + trailing: isSelected + ? Icon(Icons.check, color: Theme.of(context).primaryColor) + : null, onTap: () { if (!isSelected) { // Dispatch event to SettingsBloc diff --git a/lib/settings/view/theme_settings_page.dart b/lib/settings/view/theme_settings_page.dart index ea764a02..25d61177 100644 --- a/lib/settings/view/theme_settings_page.dart +++ b/lib/settings/view/theme_settings_page.dart @@ -128,13 +128,12 @@ class ThemeSettingsPage extends StatelessWidget { const SizedBox(height: AppSpacing.sm), DropdownButtonFormField( value: currentValue, - items: - items.map((T value) { - return DropdownMenuItem( - value: value, - child: Text(itemToString(value)), - ); - }).toList(), + items: items.map((T value) { + return DropdownMenuItem( + value: value, + child: Text(itemToString(value)), + ); + }).toList(), onChanged: onChanged, decoration: const InputDecoration( border: OutlineInputBorder(), diff --git a/lib/shared/widgets/headline_tile_image_start.dart b/lib/shared/widgets/headline_tile_image_start.dart index 1d5fd59e..c368001f 100644 --- a/lib/shared/widgets/headline_tile_image_start.dart +++ b/lib/shared/widgets/headline_tile_image_start.dart @@ -63,40 +63,39 @@ class HeadlineTileImageStart extends StatelessWidget { height: 72, child: ClipRRect( borderRadius: BorderRadius.circular(AppSpacing.xs), - child: - headline.imageUrl != null - ? Image.network( - headline.imageUrl!, - fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return ColoredBox( - color: colorScheme.surfaceContainerHighest, - child: const Center( - child: CircularProgressIndicator( - strokeWidth: 2, - ), + child: headline.imageUrl != null + ? Image.network( + headline.imageUrl!, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return ColoredBox( + color: colorScheme.surfaceContainerHighest, + child: const Center( + child: CircularProgressIndicator( + strokeWidth: 2, ), - ); - }, - errorBuilder: - (context, error, stackTrace) => ColoredBox( - color: colorScheme.surfaceContainerHighest, - child: Icon( - Icons.broken_image_outlined, - color: colorScheme.onSurfaceVariant, - size: AppSpacing.xl, - ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) => + ColoredBox( + color: colorScheme.surfaceContainerHighest, + child: Icon( + Icons.broken_image_outlined, + color: colorScheme.onSurfaceVariant, + size: AppSpacing.xl, ), - ) - : ColoredBox( - color: colorScheme.surfaceContainerHighest, - child: Icon( - Icons.image_not_supported_outlined, - color: colorScheme.onSurfaceVariant, - size: AppSpacing.xl, - ), + ), + ) + : ColoredBox( + color: colorScheme.surfaceContainerHighest, + child: Icon( + Icons.image_not_supported_outlined, + color: colorScheme.onSurfaceVariant, + size: AppSpacing.xl, ), + ), ), ), const SizedBox(width: AppSpacing.md), // Always add spacing diff --git a/lib/shared/widgets/headline_tile_image_top.dart b/lib/shared/widgets/headline_tile_image_top.dart index 52da2377..505ce238 100644 --- a/lib/shared/widgets/headline_tile_image_top.dart +++ b/lib/shared/widgets/headline_tile_image_top.dart @@ -61,46 +61,44 @@ class HeadlineTileImageTop extends StatelessWidget { topLeft: Radius.circular(AppSpacing.xs), topRight: Radius.circular(AppSpacing.xs), ), - child: - headline.imageUrl != null - ? Image.network( - headline.imageUrl!, - width: double.infinity, - height: 180, // Standard large image height - fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Container( - width: double.infinity, - height: 180, - color: colorScheme.surfaceContainerHighest, - child: const Center( - child: CircularProgressIndicator(strokeWidth: 2), - ), - ); - }, - errorBuilder: - (context, error, stackTrace) => Container( - width: double.infinity, - height: 180, - color: colorScheme.surfaceContainerHighest, - child: Icon( - Icons.broken_image_outlined, - color: colorScheme.onSurfaceVariant, - size: AppSpacing.xxl, - ), - ), - ) - : Container( + child: headline.imageUrl != null + ? Image.network( + headline.imageUrl!, + width: double.infinity, + height: 180, // Standard large image height + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + width: double.infinity, + height: 180, + color: colorScheme.surfaceContainerHighest, + child: const Center( + child: CircularProgressIndicator(strokeWidth: 2), + ), + ); + }, + errorBuilder: (context, error, stackTrace) => Container( width: double.infinity, height: 180, color: colorScheme.surfaceContainerHighest, child: Icon( - Icons.image_not_supported_outlined, + Icons.broken_image_outlined, color: colorScheme.onSurfaceVariant, size: AppSpacing.xxl, ), ), + ) + : Container( + width: double.infinity, + height: 180, + color: colorScheme.surfaceContainerHighest, + child: Icon( + Icons.image_not_supported_outlined, + color: colorScheme.onSurfaceVariant, + size: AppSpacing.xxl, + ), + ), ), ), Padding(