From bc82b3ff417eea16f38e7b223a20f5a3a23a27b4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 27 Jun 2025 18:21:30 +0100 Subject: [PATCH 1/7] chore(config): add app configuration bloc boilerplate - Created bloc, event, state files - Initial state implemented --- .../bloc/app_configuration_bloc.dart | 13 +++++++++++++ .../bloc/app_configuration_event.dart | 8 ++++++++ .../bloc/app_configuration_state.dart | 10 ++++++++++ 3 files changed, 31 insertions(+) create mode 100644 lib/app_configuration/bloc/app_configuration_bloc.dart create mode 100644 lib/app_configuration/bloc/app_configuration_event.dart create mode 100644 lib/app_configuration/bloc/app_configuration_state.dart diff --git a/lib/app_configuration/bloc/app_configuration_bloc.dart b/lib/app_configuration/bloc/app_configuration_bloc.dart new file mode 100644 index 00000000..8849ce50 --- /dev/null +++ b/lib/app_configuration/bloc/app_configuration_bloc.dart @@ -0,0 +1,13 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'app_configuration_event.dart'; +part 'app_configuration_state.dart'; + +class AppConfigurationBloc extends Bloc { + AppConfigurationBloc() : super(AppConfigurationInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + } +} diff --git a/lib/app_configuration/bloc/app_configuration_event.dart b/lib/app_configuration/bloc/app_configuration_event.dart new file mode 100644 index 00000000..191c202a --- /dev/null +++ b/lib/app_configuration/bloc/app_configuration_event.dart @@ -0,0 +1,8 @@ +part of 'app_configuration_bloc.dart'; + +sealed class AppConfigurationEvent extends Equatable { + const AppConfigurationEvent(); + + @override + List get props => []; +} diff --git a/lib/app_configuration/bloc/app_configuration_state.dart b/lib/app_configuration/bloc/app_configuration_state.dart new file mode 100644 index 00000000..9e0f9ecf --- /dev/null +++ b/lib/app_configuration/bloc/app_configuration_state.dart @@ -0,0 +1,10 @@ +part of 'app_configuration_bloc.dart'; + +sealed class AppConfigurationState extends Equatable { + const AppConfigurationState(); + + @override + List get props => []; +} + +final class AppConfigurationInitial extends AppConfigurationState {} From 8621abca033de8fb7028935332ca493554359280 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 27 Jun 2025 20:40:54 +0100 Subject: [PATCH 2/7] chore: update package version in pubspec.lock --- pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index af182faf..9e503803 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -296,7 +296,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "9cbe2dcf84860f5be655d091b9e15bc1d48fa3dc" + resolved-ref: "096ceed8957af0935e950818b657617510b9a9ba" url: "https://github.com/headlines-toolkit/ht-shared.git" source: git version: "0.0.0" From 77f3c10f50c7f2b4f36ecc7667bdc53d5e5c4b64 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 27 Jun 2025 20:41:08 +0100 Subject: [PATCH 3/7] feat(config): implement app config bloc - Load, update app config - Handle field changes - Manage config state --- .../bloc/app_configuration_bloc.dart | 93 ++++++++++++++++++- .../bloc/app_configuration_event.dart | 49 +++++++++- .../bloc/app_configuration_state.dart | 67 +++++++++++-- 3 files changed, 197 insertions(+), 12 deletions(-) diff --git a/lib/app_configuration/bloc/app_configuration_bloc.dart b/lib/app_configuration/bloc/app_configuration_bloc.dart index 8849ce50..ce56564d 100644 --- a/lib/app_configuration/bloc/app_configuration_bloc.dart +++ b/lib/app_configuration/bloc/app_configuration_bloc.dart @@ -1,13 +1,98 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_shared/ht_shared.dart'; // Use AppConfig from ht_shared part 'app_configuration_event.dart'; part 'app_configuration_state.dart'; class AppConfigurationBloc extends Bloc { - AppConfigurationBloc() : super(AppConfigurationInitial()) { - on((event, emit) { - // TODO: implement event handler - }); + AppConfigurationBloc({ + required HtDataRepository appConfigRepository, + }) : _appConfigRepository = appConfigRepository, + super(const AppConfigurationState()) { + on(_onAppConfigurationLoaded); + on(_onAppConfigurationUpdated); + on(_onAppConfigurationFieldChanged); + } + + final HtDataRepository _appConfigRepository; + + Future _onAppConfigurationLoaded( + AppConfigurationLoaded event, + Emitter emit, + ) async { + emit(state.copyWith(status: AppConfigurationStatus.loading)); + try { + final appConfig = await _appConfigRepository.read(id: 'app_config'); + emit( + state.copyWith( + status: AppConfigurationStatus.success, + appConfig: appConfig, + isDirty: false, + ), + ); + } on HtHttpException catch (e) { + emit( + state.copyWith( + status: AppConfigurationStatus.failure, + errorMessage: e.message, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: AppConfigurationStatus.failure, + errorMessage: 'An unknown error occurred: $e', + ), + ); + } + } + + Future _onAppConfigurationUpdated( + AppConfigurationUpdated event, + Emitter emit, + ) async { + emit(state.copyWith(status: AppConfigurationStatus.loading)); + try { + final updatedConfig = await _appConfigRepository.update( + id: event.appConfig.id, + item: event.appConfig, + ); + emit( + state.copyWith( + status: AppConfigurationStatus.success, + appConfig: updatedConfig, + isDirty: false, + ), + ); + } on HtHttpException catch (e) { + emit( + state.copyWith( + status: AppConfigurationStatus.failure, + errorMessage: e.message, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: AppConfigurationStatus.failure, + errorMessage: 'An unknown error occurred: $e', + ), + ); + } + } + + void _onAppConfigurationFieldChanged( + AppConfigurationFieldChanged event, + Emitter emit, + ) { + emit( + state.copyWith( + appConfig: event.appConfig, + isDirty: true, + clearErrorMessage: true, // Clear any previous error messages + ), + ); } } diff --git a/lib/app_configuration/bloc/app_configuration_event.dart b/lib/app_configuration/bloc/app_configuration_event.dart index 191c202a..fdf8b1a0 100644 --- a/lib/app_configuration/bloc/app_configuration_event.dart +++ b/lib/app_configuration/bloc/app_configuration_event.dart @@ -1,8 +1,53 @@ part of 'app_configuration_bloc.dart'; -sealed class AppConfigurationEvent extends Equatable { +/// Abstract base class for all events in the AppConfigurationBloc. +abstract class AppConfigurationEvent extends Equatable { + /// {@macro app_configuration_event} const AppConfigurationEvent(); @override - List get props => []; + List get props => []; +} + +/// {@template app_configuration_loaded} +/// Event to request the loading of the application configuration. +/// {@endtemplate} +class AppConfigurationLoaded extends AppConfigurationEvent { + /// {@macro app_configuration_loaded} + const AppConfigurationLoaded(); +} + +/// {@template app_configuration_updated} +/// Event to request the update of the application configuration. +/// +/// Carries the new [appConfig] object to be saved. +/// {@endtemplate} +class AppConfigurationUpdated extends AppConfigurationEvent { + /// {@macro app_configuration_updated} + const AppConfigurationUpdated(this.appConfig); + + /// The updated application configuration. + final AppConfig appConfig; + + @override + List get props => [appConfig]; +} + +/// {@template app_configuration_field_changed} +/// Event to notify that a field in the application configuration has changed. +/// +/// This event is used to update the local state and mark it as dirty, +/// without immediately triggering a backend save. +/// {@endtemplate} +class AppConfigurationFieldChanged extends AppConfigurationEvent { + /// {@macro app_configuration_field_changed} + const AppConfigurationFieldChanged({ + this.appConfig, + }); + + /// The partially or fully updated AppConfig object. + final AppConfig? appConfig; + + @override + List get props => [appConfig]; } diff --git a/lib/app_configuration/bloc/app_configuration_state.dart b/lib/app_configuration/bloc/app_configuration_state.dart index 9e0f9ecf..8caf8c19 100644 --- a/lib/app_configuration/bloc/app_configuration_state.dart +++ b/lib/app_configuration/bloc/app_configuration_state.dart @@ -1,10 +1,65 @@ part of 'app_configuration_bloc.dart'; -sealed class AppConfigurationState extends Equatable { - const AppConfigurationState(); - - @override - List get props => []; +/// Represents the status of the AppConfigurationBloc. +enum AppConfigurationStatus { + /// The configuration is in its initial state. + initial, + + /// The configuration is currently being loaded. + loading, + + /// The configuration has been successfully loaded or updated. + success, + + /// An error occurred during loading or updating the configuration. + failure, } -final class AppConfigurationInitial extends AppConfigurationState {} +/// {@template app_configuration_state} +/// State for the AppConfigurationBloc. +/// {@endtemplate} +class AppConfigurationState extends Equatable { + /// {@macro app_configuration_state} + const AppConfigurationState({ + this.status = AppConfigurationStatus.initial, + this.appConfig, + this.errorMessage, + this.isDirty = false, + }); + + /// The current status of the application configuration. + final AppConfigurationStatus status; + + /// The loaded or updated application configuration. + final AppConfig? appConfig; + + /// An error message if an operation failed. + final String? errorMessage; + + /// Indicates if there are unsaved changes to the configuration. + final bool isDirty; + + /// Creates a copy of the current state with updated values. + AppConfigurationState copyWith({ + AppConfigurationStatus? status, + AppConfig? appConfig, + String? errorMessage, + bool? isDirty, + bool clearErrorMessage = false, + }) { + return AppConfigurationState( + status: status ?? this.status, + appConfig: appConfig ?? this.appConfig, + errorMessage: clearErrorMessage ? null : errorMessage ?? this.errorMessage, + isDirty: isDirty ?? this.isDirty, + ); + } + + @override + List get props => [ + status, + appConfig, + errorMessage, + isDirty, + ]; +} From 13764480b6597290a84f8a45f04e640de6af5b03 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 27 Jun 2025 20:41:16 +0100 Subject: [PATCH 4/7] feat: Implement app configuration page - Allows editing remote app configuration - Uses tabs for different config sections - Implemented confirmation dialog before saving - Shows success/error snackbars --- .../view/app_configuration_page.dart | 884 +++++++++++++++++- 1 file changed, 879 insertions(+), 5 deletions(-) diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index 09833605..181a8ddf 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -1,17 +1,891 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:ht_dashboard/app_configuration/bloc/app_configuration_bloc.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'; // For AppConfig and its nested models /// {@template app_configuration_page} -/// A placeholder page for App Configuration. +/// A page for managing the application's remote configuration. +/// +/// This page allows administrators to view and modify various application +/// settings that affect the live mobile app. Due to the sensitive nature +/// of these settings, changes require explicit confirmation. /// {@endtemplate} -class AppConfigurationPage extends StatelessWidget { +class AppConfigurationPage extends StatefulWidget { /// {@macro app_configuration_page} const AppConfigurationPage({super.key}); + @override + State createState() => _AppConfigurationPageState(); +} + +class _AppConfigurationPageState extends State + with SingleTickerProviderStateMixin { + late final TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 5, vsync: this); // 5 tabs for AppConfig properties + context.read().add(const AppConfigurationLoaded()); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return const Scaffold( - body: Center( - child: Text('App Configuration Page'), + return Scaffold( + appBar: AppBar( + title: Text( + 'App Configuration', + style: Theme.of(context).textTheme.headlineSmall, + ), + bottom: TabBar( + controller: _tabController, + isScrollable: true, + tabs: const [ + Tab(text: 'User Preferences'), + Tab(text: 'Ad Config'), + Tab(text: 'Account Actions'), + Tab(text: 'Kill Switch'), + Tab(text: 'Force Update'), + ], + ), + ), + body: BlocConsumer( + listener: (context, state) { + if (state.status == AppConfigurationStatus.success && state.isDirty == false) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text( + 'App configuration saved successfully!', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + ); + } else if (state.status == AppConfigurationStatus.failure) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text( + 'Error: ${state.errorMessage ?? "Unknown error"}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onError, + ), + ), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + }, + builder: (context, state) { + if (state.status == AppConfigurationStatus.loading || + state.status == AppConfigurationStatus.initial) { + return const LoadingStateWidget( + icon: Icons.settings_applications_outlined, + headline: 'Loading Configuration', + subheadline: 'Please wait while settings are loaded...', + ); + } else if (state.status == AppConfigurationStatus.failure) { + return FailureStateWidget( + message: state.errorMessage ?? 'Failed to load configuration.', + onRetry: () { + context.read().add(const AppConfigurationLoaded()); + }, + ); + } else if (state.status == AppConfigurationStatus.success && + state.appConfig != null) { + final appConfig = state.appConfig!; + return TabBarView( + controller: _tabController, + children: [ + _buildUserPreferenceLimitsTab(context, appConfig), + _buildAdConfigTab(context, appConfig), + _buildAccountActionConfigTab(context, appConfig), + _buildKillSwitchTab(context, appConfig), + _buildForceUpdateTab(context, appConfig), + ], + ); + } + return const InitialStateWidget( + icon: Icons.settings_applications_outlined, + headline: 'App Configuration', + subheadline: 'Load application settings from the backend.', + ); // Fallback + }, + ), + bottomNavigationBar: _buildBottomAppBar(context), + ); + } + + Widget _buildBottomAppBar(BuildContext context) { + final isDirty = context.select((AppConfigurationBloc bloc) => bloc.state.isDirty); + final appConfig = context.select((AppConfigurationBloc bloc) => bloc.state.appConfig); + + return BottomAppBar( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.md), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: isDirty + ? () { + // Discard changes: reload original config + context.read().add(const AppConfigurationLoaded()); + } + : null, + child: const Text('Discard Changes'), + ), + SizedBox(width: AppSpacing.md), + ElevatedButton( + onPressed: isDirty + ? () async { + final confirmed = await _showConfirmationDialog(context); + if (confirmed && appConfig != null) { + context.read().add( + AppConfigurationUpdated(appConfig), + ); + } + } + : null, + child: const Text('Save Changes'), + ), + ], + ), + ), + ); + } + + Future _showConfirmationDialog(BuildContext context) async { + return await showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text( + 'Confirm Configuration Update', + style: Theme.of(dialogContext).textTheme.titleLarge, + ), + content: Text( + 'Are you sure you want to apply these changes to the live application configuration? This is a critical operation.', + style: Theme.of(dialogContext).textTheme.bodyMedium, + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(false), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () => Navigator.of(dialogContext).pop(true), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(dialogContext).colorScheme.error, + foregroundColor: Theme.of(dialogContext).colorScheme.onError, + ), + child: const Text('Confirm Save'), + ), + ], + ); + }, + ) ?? + false; + } + + Widget _buildUserPreferenceLimitsTab(BuildContext context, AppConfig appConfig) { + final userPreferenceLimits = appConfig.userPreferenceLimits; + return SingleChildScrollView( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'User Preference Limits', + style: Theme.of(context).textTheme.headlineSmall, + ), + SizedBox(height: AppSpacing.md), + Text( + 'These settings define the maximum number of items a user can follow or save, tiered by user role. Changes here directly impact user capabilities.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + SizedBox(height: AppSpacing.lg), + _buildIntField( + context, + label: 'Guest Followed Items Limit', + description: 'Max countries, sources, or categories a Guest user can follow (each).', + value: userPreferenceLimits.guestFollowedItemsLimit, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + userPreferenceLimits: userPreferenceLimits.copyWith( + guestFollowedItemsLimit: value, + ), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Guest Saved Headlines Limit', + description: 'Max headlines a Guest user can save.', + value: userPreferenceLimits.guestSavedHeadlinesLimit, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + userPreferenceLimits: userPreferenceLimits.copyWith( + guestSavedHeadlinesLimit: value, + ), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Authenticated Followed Items Limit', + description: 'Max countries, sources, or categories an Authenticated user can follow (each).', + value: userPreferenceLimits.authenticatedFollowedItemsLimit, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + userPreferenceLimits: userPreferenceLimits.copyWith( + authenticatedFollowedItemsLimit: value, + ), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Authenticated Saved Headlines Limit', + description: 'Max headlines an Authenticated user can save.', + value: userPreferenceLimits.authenticatedSavedHeadlinesLimit, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + userPreferenceLimits: userPreferenceLimits.copyWith( + authenticatedSavedHeadlinesLimit: value, + ), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Premium Followed Items Limit', + description: 'Max countries, sources, or categories a Premium user can follow (each).', + value: userPreferenceLimits.premiumFollowedItemsLimit, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + userPreferenceLimits: userPreferenceLimits.copyWith( + premiumFollowedItemsLimit: value, + ), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Premium Saved Headlines Limit', + description: 'Max headlines a Premium user can save.', + value: userPreferenceLimits.premiumSavedHeadlinesLimit, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + userPreferenceLimits: userPreferenceLimits.copyWith( + premiumSavedHeadlinesLimit: value, + ), + ), + ), + ); + }, + ), + ], + ), + ); + } + + Widget _buildAdConfigTab(BuildContext context, AppConfig appConfig) { + final adConfig = appConfig.adConfig; + return SingleChildScrollView( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ad Configuration', + style: Theme.of(context).textTheme.headlineSmall, + ), + SizedBox(height: AppSpacing.md), + Text( + 'These settings control how ads are injected and displayed in the application, tiered by user role. AdFrequency determines how often an ad can be injected, and AdPlacementInterval sets a minimum number of primary items before the first ad.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + SizedBox(height: AppSpacing.lg), + _buildIntField( + context, + label: 'Guest Ad Frequency', + description: 'How often an ad can be injected for Guest users (e.g., 5 means after every 5 primary items).', + value: adConfig.guestAdFrequency, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith(guestAdFrequency: value), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Guest Ad Placement Interval', + description: 'Minimum primary items before the first ad for Guest users.', + value: adConfig.guestAdPlacementInterval, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith(guestAdPlacementInterval: value), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Authenticated Ad Frequency', + description: 'How often an ad can be injected for Authenticated users.', + value: adConfig.authenticatedAdFrequency, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith(authenticatedAdFrequency: value), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Authenticated Ad Placement Interval', + description: 'Minimum primary items before the first ad for Authenticated users.', + value: adConfig.authenticatedAdPlacementInterval, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith(authenticatedAdPlacementInterval: value), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Premium Ad Frequency', + description: 'How often an ad can be injected for Premium users (0 for no ads).', + value: adConfig.premiumAdFrequency, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith(premiumAdFrequency: value), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Premium Ad Placement Interval', + description: 'Minimum primary items before the first ad for Premium users.', + value: adConfig.premiumAdPlacementInterval, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith(premiumAdPlacementInterval: value), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Guest Articles Before Interstitial Ads', + description: 'Number of articles a Guest user reads before an interstitial ad is shown.', + value: adConfig.guestArticlesToReadBeforeShowingInterstitialAds, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith( + guestArticlesToReadBeforeShowingInterstitialAds: value, + ), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Standard User Articles Before Interstitial Ads', + description: 'Number of articles a Standard user reads before an interstitial ad is shown.', + value: adConfig.standardUserArticlesToReadBeforeShowingInterstitialAds, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith( + standardUserArticlesToReadBeforeShowingInterstitialAds: value, + ), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Premium User Articles Before Interstitial Ads', + description: 'Number of articles a Premium user reads before an interstitial ad is shown.', + value: adConfig.premiumUserArticlesToReadBeforeShowingInterstitialAds, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith( + premiumUserArticlesToReadBeforeShowingInterstitialAds: value, + ), + ), + ), + ); + }, + ), + ], + ), + ); + } + + Widget _buildAccountActionConfigTab(BuildContext context, AppConfig appConfig) { + final accountActionConfig = appConfig.accountActionConfig; + return SingleChildScrollView( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Account Action Configuration', + style: Theme.of(context).textTheme.headlineSmall, + ), + SizedBox(height: AppSpacing.md), + Text( + 'These settings control the display frequency of in-feed account actions (e.g., link account, upgrade prompts), tiered by user role.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + SizedBox(height: AppSpacing.lg), + _buildIntField( + context, + label: 'Guest Days Between Account Actions', + description: 'Minimum days between showing account actions to Guest users.', + value: accountActionConfig.guestDaysBetweenAccountActions, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + accountActionConfig: accountActionConfig.copyWith( + guestDaysBetweenAccountActions: value, + ), + ), + ), + ); + }, + ), + _buildIntField( + context, + label: 'Standard User Days Between Account Actions', + description: 'Minimum days between showing account actions to Standard users.', + value: accountActionConfig.standardUserDaysBetweenAccountActions, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + accountActionConfig: accountActionConfig.copyWith( + standardUserDaysBetweenAccountActions: value, + ), + ), + ), + ); + }, + ), + ], + ), + ); + } + + Widget _buildKillSwitchTab(BuildContext context, AppConfig appConfig) { + return SingleChildScrollView( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Kill Switch & App Status', + style: Theme.of(context).textTheme.headlineSmall, + ), + SizedBox(height: AppSpacing.md), + Text( + 'WARNING: These settings can disable the entire mobile application. Use with extreme caution.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: AppSpacing.lg), + _buildSwitchField( + context, + label: 'Kill Switch Enabled', + description: 'If enabled, the app\'s operational status will be enforced.', + value: appConfig.killSwitchEnabled, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(killSwitchEnabled: value), + ), + ); + }, + ), + _buildDropdownField( + context, + label: 'App Operational Status', + description: 'The current operational status of the app (e.g., active, maintenance, disabled).', + value: appConfig.appOperationalStatus, + items: RemoteAppStatus.values, + itemLabelBuilder: (status) => status.name, + onChanged: (value) { + if (value != null) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(appOperationalStatus: value), + ), + ); + } + }, + ), + _buildTextField( + context, + label: 'Maintenance Message', + description: 'Message displayed when the app is in maintenance mode.', + value: appConfig.maintenanceMessage, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(maintenanceMessage: value), + ), + ); + }, + ), + _buildTextField( + context, + label: 'Disabled Message', + description: 'Message displayed when the app is permanently disabled.', + value: appConfig.disabledMessage, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(disabledMessage: value), + ), + ); + }, + ), + ], + ), + ); + } + + Widget _buildForceUpdateTab(BuildContext context, AppConfig appConfig) { + return SingleChildScrollView( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Force Update Configuration', + style: Theme.of(context).textTheme.headlineSmall, + ), + SizedBox(height: AppSpacing.md), + Text( + 'These settings control app version enforcement. Users on versions below the minimum allowed will be forced to update.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + SizedBox(height: AppSpacing.lg), + _buildTextField( + context, + label: 'Minimum Allowed App Version', + description: 'The lowest app version allowed to run (e.g., "1.2.0").', + value: appConfig.minAllowedAppVersion, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(minAllowedAppVersion: value), + ), + ); + }, + ), + _buildTextField( + context, + label: 'Latest App Version', + description: 'The latest available app version (e.g., "1.5.0").', + value: appConfig.latestAppVersion, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(latestAppVersion: value), + ), + ); + }, + ), + _buildTextField( + context, + label: 'Update Required Message', + description: 'Message displayed when a force update is required.', + value: appConfig.updateRequiredMessage, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(updateRequiredMessage: value), + ), + ); + }, + ), + _buildTextField( + context, + label: 'Update Optional Message', + description: 'Message displayed for an optional update.', + value: appConfig.updateOptionalMessage, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(updateOptionalMessage: value), + ), + ); + }, + ), + _buildTextField( + context, + label: 'iOS Store URL', + description: 'URL to the app on the Apple App Store.', + value: appConfig.iosStoreUrl, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(iosStoreUrl: value), + ), + ); + }, + ), + _buildTextField( + context, + label: 'Android Store URL', + description: 'URL to the app on the Google Play Store.', + value: appConfig.androidStoreUrl, + onChanged: (value) { + context.read().add( + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(androidStoreUrl: value), + ), + ); + }, + ), + ], + ), + ); + } + + Widget _buildIntField( + BuildContext context, { + required String label, + required String description, + required int value, + required ValueChanged onChanged, + }) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: Theme.of(context).textTheme.titleMedium, + ), + SizedBox(height: AppSpacing.xs), + Text( + description, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + SizedBox(height: AppSpacing.xs), + TextFormField( + initialValue: value.toString(), + keyboardType: TextInputType.number, + decoration: const InputDecoration( + border: OutlineInputBorder(), + isDense: true, + ), + onChanged: (text) { + final parsedValue = int.tryParse(text); + if (parsedValue != null) { + onChanged(parsedValue); + } + }, + ), + ], + ), + ); + } + + Widget _buildTextField( + BuildContext context, { + required String label, + required String description, + required String? value, + required ValueChanged onChanged, + }) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: Theme.of(context).textTheme.titleMedium, + ), + SizedBox(height: AppSpacing.xs), + Text( + description, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + SizedBox(height: AppSpacing.xs), + TextFormField( + initialValue: value, + decoration: const InputDecoration( + border: OutlineInputBorder(), + isDense: true, + ), + onChanged: onChanged, + ), + ], + ), + ); + } + + Widget _buildSwitchField( + BuildContext context, { + required String label, + required String description, + required bool value, + required ValueChanged onChanged, + }) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: Theme.of(context).textTheme.titleMedium, + ), + SizedBox(height: AppSpacing.xs), + Text( + description, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + SwitchListTile( + title: Text(label), + value: value, + onChanged: onChanged, + contentPadding: EdgeInsets.zero, + ), + ], + ), + ); + } + + Widget _buildDropdownField( + BuildContext context, { + required String label, + required String description, + required T value, + required List items, + required String Function(T) itemLabelBuilder, + required ValueChanged onChanged, + }) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: Theme.of(context).textTheme.titleMedium, + ), + SizedBox(height: AppSpacing.xs), + Text( + description, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + SizedBox(height: AppSpacing.xs), + DropdownButtonFormField( + value: value, + decoration: const InputDecoration( + border: OutlineInputBorder(), + isDense: true, + ), + items: items.map((item) { + return DropdownMenuItem( + value: item, + child: Text(itemLabelBuilder(item)), + ); + }).toList(), + onChanged: onChanged, + ), + ], ), ); } From 5e76d6a4dabd2b205266540349ba2fe81fd05942 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 27 Jun 2025 20:41:29 +0100 Subject: [PATCH 5/7] refactor(auth): improve role-based auth - Clarify role handling logic - Improve readability --- lib/app/bloc/app_bloc.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index ad5d2b74..e4f38a3b 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -56,7 +56,8 @@ class AppBloc extends Bloc { status = AppStatus.authenticated; // ignore: no_default_cases default: // Fallback for any other roles not explicitly handled - status = AppStatus.unauthenticated; // Treat other roles as unauthenticated for dashboard + status = AppStatus + .unauthenticated; // Treat other roles as unauthenticated for dashboard } // Emit user and status update From 5a4c677806491744474cd74f4abb229663fb91ac Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 27 Jun 2025 20:41:53 +0100 Subject: [PATCH 6/7] style: format --- lib/app/bloc/app_state.dart | 8 +- lib/app/view/app.dart | 8 +- .../bloc/app_configuration_bloc.dart | 7 +- .../bloc/app_configuration_state.dart | 14 +- .../view/app_configuration_page.dart | 459 ++++++++++-------- lib/router/router.dart | 3 +- 6 files changed, 277 insertions(+), 222 deletions(-) diff --git a/lib/app/bloc/app_state.dart b/lib/app/bloc/app_state.dart index 04d11c18..70f3c35b 100644 --- a/lib/app/bloc/app_state.dart +++ b/lib/app/bloc/app_state.dart @@ -48,8 +48,8 @@ class AppState extends Equatable { @override List get props => [ - status, - user, - environment, - ]; + status, + user, + environment, + ]; } diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index f8d3b81d..72d9d423 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -24,7 +24,7 @@ class App extends StatelessWidget { required HtDataRepository htSourcesRepository, required HtDataRepository htUserAppSettingsRepository, required HtDataRepository - htUserContentPreferencesRepository, + htUserContentPreferencesRepository, required HtDataRepository htAppConfigRepository, required HtKVStorageService kvStorageService, required AppEnvironment environment, @@ -47,7 +47,7 @@ class App extends StatelessWidget { final HtDataRepository _htSourcesRepository; final HtDataRepository _htUserAppSettingsRepository; final HtDataRepository - _htUserContentPreferencesRepository; + _htUserContentPreferencesRepository; final HtDataRepository _htAppConfigRepository; final HtKVStorageService _kvStorageService; final AppEnvironment _environment; @@ -71,8 +71,8 @@ class App extends StatelessWidget { BlocProvider( create: (context) => AppBloc( authenticationRepository: context.read(), - userAppSettingsRepository: - context.read>(), + userAppSettingsRepository: context + .read>(), appConfigRepository: context.read>(), environment: _environment, ), diff --git a/lib/app_configuration/bloc/app_configuration_bloc.dart b/lib/app_configuration/bloc/app_configuration_bloc.dart index ce56564d..61d4c9ec 100644 --- a/lib/app_configuration/bloc/app_configuration_bloc.dart +++ b/lib/app_configuration/bloc/app_configuration_bloc.dart @@ -6,11 +6,12 @@ import 'package:ht_shared/ht_shared.dart'; // Use AppConfig from ht_shared part 'app_configuration_event.dart'; part 'app_configuration_state.dart'; -class AppConfigurationBloc extends Bloc { +class AppConfigurationBloc + extends Bloc { AppConfigurationBloc({ required HtDataRepository appConfigRepository, - }) : _appConfigRepository = appConfigRepository, - super(const AppConfigurationState()) { + }) : _appConfigRepository = appConfigRepository, + super(const AppConfigurationState()) { on(_onAppConfigurationLoaded); on(_onAppConfigurationUpdated); on(_onAppConfigurationFieldChanged); diff --git a/lib/app_configuration/bloc/app_configuration_state.dart b/lib/app_configuration/bloc/app_configuration_state.dart index 8caf8c19..f91ce76d 100644 --- a/lib/app_configuration/bloc/app_configuration_state.dart +++ b/lib/app_configuration/bloc/app_configuration_state.dart @@ -50,16 +50,18 @@ class AppConfigurationState extends Equatable { return AppConfigurationState( status: status ?? this.status, appConfig: appConfig ?? this.appConfig, - errorMessage: clearErrorMessage ? null : errorMessage ?? this.errorMessage, + errorMessage: clearErrorMessage + ? null + : errorMessage ?? this.errorMessage, isDirty: isDirty ?? this.isDirty, ); } @override List get props => [ - status, - appConfig, - errorMessage, - isDirty, - ]; + status, + appConfig, + errorMessage, + isDirty, + ]; } diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index 181a8ddf..f4423b29 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -27,7 +27,10 @@ class _AppConfigurationPageState extends State @override void initState() { super.initState(); - _tabController = TabController(length: 5, vsync: this); // 5 tabs for AppConfig properties + _tabController = TabController( + length: 5, + vsync: this, + ); // 5 tabs for AppConfig properties context.read().add(const AppConfigurationLoaded()); } @@ -59,7 +62,8 @@ class _AppConfigurationPageState extends State ), body: BlocConsumer( listener: (context, state) { - if (state.status == AppConfigurationStatus.success && state.isDirty == false) { + if (state.status == AppConfigurationStatus.success && + state.isDirty == false) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( @@ -67,8 +71,8 @@ class _AppConfigurationPageState extends State content: Text( 'App configuration saved successfully!', 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, ), @@ -81,8 +85,8 @@ class _AppConfigurationPageState extends State content: Text( 'Error: ${state.errorMessage ?? "Unknown error"}', 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, ), @@ -101,7 +105,9 @@ class _AppConfigurationPageState extends State return FailureStateWidget( message: state.errorMessage ?? 'Failed to load configuration.', onRetry: () { - context.read().add(const AppConfigurationLoaded()); + context.read().add( + const AppConfigurationLoaded(), + ); }, ); } else if (state.status == AppConfigurationStatus.success && @@ -130,8 +136,12 @@ class _AppConfigurationPageState extends State } Widget _buildBottomAppBar(BuildContext context) { - final isDirty = context.select((AppConfigurationBloc bloc) => bloc.state.isDirty); - final appConfig = context.select((AppConfigurationBloc bloc) => bloc.state.appConfig); + final isDirty = context.select( + (AppConfigurationBloc bloc) => bloc.state.isDirty, + ); + final appConfig = context.select( + (AppConfigurationBloc bloc) => bloc.state.appConfig, + ); return BottomAppBar( child: Padding( @@ -143,7 +153,9 @@ class _AppConfigurationPageState extends State onPressed: isDirty ? () { // Discard changes: reload original config - context.read().add(const AppConfigurationLoaded()); + context.read().add( + const AppConfigurationLoaded(), + ); } : null, child: const Text('Discard Changes'), @@ -155,8 +167,8 @@ class _AppConfigurationPageState extends State final confirmed = await _showConfirmationDialog(context); if (confirmed && appConfig != null) { context.read().add( - AppConfigurationUpdated(appConfig), - ); + AppConfigurationUpdated(appConfig), + ); } } : null, @@ -190,7 +202,9 @@ class _AppConfigurationPageState extends State onPressed: () => Navigator.of(dialogContext).pop(true), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(dialogContext).colorScheme.error, - foregroundColor: Theme.of(dialogContext).colorScheme.onError, + foregroundColor: Theme.of( + dialogContext, + ).colorScheme.onError, ), child: const Text('Confirm Save'), ), @@ -201,7 +215,10 @@ class _AppConfigurationPageState extends State false; } - Widget _buildUserPreferenceLimitsTab(BuildContext context, AppConfig appConfig) { + Widget _buildUserPreferenceLimitsTab( + BuildContext context, + AppConfig appConfig, + ) { final userPreferenceLimits = appConfig.userPreferenceLimits; return SingleChildScrollView( padding: const EdgeInsets.all(AppSpacing.lg), @@ -216,25 +233,26 @@ class _AppConfigurationPageState extends State Text( 'These settings define the maximum number of items a user can follow or save, tiered by user role. Changes here directly impact user capabilities.', 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), + ), ), SizedBox(height: AppSpacing.lg), _buildIntField( context, label: 'Guest Followed Items Limit', - description: 'Max countries, sources, or categories a Guest user can follow (each).', + description: + 'Max countries, sources, or categories a Guest user can follow (each).', value: userPreferenceLimits.guestFollowedItemsLimit, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - userPreferenceLimits: userPreferenceLimits.copyWith( - guestFollowedItemsLimit: value, - ), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + userPreferenceLimits: userPreferenceLimits.copyWith( + guestFollowedItemsLimit: value, ), - ); + ), + ), + ); }, ), _buildIntField( @@ -244,31 +262,32 @@ class _AppConfigurationPageState extends State value: userPreferenceLimits.guestSavedHeadlinesLimit, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - userPreferenceLimits: userPreferenceLimits.copyWith( - guestSavedHeadlinesLimit: value, - ), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + userPreferenceLimits: userPreferenceLimits.copyWith( + guestSavedHeadlinesLimit: value, ), - ); + ), + ), + ); }, ), _buildIntField( context, label: 'Authenticated Followed Items Limit', - description: 'Max countries, sources, or categories an Authenticated user can follow (each).', + description: + 'Max countries, sources, or categories an Authenticated user can follow (each).', value: userPreferenceLimits.authenticatedFollowedItemsLimit, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - userPreferenceLimits: userPreferenceLimits.copyWith( - authenticatedFollowedItemsLimit: value, - ), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + userPreferenceLimits: userPreferenceLimits.copyWith( + authenticatedFollowedItemsLimit: value, ), - ); + ), + ), + ); }, ), _buildIntField( @@ -278,31 +297,32 @@ class _AppConfigurationPageState extends State value: userPreferenceLimits.authenticatedSavedHeadlinesLimit, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - userPreferenceLimits: userPreferenceLimits.copyWith( - authenticatedSavedHeadlinesLimit: value, - ), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + userPreferenceLimits: userPreferenceLimits.copyWith( + authenticatedSavedHeadlinesLimit: value, ), - ); + ), + ), + ); }, ), _buildIntField( context, label: 'Premium Followed Items Limit', - description: 'Max countries, sources, or categories a Premium user can follow (each).', + description: + 'Max countries, sources, or categories a Premium user can follow (each).', value: userPreferenceLimits.premiumFollowedItemsLimit, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - userPreferenceLimits: userPreferenceLimits.copyWith( - premiumFollowedItemsLimit: value, - ), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + userPreferenceLimits: userPreferenceLimits.copyWith( + premiumFollowedItemsLimit: value, ), - ); + ), + ), + ); }, ), _buildIntField( @@ -312,14 +332,14 @@ class _AppConfigurationPageState extends State value: userPreferenceLimits.premiumSavedHeadlinesLimit, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - userPreferenceLimits: userPreferenceLimits.copyWith( - premiumSavedHeadlinesLimit: value, - ), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + userPreferenceLimits: userPreferenceLimits.copyWith( + premiumSavedHeadlinesLimit: value, ), - ); + ), + ), + ); }, ), ], @@ -342,149 +362,170 @@ class _AppConfigurationPageState extends State Text( 'These settings control how ads are injected and displayed in the application, tiered by user role. AdFrequency determines how often an ad can be injected, and AdPlacementInterval sets a minimum number of primary items before the first ad.', 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), + ), ), SizedBox(height: AppSpacing.lg), _buildIntField( context, label: 'Guest Ad Frequency', - description: 'How often an ad can be injected for Guest users (e.g., 5 means after every 5 primary items).', + description: + 'How often an ad can be injected for Guest users (e.g., 5 means after every 5 primary items).', value: adConfig.guestAdFrequency, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - adConfig: adConfig.copyWith(guestAdFrequency: value), - ), - ), - ); + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith(guestAdFrequency: value), + ), + ), + ); }, ), _buildIntField( context, label: 'Guest Ad Placement Interval', - description: 'Minimum primary items before the first ad for Guest users.', + description: + 'Minimum primary items before the first ad for Guest users.', value: adConfig.guestAdPlacementInterval, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - adConfig: adConfig.copyWith(guestAdPlacementInterval: value), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith( + guestAdPlacementInterval: value, ), - ); + ), + ), + ); }, ), _buildIntField( context, label: 'Authenticated Ad Frequency', - description: 'How often an ad can be injected for Authenticated users.', + description: + 'How often an ad can be injected for Authenticated users.', value: adConfig.authenticatedAdFrequency, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - adConfig: adConfig.copyWith(authenticatedAdFrequency: value), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith( + authenticatedAdFrequency: value, ), - ); + ), + ), + ); }, ), _buildIntField( context, label: 'Authenticated Ad Placement Interval', - description: 'Minimum primary items before the first ad for Authenticated users.', + description: + 'Minimum primary items before the first ad for Authenticated users.', value: adConfig.authenticatedAdPlacementInterval, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - adConfig: adConfig.copyWith(authenticatedAdPlacementInterval: value), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith( + authenticatedAdPlacementInterval: value, ), - ); + ), + ), + ); }, ), _buildIntField( context, label: 'Premium Ad Frequency', - description: 'How often an ad can be injected for Premium users (0 for no ads).', + description: + 'How often an ad can be injected for Premium users (0 for no ads).', value: adConfig.premiumAdFrequency, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - adConfig: adConfig.copyWith(premiumAdFrequency: value), - ), - ), - ); + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith(premiumAdFrequency: value), + ), + ), + ); }, ), _buildIntField( context, label: 'Premium Ad Placement Interval', - description: 'Minimum primary items before the first ad for Premium users.', + description: + 'Minimum primary items before the first ad for Premium users.', value: adConfig.premiumAdPlacementInterval, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - adConfig: adConfig.copyWith(premiumAdPlacementInterval: value), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith( + premiumAdPlacementInterval: value, ), - ); + ), + ), + ); }, ), _buildIntField( context, label: 'Guest Articles Before Interstitial Ads', - description: 'Number of articles a Guest user reads before an interstitial ad is shown.', + description: + 'Number of articles a Guest user reads before an interstitial ad is shown.', value: adConfig.guestArticlesToReadBeforeShowingInterstitialAds, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - adConfig: adConfig.copyWith( - guestArticlesToReadBeforeShowingInterstitialAds: value, - ), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith( + guestArticlesToReadBeforeShowingInterstitialAds: value, ), - ); + ), + ), + ); }, ), _buildIntField( context, label: 'Standard User Articles Before Interstitial Ads', - description: 'Number of articles a Standard user reads before an interstitial ad is shown.', - value: adConfig.standardUserArticlesToReadBeforeShowingInterstitialAds, + description: + 'Number of articles a Standard user reads before an interstitial ad is shown.', + value: + adConfig.standardUserArticlesToReadBeforeShowingInterstitialAds, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - adConfig: adConfig.copyWith( - standardUserArticlesToReadBeforeShowingInterstitialAds: value, - ), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith( + standardUserArticlesToReadBeforeShowingInterstitialAds: + value, ), - ); + ), + ), + ); }, ), _buildIntField( context, label: 'Premium User Articles Before Interstitial Ads', - description: 'Number of articles a Premium user reads before an interstitial ad is shown.', - value: adConfig.premiumUserArticlesToReadBeforeShowingInterstitialAds, + description: + 'Number of articles a Premium user reads before an interstitial ad is shown.', + value: + adConfig.premiumUserArticlesToReadBeforeShowingInterstitialAds, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - adConfig: adConfig.copyWith( - premiumUserArticlesToReadBeforeShowingInterstitialAds: value, - ), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + adConfig: adConfig.copyWith( + premiumUserArticlesToReadBeforeShowingInterstitialAds: + value, ), - ); + ), + ), + ); }, ), ], @@ -492,7 +533,10 @@ class _AppConfigurationPageState extends State ); } - Widget _buildAccountActionConfigTab(BuildContext context, AppConfig appConfig) { + Widget _buildAccountActionConfigTab( + BuildContext context, + AppConfig appConfig, + ) { final accountActionConfig = appConfig.accountActionConfig; return SingleChildScrollView( padding: const EdgeInsets.all(AppSpacing.lg), @@ -507,42 +551,44 @@ class _AppConfigurationPageState extends State Text( 'These settings control the display frequency of in-feed account actions (e.g., link account, upgrade prompts), tiered by user role.', 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), + ), ), SizedBox(height: AppSpacing.lg), _buildIntField( context, label: 'Guest Days Between Account Actions', - description: 'Minimum days between showing account actions to Guest users.', + description: + 'Minimum days between showing account actions to Guest users.', value: accountActionConfig.guestDaysBetweenAccountActions, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - accountActionConfig: accountActionConfig.copyWith( - guestDaysBetweenAccountActions: value, - ), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + accountActionConfig: accountActionConfig.copyWith( + guestDaysBetweenAccountActions: value, ), - ); + ), + ), + ); }, ), _buildIntField( context, label: 'Standard User Days Between Account Actions', - description: 'Minimum days between showing account actions to Standard users.', + description: + 'Minimum days between showing account actions to Standard users.', value: accountActionConfig.standardUserDaysBetweenAccountActions, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith( - accountActionConfig: accountActionConfig.copyWith( - standardUserDaysBetweenAccountActions: value, - ), - ), + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith( + accountActionConfig: accountActionConfig.copyWith( + standardUserDaysBetweenAccountActions: value, ), - ); + ), + ), + ); }, ), ], @@ -564,65 +610,69 @@ class _AppConfigurationPageState extends State Text( 'WARNING: These settings can disable the entire mobile application. Use with extreme caution.', 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, + ), ), SizedBox(height: AppSpacing.lg), _buildSwitchField( context, label: 'Kill Switch Enabled', - description: 'If enabled, the app\'s operational status will be enforced.', + description: + 'If enabled, the app\'s operational status will be enforced.', value: appConfig.killSwitchEnabled, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(killSwitchEnabled: value), - ), - ); + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(killSwitchEnabled: value), + ), + ); }, ), _buildDropdownField( context, label: 'App Operational Status', - description: 'The current operational status of the app (e.g., active, maintenance, disabled).', + description: + 'The current operational status of the app (e.g., active, maintenance, disabled).', value: appConfig.appOperationalStatus, items: RemoteAppStatus.values, itemLabelBuilder: (status) => status.name, onChanged: (value) { if (value != null) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(appOperationalStatus: value), - ), - ); + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(appOperationalStatus: value), + ), + ); } }, ), _buildTextField( context, label: 'Maintenance Message', - description: 'Message displayed when the app is in maintenance mode.', + description: + 'Message displayed when the app is in maintenance mode.', value: appConfig.maintenanceMessage, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(maintenanceMessage: value), - ), - ); + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(maintenanceMessage: value), + ), + ); }, ), _buildTextField( context, label: 'Disabled Message', - description: 'Message displayed when the app is permanently disabled.', + description: + 'Message displayed when the app is permanently disabled.', value: appConfig.disabledMessage, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(disabledMessage: value), - ), - ); + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(disabledMessage: value), + ), + ); }, ), ], @@ -644,21 +694,22 @@ class _AppConfigurationPageState extends State Text( 'These settings control app version enforcement. Users on versions below the minimum allowed will be forced to update.', 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), + ), ), SizedBox(height: AppSpacing.lg), _buildTextField( context, label: 'Minimum Allowed App Version', - description: 'The lowest app version allowed to run (e.g., "1.2.0").', + description: + 'The lowest app version allowed to run (e.g., "1.2.0").', value: appConfig.minAllowedAppVersion, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(minAllowedAppVersion: value), - ), - ); + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(minAllowedAppVersion: value), + ), + ); }, ), _buildTextField( @@ -668,10 +719,10 @@ class _AppConfigurationPageState extends State value: appConfig.latestAppVersion, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(latestAppVersion: value), - ), - ); + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(latestAppVersion: value), + ), + ); }, ), _buildTextField( @@ -681,10 +732,10 @@ class _AppConfigurationPageState extends State value: appConfig.updateRequiredMessage, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(updateRequiredMessage: value), - ), - ); + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(updateRequiredMessage: value), + ), + ); }, ), _buildTextField( @@ -694,10 +745,10 @@ class _AppConfigurationPageState extends State value: appConfig.updateOptionalMessage, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(updateOptionalMessage: value), - ), - ); + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(updateOptionalMessage: value), + ), + ); }, ), _buildTextField( @@ -707,10 +758,10 @@ class _AppConfigurationPageState extends State value: appConfig.iosStoreUrl, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(iosStoreUrl: value), - ), - ); + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(iosStoreUrl: value), + ), + ); }, ), _buildTextField( @@ -720,10 +771,10 @@ class _AppConfigurationPageState extends State value: appConfig.androidStoreUrl, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(androidStoreUrl: value), - ), - ); + AppConfigurationFieldChanged( + appConfig: appConfig.copyWith(androidStoreUrl: value), + ), + ); }, ), ], @@ -751,8 +802,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), + ), ), SizedBox(height: AppSpacing.xs), TextFormField( @@ -794,8 +845,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), + ), ), SizedBox(height: AppSpacing.xs), TextFormField( @@ -831,8 +882,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), + ), ), SwitchListTile( title: Text(label), @@ -867,8 +918,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), + ), ), SizedBox(height: AppSpacing.xs), DropdownButtonFormField( diff --git a/lib/router/router.dart b/lib/router/router.dart index 070dfec6..dcacbdff 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -99,7 +99,8 @@ GoRouter createRouter({ builder: (BuildContext context, GoRouterState state) { final l10n = context.l10n; const String headline = 'Sign In to Dashboard'; - const String subHeadline = 'Enter your email to get a verification code.'; + const String subHeadline = + 'Enter your email to get a verification code.'; const bool showAnonymousButton = false; return BlocProvider( From ddc396950e3412e53f1bb0cf15735f024c6cc7ec Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 27 Jun 2025 20:43:08 +0100 Subject: [PATCH 7/7] lint: fix --- analysis_options.yaml | 2 + lib/app/bloc/app_bloc.dart | 8 +--- lib/app/view/app.dart | 5 +-- lib/app/view/app_shell.dart | 1 - .../view/app_configuration_page.dart | 38 +++++++++---------- lib/router/router.dart | 13 +++---- 6 files changed, 31 insertions(+), 36 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index e324f571..e1e474b8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,6 +1,8 @@ analyzer: errors: avoid_print: ignore + document_ignores: ignore + lines_longer_than_80_chars: ignore include: package:very_good_analysis/analysis_options.9.0.0.yaml linter: rules: diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index e4f38a3b..04d062af 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -3,8 +3,8 @@ 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_data_repository/ht_data_repository.dart'; import 'package:ht_dashboard/app/config/config.dart' as local_config; +import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; part 'app_event.dart'; @@ -21,11 +21,7 @@ class AppBloc extends Bloc { _appConfigRepository = appConfigRepository, _environment = environment, super( - const AppState( - user: null, - status: AppStatus.initial, - environment: null, - ), + const AppState(), ) { on(_onAppUserChanged); on(_onLogoutRequested); diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 72d9d423..15ef992d 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -5,14 +5,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ht_auth_repository/ht_auth_repository.dart'; -import 'package:ht_data_repository/ht_data_repository.dart'; -import 'package:ht_kv_storage_service/ht_kv_storage_service.dart'; import 'package:ht_dashboard/app/bloc/app_bloc.dart'; import 'package:ht_dashboard/app/config/app_environment.dart'; import 'package:ht_dashboard/authentication/bloc/authentication_bloc.dart'; import 'package:ht_dashboard/l10n/app_localizations.dart'; -import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/router/router.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'; class App extends StatelessWidget { diff --git a/lib/app/view/app_shell.dart b/lib/app/view/app_shell.dart index 604e6f1c..f48f7de7 100644 --- a/lib/app/view/app_shell.dart +++ b/lib/app/view/app_shell.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:go_router/go_router.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; -import 'package:ht_dashboard/router/routes.dart'; /// A responsive scaffold shell for the main application sections. /// diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index f4423b29..da2fab0e 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -160,7 +160,7 @@ class _AppConfigurationPageState extends State : null, child: const Text('Discard Changes'), ), - SizedBox(width: AppSpacing.md), + const SizedBox(width: AppSpacing.md), ElevatedButton( onPressed: isDirty ? () async { @@ -229,14 +229,14 @@ class _AppConfigurationPageState extends State 'User Preference Limits', style: Theme.of(context).textTheme.headlineSmall, ), - SizedBox(height: AppSpacing.md), + const SizedBox(height: AppSpacing.md), Text( 'These settings define the maximum number of items a user can follow or save, tiered by user role. Changes here directly impact user capabilities.', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), ), - SizedBox(height: AppSpacing.lg), + const SizedBox(height: AppSpacing.lg), _buildIntField( context, label: 'Guest Followed Items Limit', @@ -358,14 +358,14 @@ class _AppConfigurationPageState extends State 'Ad Configuration', style: Theme.of(context).textTheme.headlineSmall, ), - SizedBox(height: AppSpacing.md), + const SizedBox(height: AppSpacing.md), Text( 'These settings control how ads are injected and displayed in the application, tiered by user role. AdFrequency determines how often an ad can be injected, and AdPlacementInterval sets a minimum number of primary items before the first ad.', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), ), - SizedBox(height: AppSpacing.lg), + const SizedBox(height: AppSpacing.lg), _buildIntField( context, label: 'Guest Ad Frequency', @@ -547,14 +547,14 @@ class _AppConfigurationPageState extends State 'Account Action Configuration', style: Theme.of(context).textTheme.headlineSmall, ), - SizedBox(height: AppSpacing.md), + const SizedBox(height: AppSpacing.md), Text( 'These settings control the display frequency of in-feed account actions (e.g., link account, upgrade prompts), tiered by user role.', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), ), - SizedBox(height: AppSpacing.lg), + const SizedBox(height: AppSpacing.lg), _buildIntField( context, label: 'Guest Days Between Account Actions', @@ -606,7 +606,7 @@ class _AppConfigurationPageState extends State 'Kill Switch & App Status', style: Theme.of(context).textTheme.headlineSmall, ), - SizedBox(height: AppSpacing.md), + const SizedBox(height: AppSpacing.md), Text( 'WARNING: These settings can disable the entire mobile application. Use with extreme caution.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( @@ -614,12 +614,12 @@ class _AppConfigurationPageState extends State fontWeight: FontWeight.bold, ), ), - SizedBox(height: AppSpacing.lg), + const SizedBox(height: AppSpacing.lg), _buildSwitchField( context, label: 'Kill Switch Enabled', description: - 'If enabled, the app\'s operational status will be enforced.', + "If enabled, the app's operational status will be enforced.", value: appConfig.killSwitchEnabled, onChanged: (value) { context.read().add( @@ -690,14 +690,14 @@ class _AppConfigurationPageState extends State 'Force Update Configuration', style: Theme.of(context).textTheme.headlineSmall, ), - SizedBox(height: AppSpacing.md), + const SizedBox(height: AppSpacing.md), Text( 'These settings control app version enforcement. Users on versions below the minimum allowed will be forced to update.', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), ), - SizedBox(height: AppSpacing.lg), + const SizedBox(height: AppSpacing.lg), _buildTextField( context, label: 'Minimum Allowed App Version', @@ -798,14 +798,14 @@ class _AppConfigurationPageState extends State label, style: Theme.of(context).textTheme.titleMedium, ), - SizedBox(height: AppSpacing.xs), + const SizedBox(height: AppSpacing.xs), Text( description, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), ), - SizedBox(height: AppSpacing.xs), + const SizedBox(height: AppSpacing.xs), TextFormField( initialValue: value.toString(), keyboardType: TextInputType.number, @@ -841,14 +841,14 @@ class _AppConfigurationPageState extends State label, style: Theme.of(context).textTheme.titleMedium, ), - SizedBox(height: AppSpacing.xs), + const SizedBox(height: AppSpacing.xs), Text( description, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), ), - SizedBox(height: AppSpacing.xs), + const SizedBox(height: AppSpacing.xs), TextFormField( initialValue: value, decoration: const InputDecoration( @@ -878,7 +878,7 @@ class _AppConfigurationPageState extends State label, style: Theme.of(context).textTheme.titleMedium, ), - SizedBox(height: AppSpacing.xs), + const SizedBox(height: AppSpacing.xs), Text( description, style: Theme.of(context).textTheme.bodySmall?.copyWith( @@ -914,14 +914,14 @@ class _AppConfigurationPageState extends State label, style: Theme.of(context).textTheme.titleMedium, ), - SizedBox(height: AppSpacing.xs), + const SizedBox(height: AppSpacing.xs), Text( description, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), ), - SizedBox(height: AppSpacing.xs), + const SizedBox(height: AppSpacing.xs), DropdownButtonFormField( value: value, decoration: const InputDecoration( diff --git a/lib/router/router.dart b/lib/router/router.dart index dcacbdff..9d996c3a 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -5,20 +5,19 @@ import 'package:ht_auth_repository/ht_auth_repository.dart'; import 'package:ht_dashboard/app/bloc/app_bloc.dart'; import 'package:ht_dashboard/app/config/config.dart' as local_config; import 'package:ht_dashboard/app/view/app_shell.dart'; +import 'package:ht_dashboard/app_configuration/view/app_configuration_page.dart'; import 'package:ht_dashboard/authentication/bloc/authentication_bloc.dart'; 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/l10n/l10n.dart'; -import 'package:ht_dashboard/router/routes.dart'; -import 'package:ht_dashboard/app_configuration/view/app_configuration_page.dart'; import 'package:ht_dashboard/content_management/view/categories_page.dart'; import 'package:ht_dashboard/content_management/view/content_management_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/dashboard/view/dashboard_page.dart'; +import 'package:ht_dashboard/l10n/l10n.dart'; +import 'package:ht_dashboard/router/routes.dart'; import 'package:ht_dashboard/settings/view/settings_page.dart'; -import 'package:ht_shared/ht_shared.dart'; /// Creates and configures the GoRouter instance for the application. /// @@ -98,10 +97,10 @@ GoRouter createRouter({ name: Routes.authenticationName, builder: (BuildContext context, GoRouterState state) { final l10n = context.l10n; - const String headline = 'Sign In to Dashboard'; - const String subHeadline = + const headline = 'Sign In to Dashboard'; + const subHeadline = 'Enter your email to get a verification code.'; - const bool showAnonymousButton = false; + const showAnonymousButton = false; return BlocProvider( create: (context) => AuthenticationBloc(