diff --git a/README.md b/README.md index 1a4e236..1dd4603 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Control the behavior and appearance of the `ht_main` mobile application remotely #### 📊 **Intuitive User Interface** Built with Flutter, the dashboard provides a responsive and user-friendly experience across various web browsers and screen sizes. +* **User-Friendly Error Handling:** Displays clear, localized error messages for a smooth and understandable user experience when issues arise. * **Benefit for you:** A modern, maintainable, and visually appealing interface for your administrative tasks. ✨ diff --git a/analysis_options.yaml b/analysis_options.yaml index 2cc6fd2..35fe669 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,5 +1,6 @@ analyzer: errors: + avoid_bool_literals_in_conditional_expressions: ignore avoid_catches_without_on_clauses: ignore avoid_print: ignore avoid_redundant_argument_values: ignore diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index af2ec3f..765a49d 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -19,6 +19,7 @@ import 'package:ht_dashboard/shared/theme/app_theme.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_kv_storage_service/ht_kv_storage_service.dart'; import 'package:ht_shared/ht_shared.dart' hide AppStatus; +import 'package:ht_ui_kit/ht_ui_kit.dart'; import 'package:logging/logging.dart'; class App extends StatelessWidget { @@ -29,7 +30,8 @@ class App extends StatelessWidget { required HtDataRepository htCountriesRepository, required HtDataRepository htSourcesRepository, required HtDataRepository htUserAppSettingsRepository, - required HtDataRepository htUserContentPreferencesRepository, + required HtDataRepository + htUserContentPreferencesRepository, required HtDataRepository htRemoteConfigRepository, required HtDataRepository htDashboardSummaryRepository, required HtKVStorageService kvStorageService, @@ -53,7 +55,8 @@ class App extends StatelessWidget { final HtDataRepository _htCountriesRepository; final HtDataRepository _htSourcesRepository; final HtDataRepository _htUserAppSettingsRepository; - final HtDataRepository _htUserContentPreferencesRepository; + final HtDataRepository + _htUserContentPreferencesRepository; final HtDataRepository _htRemoteConfigRepository; final HtDataRepository _htDashboardSummaryRepository; final HtKVStorageService _kvStorageService; @@ -79,10 +82,10 @@ class App extends StatelessWidget { BlocProvider( create: (context) => AppBloc( authenticationRepository: context.read(), - userAppSettingsRepository: - context.read>(), - appConfigRepository: - context.read>(), + userAppSettingsRepository: context + .read>(), + appConfigRepository: context + .read>(), environment: _environment, logger: Logger('AppBloc'), ), @@ -94,8 +97,8 @@ class App extends StatelessWidget { ), BlocProvider( create: (context) => AppConfigurationBloc( - remoteConfigRepository: - context.read>(), + remoteConfigRepository: context + .read>(), ), ), BlocProvider( @@ -107,10 +110,10 @@ class App extends StatelessWidget { ), BlocProvider( create: (context) => DashboardBloc( - dashboardSummaryRepository: - context.read>(), - appConfigRepository: - context.read>(), + dashboardSummaryRepository: context + .read>(), + appConfigRepository: context + .read>(), headlinesRepository: context.read>(), ), ), @@ -209,9 +212,11 @@ class _AppViewState extends State<_AppView> { child: MaterialApp.router( debugShowCheckedModeBanner: false, routerConfig: _router, - localizationsDelegates: - AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, + localizationsDelegates: const [ + HtUiKitLocalizations.delegate, + ...AppLocalizations.localizationsDelegates, + ], + supportedLocales: HtUiKitLocalizations.supportedLocales, theme: baseTheme == AppBaseTheme.dark ? darkThemeData : lightThemeData, diff --git a/lib/app_configuration/bloc/app_configuration_bloc.dart b/lib/app_configuration/bloc/app_configuration_bloc.dart index 7c443c2..25bfd9e 100644 --- a/lib/app_configuration/bloc/app_configuration_bloc.dart +++ b/lib/app_configuration/bloc/app_configuration_bloc.dart @@ -43,14 +43,14 @@ class AppConfigurationBloc emit( state.copyWith( status: AppConfigurationStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: AppConfigurationStatus.failure, - errorMessage: 'An unknown error occurred: $e', + exception: UnknownException('An unknown error occurred: $e'), ), ); } @@ -79,14 +79,14 @@ class AppConfigurationBloc emit( state.copyWith( status: AppConfigurationStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: AppConfigurationStatus.failure, - errorMessage: 'An unknown error occurred: $e', + exception: UnknownException('An unknown error occurred: $e'), ), ); } diff --git a/lib/app_configuration/bloc/app_configuration_event.dart b/lib/app_configuration/bloc/app_configuration_event.dart index 0fc89bf..0abacbe 100644 --- a/lib/app_configuration/bloc/app_configuration_event.dart +++ b/lib/app_configuration/bloc/app_configuration_event.dart @@ -20,7 +20,7 @@ class AppConfigurationLoaded extends AppConfigurationEvent { /// {@template app_configuration_updated} /// Event to request the update of the application configuration. /// -/// Carries the new [appConfig] object to be saved. +/// Carries the new "appConfig" object to be saved. /// {@endtemplate} class AppConfigurationUpdated extends AppConfigurationEvent { /// {@macro app_configuration_updated} diff --git a/lib/app_configuration/bloc/app_configuration_state.dart b/lib/app_configuration/bloc/app_configuration_state.dart index 52f6dd6..9ecc220 100644 --- a/lib/app_configuration/bloc/app_configuration_state.dart +++ b/lib/app_configuration/bloc/app_configuration_state.dart @@ -24,7 +24,7 @@ class AppConfigurationState extends Equatable { this.status = AppConfigurationStatus.initial, this.remoteConfig, this.originalRemoteConfig, - this.errorMessage, + this.exception, this.isDirty = false, this.showSaveSuccess = false, }); @@ -38,8 +38,8 @@ class AppConfigurationState extends Equatable { /// The original application configuration loaded from the backend. final RemoteConfig? originalRemoteConfig; - /// An error message if an operation failed. - final String? errorMessage; + /// An error exception if an operation failed. + final HtHttpException? exception; /// Indicates if there are unsaved changes to the configuration. final bool isDirty; @@ -52,7 +52,7 @@ class AppConfigurationState extends Equatable { AppConfigurationStatus? status, RemoteConfig? remoteConfig, RemoteConfig? originalRemoteConfig, - String? errorMessage, + HtHttpException? exception, bool? isDirty, bool clearErrorMessage = false, bool? showSaveSuccess, @@ -62,9 +62,7 @@ class AppConfigurationState extends Equatable { status: status ?? this.status, remoteConfig: remoteConfig ?? this.remoteConfig, originalRemoteConfig: originalRemoteConfig ?? this.originalRemoteConfig, - errorMessage: clearErrorMessage - ? null - : errorMessage ?? this.errorMessage, + exception: clearErrorMessage ? null : exception ?? this.exception, isDirty: isDirty ?? this.isDirty, showSaveSuccess: clearShowSaveSuccess ? false @@ -77,7 +75,7 @@ class AppConfigurationState extends Equatable { status, remoteConfig, originalRemoteConfig, - errorMessage, + exception, isDirty, showSaveSuccess, ]; diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index c036b3a..bdbe45b 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -5,6 +5,7 @@ import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/shared/constants/app_spacing.dart'; import 'package:ht_dashboard/shared/widgets/widgets.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:ht_ui_kit/ht_ui_kit.dart'; // Import for toFriendlyMessage /// {@template app_configuration_page} /// A page for managing the application's remote configuration. @@ -53,8 +54,8 @@ class _AppConfigurationPageState extends State { child: Text( l10n.appConfigurationPageDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), ), @@ -70,28 +71,27 @@ class _AppConfigurationPageState extends State { content: Text( l10n.appConfigSaveSuccessMessage, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onPrimary, - ), + color: Theme.of(context).colorScheme.onPrimary, + ), ), backgroundColor: Theme.of(context).colorScheme.primary, ), ); // Clear the showSaveSuccess flag after showing the snackbar context.read().add( - const AppConfigurationFieldChanged(), - ); - } else if (state.status == AppConfigurationStatus.failure) { + const AppConfigurationFieldChanged(), + ); + } else if (state.status == AppConfigurationStatus.failure && + state.exception != null) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Text( - l10n.appConfigSaveErrorMessage( - state.errorMessage ?? l10n.unknownError, - ), + state.exception!.toFriendlyMessage(context), style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onError, - ), + color: Theme.of(context).colorScheme.onError, + ), ), backgroundColor: Theme.of(context).colorScheme.error, ), @@ -108,12 +108,11 @@ class _AppConfigurationPageState extends State { ); } else if (state.status == AppConfigurationStatus.failure) { return FailureStateWidget( - message: - state.errorMessage ?? l10n.failedToLoadConfigurationMessage, + exception: state.exception!, onRetry: () { context.read().add( - const AppConfigurationLoaded(), - ); + const AppConfigurationLoaded(), + ); }, ); } else if (state.status == AppConfigurationStatus.success && @@ -191,8 +190,8 @@ class _AppConfigurationPageState extends State { ? () { // Discard changes: revert to original config context.read().add( - const AppConfigurationDiscarded(), - ); + const AppConfigurationDiscarded(), + ); } : null, child: Text(context.l10n.discardChangesButton), @@ -202,10 +201,12 @@ class _AppConfigurationPageState extends State { onPressed: isDirty ? () async { final confirmed = await _showConfirmationDialog(context); - if (context.mounted && confirmed && remoteConfig != null) { + if (context.mounted && + confirmed && + remoteConfig != null) { context.read().add( - AppConfigurationUpdated(remoteConfig), - ); + AppConfigurationUpdated(remoteConfig), + ); } } : null, @@ -263,8 +264,8 @@ class _AppConfigurationPageState extends State { Text( l10n.userContentLimitsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), ExpansionTile( @@ -278,10 +279,10 @@ class _AppConfigurationPageState extends State { remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -298,10 +299,10 @@ class _AppConfigurationPageState extends State { remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -318,10 +319,10 @@ class _AppConfigurationPageState extends State { remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -331,7 +332,10 @@ class _AppConfigurationPageState extends State { ); } - Widget _buildAdConfigSection(BuildContext context, RemoteConfig remoteConfig) { + Widget _buildAdConfigSection( + BuildContext context, + RemoteConfig remoteConfig, + ) { final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -339,8 +343,8 @@ class _AppConfigurationPageState extends State { Text( l10n.adSettingsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), ExpansionTile( @@ -354,10 +358,10 @@ class _AppConfigurationPageState extends State { remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -374,10 +378,10 @@ class _AppConfigurationPageState extends State { remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -394,10 +398,10 @@ class _AppConfigurationPageState extends State { remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -418,8 +422,8 @@ class _AppConfigurationPageState extends State { Text( l10n.inAppPromptsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), ExpansionTile( @@ -433,10 +437,10 @@ class _AppConfigurationPageState extends State { remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -453,10 +457,10 @@ class _AppConfigurationPageState extends State { remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -467,7 +471,9 @@ class _AppConfigurationPageState extends State { } Widget _buildAppStatusSection( - BuildContext context, RemoteConfig remoteConfig) { + BuildContext context, + RemoteConfig remoteConfig, + ) { final l10n = context.l10n; return SingleChildScrollView( padding: const EdgeInsets.all(AppSpacing.lg), @@ -477,9 +483,9 @@ class _AppConfigurationPageState extends State { Text( l10n.appOperationalStatusWarning, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.error, - fontWeight: FontWeight.bold, - ), + color: Theme.of(context).colorScheme.error, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: AppSpacing.lg), SwitchListTile( @@ -488,14 +494,14 @@ class _AppConfigurationPageState extends State { value: remoteConfig.appStatus.isUnderMaintenance, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: remoteConfig.copyWith( - appStatus: remoteConfig.appStatus.copyWith( - isUnderMaintenance: value, - ), - ), + AppConfigurationFieldChanged( + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + isUnderMaintenance: value, ), - ); + ), + ), + ); }, ), _buildTextField( @@ -505,14 +511,14 @@ class _AppConfigurationPageState extends State { value: remoteConfig.appStatus.latestAppVersion, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: remoteConfig.copyWith( - appStatus: remoteConfig.appStatus.copyWith( - latestAppVersion: value, - ), - ), + AppConfigurationFieldChanged( + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + latestAppVersion: value, ), - ); + ), + ), + ); }, ), SwitchListTile( @@ -521,14 +527,14 @@ class _AppConfigurationPageState extends State { value: remoteConfig.appStatus.isLatestVersionOnly, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: remoteConfig.copyWith( - appStatus: remoteConfig.appStatus.copyWith( - isLatestVersionOnly: value, - ), - ), + AppConfigurationFieldChanged( + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + isLatestVersionOnly: value, ), - ); + ), + ), + ); }, ), _buildTextField( @@ -538,14 +544,14 @@ class _AppConfigurationPageState extends State { value: remoteConfig.appStatus.iosUpdateUrl, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: remoteConfig.copyWith( - appStatus: remoteConfig.appStatus.copyWith( - iosUpdateUrl: value, - ), - ), + AppConfigurationFieldChanged( + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + iosUpdateUrl: value, ), - ); + ), + ), + ); }, ), _buildTextField( @@ -555,14 +561,14 @@ class _AppConfigurationPageState extends State { value: remoteConfig.appStatus.androidUpdateUrl, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: remoteConfig.copyWith( - appStatus: remoteConfig.appStatus.copyWith( - androidUpdateUrl: value, - ), - ), + AppConfigurationFieldChanged( + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + androidUpdateUrl: value, ), - ); + ), + ), + ); }, ), ], @@ -591,9 +597,8 @@ class _AppConfigurationPageState extends State { Text( description, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: - Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.xs), TextFormField( @@ -637,9 +642,8 @@ class _AppConfigurationPageState extends State { Text( description, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: - Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.xs), TextFormField( @@ -675,7 +679,8 @@ class _UserPreferenceLimitsForm extends StatefulWidget { required int value, required ValueChanged onChanged, TextEditingController? controller, - }) buildIntField; + }) + buildIntField; @override State<_UserPreferenceLimitsForm> createState() => @@ -705,20 +710,26 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { final config = widget.remoteConfig.userPreferenceConfig; switch (widget.userRole) { case AppUserRole.guestUser: - _followedItemsLimitController = - TextEditingController(text: config.guestFollowedItemsLimit.toString()); - _savedHeadlinesLimitController = - TextEditingController(text: config.guestSavedHeadlinesLimit.toString()); + _followedItemsLimitController = TextEditingController( + text: config.guestFollowedItemsLimit.toString(), + ); + _savedHeadlinesLimitController = TextEditingController( + text: config.guestSavedHeadlinesLimit.toString(), + ); case AppUserRole.standardUser: _followedItemsLimitController = TextEditingController( - text: config.authenticatedFollowedItemsLimit.toString()); + text: config.authenticatedFollowedItemsLimit.toString(), + ); _savedHeadlinesLimitController = TextEditingController( - text: config.authenticatedSavedHeadlinesLimit.toString()); + text: config.authenticatedSavedHeadlinesLimit.toString(), + ); case AppUserRole.premiumUser: _followedItemsLimitController = TextEditingController( - text: config.premiumFollowedItemsLimit.toString()); + text: config.premiumFollowedItemsLimit.toString(), + ); _savedHeadlinesLimitController = TextEditingController( - text: config.premiumSavedHeadlinesLimit.toString()); + text: config.premiumSavedHeadlinesLimit.toString(), + ); } } @@ -726,20 +737,22 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { final config = widget.remoteConfig.userPreferenceConfig; switch (widget.userRole) { case AppUserRole.guestUser: - _followedItemsLimitController.text = - config.guestFollowedItemsLimit.toString(); - _savedHeadlinesLimitController.text = - config.guestSavedHeadlinesLimit.toString(); + _followedItemsLimitController.text = config.guestFollowedItemsLimit + .toString(); + _savedHeadlinesLimitController.text = config.guestSavedHeadlinesLimit + .toString(); case AppUserRole.standardUser: - _followedItemsLimitController.text = - config.authenticatedFollowedItemsLimit.toString(); - _savedHeadlinesLimitController.text = - config.authenticatedSavedHeadlinesLimit.toString(); + _followedItemsLimitController.text = config + .authenticatedFollowedItemsLimit + .toString(); + _savedHeadlinesLimitController.text = config + .authenticatedSavedHeadlinesLimit + .toString(); case AppUserRole.premiumUser: - _followedItemsLimitController.text = - config.premiumFollowedItemsLimit.toString(); - _savedHeadlinesLimitController.text = - config.premiumSavedHeadlinesLimit.toString(); + _followedItemsLimitController.text = config.premiumFollowedItemsLimit + .toString(); + _savedHeadlinesLimitController.text = config.premiumSavedHeadlinesLimit + .toString(); } } @@ -766,8 +779,10 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { onChanged: (value) { widget.onConfigChanged( widget.remoteConfig.copyWith( - userPreferenceConfig: - _updateFollowedItemsLimit(userPreferenceConfig, value), + userPreferenceConfig: _updateFollowedItemsLimit( + userPreferenceConfig, + value, + ), ), ); }, @@ -776,14 +791,15 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { widget.buildIntField( context, label: 'Saved Headlines Limit', - description: - 'Maximum number of headlines this user role can save.', + description: 'Maximum number of headlines this user role can save.', value: _getSavedHeadlinesLimit(userPreferenceConfig), onChanged: (value) { widget.onConfigChanged( widget.remoteConfig.copyWith( - userPreferenceConfig: - _updateSavedHeadlinesLimit(userPreferenceConfig, value), + userPreferenceConfig: _updateSavedHeadlinesLimit( + userPreferenceConfig, + value, + ), ), ); }, @@ -816,7 +832,9 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { } UserPreferenceConfig _updateFollowedItemsLimit( - UserPreferenceConfig config, int value) { + UserPreferenceConfig config, + int value, + ) { switch (widget.userRole) { case AppUserRole.guestUser: return config.copyWith(guestFollowedItemsLimit: value); @@ -828,7 +846,9 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { } UserPreferenceConfig _updateSavedHeadlinesLimit( - UserPreferenceConfig config, int value) { + UserPreferenceConfig config, + int value, + ) { switch (widget.userRole) { case AppUserRole.guestUser: return config.copyWith(guestSavedHeadlinesLimit: value); @@ -858,7 +878,8 @@ class _AdConfigForm extends StatefulWidget { required int value, required ValueChanged onChanged, TextEditingController? controller, - }) buildIntField; + }) + buildIntField; @override State<_AdConfigForm> createState() => _AdConfigFormState(); @@ -868,7 +889,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { late final TextEditingController _adFrequencyController; late final TextEditingController _adPlacementIntervalController; late final TextEditingController - _articlesToReadBeforeShowingInterstitialAdsController; + _articlesToReadBeforeShowingInterstitialAdsController; @override void initState() { @@ -888,34 +909,43 @@ class _AdConfigFormState extends State<_AdConfigForm> { final adConfig = widget.remoteConfig.adConfig; switch (widget.userRole) { case AppUserRole.guestUser: - _adFrequencyController = - TextEditingController(text: adConfig.guestAdFrequency.toString()); + _adFrequencyController = TextEditingController( + text: adConfig.guestAdFrequency.toString(), + ); _adPlacementIntervalController = TextEditingController( - text: adConfig.guestAdPlacementInterval.toString()); + text: adConfig.guestAdPlacementInterval.toString(), + ); _articlesToReadBeforeShowingInterstitialAdsController = TextEditingController( - text: adConfig.guestArticlesToReadBeforeShowingInterstitialAds - .toString()); + text: adConfig.guestArticlesToReadBeforeShowingInterstitialAds + .toString(), + ); case AppUserRole.standardUser: _adFrequencyController = TextEditingController( - text: adConfig.authenticatedAdFrequency.toString()); + text: adConfig.authenticatedAdFrequency.toString(), + ); _adPlacementIntervalController = TextEditingController( - text: adConfig.authenticatedAdPlacementInterval.toString()); + text: adConfig.authenticatedAdPlacementInterval.toString(), + ); _articlesToReadBeforeShowingInterstitialAdsController = TextEditingController( - text: adConfig - .standardUserArticlesToReadBeforeShowingInterstitialAds - .toString()); + text: adConfig + .standardUserArticlesToReadBeforeShowingInterstitialAds + .toString(), + ); case AppUserRole.premiumUser: - _adFrequencyController = - TextEditingController(text: adConfig.premiumAdFrequency.toString()); + _adFrequencyController = TextEditingController( + text: adConfig.premiumAdFrequency.toString(), + ); _adPlacementIntervalController = TextEditingController( - text: adConfig.premiumAdPlacementInterval.toString()); + text: adConfig.premiumAdPlacementInterval.toString(), + ); _articlesToReadBeforeShowingInterstitialAdsController = TextEditingController( - text: adConfig - .premiumUserArticlesToReadBeforeShowingInterstitialAds - .toString()); + text: adConfig + .premiumUserArticlesToReadBeforeShowingInterstitialAds + .toString(), + ); } } @@ -924,23 +954,25 @@ class _AdConfigFormState extends State<_AdConfigForm> { switch (widget.userRole) { case AppUserRole.guestUser: _adFrequencyController.text = adConfig.guestAdFrequency.toString(); - _adPlacementIntervalController.text = - adConfig.guestAdPlacementInterval.toString(); + _adPlacementIntervalController.text = adConfig.guestAdPlacementInterval + .toString(); _articlesToReadBeforeShowingInterstitialAdsController.text = adConfig .guestArticlesToReadBeforeShowingInterstitialAds .toString(); case AppUserRole.standardUser: - _adFrequencyController.text = - adConfig.authenticatedAdFrequency.toString(); - _adPlacementIntervalController.text = - adConfig.authenticatedAdPlacementInterval.toString(); + _adFrequencyController.text = adConfig.authenticatedAdFrequency + .toString(); + _adPlacementIntervalController.text = adConfig + .authenticatedAdPlacementInterval + .toString(); _articlesToReadBeforeShowingInterstitialAdsController.text = adConfig .standardUserArticlesToReadBeforeShowingInterstitialAds .toString(); case AppUserRole.premiumUser: _adFrequencyController.text = adConfig.premiumAdFrequency.toString(); - _adPlacementIntervalController.text = - adConfig.premiumAdPlacementInterval.toString(); + _adPlacementIntervalController.text = adConfig + .premiumAdPlacementInterval + .toString(); _articlesToReadBeforeShowingInterstitialAdsController.text = adConfig .premiumUserArticlesToReadBeforeShowingInterstitialAds .toString(); @@ -1072,13 +1104,16 @@ class _AdConfigFormState extends State<_AdConfigForm> { switch (widget.userRole) { case AppUserRole.guestUser: return config.copyWith( - guestArticlesToReadBeforeShowingInterstitialAds: value); + guestArticlesToReadBeforeShowingInterstitialAds: value, + ); case AppUserRole.standardUser: return config.copyWith( - standardUserArticlesToReadBeforeShowingInterstitialAds: value); + standardUserArticlesToReadBeforeShowingInterstitialAds: value, + ); case AppUserRole.premiumUser: return config.copyWith( - premiumUserArticlesToReadBeforeShowingInterstitialAds: value); + premiumUserArticlesToReadBeforeShowingInterstitialAds: value, + ); } } } @@ -1101,7 +1136,8 @@ class _AccountActionConfigForm extends StatefulWidget { required int value, required ValueChanged onChanged, TextEditingController? controller, - }) buildIntField; + }) + buildIntField; @override State<_AccountActionConfigForm> createState() => @@ -1165,15 +1201,16 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { String _formatLabel(String enumName) { // Converts camelCase to Title Case final spaced = enumName.replaceAllMapped( - RegExp(r'([A-Z])'), (match) => ' ${match.group(1)}'); + RegExp('([A-Z])'), + (match) => ' ${match.group(1)}', + ); return '${spaced[0].toUpperCase()}${spaced.substring(1)} Days'; } @override Widget build(BuildContext context) { final accountActionConfig = widget.remoteConfig.accountActionConfig; - final relevantActionTypes = - _getDaysMap(accountActionConfig).keys.toList(); + final relevantActionTypes = _getDaysMap(accountActionConfig).keys.toList(); return Column( children: relevantActionTypes.map((actionType) { @@ -1190,9 +1227,11 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { final newConfig = widget.userRole == AppUserRole.guestUser ? accountActionConfig.copyWith( - guestDaysBetweenActions: updatedMap) + guestDaysBetweenActions: updatedMap, + ) : accountActionConfig.copyWith( - standardUserDaysBetweenActions: updatedMap); + standardUserDaysBetweenActions: updatedMap, + ); widget.onConfigChanged( widget.remoteConfig.copyWith(accountActionConfig: newConfig), diff --git a/lib/authentication/bloc/authentication_bloc.dart b/lib/authentication/bloc/authentication_bloc.dart index 586f196..5a25421 100644 --- a/lib/authentication/bloc/authentication_bloc.dart +++ b/lib/authentication/bloc/authentication_bloc.dart @@ -3,17 +3,19 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:ht_auth_repository/ht_auth_repository.dart'; -import 'package:ht_shared/ht_shared.dart' show - AuthenticationException, - ForbiddenException, - HtHttpException, - InvalidInputException, - NetworkException, - NotFoundException, - OperationFailedException, - ServerException, - UnauthorizedException, - User; +import 'package:ht_shared/ht_shared.dart' + show + AuthenticationException, + ForbiddenException, + HtHttpException, + InvalidInputException, + NetworkException, + NotFoundException, + OperationFailedException, + ServerException, + UnauthorizedException, + UnknownException, + User; part 'authentication_event.dart'; part 'authentication_state.dart'; @@ -70,16 +72,6 @@ class AuthenticationBloc AuthenticationRequestSignInCodeRequested event, Emitter emit, ) async { - // Validate email format (basic check) - if (event.email.isEmpty || !event.email.contains('@')) { - emit( - state.copyWith( - status: AuthenticationStatus.failure, - errorMessage: 'Please enter a valid email address.', - ), - ); - return; - } emit(state.copyWith(status: AuthenticationStatus.requestCodeLoading)); try { await _authenticationRepository.requestSignInCode( @@ -96,53 +88,50 @@ class AuthenticationBloc emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'Invalid input: ${e.message}', + exception: e, ), ); } on UnauthorizedException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: e.message, + exception: e, ), ); } on ForbiddenException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: e.message, + exception: e, ), ); - } on NetworkException catch (_) { + } on NetworkException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'Network error occurred.', + exception: e, ), ); } on ServerException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'Server error: ${e.message}', + exception: e, ), ); } on OperationFailedException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'Operation failed: ${e.message}', + exception: e, ), ); } on HtHttpException catch (e) { // Catch any other HtHttpException subtypes - final message = e.message.isNotEmpty - ? e.message - : 'An unspecified HTTP error occurred.'; emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'HTTP error: $message', + exception: e, ), ); } catch (e) { @@ -150,7 +139,7 @@ class AuthenticationBloc emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'An unexpected error occurred: $e', + exception: UnknownException('An unexpected error occurred: $e'), ), ); // Optionally log the stackTrace here @@ -175,42 +164,42 @@ class AuthenticationBloc emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: e.message, + exception: e, ), ); } on AuthenticationException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: e.message, + exception: e, ), ); } on NotFoundException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: e.message, + exception: e, ), ); - } on NetworkException catch (_) { + } on NetworkException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'Network error occurred.', + exception: e, ), ); } on ServerException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'Server error: ${e.message}', + exception: e, ), ); } on OperationFailedException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'Operation failed: ${e.message}', + exception: e, ), ); } on HtHttpException catch (e) { @@ -218,7 +207,7 @@ class AuthenticationBloc emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'HTTP error: ${e.message}', + exception: e, ), ); } catch (e) { @@ -226,7 +215,7 @@ class AuthenticationBloc emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'An unexpected error occurred: $e', + exception: UnknownException('An unexpected error occurred: $e'), ), ); // Optionally log the stackTrace here @@ -243,25 +232,25 @@ class AuthenticationBloc await _authenticationRepository.signOut(); // On success, the _AuthenticationStatusChanged listener will handle // emitting AuthenticationUnauthenticated. - } on NetworkException catch (_) { + } on NetworkException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'Network error occurred.', + exception: e, ), ); } on ServerException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'Server error: ${e.message}', + exception: e, ), ); } on OperationFailedException catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'Operation failed: ${e.message}', + exception: e, ), ); } on HtHttpException catch (e) { @@ -269,14 +258,14 @@ class AuthenticationBloc emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'HTTP error: ${e.message}', + exception: e, ), ); } catch (e) { emit( state.copyWith( status: AuthenticationStatus.failure, - errorMessage: 'An unexpected error occurred: $e', + exception: UnknownException('An unexpected error occurred: $e'), ), ); } diff --git a/lib/authentication/bloc/authentication_state.dart b/lib/authentication/bloc/authentication_state.dart index f4ba12b..b12250e 100644 --- a/lib/authentication/bloc/authentication_state.dart +++ b/lib/authentication/bloc/authentication_state.dart @@ -35,7 +35,7 @@ final class AuthenticationState extends Equatable { this.status = AuthenticationStatus.initial, this.user, this.email, - this.errorMessage, + this.exception, }); /// The current status of the authentication process. @@ -47,11 +47,11 @@ final class AuthenticationState extends Equatable { /// The email address involved in the current authentication flow. final String? email; - /// The error message describing an authentication failure, if any. - final String? errorMessage; + /// The error describing an authentication failure, if any. + final HtHttpException? exception; @override - List get props => [status, user, email, errorMessage]; + List get props => [status, user, email, exception]; /// Creates a copy of this [AuthenticationState] with the given fields /// replaced with the new values. @@ -59,13 +59,13 @@ final class AuthenticationState extends Equatable { AuthenticationStatus? status, User? user, String? email, - String? errorMessage, + HtHttpException? exception, }) { return AuthenticationState( status: status ?? this.status, user: user ?? this.user, email: email ?? this.email, - errorMessage: errorMessage ?? this.errorMessage, + exception: exception ?? this.exception, ); } } diff --git a/lib/authentication/view/authentication_page.dart b/lib/authentication/view/authentication_page.dart index a3727e1..badc86e 100644 --- a/lib/authentication/view/authentication_page.dart +++ b/lib/authentication/view/authentication_page.dart @@ -5,6 +5,7 @@ import 'package:ht_dashboard/authentication/bloc/authentication_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/router/routes.dart'; import 'package:ht_dashboard/shared/constants/app_spacing.dart'; +import 'package:ht_ui_kit/ht_ui_kit.dart'; /// {@template authentication_page} /// Displays authentication options for the dashboard. @@ -31,15 +32,16 @@ class AuthenticationPage extends StatelessWidget { child: BlocConsumer( // Listener remains crucial for feedback (errors) listener: (context, state) { - if (state.status == AuthenticationStatus.failure) { + if (state.status == AuthenticationStatus.failure && + state.exception != null) { + final friendlyMessage = state.exception!.toFriendlyMessage( + context, + ); ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text( - // Provide a more user-friendly error message if possible - state.errorMessage!, - ), + content: Text(friendlyMessage), backgroundColor: colorScheme.error, ), ); @@ -50,7 +52,8 @@ class AuthenticationPage extends StatelessWidget { // email flow pages. }, builder: (context, state) { - final isLoading = state.status == AuthenticationStatus.loading || + final isLoading = + state.status == AuthenticationStatus.loading || state.status == AuthenticationStatus.requestCodeLoading; return Padding( diff --git a/lib/authentication/view/email_code_verification_page.dart b/lib/authentication/view/email_code_verification_page.dart index 1635a59..33a3ec4 100644 --- a/lib/authentication/view/email_code_verification_page.dart +++ b/lib/authentication/view/email_code_verification_page.dart @@ -6,6 +6,7 @@ import 'package:ht_dashboard/app/config/config.dart'; import 'package:ht_dashboard/authentication/bloc/authentication_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/shared/constants/app_spacing.dart'; +import 'package:ht_ui_kit/ht_ui_kit.dart'; /// {@template email_code_verification_page} /// Page where the user enters the 6-digit code sent to their email @@ -29,12 +30,13 @@ class EmailCodeVerificationPage extends StatelessWidget { body: SafeArea( child: BlocConsumer( listener: (context, state) { - if (state.status == AuthenticationStatus.failure) { + if (state.status == AuthenticationStatus.failure && + state.exception != null) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.errorMessage!), + content: Text(state.exception!.toFriendlyMessage(context)), backgroundColor: colorScheme.error, ), ); diff --git a/lib/authentication/view/request_code_page.dart b/lib/authentication/view/request_code_page.dart index ca9116b..25b6757 100644 --- a/lib/authentication/view/request_code_page.dart +++ b/lib/authentication/view/request_code_page.dart @@ -10,6 +10,7 @@ import 'package:ht_dashboard/authentication/bloc/authentication_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/router/routes.dart'; import 'package:ht_dashboard/shared/constants/app_spacing.dart'; +import 'package:ht_ui_kit/ht_ui_kit.dart'; /// {@template request_code_page} /// Page for initiating the email code sign-in process. @@ -70,12 +71,13 @@ class _RequestCodeView extends StatelessWidget { body: SafeArea( child: BlocConsumer( listener: (context, state) { - if (state.status == AuthenticationStatus.failure) { + if (state.status == AuthenticationStatus.failure && + state.exception != null) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.errorMessage!), + content: Text(state.exception!.toFriendlyMessage(context)), backgroundColor: colorScheme.error, ), ); @@ -92,7 +94,7 @@ class _RequestCodeView extends StatelessWidget { // BuildWhen prevents unnecessary rebuilds if only listening buildWhen: (previous, current) => previous.status != current.status || - previous.errorMessage != current.errorMessage || + previous.exception != current.exception || previous.email != current.email, builder: (context, state) { final isLoading = diff --git a/lib/content_management/bloc/content_management_bloc.dart b/lib/content_management/bloc/content_management_bloc.dart index 0d0a258..12d5b43 100644 --- a/lib/content_management/bloc/content_management_bloc.dart +++ b/lib/content_management/bloc/content_management_bloc.dart @@ -78,14 +78,14 @@ class ContentManagementBloc emit( state.copyWith( headlinesStatus: ContentManagementStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( headlinesStatus: ContentManagementStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } @@ -105,14 +105,14 @@ class ContentManagementBloc emit( state.copyWith( headlinesStatus: ContentManagementStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( headlinesStatus: ContentManagementStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } @@ -157,14 +157,14 @@ class ContentManagementBloc emit( state.copyWith( topicsStatus: ContentManagementStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( topicsStatus: ContentManagementStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } @@ -184,14 +184,14 @@ class ContentManagementBloc emit( state.copyWith( topicsStatus: ContentManagementStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( topicsStatus: ContentManagementStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } @@ -236,14 +236,14 @@ class ContentManagementBloc emit( state.copyWith( sourcesStatus: ContentManagementStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( sourcesStatus: ContentManagementStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } @@ -263,14 +263,14 @@ class ContentManagementBloc emit( state.copyWith( sourcesStatus: ContentManagementStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( sourcesStatus: ContentManagementStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } diff --git a/lib/content_management/bloc/content_management_state.dart b/lib/content_management/bloc/content_management_state.dart index 8728613..b0cc0dd 100644 --- a/lib/content_management/bloc/content_management_state.dart +++ b/lib/content_management/bloc/content_management_state.dart @@ -32,7 +32,7 @@ class ContentManagementState extends Equatable { this.sources = const [], this.sourcesCursor, this.sourcesHasMore = false, - this.errorMessage, + this.exception, }); /// The currently active tab in the content management section. @@ -74,8 +74,8 @@ class ContentManagementState extends Equatable { /// Indicates if there are more sources to load. final bool sourcesHasMore; - /// Error message if an operation fails. - final String? errorMessage; + /// The error describing an operation failure, if any. + final HtHttpException? exception; /// Creates a copy of this [ContentManagementState] with updated values. ContentManagementState copyWith({ @@ -92,7 +92,7 @@ class ContentManagementState extends Equatable { List? sources, String? sourcesCursor, bool? sourcesHasMore, - String? errorMessage, + HtHttpException? exception, }) { return ContentManagementState( activeTab: activeTab ?? this.activeTab, @@ -108,7 +108,7 @@ class ContentManagementState extends Equatable { sources: sources ?? this.sources, sourcesCursor: sourcesCursor ?? this.sourcesCursor, sourcesHasMore: sourcesHasMore ?? this.sourcesHasMore, - errorMessage: errorMessage, + exception: exception ?? this.exception, ); } @@ -127,6 +127,6 @@ class ContentManagementState extends Equatable { sources, sourcesCursor, sourcesHasMore, - errorMessage, + exception, ]; } diff --git a/lib/content_management/bloc/create_headline/create_headline_bloc.dart b/lib/content_management/bloc/create_headline/create_headline_bloc.dart index 5905b6c..d3b3488 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -17,11 +17,11 @@ class CreateHeadlineBloc required HtDataRepository sourcesRepository, required HtDataRepository topicsRepository, required HtDataRepository countriesRepository, - }) : _headlinesRepository = headlinesRepository, - _sourcesRepository = sourcesRepository, - _topicsRepository = topicsRepository, - _countriesRepository = countriesRepository, - super(const CreateHeadlineState()) { + }) : _headlinesRepository = headlinesRepository, + _sourcesRepository = sourcesRepository, + _topicsRepository = topicsRepository, + _countriesRepository = countriesRepository, + super(const CreateHeadlineState()) { on(_onDataLoaded); on(_onTitleChanged); on(_onExcerptChanged); @@ -46,8 +46,11 @@ class CreateHeadlineBloc ) async { emit(state.copyWith(status: CreateHeadlineStatus.loading)); try { - final [sourcesResponse, topicsResponse, countriesResponse] = - await Future.wait([ + final [ + sourcesResponse, + topicsResponse, + countriesResponse, + ] = await Future.wait([ _sourcesRepository.readAll(), _topicsRepository.readAll(), _countriesRepository.readAll(), @@ -55,8 +58,7 @@ class CreateHeadlineBloc final sources = (sourcesResponse as PaginatedResponse).items; final topics = (topicsResponse as PaginatedResponse).items; - final countries = - (countriesResponse as PaginatedResponse).items; + final countries = (countriesResponse as PaginatedResponse).items; emit( state.copyWith( @@ -70,14 +72,14 @@ class CreateHeadlineBloc emit( state.copyWith( status: CreateHeadlineStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: CreateHeadlineStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } @@ -178,14 +180,14 @@ class CreateHeadlineBloc emit( state.copyWith( status: CreateHeadlineStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: CreateHeadlineStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } diff --git a/lib/content_management/bloc/create_headline/create_headline_state.dart b/lib/content_management/bloc/create_headline/create_headline_state.dart index 26be5df..7293482 100644 --- a/lib/content_management/bloc/create_headline/create_headline_state.dart +++ b/lib/content_management/bloc/create_headline/create_headline_state.dart @@ -33,7 +33,7 @@ final class CreateHeadlineState extends Equatable { this.topics = const [], this.countries = const [], this.contentStatus = ContentStatus.active, - this.errorMessage, + this.exception, this.createdHeadline, }); @@ -49,7 +49,7 @@ final class CreateHeadlineState extends Equatable { final List topics; final List countries; final ContentStatus contentStatus; - final String? errorMessage; + final HtHttpException? exception; final Headline? createdHeadline; /// Returns true if the form is valid and can be submitted. @@ -75,7 +75,7 @@ final class CreateHeadlineState extends Equatable { List? topics, List? countries, ContentStatus? contentStatus, - String? errorMessage, + HtHttpException? exception, Headline? createdHeadline, }) { return CreateHeadlineState( @@ -91,26 +91,26 @@ final class CreateHeadlineState extends Equatable { topics: topics ?? this.topics, countries: countries ?? this.countries, contentStatus: contentStatus ?? this.contentStatus, - errorMessage: errorMessage, + exception: exception, createdHeadline: createdHeadline ?? this.createdHeadline, ); } @override List get props => [ - status, - title, - excerpt, - url, - imageUrl, - source, - topic, - eventCountry, - sources, - topics, - countries, - contentStatus, - errorMessage, - createdHeadline, - ]; + status, + title, + excerpt, + url, + imageUrl, + source, + topic, + eventCountry, + sources, + topics, + countries, + contentStatus, + exception, + createdHeadline, + ]; } diff --git a/lib/content_management/bloc/create_source/create_source_bloc.dart b/lib/content_management/bloc/create_source/create_source_bloc.dart index 7bb78aa..9cf6b1c 100644 --- a/lib/content_management/bloc/create_source/create_source_bloc.dart +++ b/lib/content_management/bloc/create_source/create_source_bloc.dart @@ -51,14 +51,14 @@ class CreateSourceBloc extends Bloc { emit( state.copyWith( status: CreateSourceStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: CreateSourceStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } @@ -151,14 +151,14 @@ class CreateSourceBloc extends Bloc { emit( state.copyWith( status: CreateSourceStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: CreateSourceStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } diff --git a/lib/content_management/bloc/create_source/create_source_state.dart b/lib/content_management/bloc/create_source/create_source_state.dart index 7f6bece..0384e38 100644 --- a/lib/content_management/bloc/create_source/create_source_state.dart +++ b/lib/content_management/bloc/create_source/create_source_state.dart @@ -31,7 +31,7 @@ final class CreateSourceState extends Equatable { this.headquarters, this.countries = const [], this.contentStatus = ContentStatus.active, - this.errorMessage, + this.exception, this.createdSource, }); @@ -44,7 +44,7 @@ final class CreateSourceState extends Equatable { final Country? headquarters; final List countries; final ContentStatus contentStatus; - final String? errorMessage; + final HtHttpException? exception; final Source? createdSource; /// Returns true if the form is valid and can be submitted. @@ -66,7 +66,7 @@ final class CreateSourceState extends Equatable { ValueGetter? headquarters, List? countries, ContentStatus? contentStatus, - String? errorMessage, + HtHttpException? exception, Source? createdSource, }) { return CreateSourceState( @@ -79,7 +79,7 @@ final class CreateSourceState extends Equatable { headquarters: headquarters != null ? headquarters() : this.headquarters, countries: countries ?? this.countries, contentStatus: contentStatus ?? this.contentStatus, - errorMessage: errorMessage, + exception: exception, createdSource: createdSource ?? this.createdSource, ); } @@ -95,7 +95,7 @@ final class CreateSourceState extends Equatable { headquarters, countries, contentStatus, - errorMessage, + exception, createdSource, ]; } diff --git a/lib/content_management/bloc/create_topic/create_topic_bloc.dart b/lib/content_management/bloc/create_topic/create_topic_bloc.dart index f7f1746..92bc7b0 100644 --- a/lib/content_management/bloc/create_topic/create_topic_bloc.dart +++ b/lib/content_management/bloc/create_topic/create_topic_bloc.dart @@ -12,8 +12,8 @@ class CreateTopicBloc extends Bloc { /// {@macro create_topic_bloc} CreateTopicBloc({ required HtDataRepository topicsRepository, - }) : _topicsRepository = topicsRepository, - super(const CreateTopicState()) { + }) : _topicsRepository = topicsRepository, + super(const CreateTopicState()) { on(_onNameChanged); on(_onDescriptionChanged); on(_onIconUrlChanged); @@ -102,14 +102,14 @@ class CreateTopicBloc extends Bloc { emit( state.copyWith( status: CreateTopicStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: CreateTopicStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } diff --git a/lib/content_management/bloc/create_topic/create_topic_state.dart b/lib/content_management/bloc/create_topic/create_topic_state.dart index 0fbc613..a25b92f 100644 --- a/lib/content_management/bloc/create_topic/create_topic_state.dart +++ b/lib/content_management/bloc/create_topic/create_topic_state.dart @@ -24,7 +24,7 @@ final class CreateTopicState extends Equatable { this.description = '', this.iconUrl = '', this.contentStatus = ContentStatus.active, - this.errorMessage, + this.exception, this.createdTopic, }); @@ -33,7 +33,7 @@ final class CreateTopicState extends Equatable { final String description; final String iconUrl; final ContentStatus contentStatus; - final String? errorMessage; + final HtHttpException? exception; final Topic? createdTopic; /// Returns true if the form is valid and can be submitted. @@ -47,7 +47,7 @@ final class CreateTopicState extends Equatable { String? description, String? iconUrl, ContentStatus? contentStatus, - String? errorMessage, + HtHttpException? exception, Topic? createdTopic, }) { return CreateTopicState( @@ -56,19 +56,19 @@ final class CreateTopicState extends Equatable { description: description ?? this.description, iconUrl: iconUrl ?? this.iconUrl, contentStatus: contentStatus ?? this.contentStatus, - errorMessage: errorMessage, + exception: exception, createdTopic: createdTopic ?? this.createdTopic, ); } @override List get props => [ - status, - name, - description, - iconUrl, - contentStatus, - errorMessage, - createdTopic, - ]; + status, + name, + description, + iconUrl, + contentStatus, + exception, + createdTopic, + ]; } diff --git a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart index 3e4e503..6e8fe24 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart @@ -84,14 +84,14 @@ class EditHeadlineBloc extends Bloc { emit( state.copyWith( status: EditHeadlineStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: EditHeadlineStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } @@ -196,7 +196,9 @@ class EditHeadlineBloc extends Bloc { emit( state.copyWith( status: EditHeadlineStatus.failure, - errorMessage: 'Cannot update: Original headline data not loaded.', + exception: const UnknownException( + 'Cannot update: Original headline data not loaded.', + ), ), ); return; @@ -227,14 +229,14 @@ class EditHeadlineBloc extends Bloc { emit( state.copyWith( status: EditHeadlineStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: EditHeadlineStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } diff --git a/lib/content_management/bloc/edit_headline/edit_headline_state.dart b/lib/content_management/bloc/edit_headline/edit_headline_state.dart index 5cf98ea..ffbccc3 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_state.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_state.dart @@ -34,7 +34,7 @@ final class EditHeadlineState extends Equatable { this.topics = const [], this.countries = const [], this.contentStatus = ContentStatus.active, - this.errorMessage, + this.exception, this.updatedHeadline, }); @@ -51,7 +51,7 @@ final class EditHeadlineState extends Equatable { final List topics; final List countries; final ContentStatus contentStatus; - final String? errorMessage; + final HtHttpException? exception; final Headline? updatedHeadline; /// Returns true if the form is valid and can be submitted. @@ -78,7 +78,7 @@ final class EditHeadlineState extends Equatable { List? topics, List? countries, ContentStatus? contentStatus, - String? errorMessage, + HtHttpException? exception, Headline? updatedHeadline, }) { return EditHeadlineState( @@ -95,7 +95,7 @@ final class EditHeadlineState extends Equatable { topics: topics ?? this.topics, countries: countries ?? this.countries, contentStatus: contentStatus ?? this.contentStatus, - errorMessage: errorMessage, + exception: exception, updatedHeadline: updatedHeadline ?? this.updatedHeadline, ); } @@ -115,7 +115,7 @@ final class EditHeadlineState extends Equatable { topics, countries, contentStatus, - errorMessage, + exception, updatedHeadline, ]; } diff --git a/lib/content_management/bloc/edit_source/edit_source_bloc.dart b/lib/content_management/bloc/edit_source/edit_source_bloc.dart index 45b6536..1181a03 100644 --- a/lib/content_management/bloc/edit_source/edit_source_bloc.dart +++ b/lib/content_management/bloc/edit_source/edit_source_bloc.dart @@ -64,14 +64,14 @@ class EditSourceBloc extends Bloc { emit( state.copyWith( status: EditSourceStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: EditSourceStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } @@ -162,7 +162,9 @@ class EditSourceBloc extends Bloc { emit( state.copyWith( status: EditSourceStatus.failure, - errorMessage: 'Cannot update: Original source data not loaded.', + exception: const UnknownException( + 'Cannot update: Original source data not loaded.', + ), ), ); return; @@ -192,14 +194,14 @@ class EditSourceBloc extends Bloc { emit( state.copyWith( status: EditSourceStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: EditSourceStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } diff --git a/lib/content_management/bloc/edit_source/edit_source_state.dart b/lib/content_management/bloc/edit_source/edit_source_state.dart index b0a2805..1943a68 100644 --- a/lib/content_management/bloc/edit_source/edit_source_state.dart +++ b/lib/content_management/bloc/edit_source/edit_source_state.dart @@ -31,7 +31,7 @@ final class EditSourceState extends Equatable { this.headquarters, this.countries = const [], this.contentStatus = ContentStatus.active, - this.errorMessage, + this.exception, this.updatedSource, }); @@ -45,7 +45,7 @@ final class EditSourceState extends Equatable { final Country? headquarters; final List countries; final ContentStatus contentStatus; - final String? errorMessage; + final HtHttpException? exception; final Source? updatedSource; /// Returns true if the form is valid and can be submitted. @@ -68,7 +68,7 @@ final class EditSourceState extends Equatable { ValueGetter? headquarters, List? countries, ContentStatus? contentStatus, - String? errorMessage, + HtHttpException? exception, Source? updatedSource, }) { return EditSourceState( @@ -82,7 +82,7 @@ final class EditSourceState extends Equatable { headquarters: headquarters != null ? headquarters() : this.headquarters, countries: countries ?? this.countries, contentStatus: contentStatus ?? this.contentStatus, - errorMessage: errorMessage, + exception: exception, updatedSource: updatedSource ?? this.updatedSource, ); } @@ -99,7 +99,7 @@ final class EditSourceState extends Equatable { headquarters, countries, contentStatus, - errorMessage, + exception, updatedSource, ]; } diff --git a/lib/content_management/bloc/edit_topic/edit_topic_bloc.dart b/lib/content_management/bloc/edit_topic/edit_topic_bloc.dart index 32b1a5f..a15308f 100644 --- a/lib/content_management/bloc/edit_topic/edit_topic_bloc.dart +++ b/lib/content_management/bloc/edit_topic/edit_topic_bloc.dart @@ -12,9 +12,9 @@ class EditTopicBloc extends Bloc { EditTopicBloc({ required HtDataRepository topicsRepository, required String topicId, - }) : _topicsRepository = topicsRepository, - _topicId = topicId, - super(const EditTopicState()) { + }) : _topicsRepository = topicsRepository, + _topicId = topicId, + super(const EditTopicState()) { on(_onLoaded); on(_onNameChanged); on(_onDescriptionChanged); @@ -47,14 +47,14 @@ class EditTopicBloc extends Bloc { emit( state.copyWith( status: EditTopicStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: EditTopicStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } @@ -121,7 +121,9 @@ class EditTopicBloc extends Bloc { emit( state.copyWith( status: EditTopicStatus.failure, - errorMessage: 'Cannot update: Original topic data not loaded.', + exception: const UnknownException( + 'Cannot update: Original topic data not loaded.', + ), ), ); return; @@ -152,14 +154,14 @@ class EditTopicBloc extends Bloc { emit( state.copyWith( status: EditTopicStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: EditTopicStatus.failure, - errorMessage: e.toString(), + exception: UnknownException('An unexpected error occurred: $e'), ), ); } diff --git a/lib/content_management/bloc/edit_topic/edit_topic_state.dart b/lib/content_management/bloc/edit_topic/edit_topic_state.dart index 7ee2b99..e2a6ad3 100644 --- a/lib/content_management/bloc/edit_topic/edit_topic_state.dart +++ b/lib/content_management/bloc/edit_topic/edit_topic_state.dart @@ -27,7 +27,7 @@ final class EditTopicState extends Equatable { this.description = '', this.iconUrl = '', this.contentStatus = ContentStatus.active, - this.errorMessage, + this.exception, this.updatedTopic, }); @@ -37,7 +37,7 @@ final class EditTopicState extends Equatable { final String description; final String iconUrl; final ContentStatus contentStatus; - final String? errorMessage; + final HtHttpException? exception; final Topic? updatedTopic; /// Returns true if the form is valid and can be submitted. @@ -52,7 +52,7 @@ final class EditTopicState extends Equatable { String? description, String? iconUrl, ContentStatus? contentStatus, - String? errorMessage, + HtHttpException? exception, Topic? updatedTopic, }) { return EditTopicState( @@ -62,20 +62,20 @@ final class EditTopicState extends Equatable { description: description ?? this.description, iconUrl: iconUrl ?? this.iconUrl, contentStatus: contentStatus ?? this.contentStatus, - errorMessage: errorMessage ?? this.errorMessage, + exception: exception, updatedTopic: updatedTopic ?? this.updatedTopic, ); } @override List get props => [ - status, - initialTopic, - name, - description, - iconUrl, - contentStatus, - errorMessage, - updatedTopic, - ]; + status, + initialTopic, + name, + description, + iconUrl, + contentStatus, + exception, + updatedTopic, + ]; } diff --git a/lib/content_management/view/content_management_page.dart b/lib/content_management/view/content_management_page.dart index 1585bbe..cfbc6a9 100644 --- a/lib/content_management/view/content_management_page.dart +++ b/lib/content_management/view/content_management_page.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart'; -import 'package:ht_dashboard/content_management/view/topics_page.dart'; import 'package:ht_dashboard/content_management/view/headlines_page.dart'; import 'package:ht_dashboard/content_management/view/sources_page.dart'; +import 'package:ht_dashboard/content_management/view/topics_page.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/router/routes.dart'; import 'package:ht_dashboard/shared/constants/app_spacing.dart'; diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index 339a1cc..b751f80 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -8,6 +8,7 @@ import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:ht_ui_kit/ht_ui_kit.dart'; /// {@template create_headline_page} /// A page for creating a new headline. @@ -95,7 +96,7 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.errorMessage ?? l10n.unknownError), + content: Text(state.exception!.toFriendlyMessage(context)), backgroundColor: Theme.of(context).colorScheme.error, ), ); @@ -115,7 +116,7 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { state.topics.isEmpty && state.countries.isEmpty) { return FailureStateWidget( - message: state.errorMessage ?? l10n.unknownError, + exception: state.exception!, onRetry: () => context.read().add( const CreateHeadlineDataLoaded(), ), diff --git a/lib/content_management/view/create_source_page.dart b/lib/content_management/view/create_source_page.dart index 80596a6..a11f2af 100644 --- a/lib/content_management/view/create_source_page.dart +++ b/lib/content_management/view/create_source_page.dart @@ -9,6 +9,7 @@ import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:ht_ui_kit/ht_ui_kit.dart'; /// {@template create_source_page} /// A page for creating a new source. @@ -94,7 +95,7 @@ class _CreateSourceViewState extends State<_CreateSourceView> { ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.errorMessage ?? l10n.unknownError), + content: Text(state.exception!.toFriendlyMessage(context)), backgroundColor: Theme.of(context).colorScheme.error, ), ); @@ -112,7 +113,7 @@ class _CreateSourceViewState extends State<_CreateSourceView> { if (state.status == CreateSourceStatus.failure && state.countries.isEmpty) { return FailureStateWidget( - message: state.errorMessage ?? l10n.unknownError, + exception: state.exception!, onRetry: () => context.read().add( const CreateSourceDataLoaded(), ), diff --git a/lib/content_management/view/create_topic_page.dart b/lib/content_management/view/create_topic_page.dart index adc2d0c..4b617c0 100644 --- a/lib/content_management/view/create_topic_page.dart +++ b/lib/content_management/view/create_topic_page.dart @@ -8,6 +8,7 @@ import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:ht_ui_kit/ht_ui_kit.dart'; /// {@template create_topic_page} /// A page for creating a new topic. @@ -92,7 +93,7 @@ class _CreateTopicViewState extends State<_CreateTopicView> { ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.errorMessage ?? l10n.unknownError), + content: Text(state.exception!.toFriendlyMessage(context)), backgroundColor: Theme.of(context).colorScheme.error, ), ); diff --git a/lib/content_management/view/edit_headline_page.dart b/lib/content_management/view/edit_headline_page.dart index 6a57aec..6505d56 100644 --- a/lib/content_management/view/edit_headline_page.dart +++ b/lib/content_management/view/edit_headline_page.dart @@ -7,6 +7,7 @@ import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:ht_ui_kit/ht_ui_kit.dart'; /// {@template edit_headline_page} /// A page for editing an existing headline. @@ -124,7 +125,7 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.errorMessage ?? l10n.unknownError), + content: Text(state.exception!.toFriendlyMessage(context)), backgroundColor: Theme.of(context).colorScheme.error, ), ); @@ -148,7 +149,7 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { if (state.status == EditHeadlineStatus.failure && state.initialHeadline == null) { return FailureStateWidget( - message: state.errorMessage ?? l10n.unknownError, + exception: state.exception!, onRetry: () => context.read().add( const EditHeadlineLoaded(), ), diff --git a/lib/content_management/view/edit_source_page.dart b/lib/content_management/view/edit_source_page.dart index 469bbd5..73ee884 100644 --- a/lib/content_management/view/edit_source_page.dart +++ b/lib/content_management/view/edit_source_page.dart @@ -7,6 +7,7 @@ import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:ht_ui_kit/ht_ui_kit.dart'; /// {@template edit_source_page} /// A page for editing an existing source. @@ -120,7 +121,7 @@ class _EditSourceViewState extends State<_EditSourceView> { ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.errorMessage ?? l10n.unknownError), + content: Text(state.exception!.toFriendlyMessage(context)), backgroundColor: Theme.of(context).colorScheme.error, ), ); @@ -144,7 +145,7 @@ class _EditSourceViewState extends State<_EditSourceView> { if (state.status == EditSourceStatus.failure && state.initialSource == null) { return FailureStateWidget( - message: state.errorMessage ?? l10n.unknownError, + exception: state.exception!, onRetry: () => context.read().add(const EditSourceLoaded()), ); diff --git a/lib/content_management/view/edit_topic_page.dart b/lib/content_management/view/edit_topic_page.dart index 66c1320..e2df650 100644 --- a/lib/content_management/view/edit_topic_page.dart +++ b/lib/content_management/view/edit_topic_page.dart @@ -7,6 +7,7 @@ import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:ht_ui_kit/ht_ui_kit.dart'; /// {@template edit_topic_page} /// A page for editing an existing topic. @@ -107,7 +108,7 @@ class _EditTopicViewState extends State<_EditTopicView> { SnackBar(content: Text(l10n.topicUpdatedSuccessfully)), ); context.read().add( - TopicUpdated(state.updatedTopic!), + TopicUpdated(state.updatedTopic!), ); context.pop(); } @@ -116,7 +117,7 @@ class _EditTopicViewState extends State<_EditTopicView> { ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.errorMessage ?? l10n.unknownError), + content: Text(state.exception!.toFriendlyMessage(context)), backgroundColor: Theme.of(context).colorScheme.error, ), ); @@ -139,7 +140,7 @@ class _EditTopicViewState extends State<_EditTopicView> { if (state.status == EditTopicStatus.failure && state.initialTopic == null) { return FailureStateWidget( - message: state.errorMessage ?? l10n.unknownError, + exception: state.exception!, onRetry: () => context.read().add( const EditTopicLoaded(), ), @@ -160,9 +161,9 @@ class _EditTopicViewState extends State<_EditTopicView> { labelText: l10n.topicName, border: const OutlineInputBorder(), ), - onChanged: (value) => context - .read() - .add(EditTopicNameChanged(value)), + onChanged: (value) => context.read().add( + EditTopicNameChanged(value), + ), ), const SizedBox(height: AppSpacing.lg), TextFormField( @@ -172,9 +173,9 @@ class _EditTopicViewState extends State<_EditTopicView> { border: const OutlineInputBorder(), ), maxLines: 3, - onChanged: (value) => context - .read() - .add(EditTopicDescriptionChanged(value)), + onChanged: (value) => context.read().add( + EditTopicDescriptionChanged(value), + ), ), const SizedBox(height: AppSpacing.lg), TextFormField( @@ -183,9 +184,9 @@ class _EditTopicViewState extends State<_EditTopicView> { labelText: l10n.iconUrl, border: const OutlineInputBorder(), ), - onChanged: (value) => context - .read() - .add(EditTopicIconUrlChanged(value)), + onChanged: (value) => context.read().add( + EditTopicIconUrlChanged(value), + ), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( diff --git a/lib/content_management/view/headlines_page.dart b/lib/content_management/view/headlines_page.dart index 97788f5..41f89c6 100644 --- a/lib/content_management/view/headlines_page.dart +++ b/lib/content_management/view/headlines_page.dart @@ -52,7 +52,7 @@ class _HeadlinesPageState extends State { if (state.headlinesStatus == ContentManagementStatus.failure) { return FailureStateWidget( - message: state.errorMessage ?? l10n.unknownError, + exception: state.exception!, onRetry: () => context.read().add( const LoadHeadlinesRequested(limit: kDefaultRowsPerPage), ), diff --git a/lib/content_management/view/sources_page.dart b/lib/content_management/view/sources_page.dart index a353755..d06451d 100644 --- a/lib/content_management/view/sources_page.dart +++ b/lib/content_management/view/sources_page.dart @@ -53,7 +53,7 @@ class _SourcesPageState extends State { if (state.sourcesStatus == ContentManagementStatus.failure) { return FailureStateWidget( - message: state.errorMessage ?? l10n.unknownError, + exception: state.exception!, onRetry: () => context.read().add( const LoadSourcesRequested(limit: kDefaultRowsPerPage), ), diff --git a/lib/content_management/view/topics_page.dart b/lib/content_management/view/topics_page.dart index 9f82375..a6540b1 100644 --- a/lib/content_management/view/topics_page.dart +++ b/lib/content_management/view/topics_page.dart @@ -49,7 +49,7 @@ class _TopicPageState extends State { if (state.topicsStatus == ContentManagementStatus.failure) { return FailureStateWidget( - message: state.errorMessage ?? l10n.unknownError, + exception: state.exception!, onRetry: () => context.read().add( const LoadTopicsRequested(limit: kDefaultRowsPerPage), ), @@ -208,7 +208,9 @@ class _TopicsDataSource extends DataTableSource { if (hasMore) { // When loading, we show an extra row for the spinner. // Otherwise, we just indicate that there are more rows. - return isLoading ? topics.length + 1 : topics.length + kDefaultRowsPerPage; + return isLoading + ? topics.length + 1 + : topics.length + kDefaultRowsPerPage; } return topics.length; } diff --git a/lib/dashboard/bloc/dashboard_bloc.dart b/lib/dashboard/bloc/dashboard_bloc.dart index 7a3e6cf..1857da4 100644 --- a/lib/dashboard/bloc/dashboard_bloc.dart +++ b/lib/dashboard/bloc/dashboard_bloc.dart @@ -60,14 +60,14 @@ class DashboardBloc extends Bloc { emit( state.copyWith( status: DashboardStatus.failure, - errorMessage: e.message, + exception: e, ), ); } catch (e) { emit( state.copyWith( status: DashboardStatus.failure, - errorMessage: 'An unknown error occurred: $e', + exception: UnknownException('An unknown error occurred: $e'), ), ); } diff --git a/lib/dashboard/bloc/dashboard_state.dart b/lib/dashboard/bloc/dashboard_state.dart index 7f37199..bfbfe4d 100644 --- a/lib/dashboard/bloc/dashboard_state.dart +++ b/lib/dashboard/bloc/dashboard_state.dart @@ -22,28 +22,28 @@ final class DashboardState extends Equatable { this.summary, this.appConfig, this.recentHeadlines = const [], - this.errorMessage, + this.exception, }); final DashboardStatus status; final DashboardSummary? summary; final RemoteConfig? appConfig; final List recentHeadlines; - final String? errorMessage; + final HtHttpException? exception; DashboardState copyWith({ DashboardStatus? status, DashboardSummary? summary, RemoteConfig? appConfig, List? recentHeadlines, - String? errorMessage, + HtHttpException? exception, }) { return DashboardState( status: status ?? this.status, summary: summary ?? this.summary, appConfig: appConfig ?? this.appConfig, recentHeadlines: recentHeadlines ?? this.recentHeadlines, - errorMessage: errorMessage ?? this.errorMessage, + exception: exception ?? this.exception, ); } @@ -53,6 +53,6 @@ final class DashboardState extends Equatable { summary, appConfig, recentHeadlines, - errorMessage, + exception, ]; } diff --git a/lib/dashboard/view/dashboard_page.dart b/lib/dashboard/view/dashboard_page.dart index 418b1f2..83df1da 100644 --- a/lib/dashboard/view/dashboard_page.dart +++ b/lib/dashboard/view/dashboard_page.dart @@ -43,7 +43,7 @@ class _DashboardPageState extends State { } if (state.status == DashboardStatus.failure) { return FailureStateWidget( - message: state.errorMessage ?? l10n.dashboardLoadFailure, + exception: state.exception!, onRetry: () { context.read().add(DashboardSummaryLoaded()); }, diff --git a/lib/main.dart b/lib/main.dart index 34b787b..d42b6c6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,7 @@ import 'package:ht_dashboard/app/config/config.dart'; import 'package:ht_dashboard/bootstrap.dart'; // Define the current application environment (production/development/demo). -const AppEnvironment appEnvironment = AppEnvironment.demo; +const AppEnvironment appEnvironment = AppEnvironment.development; @JS('removeSplashFromWeb') external void removeSplashFromWeb(); diff --git a/lib/router/router.dart b/lib/router/router.dart index 67bc9d1..60de487 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -11,12 +11,12 @@ import 'package:ht_dashboard/authentication/view/authentication_page.dart'; import 'package:ht_dashboard/authentication/view/email_code_verification_page.dart'; import 'package:ht_dashboard/authentication/view/request_code_page.dart'; import 'package:ht_dashboard/content_management/view/content_management_page.dart'; -import 'package:ht_dashboard/content_management/view/create_topic_page.dart'; import 'package:ht_dashboard/content_management/view/create_headline_page.dart'; import 'package:ht_dashboard/content_management/view/create_source_page.dart'; -import 'package:ht_dashboard/content_management/view/edit_topic_page.dart'; +import 'package:ht_dashboard/content_management/view/create_topic_page.dart'; import 'package:ht_dashboard/content_management/view/edit_headline_page.dart'; import 'package:ht_dashboard/content_management/view/edit_source_page.dart'; +import 'package:ht_dashboard/content_management/view/edit_topic_page.dart'; import 'package:ht_dashboard/dashboard/view/dashboard_page.dart'; import 'package:ht_dashboard/router/routes.dart'; import 'package:ht_dashboard/settings/view/settings_page.dart'; diff --git a/lib/settings/bloc/settings_bloc.dart b/lib/settings/bloc/settings_bloc.dart index d6222a1..29a2c48 100644 --- a/lib/settings/bloc/settings_bloc.dart +++ b/lib/settings/bloc/settings_bloc.dart @@ -56,12 +56,12 @@ class SettingsBloc extends Bloc { emit(SettingsLoadSuccess(userAppSettings: defaultSettings)); } on HtHttpException catch (e) { emit( - SettingsLoadFailure(e.message, userAppSettings: state.userAppSettings), + SettingsLoadFailure(e, userAppSettings: state.userAppSettings), ); } catch (e) { emit( SettingsLoadFailure( - 'An unexpected error occurred: $e', + UnknownException('An unexpected error occurred: $e'), userAppSettings: state.userAppSettings, ), ); @@ -82,14 +82,14 @@ class SettingsBloc extends Bloc { } on HtHttpException catch (e) { emit( SettingsUpdateFailure( - e.message, + e, userAppSettings: state.userAppSettings, ), ); } catch (e) { emit( SettingsUpdateFailure( - 'An unexpected error occurred: $e', + UnknownException('An unexpected error occurred: $e'), userAppSettings: state.userAppSettings, ), ); diff --git a/lib/settings/bloc/settings_state.dart b/lib/settings/bloc/settings_state.dart index aa16450..ab9de65 100644 --- a/lib/settings/bloc/settings_state.dart +++ b/lib/settings/bloc/settings_state.dart @@ -39,13 +39,13 @@ final class SettingsLoadSuccess extends SettingsState { /// {@endtemplate} final class SettingsLoadFailure extends SettingsState { /// {@macro settings_load_failure} - const SettingsLoadFailure(this.errorMessage, {super.userAppSettings}); + const SettingsLoadFailure(this.exception, {super.userAppSettings}); - /// The error message describing the failure. - final String errorMessage; + /// The error exception describing the failure. + final HtHttpException exception; @override - List get props => [errorMessage, userAppSettings]; + List get props => [exception, userAppSettings]; } /// {@template settings_update_in_progress} @@ -69,11 +69,11 @@ final class SettingsUpdateSuccess extends SettingsState { /// {@endtemplate} final class SettingsUpdateFailure extends SettingsState { /// {@macro settings_update_failure} - const SettingsUpdateFailure(this.errorMessage, {super.userAppSettings}); + const SettingsUpdateFailure(this.exception, {super.userAppSettings}); - /// The error message describing the failure. - final String errorMessage; + /// The error exception describing the failure. + final HtHttpException exception; @override - List get props => [errorMessage, userAppSettings]; + List get props => [exception, userAppSettings]; } diff --git a/lib/settings/view/settings_page.dart b/lib/settings/view/settings_page.dart index 38ea659..9a1ce91 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -8,6 +8,7 @@ import 'package:ht_dashboard/shared/constants/app_spacing.dart'; import 'package:ht_dashboard/shared/widgets/widgets.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:ht_ui_kit/ht_ui_kit.dart'; // Import for toFriendlyMessage /// {@template settings_page} /// A page for user settings, allowing customization of theme and language. @@ -79,7 +80,7 @@ class _SettingsView extends StatelessWidget { ..showSnackBar( SnackBar( content: Text( - l10n.settingsSaveErrorMessage(state.errorMessage), + state.exception.toFriendlyMessage(context), ), ), ); @@ -104,7 +105,7 @@ class _SettingsView extends StatelessWidget { ); } else if (state is SettingsLoadFailure) { return FailureStateWidget( - message: l10n.failedToLoadSettingsMessage(state.errorMessage), + exception: state.exception, onRetry: () { context.read().add( SettingsLoaded( diff --git a/lib/shared/widgets/failure_state_widget.dart b/lib/shared/widgets/failure_state_widget.dart index 4bea388..7848c5a 100644 --- a/lib/shared/widgets/failure_state_widget.dart +++ b/lib/shared/widgets/failure_state_widget.dart @@ -1,22 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:ht_shared/ht_shared.dart'; +import 'package:ht_ui_kit/ht_ui_kit.dart'; /// A widget to display an error message and an optional retry button. class FailureStateWidget extends StatelessWidget { /// Creates a [FailureStateWidget]. /// - /// The [message] is the error message to display. + /// This widget accepts an [exception] of type [HtHttpException] + /// and uses the `toFriendlyMessage` extension from `ht_ui_kit` + /// to display a localized, user-friendly error message. /// /// The [onRetry] is an optional callback to be called /// when the retry button is pressed. const FailureStateWidget({ - required this.message, + required this.exception, super.key, this.onRetry, this.retryButtonText, }); - /// The error message to display. - final String message; + /// The error exception to display. + /// This exception will be converted to a friendly, localized message. + final HtHttpException exception; /// An optional callback to be called when the retry button is pressed. final VoidCallback? onRetry; @@ -26,12 +31,13 @@ class FailureStateWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final friendlyMessage = exception.toFriendlyMessage(context); return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - message, + friendlyMessage, style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, ), diff --git a/pubspec.lock b/pubspec.lock index d817253..e621731 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -277,7 +277,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "0b56d92624769ca3175d5ce2c7da27ab29514f8a" + resolved-ref: "6484a5641c3d633d286ea5848c5b7cf1e723ebc1" url: "https://github.com/headlines-toolkit/ht-http-client.git" source: git version: "0.0.0" @@ -308,6 +308,15 @@ packages: url: "https://github.com/headlines-toolkit/ht-shared.git" source: git version: "0.0.0" + ht_ui_kit: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: c33ec118041a9de02c96b177a88d646a08abd396 + url: "https://github.com/headlines-toolkit/ht-ui-kit.git" + source: git + version: "0.0.0" http: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f47610d..4699a58 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,9 @@ dependencies: ht_shared: git: url: https://github.com/headlines-toolkit/ht-shared.git + ht_ui_kit: + git: + url: https://github.com/headlines-toolkit/ht-ui-kit.git intl: ^0.20.2 logging: ^1.3.0 timeago: ^3.7.1