diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index a287481b..2dfe79ae 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -17,18 +17,20 @@ part 'app_state.dart'; class AppBloc extends Bloc { AppBloc({ required AuthRepository authenticationRepository, - required DataRepository userAppSettingsRepository, + required DataRepository appSettingsRepository, required DataRepository appConfigRepository, required local_config.AppEnvironment environment, Logger? logger, }) : _authenticationRepository = authenticationRepository, - _userAppSettingsRepository = userAppSettingsRepository, + _appSettingsRepository = appSettingsRepository, _appConfigRepository = appConfigRepository, _logger = logger ?? Logger('AppBloc'), super(AppState(environment: environment)) { on(_onAppUserChanged); on(_onLogoutRequested); - on(_onAppUserAppSettingsChanged); + on( + _onAppUserAppSettingsChanged, + ); _userSubscription = _authenticationRepository.authStateChanges.listen( (User? user) => add(AppUserChanged(user)), @@ -36,7 +38,7 @@ class AppBloc extends Bloc { } final AuthRepository _authenticationRepository; - final DataRepository _userAppSettingsRepository; + final DataRepository _appSettingsRepository; final DataRepository _appConfigRepository; final Logger _logger; late final StreamSubscription _userSubscription; @@ -68,16 +70,16 @@ class AppBloc extends Bloc { // If user is authenticated, load their app settings if (status == AppStatus.authenticated && user != null) { try { - final userAppSettings = await _userAppSettingsRepository.read( + final appSettings = await _appSettingsRepository.read( id: user.id, ); - emit(state.copyWith(userAppSettings: userAppSettings)); + emit(state.copyWith(appSettings: appSettings)); } on NotFoundException { // If settings not found, create default ones _logger.info( 'User app settings not found for user ${user.id}. Creating default.', ); - final defaultSettings = UserAppSettings( + final defaultSettings = AppSettings( id: user.id, displaySettings: const DisplaySettings( baseTheme: AppBaseTheme.system, @@ -92,15 +94,14 @@ class AppBloc extends Bloc { 'Default language "en" not found in language fixtures.', ), ), - feedPreferences: const FeedDisplayPreferences( - headlineDensity: HeadlineDensity.standard, - headlineImageStyle: HeadlineImageStyle.largeThumbnail, - showSourceInHeadlineFeed: true, - showPublishDateInHeadlineFeed: true, + feedSettings: const FeedSettings( + feedItemDensity: FeedItemDensity.standard, + feedItemImageStyle: FeedItemImageStyle.largeThumbnail, + feedItemClickBehavior: FeedItemClickBehavior.defaultBehavior, ), ); - await _userAppSettingsRepository.create(item: defaultSettings); - emit(state.copyWith(userAppSettings: defaultSettings)); + await _appSettingsRepository.create(item: defaultSettings); + emit(state.copyWith(appSettings: defaultSettings)); } on HttpException catch (e, s) { // Handle HTTP exceptions during settings load _logger.severe( @@ -108,7 +109,7 @@ class AppBloc extends Bloc { e, s, ); - emit(state.copyWith(clearUserAppSettings: true)); + emit(state.copyWith(clearAppSettings: true)); } catch (e, s) { // Handle any other unexpected errors _logger.severe( @@ -116,11 +117,11 @@ class AppBloc extends Bloc { e, s, ); - emit(state.copyWith(clearUserAppSettings: true)); + emit(state.copyWith(clearAppSettings: true)); } } else { // If user is unauthenticated or anonymous, clear app settings - emit(state.copyWith(clearUserAppSettings: true)); + emit(state.copyWith(clearAppSettings: true)); } } @@ -128,13 +129,13 @@ class AppBloc extends Bloc { AppUserAppSettingsChanged event, Emitter emit, ) { - emit(state.copyWith(userAppSettings: event.userAppSettings)); + emit(state.copyWith(appSettings: event.appSettings)); } void _onLogoutRequested(AppLogoutRequested event, Emitter emit) { unawaited(_authenticationRepository.signOut()); emit( - state.copyWith(clearUserAppSettings: true), + state.copyWith(clearAppSettings: true), ); } diff --git a/lib/app/bloc/app_event.dart b/lib/app/bloc/app_event.dart index 3a78d834..ed9e79a3 100644 --- a/lib/app/bloc/app_event.dart +++ b/lib/app/bloc/app_event.dart @@ -1,13 +1,13 @@ part of 'app_bloc.dart'; -abstract class AppEvent extends Equatable { +sealed class AppEvent extends Equatable { const AppEvent(); @override List get props => []; } -class AppUserChanged extends AppEvent { +final class AppUserChanged extends AppEvent { const AppUserChanged(this.user); final User? user; @@ -16,24 +16,17 @@ class AppUserChanged extends AppEvent { List get props => [user]; } -/// {@template app_logout_requested} -/// Event to request user logout. -/// {@endtemplate} -class AppLogoutRequested extends AppEvent { - /// {@macro app_logout_requested} +final class AppLogoutRequested extends AppEvent { const AppLogoutRequested(); } -/// {@template app_user_app_settings_changed} -/// Event to notify that user application settings have changed. -/// {@endtemplate} +/// Event for when the user's app settings are changed. final class AppUserAppSettingsChanged extends AppEvent { - /// {@macro app_user_app_settings_changed} - const AppUserAppSettingsChanged(this.userAppSettings); + const AppUserAppSettingsChanged(this.appSettings); - /// The updated user application settings. - final UserAppSettings userAppSettings; + /// The new user app settings. + final AppSettings appSettings; @override - List get props => [userAppSettings]; + List get props => [appSettings]; } diff --git a/lib/app/bloc/app_state.dart b/lib/app/bloc/app_state.dart index 186e6f42..af3898d3 100644 --- a/lib/app/bloc/app_state.dart +++ b/lib/app/bloc/app_state.dart @@ -1,69 +1,47 @@ part of 'app_bloc.dart'; -/// Represents the application's authentication status. enum AppStatus { - /// The application is initializing and the status is unknown. + /// The app is in its initial state, typically before any authentication + /// checks have been performed. initial, - /// The user is authenticated. + /// The user is authenticated and has a valid session. authenticated, - /// The user is unauthenticated. + /// The user is unauthenticated, meaning they are not logged in. unauthenticated, - /// The user is anonymous (signed in using an anonymous provider). + /// The user is authenticated anonymously. anonymous, } -/// {@template app_state} -/// Represents the overall state of the application, including authentication -/// status, current user, environment, and user-specific settings. -/// {@endtemplate} -class AppState extends Equatable { - /// {@macro app_state} +final class AppState extends Equatable { const AppState({ + required this.environment, this.status = AppStatus.initial, this.user, - this.environment, - this.userAppSettings, + this.appSettings, }); - /// The current authentication status of the application. final AppStatus status; - - /// The current user details. Null if unauthenticated. final User? user; + final AppSettings? appSettings; + final local_config.AppEnvironment environment; - /// The current application environment (e.g., production, development, demo). - final local_config.AppEnvironment? environment; - - /// The current user application settings. Null if not loaded or unauthenticated. - final UserAppSettings? userAppSettings; - - /// Creates a copy of the current state with updated values. AppState copyWith({ AppStatus? status, User? user, - local_config.AppEnvironment? environment, - UserAppSettings? userAppSettings, - bool clearEnvironment = false, - bool clearUserAppSettings = false, + AppSettings? appSettings, + bool clearAppSettings = false, }) { return AppState( status: status ?? this.status, user: user ?? this.user, - environment: clearEnvironment ? null : environment ?? this.environment, - userAppSettings: clearUserAppSettings - ? null - : userAppSettings ?? this.userAppSettings, + appSettings: clearAppSettings ? null : appSettings ?? this.appSettings, + environment: environment, ); } @override - List get props => [ - status, - user, - environment, - userAppSettings, - ]; + List get props => [status, user, appSettings, environment]; } diff --git a/lib/app/config/app_config.dart b/lib/app/config/app_config.dart index 9fd4c666..f3cec4e5 100644 --- a/lib/app/config/app_config.dart +++ b/lib/app/config/app_config.dart @@ -34,7 +34,7 @@ class AppConfig { /// A factory constructor for the demo environment. factory AppConfig.demo() => AppConfig( environment: AppEnvironment.demo, - baseUrl: '', // No API access needed for in-memory demo + baseUrl: '', ); /// A factory constructor for the development environment. diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index ea1ca853..764c93e9 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -2,7 +2,7 @@ // ignore_for_file: deprecated_member_use import 'package:auth_repository/auth_repository.dart'; -import 'package:core/core.dart' hide AppStatus; +import 'package:core/core.dart'; import 'package:data_repository/data_repository.dart'; import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; @@ -33,7 +33,7 @@ class App extends StatelessWidget { required DataRepository headlinesRepository, required DataRepository topicsRepository, required DataRepository sourcesRepository, - required DataRepository userAppSettingsRepository, + required DataRepository appSettingsRepository, required DataRepository userContentPreferencesRepository, required DataRepository remoteConfigRepository, @@ -49,7 +49,7 @@ class App extends StatelessWidget { _headlinesRepository = headlinesRepository, _topicsRepository = topicsRepository, _sourcesRepository = sourcesRepository, - _userAppSettingsRepository = userAppSettingsRepository, + _appSettingsRepository = appSettingsRepository, _userContentPreferencesRepository = userContentPreferencesRepository, _remoteConfigRepository = remoteConfigRepository, _kvStorageService = storageService, @@ -64,7 +64,7 @@ class App extends StatelessWidget { final DataRepository _headlinesRepository; final DataRepository _topicsRepository; final DataRepository _sourcesRepository; - final DataRepository _userAppSettingsRepository; + final DataRepository _appSettingsRepository; final DataRepository _userContentPreferencesRepository; final DataRepository _remoteConfigRepository; @@ -86,7 +86,7 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _headlinesRepository), RepositoryProvider.value(value: _topicsRepository), RepositoryProvider.value(value: _sourcesRepository), - RepositoryProvider.value(value: _userAppSettingsRepository), + RepositoryProvider.value(value: _appSettingsRepository), RepositoryProvider.value(value: _userContentPreferencesRepository), RepositoryProvider.value(value: _remoteConfigRepository), RepositoryProvider.value(value: _dashboardSummaryRepository), @@ -106,8 +106,8 @@ class App extends StatelessWidget { BlocProvider( create: (context) => AppBloc( authenticationRepository: context.read(), - userAppSettingsRepository: context - .read>(), + appSettingsRepository: context + .read>(), appConfigRepository: context.read>(), environment: _environment, logger: Logger('AppBloc'), @@ -214,20 +214,19 @@ class _AppViewState extends State<_AppView> { return BlocListener( listenWhen: (previous, current) => previous.status != current.status || - previous.userAppSettings != current.userAppSettings, + previous.appSettings != current.appSettings, listener: (context, state) { _statusNotifier.value = state.status; }, child: BlocBuilder( builder: (context, state) { - final userAppSettings = state.userAppSettings; - final baseTheme = userAppSettings?.displaySettings.baseTheme; - final accentTheme = userAppSettings?.displaySettings.accentTheme; - final fontFamily = userAppSettings?.displaySettings.fontFamily; - final textScaleFactor = - userAppSettings?.displaySettings.textScaleFactor; - final fontWeight = userAppSettings?.displaySettings.fontWeight; - final language = userAppSettings?.language; + final appSettings = state.appSettings; + final baseTheme = appSettings?.displaySettings.baseTheme; + final accentTheme = appSettings?.displaySettings.accentTheme; + final fontFamily = appSettings?.displaySettings.fontFamily; + final textScaleFactor = appSettings?.displaySettings.textScaleFactor; + final fontWeight = appSettings?.displaySettings.fontWeight; + final language = appSettings?.language; final lightThemeData = lightTheme( scheme: accentTheme?.toFlexScheme ?? FlexScheme.materialHc, diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index ff746207..34cb31ad 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/bloc/app_configuration_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/view/tabs/advertisements_configuration_tab.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/view/tabs/feed_configuration_tab.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/view/tabs/general_configuration_tab.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/push_notification_settings_form.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/view/tabs/app_configuration_tab.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/view/tabs/features_configuration_tab.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/view/tabs/user_configuration_tab.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/about_icon.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -31,7 +30,7 @@ class _AppConfigurationPageState extends State @override void initState() { super.initState(); - _tabController = TabController(length: 4, vsync: this); + _tabController = TabController(length: 3, vsync: this); context.read().add(const AppConfigurationLoaded()); } @@ -66,10 +65,9 @@ class _AppConfigurationPageState extends State tabAlignment: TabAlignment.start, isScrollable: true, tabs: [ - Tab(text: l10n.generalTab), - Tab(text: l10n.feedTab), - Tab(text: l10n.advertisementsTab), - Tab(text: l10n.notificationsTab), + Tab(text: l10n.appTab), + Tab(text: l10n.featuresTab), + Tab(text: l10n.userTab), ], ), ), @@ -134,7 +132,7 @@ class _AppConfigurationPageState extends State return TabBarView( controller: _tabController, children: [ - GeneralConfigurationTab( + AppConfigurationTab( remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( @@ -142,7 +140,7 @@ class _AppConfigurationPageState extends State ); }, ), - FeedConfigurationTab( + FeaturesConfigurationTab( remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( @@ -150,15 +148,7 @@ class _AppConfigurationPageState extends State ); }, ), - AdvertisementsConfigurationTab( - remoteConfig: remoteConfig, - onConfigChanged: (newConfig) { - context.read().add( - AppConfigurationFieldChanged(remoteConfig: newConfig), - ); - }, - ), - PushNotificationSettingsForm( + UserConfigurationTab( remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( diff --git a/lib/app_configuration/view/tabs/advertisements_configuration_tab.dart b/lib/app_configuration/view/tabs/advertisements_configuration_tab.dart deleted file mode 100644 index a7797129..00000000 --- a/lib/app_configuration/view/tabs/advertisements_configuration_tab.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/ad_config_form.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/ad_platform_config_form.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/article_ad_settings_form.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_ad_settings_form.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/interstitial_ad_settings_form.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:ui_kit/ui_kit.dart'; - -/// {@template advertisements_configuration_tab} -/// A widget representing the "Advertisements" tab in the App Configuration page. -/// -/// This tab allows configuration of various ad settings. -/// {@endtemplate} -class AdvertisementsConfigurationTab extends StatefulWidget { - /// {@macro advertisements_configuration_tab} - const AdvertisementsConfigurationTab({ - required this.remoteConfig, - required this.onConfigChanged, - super.key, - }); - - /// The current [RemoteConfig] object. - final RemoteConfig remoteConfig; - - /// Callback to notify parent of changes to the [RemoteConfig]. - final ValueChanged onConfigChanged; - - @override - State createState() => - _AdvertisementsConfigurationTabState(); -} - -class _AdvertisementsConfigurationTabState - extends State { - /// Notifier for the index of the currently expanded top-level ExpansionTile. - /// - /// A value of `null` means no tile is expanded. - final ValueNotifier _expandedTileIndex = ValueNotifier(null); - - @override - void dispose() { - _expandedTileIndex.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - final adConfig = widget.remoteConfig.adConfig; - - return ListView( - padding: const EdgeInsets.all(AppSpacing.lg), - children: [ - // Global Ad Configuration (AdConfigForm now includes the global switch) - AdConfigForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), - const SizedBox(height: AppSpacing.lg), - // Top-level ExpansionTile for Ad Platform Configuration - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 0; - return ExpansionTile( - key: ValueKey('adPlatformConfigTile_$expandedIndex'), - title: Text(l10n.adPlatformConfigurationTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - onExpansionChanged: adConfig.enabled - ? (isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; - } - : null, - initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled, - enabled: adConfig.enabled, - children: [ - AdPlatformConfigForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), - ], - ); - }, - ), - const SizedBox(height: AppSpacing.lg), - // Top-level ExpansionTile for Feed Ad Settings - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 1; - return ExpansionTile( - key: ValueKey('feedAdSettingsTile_$expandedIndex'), - title: Text(l10n.feedAdSettingsTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - onExpansionChanged: adConfig.enabled - ? (isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; - } - : null, - initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled, - enabled: adConfig.enabled, - children: [ - FeedAdSettingsForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), - ], - ); - }, - ), - const SizedBox(height: AppSpacing.lg), - // Top-level ExpansionTile for Article Ad Settings - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 2; - return ExpansionTile( - key: ValueKey('articleAdSettingsTile_$expandedIndex'), - title: Text(l10n.articleAdSettingsTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - onExpansionChanged: adConfig.enabled - ? (isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; - } - : null, - initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled, - enabled: adConfig.enabled, - children: [ - ArticleAdSettingsForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), - ], - ); - }, - ), - const SizedBox(height: AppSpacing.lg), - // Top-level ExpansionTile for Interstitial Ad Settings - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 3; - return ExpansionTile( - key: ValueKey('interstitialAdSettingsTile_$expandedIndex'), - title: Text(l10n.interstitialAdSettingsTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - onExpansionChanged: adConfig.enabled - ? (isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; - } - : null, - initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled, - enabled: adConfig.enabled, - children: [ - InterstitialAdSettingsForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), - ], - ); - }, - ), - ], - ); - } -} diff --git a/lib/app_configuration/view/tabs/app_configuration_tab.dart b/lib/app_configuration/view/tabs/app_configuration_tab.dart new file mode 100644 index 00000000..fd5d81f9 --- /dev/null +++ b/lib/app_configuration/view/tabs/app_configuration_tab.dart @@ -0,0 +1,119 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/general_app_config_form.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/update_config_form.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template app_configuration_tab} +/// A widget representing the "App" tab in the App Configuration page. +/// +/// This tab allows configuration of application-level settings like +/// maintenance mode, force updates, and general app settings. +/// {@endtemplate} +class AppConfigurationTab extends StatefulWidget { + /// {@macro app_configuration_tab} + const AppConfigurationTab({ + required this.remoteConfig, + required this.onConfigChanged, + super.key, + }); + + /// The current [RemoteConfig] object. + final RemoteConfig remoteConfig; + + /// Callback to notify parent of changes to the [RemoteConfig]. + final ValueChanged onConfigChanged; + + @override + State createState() => _AppConfigurationTabState(); +} + +class _AppConfigurationTabState extends State { + /// Notifier for the index of the currently expanded top-level ExpansionTile. + /// + /// A value of `null` means no tile is expanded. + final ValueNotifier _expandedTileIndex = ValueNotifier(null); + + @override + void dispose() { + _expandedTileIndex.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + final appConfig = widget.remoteConfig.app; + final maintenanceConfig = appConfig.maintenance; + + return ListView( + padding: const EdgeInsets.all(AppSpacing.lg), + children: [ + // Maintenance Config as a direct SwitchListTile + SwitchListTile( + title: Text(l10n.isUnderMaintenanceLabel), + subtitle: Text(l10n.isUnderMaintenanceDescription), + value: maintenanceConfig.isUnderMaintenance, + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + app: appConfig.copyWith( + maintenance: maintenanceConfig.copyWith( + isUnderMaintenance: value, + ), + ), + ), + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + + // Update Config + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 1; + return ExpansionTile( + key: ValueKey('updateConfigTile_$expandedIndex'), + title: Text(l10n.appUpdateManagementTitle), + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + children: [ + UpdateConfigForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + + // General App Config + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 2; + return ExpansionTile( + key: ValueKey('generalAppConfigTile_$expandedIndex'), + title: Text(l10n.appLegalInformationTitle), + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + children: [ + GeneralAppConfigForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ); + }, + ), + ], + ); + } +} diff --git a/lib/app_configuration/view/tabs/feed_configuration_tab.dart b/lib/app_configuration/view/tabs/features_configuration_tab.dart similarity index 55% rename from lib/app_configuration/view/tabs/feed_configuration_tab.dart rename to lib/app_configuration/view/tabs/features_configuration_tab.dart index 35e8c186..fac84584 100644 --- a/lib/app_configuration/view/tabs/feed_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -1,20 +1,24 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/ad_config_form.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/ad_platform_config_form.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_ad_settings_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_decorator_form.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/saved_filter_limits_section.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/user_preference_limits_form.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/navigation_ad_settings_form.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/push_notification_settings_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/feed_decorator_type_l10n.dart'; import 'package:ui_kit/ui_kit.dart'; -/// {@template feed_configuration_tab} -/// A widget representing the "Feed" tab in the App Configuration page. +/// {@template features_configuration_tab} +/// A widget representing the "Features" tab in the App Configuration page. /// -/// This tab allows configuration of user content limits and feed decorators. +/// This tab allows configuration of user-facing features like ads, +/// push notifications, and feed settings. /// {@endtemplate} -class FeedConfigurationTab extends StatefulWidget { - /// {@macro feed_configuration_tab} - const FeedConfigurationTab({ +class FeaturesConfigurationTab extends StatefulWidget { + /// {@macro features_configuration_tab} + const FeaturesConfigurationTab({ required this.remoteConfig, required this.onConfigChanged, super.key, @@ -27,10 +31,11 @@ class FeedConfigurationTab extends StatefulWidget { final ValueChanged onConfigChanged; @override - State createState() => _FeedConfigurationTabState(); + State createState() => + _FeaturesConfigurationTabState(); } -class _FeedConfigurationTabState extends State { +class _FeaturesConfigurationTabState extends State { /// Notifier for the index of the currently expanded top-level ExpansionTile. /// /// A value of `null` means no tile is expanded. @@ -49,35 +54,35 @@ class _FeedConfigurationTabState extends State { return ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ - // Top-level ExpansionTile for User Content Limits + // Advertisements ValueListenableBuilder( valueListenable: _expandedTileIndex, builder: (context, expandedIndex, child) { const tileIndex = 0; return ExpansionTile( - key: ValueKey('userContentLimitsTile_$expandedIndex'), - title: Text(l10n.userContentLimitsTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, + key: ValueKey('advertisementsTile_$expandedIndex'), + title: Text(l10n.advertisementsTab), onExpansionChanged: (isExpanded) { _expandedTileIndex.value = isExpanded ? tileIndex : null; }, initiallyExpanded: expandedIndex == tileIndex, children: [ - Text( - l10n.userContentLimitsDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + AdConfigForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + const SizedBox(height: AppSpacing.lg), + AdPlatformConfigForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + const SizedBox(height: AppSpacing.lg), + FeedAdSettingsForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, ), const SizedBox(height: AppSpacing.lg), - UserPreferenceLimitsForm( + NavigationAdSettingsForm( remoteConfig: widget.remoteConfig, onConfigChanged: widget.onConfigChanged, ), @@ -86,35 +91,21 @@ class _FeedConfigurationTabState extends State { }, ), const SizedBox(height: AppSpacing.lg), - // New Top-level ExpansionTile for User Preset Limits + + // Push Notifications ValueListenableBuilder( valueListenable: _expandedTileIndex, builder: (context, expandedIndex, child) { const tileIndex = 1; return ExpansionTile( - key: ValueKey('savedFeedFilterLimitsTile_$expandedIndex'), - title: Text(l10n.savedFeedFilterLimitsTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, + key: ValueKey('pushNotificationsTile_$expandedIndex'), + title: Text(l10n.notificationsTab), onExpansionChanged: (isExpanded) { _expandedTileIndex.value = isExpanded ? tileIndex : null; }, initiallyExpanded: expandedIndex == tileIndex, children: [ - Text( - l10n.savedFeedFilterLimitsDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - ), - const SizedBox(height: AppSpacing.lg), - SavedFilterLimitsSection( + PushNotificationSettingsForm( remoteConfig: widget.remoteConfig, onConfigChanged: widget.onConfigChanged, ), @@ -123,7 +114,8 @@ class _FeedConfigurationTabState extends State { }, ), const SizedBox(height: AppSpacing.lg), - // New Top-level ExpansionTile for Feed Decorators + + // Feed Decorators ValueListenableBuilder( valueListenable: _expandedTileIndex, builder: (context, expandedIndex, child) { @@ -151,7 +143,6 @@ class _FeedConfigurationTabState extends State { ), ), const SizedBox(height: AppSpacing.lg), - // Individual ExpansionTiles for each Feed Decorator, nested for (final decoratorType in FeedDecoratorType.values) Padding( padding: const EdgeInsets.only(bottom: AppSpacing.md), @@ -166,37 +157,7 @@ class _FeedConfigurationTabState extends State { children: [ FeedDecoratorForm( decoratorType: decoratorType, - remoteConfig: widget.remoteConfig.copyWith( - feedDecoratorConfig: - Map.from( - widget.remoteConfig.feedDecoratorConfig, - )..putIfAbsent( - decoratorType, - () => FeedDecoratorConfig( - category: - decoratorType == - FeedDecoratorType - .suggestedTopics || - decoratorType == - FeedDecoratorType - .suggestedSources - ? FeedDecoratorCategory - .contentCollection - : FeedDecoratorCategory.callToAction, - enabled: false, - visibleTo: const {}, - itemsToDisplay: - decoratorType == - FeedDecoratorType - .suggestedTopics || - decoratorType == - FeedDecoratorType - .suggestedSources - ? 0 - : null, - ), - ), - ), + remoteConfig: widget.remoteConfig, onConfigChanged: widget.onConfigChanged, ), ], diff --git a/lib/app_configuration/view/tabs/general_configuration_tab.dart b/lib/app_configuration/view/tabs/general_configuration_tab.dart deleted file mode 100644 index edb52593..00000000 --- a/lib/app_configuration/view/tabs/general_configuration_tab.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_config_form_fields.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:ui_kit/ui_kit.dart'; - -/// {@template general_configuration_tab} -/// A widget representing the "General" tab in the App Configuration page. -/// -/// This tab allows configuration of maintenance mode and force update settings. -/// {@endtemplate} -class GeneralConfigurationTab extends StatefulWidget { - /// {@macro general_configuration_tab} - const GeneralConfigurationTab({ - required this.remoteConfig, - required this.onConfigChanged, - super.key, - }); - - /// The current [RemoteConfig] object. - final RemoteConfig remoteConfig; - - /// Callback to notify parent of changes to the [RemoteConfig]. - final ValueChanged onConfigChanged; - - @override - State createState() => - _GeneralConfigurationTabState(); -} - -class _GeneralConfigurationTabState extends State { - /// Notifier for the index of the currently expanded top-level ExpansionTile. - /// - /// A value of `null` means no tile is expanded. - final ValueNotifier _expandedTileIndex = ValueNotifier(null); - - @override - void dispose() { - _expandedTileIndex.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - - return ListView( - padding: const EdgeInsets.all(AppSpacing.lg), - children: [ - // Top-level ExpansionTile for Maintenance Section - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 0; - return ExpansionTile( - key: ValueKey('maintenanceModeTile_$expandedIndex'), - title: Text(l10n.maintenanceModeTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - onExpansionChanged: (isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; - }, - initiallyExpanded: expandedIndex == tileIndex, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.maintenanceModeDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - ), - const SizedBox(height: AppSpacing.lg), - SwitchListTile( - title: Text(l10n.isUnderMaintenanceLabel), - subtitle: Text(l10n.isUnderMaintenanceDescription), - value: widget.remoteConfig.appStatus.isUnderMaintenance, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - appStatus: widget.remoteConfig.appStatus.copyWith( - isUnderMaintenance: value, - ), - ), - ); - }, - ), - ], - ), - ], - ); - }, - ), - const SizedBox(height: AppSpacing.lg), - // Top-level ExpansionTile for Force Update Section - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 1; - return ExpansionTile( - key: ValueKey('forceUpdateTile_$expandedIndex'), - title: Text(l10n.forceUpdateTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - onExpansionChanged: (isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; - }, - initiallyExpanded: expandedIndex == tileIndex, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.forceUpdateDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - ), - const SizedBox(height: AppSpacing.lg), - AppConfigTextField( - label: l10n.latestAppVersionLabel, - description: l10n.latestAppVersionDescription, - value: widget.remoteConfig.appStatus.latestAppVersion, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - appStatus: widget.remoteConfig.appStatus.copyWith( - latestAppVersion: value, - ), - ), - ); - }, - ), - SwitchListTile( - title: Text(l10n.isLatestVersionOnlyLabel), - subtitle: Text(l10n.isLatestVersionOnlyDescription), - value: widget.remoteConfig.appStatus.isLatestVersionOnly, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - appStatus: widget.remoteConfig.appStatus.copyWith( - isLatestVersionOnly: value, - ), - ), - ); - }, - ), - AppConfigTextField( - label: l10n.iosUpdateUrlLabel, - description: l10n.iosUpdateUrlDescription, - value: widget.remoteConfig.appStatus.iosUpdateUrl, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - appStatus: widget.remoteConfig.appStatus.copyWith( - iosUpdateUrl: value, - ), - ), - ); - }, - ), - AppConfigTextField( - label: l10n.androidUpdateUrlLabel, - description: l10n.androidUpdateUrlDescription, - value: widget.remoteConfig.appStatus.androidUpdateUrl, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - appStatus: widget.remoteConfig.appStatus.copyWith( - androidUpdateUrl: value, - ), - ), - ); - }, - ), - ], - ), - ], - ); - }, - ), - ], - ); - } -} diff --git a/lib/app_configuration/view/tabs/user_configuration_tab.dart b/lib/app_configuration/view/tabs/user_configuration_tab.dart new file mode 100644 index 00000000..6a28176b --- /dev/null +++ b/lib/app_configuration/view/tabs/user_configuration_tab.dart @@ -0,0 +1,97 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/saved_filter_limits_section.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/user_limits_config_form.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template user_configuration_tab} +/// A widget representing the "User" tab in the App Configuration page. +/// +/// This tab allows configuration of user-specific limits and settings. +/// {@endtemplate} +class UserConfigurationTab extends StatefulWidget { + /// {@macro user_configuration_tab} + const UserConfigurationTab({ + required this.remoteConfig, + required this.onConfigChanged, + super.key, + }); + + /// The current [RemoteConfig] object. + final RemoteConfig remoteConfig; + + /// Callback to notify parent of changes to the [RemoteConfig]. + final ValueChanged onConfigChanged; + + @override + State createState() => _UserConfigurationTabState(); +} + +class _UserConfigurationTabState extends State { + /// Notifier for the index of the currently expanded top-level ExpansionTile. + /// + /// A value of `null` means no tile is expanded. + final ValueNotifier _expandedTileIndex = ValueNotifier(null); + + @override + void dispose() { + _expandedTileIndex.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + + return ListView( + padding: const EdgeInsets.all(AppSpacing.lg), + children: [ + // User Content Limits (Followed Items, Saved Headlines) + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 0; + return ExpansionTile( + key: ValueKey('userContentLimitsTile_$expandedIndex'), + title: Text(l10n.userContentLimitsTitle), + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + children: [ + UserLimitsConfigForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + + // Saved Filter Limits (Headline and Source) + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 1; + return ExpansionTile( + key: ValueKey('savedFilterLimitsTile_$expandedIndex'), + title: Text(l10n.savedFeedFilterLimitsTitle), + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + children: [ + SavedFilterLimitsSection( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ); + }, + ), + ], + ); + } +} diff --git a/lib/app_configuration/widgets/ad_config_form.dart b/lib/app_configuration/widgets/ad_config_form.dart index 5415852c..a74323e8 100644 --- a/lib/app_configuration/widgets/ad_config_form.dart +++ b/lib/app_configuration/widgets/ad_config_form.dart @@ -7,7 +7,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; /// /// This widget primarily controls the global enable/disable switch for ads. /// {@endtemplate} -class AdConfigForm extends StatefulWidget { +class AdConfigForm extends StatelessWidget { /// {@macro ad_config_form} const AdConfigForm({ required this.remoteConfig, @@ -21,14 +21,10 @@ class AdConfigForm extends StatefulWidget { /// Callback to notify parent of changes to the [RemoteConfig]. final ValueChanged onConfigChanged; - @override - State createState() => _AdConfigFormState(); -} - -class _AdConfigFormState extends State { @override Widget build(BuildContext context) { - final adConfig = widget.remoteConfig.adConfig; + final features = remoteConfig.features; + final ads = features.ads; final l10n = AppLocalizationsX(context).l10n; return Column( @@ -36,11 +32,13 @@ class _AdConfigFormState extends State { children: [ SwitchListTile( title: Text(l10n.enableGlobalAdsLabel), - value: adConfig.enabled, + value: ads.enabled, onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith(enabled: value), + onConfigChanged( + remoteConfig.copyWith( + features: features.copyWith( + ads: ads.copyWith(enabled: value), + ), ), ); }, diff --git a/lib/app_configuration/widgets/ad_platform_config_form.dart b/lib/app_configuration/widgets/ad_platform_config_form.dart index 5c8024db..475dd00d 100644 --- a/lib/app_configuration/widgets/ad_platform_config_form.dart +++ b/lib/app_configuration/widgets/ad_platform_config_form.dart @@ -35,14 +35,15 @@ class _AdPlatformConfigFormState extends State { @override void initState() { super.initState(); - _selectedPlatform = widget.remoteConfig.adConfig.primaryAdPlatform; + _selectedPlatform = widget.remoteConfig.features.ads.primaryAdPlatform; _initializeControllers(); } @override void didUpdateWidget(covariant AdPlatformConfigForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.remoteConfig.adConfig != oldWidget.remoteConfig.adConfig) { + if (widget.remoteConfig.features.ads != + oldWidget.remoteConfig.features.ads) { _updateControllers(); } } @@ -51,49 +52,34 @@ class _AdPlatformConfigFormState extends State { _platformAdIdentifierControllers = { for (final platform in AdPlatformType.values) platform: { - 'feedNativeAdId': TextEditingController( + 'nativeAdId': TextEditingController( text: widget .remoteConfig - .adConfig + .features + .ads .platformAdIdentifiers[platform] - ?.feedNativeAdId ?? + ?.nativeAdId ?? '', ), - 'feedBannerAdId': TextEditingController( + 'bannerAdId': TextEditingController( text: widget .remoteConfig - .adConfig + .features + .ads .platformAdIdentifiers[platform] - ?.feedBannerAdId ?? + ?.bannerAdId ?? '', ), - 'feedToArticleInterstitialAdId': TextEditingController( + 'interstitialAdId': TextEditingController( text: widget .remoteConfig - .adConfig + .features + .ads .platformAdIdentifiers[platform] - ?.feedToArticleInterstitialAdId ?? - '', - ), - 'inArticleNativeAdId': TextEditingController( - text: - widget - .remoteConfig - .adConfig - .platformAdIdentifiers[platform] - ?.inArticleNativeAdId ?? - '', - ), - 'inArticleBannerAdId': TextEditingController( - text: - widget - .remoteConfig - .adConfig - .platformAdIdentifiers[platform] - ?.inArticleBannerAdId ?? + ?.interstitialAdId ?? '', ), }, @@ -102,95 +88,29 @@ class _AdPlatformConfigFormState extends State { void _updateControllers() { for (final platform in AdPlatformType.values) { - final feedNativeAdId = - widget - .remoteConfig - .adConfig - .platformAdIdentifiers[platform] - ?.feedNativeAdId ?? - ''; - if (_platformAdIdentifierControllers[platform]!['feedNativeAdId']?.text != - feedNativeAdId) { - _platformAdIdentifierControllers[platform]!['feedNativeAdId']?.text = - feedNativeAdId; - _platformAdIdentifierControllers[platform]!['feedNativeAdId'] - ?.selection = TextSelection.collapsed( - offset: feedNativeAdId.length, - ); - } + final identifiers = + widget.remoteConfig.features.ads.platformAdIdentifiers[platform]; - final feedBannerAdId = - widget - .remoteConfig - .adConfig - .platformAdIdentifiers[platform] - ?.feedBannerAdId ?? - ''; - if (_platformAdIdentifierControllers[platform]!['feedBannerAdId']?.text != - feedBannerAdId) { - _platformAdIdentifierControllers[platform]!['feedBannerAdId']?.text = - feedBannerAdId; - _platformAdIdentifierControllers[platform]!['feedBannerAdId'] - ?.selection = TextSelection.collapsed( - offset: feedBannerAdId.length, - ); + final nativeAdId = identifiers?.nativeAdId ?? ''; + if (_platformAdIdentifierControllers[platform]!['nativeAdId']?.text != + nativeAdId) { + _platformAdIdentifierControllers[platform]!['nativeAdId']?.text = + nativeAdId; } - final feedToArticleInterstitialAdId = - widget - .remoteConfig - .adConfig - .platformAdIdentifiers[platform] - ?.feedToArticleInterstitialAdId ?? - ''; - if (_platformAdIdentifierControllers[platform]!['feedToArticleInterstitialAdId'] - ?.text != - feedToArticleInterstitialAdId) { - _platformAdIdentifierControllers[platform]!['feedToArticleInterstitialAdId'] - ?.text = - feedToArticleInterstitialAdId; - _platformAdIdentifierControllers[platform]!['feedToArticleInterstitialAdId'] - ?.selection = TextSelection.collapsed( - offset: feedToArticleInterstitialAdId.length, - ); - } - - final inArticleNativeAdId = - widget - .remoteConfig - .adConfig - .platformAdIdentifiers[platform] - ?.inArticleNativeAdId ?? - ''; - if (_platformAdIdentifierControllers[platform]!['inArticleNativeAdId'] - ?.text != - inArticleNativeAdId) { - _platformAdIdentifierControllers[platform]!['inArticleNativeAdId'] - ?.text = - inArticleNativeAdId; - _platformAdIdentifierControllers[platform]!['inArticleNativeAdId'] - ?.selection = TextSelection.collapsed( - offset: inArticleNativeAdId.length, - ); + final bannerAdId = identifiers?.bannerAdId ?? ''; + if (_platformAdIdentifierControllers[platform]!['bannerAdId']?.text != + bannerAdId) { + _platformAdIdentifierControllers[platform]!['bannerAdId']?.text = + bannerAdId; } - final inArticleBannerAdId = - widget - .remoteConfig - .adConfig - .platformAdIdentifiers[platform] - ?.inArticleBannerAdId ?? - ''; - if (_platformAdIdentifierControllers[platform]!['inArticleBannerAdId'] + final interstitialAdId = identifiers?.interstitialAdId ?? ''; + if (_platformAdIdentifierControllers[platform]!['interstitialAdId'] ?.text != - inArticleBannerAdId) { - _platformAdIdentifierControllers[platform]!['inArticleBannerAdId'] - ?.text = - inArticleBannerAdId; - _platformAdIdentifierControllers[platform]!['inArticleBannerAdId'] - ?.selection = TextSelection.collapsed( - offset: inArticleBannerAdId.length, - ); + interstitialAdId) { + _platformAdIdentifierControllers[platform]!['interstitialAdId']?.text = + interstitialAdId; } } } @@ -208,7 +128,7 @@ class _AdPlatformConfigFormState extends State { @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; - final adConfig = widget.remoteConfig.adConfig; + final adConfig = widget.remoteConfig.features.ads; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -226,9 +146,7 @@ class _AdPlatformConfigFormState extends State { Text( l10n.primaryAdPlatformDescription, 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), ), textAlign: TextAlign.start, ), @@ -259,8 +177,10 @@ class _AdPlatformConfigFormState extends State { }); widget.onConfigChanged( widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - primaryAdPlatform: newSelection.first, + features: widget.remoteConfig.features.copyWith( + ads: adConfig.copyWith( + primaryAdPlatform: newSelection.first, + ), ), ), ); @@ -284,9 +204,7 @@ class _AdPlatformConfigFormState extends State { Text( l10n.adUnitIdentifiersDescription, 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), ), textAlign: TextAlign.start, ), @@ -315,38 +233,33 @@ class _AdPlatformConfigFormState extends State { final controllers = _platformAdIdentifierControllers[platform]!; void updatePlatformIdentifiers(String key, String? value) { - AdPlatformIdentifiers newIdentifiers; - - switch (key) { - case 'feedNativeAdId': - newIdentifiers = platformIdentifiers.copyWith(feedNativeAdId: value); - case 'feedBannerAdId': - newIdentifiers = platformIdentifiers.copyWith(feedBannerAdId: value); - case 'feedToArticleInterstitialAdId': - newIdentifiers = platformIdentifiers.copyWith( - feedToArticleInterstitialAdId: value, - ); - case 'inArticleNativeAdId': - newIdentifiers = platformIdentifiers.copyWith( - inArticleNativeAdId: value, - ); - case 'inArticleBannerAdId': - newIdentifiers = platformIdentifiers.copyWith( - inArticleBannerAdId: value, - ); - default: - return; - } + final newIdentifiers = platformIdentifiers.copyWith( + nativeAdId: key == 'nativeAdId' + ? value + : platformIdentifiers.nativeAdId, + bannerAdId: key == 'bannerAdId' + ? value + : platformIdentifiers.bannerAdId, + interstitialAdId: key == 'interstitialAdId' + ? value + : platformIdentifiers.interstitialAdId, + ); final newPlatformAdIdentifiers = Map.from( config.platformAdIdentifiers, - )..[platform] = newIdentifiers; + )..update( + platform, + (_) => newIdentifiers, + ifAbsent: () => newIdentifiers, + ); widget.onConfigChanged( widget.remoteConfig.copyWith( - adConfig: config.copyWith( - platformAdIdentifiers: newPlatformAdIdentifiers, + features: widget.remoteConfig.features.copyWith( + ads: config.copyWith( + platformAdIdentifiers: newPlatformAdIdentifiers, + ), ), ), ); @@ -355,44 +268,26 @@ class _AdPlatformConfigFormState extends State { return Column( children: [ AppConfigTextField( - label: l10n.feedNativeAdIdLabel, - description: l10n.feedNativeAdIdDescription, - value: platformIdentifiers.feedNativeAdId, - onChanged: (value) => - updatePlatformIdentifiers('feedNativeAdId', value), - controller: controllers['feedNativeAdId'], - ), - AppConfigTextField( - label: l10n.feedBannerAdIdLabel, - description: l10n.feedBannerAdIdDescription, - value: platformIdentifiers.feedBannerAdId, - onChanged: (value) => - updatePlatformIdentifiers('feedBannerAdId', value), - controller: controllers['feedBannerAdId'], + label: l10n.nativeAdIdLabel, + description: l10n.nativeAdIdDescription, + value: platformIdentifiers.nativeAdId, + onChanged: (value) => updatePlatformIdentifiers('nativeAdId', value), + controller: controllers['nativeAdId'], ), AppConfigTextField( - label: l10n.feedToArticleInterstitialAdIdLabel, - description: l10n.feedToArticleInterstitialAdIdDescription, - value: platformIdentifiers.feedToArticleInterstitialAdId, - onChanged: (value) => - updatePlatformIdentifiers('feedToArticleInterstitialAdId', value), - controller: controllers['feedToArticleInterstitialAdId'], - ), - AppConfigTextField( - label: l10n.inArticleNativeAdIdLabel, - description: l10n.inArticleNativeAdIdDescription, - value: platformIdentifiers.inArticleNativeAdId, - onChanged: (value) => - updatePlatformIdentifiers('inArticleNativeAdId', value), - controller: controllers['inArticleNativeAdId'], + label: l10n.bannerAdIdLabel, + description: l10n.bannerAdIdDescription, + value: platformIdentifiers.bannerAdId, + onChanged: (value) => updatePlatformIdentifiers('bannerAdId', value), + controller: controllers['bannerAdId'], ), AppConfigTextField( - label: l10n.inArticleBannerAdIdLabel, - description: l10n.inArticleBannerAdIdDescription, - value: platformIdentifiers.inArticleBannerAdId, + label: l10n.interstitialAdIdLabel, + description: l10n.interstitialAdIdDescription, + value: platformIdentifiers.interstitialAdId, onChanged: (value) => - updatePlatformIdentifiers('inArticleBannerAdId', value), - controller: controllers['inArticleBannerAdId'], + updatePlatformIdentifiers('interstitialAdId', value), + controller: controllers['interstitialAdId'], ), ], ); diff --git a/lib/app_configuration/widgets/article_ad_settings_form.dart b/lib/app_configuration/widgets/article_ad_settings_form.dart deleted file mode 100644 index 2fd78f61..00000000 --- a/lib/app_configuration/widgets/article_ad_settings_form.dart +++ /dev/null @@ -1,278 +0,0 @@ -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/banner_ad_shape_l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/in_article_ad_slot_type_l10n.dart'; -import 'package:ui_kit/ui_kit.dart'; - -/// {@template article_ad_settings_form} -/// A form widget for configuring article ad settings. -/// {@endtemplate} -class ArticleAdSettingsForm extends StatefulWidget { - /// {@macro article_ad_settings_form} - const ArticleAdSettingsForm({ - required this.remoteConfig, - required this.onConfigChanged, - super.key, - }); - - /// The current [RemoteConfig] object. - final RemoteConfig remoteConfig; - - /// Callback to notify parent of changes to the [RemoteConfig]. - final ValueChanged onConfigChanged; - - @override - State createState() => _ArticleAdSettingsFormState(); -} - -class _ArticleAdSettingsFormState extends State - with SingleTickerProviderStateMixin { - late TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController( - length: AppUserRole.values.length, - vsync: this, - ); - } - - @override - void didUpdateWidget(covariant ArticleAdSettingsForm oldWidget) { - super.didUpdateWidget(oldWidget); - // No specific controller updates needed here as the UI rebuilds based on - // the remoteConfig directly. - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - final adConfig = widget.remoteConfig.adConfig; - final articleAdConfig = adConfig.articleAdConfiguration; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SwitchListTile( - title: Text(l10n.enableArticleAdsLabel), - value: articleAdConfig.enabled, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - articleAdConfiguration: articleAdConfig.copyWith( - enabled: value, - ), - ), - ), - ); - }, - ), - const SizedBox(height: AppSpacing.lg), - ExpansionTile( - title: Text(l10n.bannerAdShapeSelectionTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.bannerAdShapeSelectionDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - textAlign: TextAlign.start, - ), - const SizedBox(height: AppSpacing.lg), - Align( - alignment: AlignmentDirectional.centerStart, - child: SegmentedButton( - style: SegmentedButton.styleFrom( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.zero, - ), - ), - segments: BannerAdShape.values - .map( - (type) => ButtonSegment( - value: type, - label: Text(type.l10n(context)), - ), - ) - .toList(), - selected: {articleAdConfig.bannerAdShape}, - onSelectionChanged: (newSelection) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - articleAdConfiguration: articleAdConfig.copyWith( - bannerAdShape: newSelection.first, - ), - ), - ), - ); - }, - ), - ), - ], - ), - const SizedBox(height: AppSpacing.lg), - ExpansionTile( - title: Text(l10n.inArticleAdSlotPlacementsTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.inArticleAdSlotPlacementsDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - textAlign: TextAlign.start, - ), - const SizedBox(height: AppSpacing.lg), - Align( - alignment: AlignmentDirectional.centerStart, - child: SizedBox( - height: kTextTabBarHeight, - child: TabBar( - controller: _tabController, - tabAlignment: TabAlignment.start, - isScrollable: true, - tabs: AppUserRole.values - .map((role) => Tab(text: role.l10n(context))) - .toList(), - ), - ), - ), - const SizedBox(height: AppSpacing.lg), - SizedBox( - height: 250, - child: TabBarView( - controller: _tabController, - children: AppUserRole.values - .map( - (role) => _buildRoleSpecificFields( - context, - l10n, - role, - articleAdConfig, - ), - ) - .toList(), - ), - ), - ], - ), - ], - ); - } - - /// Builds role-specific configuration fields for in-article ad slots. - /// - /// This widget displays checkboxes for each [InArticleAdSlotType] for a - /// given [AppUserRole], allowing to enable/disable specific ad slots. - Widget _buildRoleSpecificFields( - BuildContext context, - AppLocalizations l10n, - AppUserRole role, - ArticleAdConfiguration config, - ) { - final roleSlots = config.visibleTo[role]; - - return Column( - children: [ - SwitchListTile( - title: Text(l10n.visibleToRoleLabel(role.l10n(context))), - value: roleSlots != null, - onChanged: (value) { - final newVisibleTo = - Map>.from( - config.visibleTo, - ); - if (value) { - // Default values when enabling for a role - newVisibleTo[role] = { - InArticleAdSlotType.aboveArticleContinueReadingButton: true, - InArticleAdSlotType.belowArticleContinueReadingButton: true, - }; - } else { - newVisibleTo.remove(role); - } - - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: widget.remoteConfig.adConfig.copyWith( - articleAdConfiguration: config.copyWith( - visibleTo: newVisibleTo, - ), - ), - ), - ); - }, - ), - if (roleSlots != null) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.lg, - vertical: AppSpacing.sm, - ), - child: Column( - children: [ - // SwitchListTile for each InArticleAdSlotType - for (final slotType in InArticleAdSlotType.values) - CheckboxListTile( - title: Text(slotType.l10n(context)), - value: roleSlots[slotType] ?? false, - onChanged: (value) { - final newRoleSlots = Map.from( - roleSlots, - ); - if (value ?? false) { - newRoleSlots[slotType] = true; - } else { - newRoleSlots.remove(slotType); - } - - final newVisibleTo = - Map>.from( - config.visibleTo, - )..[role] = newRoleSlots; - - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: widget.remoteConfig.adConfig.copyWith( - articleAdConfiguration: config.copyWith( - visibleTo: newVisibleTo, - ), - ), - ), - ); - }, - ), - ], - ), - ), - ], - ); - } -} diff --git a/lib/app_configuration/widgets/feed_ad_settings_form.dart b/lib/app_configuration/widgets/feed_ad_settings_form.dart index 301cf7e5..26629b3a 100644 --- a/lib/app_configuration/widgets/feed_ad_settings_form.dart +++ b/lib/app_configuration/widgets/feed_ad_settings_form.dart @@ -54,7 +54,7 @@ class _FeedAdSettingsFormState extends State /// Initializes text editing controllers for each user role based on current /// remote config values. void _initializeControllers() { - final feedAdConfig = widget.remoteConfig.adConfig.feedAdConfiguration; + final feedAdConfig = widget.remoteConfig.features.ads.feedAdConfiguration; _adFrequencyControllers = { for (final role in AppUserRole.values) role: @@ -83,7 +83,7 @@ class _FeedAdSettingsFormState extends State /// Updates text editing controllers when the widget's remote config changes. /// This ensures the form fields reflect the latest configuration. void _updateControllers() { - final feedAdConfig = widget.remoteConfig.adConfig.feedAdConfiguration; + final feedAdConfig = widget.remoteConfig.features.ads.feedAdConfiguration; for (final role in AppUserRole.values) { final newFrequencyValue = _getAdFrequency(feedAdConfig, role).toString(); if (_adFrequencyControllers[role]?.text != newFrequencyValue) { @@ -111,8 +111,8 @@ class _FeedAdSettingsFormState extends State @override void didUpdateWidget(covariant FeedAdSettingsForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.remoteConfig.adConfig.feedAdConfiguration != - oldWidget.remoteConfig.adConfig.feedAdConfiguration) { + if (widget.remoteConfig.features.ads.feedAdConfiguration != + oldWidget.remoteConfig.features.ads.feedAdConfiguration) { _updateControllers(); } } @@ -132,11 +132,12 @@ class _FeedAdSettingsFormState extends State @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; - final adConfig = widget.remoteConfig.adConfig; + final features = widget.remoteConfig.features; + final adConfig = features.ads; final feedAdConfig = adConfig.feedAdConfiguration; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return ExpansionTile( + title: Text(l10n.feedAdSettingsTitle), children: [ SwitchListTile( title: Text(l10n.enableFeedAdsLabel), @@ -144,8 +145,10 @@ class _FeedAdSettingsFormState extends State onChanged: (value) { widget.onConfigChanged( widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - feedAdConfiguration: feedAdConfig.copyWith(enabled: value), + features: features.copyWith( + ads: adConfig.copyWith( + feedAdConfiguration: feedAdConfig.copyWith(enabled: value), + ), ), ), ); @@ -192,9 +195,11 @@ class _FeedAdSettingsFormState extends State onSelectionChanged: (newSelection) { widget.onConfigChanged( widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - feedAdConfiguration: feedAdConfig.copyWith( - adType: newSelection.first, + features: features.copyWith( + ads: adConfig.copyWith( + feedAdConfiguration: feedAdConfig.copyWith( + adType: newSelection.first, + ), ), ), ), @@ -291,9 +296,11 @@ class _FeedAdSettingsFormState extends State } widget.onConfigChanged( widget.remoteConfig.copyWith( - adConfig: widget.remoteConfig.adConfig.copyWith( - feedAdConfiguration: config.copyWith( - visibleTo: newVisibleTo, + features: widget.remoteConfig.features.copyWith( + ads: widget.remoteConfig.features.ads.copyWith( + feedAdConfiguration: config.copyWith( + visibleTo: newVisibleTo, + ), ), ), ), @@ -322,9 +329,11 @@ class _FeedAdSettingsFormState extends State )..[role] = newRoleConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( - adConfig: widget.remoteConfig.adConfig.copyWith( - feedAdConfiguration: config.copyWith( - visibleTo: newVisibleTo, + features: widget.remoteConfig.features.copyWith( + ads: widget.remoteConfig.features.ads.copyWith( + feedAdConfiguration: config.copyWith( + visibleTo: newVisibleTo, + ), ), ), ), @@ -346,9 +355,11 @@ class _FeedAdSettingsFormState extends State )..[role] = newRoleConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( - adConfig: widget.remoteConfig.adConfig.copyWith( - feedAdConfiguration: config.copyWith( - visibleTo: newVisibleTo, + features: widget.remoteConfig.features.copyWith( + ads: widget.remoteConfig.features.ads.copyWith( + feedAdConfiguration: config.copyWith( + visibleTo: newVisibleTo, + ), ), ), ), diff --git a/lib/app_configuration/widgets/feed_decorator_form.dart b/lib/app_configuration/widgets/feed_decorator_form.dart index b9dc73b5..fae03eb4 100644 --- a/lib/app_configuration/widgets/feed_decorator_form.dart +++ b/lib/app_configuration/widgets/feed_decorator_form.dart @@ -50,15 +50,15 @@ class _FeedDecoratorFormState extends State @override void didUpdateWidget(covariant FeedDecoratorForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.remoteConfig.feedDecoratorConfig[widget.decoratorType] != - oldWidget.remoteConfig.feedDecoratorConfig[widget.decoratorType]) { + if (widget.remoteConfig.features.feed.decorators[widget.decoratorType] != + oldWidget.remoteConfig.features.feed.decorators[widget.decoratorType]) { _updateControllers(); } } void _initializeControllers() { final decoratorConfig = - widget.remoteConfig.feedDecoratorConfig[widget.decoratorType]!; + widget.remoteConfig.features.feed.decorators[widget.decoratorType]!; _itemsToDisplayController = TextEditingController( text: decoratorConfig.itemsToDisplay?.toString() ?? '', @@ -88,7 +88,7 @@ class _FeedDecoratorFormState extends State void _updateControllers() { final decoratorConfig = - widget.remoteConfig.feedDecoratorConfig[widget.decoratorType]!; + widget.remoteConfig.features.feed.decorators[widget.decoratorType]!; final newItemsToDisplay = decoratorConfig.itemsToDisplay?.toString() ?? ''; if (_itemsToDisplayController.text != newItemsToDisplay) { _itemsToDisplayController.text = newItemsToDisplay; @@ -146,8 +146,10 @@ class _FeedDecoratorFormState extends State @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; - final decoratorConfig = - widget.remoteConfig.feedDecoratorConfig[widget.decoratorType]!; + final features = widget.remoteConfig.features; + final feed = features.feed; + final decorators = feed.decorators; + final decoratorConfig = decorators[widget.decoratorType]!; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -157,13 +159,15 @@ class _FeedDecoratorFormState extends State value: decoratorConfig.enabled, onChanged: (value) { final newDecoratorConfig = decoratorConfig.copyWith(enabled: value); - final newFeedDecoratorConfig = + final newDecorators = Map.from( - widget.remoteConfig.feedDecoratorConfig, + decorators, )..[widget.decoratorType] = newDecoratorConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( - feedDecoratorConfig: newFeedDecoratorConfig, + features: features.copyWith( + feed: feed.copyWith(decorators: newDecorators), + ), ), ); }, @@ -177,13 +181,15 @@ class _FeedDecoratorFormState extends State final newDecoratorConfig = decoratorConfig.copyWith( itemsToDisplay: value, ); - final newFeedDecoratorConfig = + final newDecorators = Map.from( - widget.remoteConfig.feedDecoratorConfig, + decorators, )..[widget.decoratorType] = newDecoratorConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( - feedDecoratorConfig: newFeedDecoratorConfig, + features: features.copyWith( + feed: feed.copyWith(decorators: newDecorators), + ), ), ); }, @@ -260,13 +266,17 @@ class _FeedDecoratorFormState extends State final newDecoratorConfig = decoratorConfig.copyWith( visibleTo: newVisibleTo, ); - final newFeedDecoratorConfig = + final newDecorators = Map.from( - widget.remoteConfig.feedDecoratorConfig, + widget.remoteConfig.features.feed.decorators, )..[widget.decoratorType] = newDecoratorConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( - feedDecoratorConfig: newFeedDecoratorConfig, + features: widget.remoteConfig.features.copyWith( + feed: widget.remoteConfig.features.feed.copyWith( + decorators: newDecorators, + ), + ), ), ); } @@ -293,13 +303,17 @@ class _FeedDecoratorFormState extends State final newDecoratorConfig = decoratorConfig.copyWith( visibleTo: newVisibleTo, ); - final newFeedDecoratorConfig = + final newDecorators = Map.from( - widget.remoteConfig.feedDecoratorConfig, + widget.remoteConfig.features.feed.decorators, )..[widget.decoratorType] = newDecoratorConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( - feedDecoratorConfig: newFeedDecoratorConfig, + features: widget.remoteConfig.features.copyWith( + feed: widget.remoteConfig.features.feed.copyWith( + decorators: newDecorators, + ), + ), ), ); }, diff --git a/lib/app_configuration/widgets/general_app_config_form.dart b/lib/app_configuration/widgets/general_app_config_form.dart new file mode 100644 index 00000000..a0304987 --- /dev/null +++ b/lib/app_configuration/widgets/general_app_config_form.dart @@ -0,0 +1,119 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_config_form_fields.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template general_app_config_form} +/// A form widget for configuring general application settings. +/// +/// This form manages settings like Terms of Service and Privacy Policy URLs. +/// {@endtemplate} +class GeneralAppConfigForm extends StatefulWidget { + /// {@macro general_app_config_form} + const GeneralAppConfigForm({ + required this.remoteConfig, + required this.onConfigChanged, + super.key, + }); + + /// The current [RemoteConfig] object. + final RemoteConfig remoteConfig; + + /// Callback to notify parent of changes to the [RemoteConfig]. + final ValueChanged onConfigChanged; + + @override + State createState() => _GeneralAppConfigFormState(); +} + +class _GeneralAppConfigFormState extends State { + late final TextEditingController _termsUrlController; + late final TextEditingController _privacyUrlController; + + @override + void initState() { + super.initState(); + _termsUrlController = TextEditingController( + text: widget.remoteConfig.app.general.termsOfServiceUrl, + ); + _privacyUrlController = TextEditingController( + text: widget.remoteConfig.app.general.privacyPolicyUrl, + ); + } + + @override + void didUpdateWidget(covariant GeneralAppConfigForm oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.remoteConfig.app.general != oldWidget.remoteConfig.app.general) { + _termsUrlController.text = + widget.remoteConfig.app.general.termsOfServiceUrl; + _privacyUrlController.text = + widget.remoteConfig.app.general.privacyPolicyUrl; + } + } + + @override + void dispose() { + _termsUrlController.dispose(); + _privacyUrlController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + final appConfig = widget.remoteConfig.app; + final generalConfig = appConfig.general; + + return Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.generalAppConfigDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + const SizedBox(height: AppSpacing.lg), + AppConfigTextField( + label: l10n.termsOfServiceUrlLabel, + description: l10n.termsOfServiceUrlDescription, + value: generalConfig.termsOfServiceUrl, + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + app: appConfig.copyWith( + general: generalConfig.copyWith( + termsOfServiceUrl: value, + ), + ), + ), + ); + }, + controller: _termsUrlController, + ), + AppConfigTextField( + label: l10n.privacyPolicyUrlLabel, + description: l10n.privacyPolicyUrlDescription, + value: generalConfig.privacyPolicyUrl, + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + app: appConfig.copyWith( + general: generalConfig.copyWith( + privacyPolicyUrl: value, + ), + ), + ), + ); + }, + controller: _privacyUrlController, + ), + ], + ), + ); + } +} diff --git a/lib/app_configuration/widgets/interstitial_ad_settings_form.dart b/lib/app_configuration/widgets/interstitial_ad_settings_form.dart deleted file mode 100644 index 5c0193bc..00000000 --- a/lib/app_configuration/widgets/interstitial_ad_settings_form.dart +++ /dev/null @@ -1,279 +0,0 @@ -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_config_form_fields.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; -import 'package:ui_kit/ui_kit.dart'; - -/// {@template interstitial_ad_settings_form} -/// A form widget for configuring global interstitial ad settings. -/// {@endtemplate} -class InterstitialAdSettingsForm extends StatefulWidget { - /// {@macro interstitial_ad_settings_form} - const InterstitialAdSettingsForm({ - required this.remoteConfig, - required this.onConfigChanged, - super.key, - }); - - /// The current [RemoteConfig] object. - final RemoteConfig remoteConfig; - - /// Callback to notify parent of changes to the [RemoteConfig]. - final ValueChanged onConfigChanged; - - @override - State createState() => - _InterstitialAdSettingsFormState(); -} - -class _InterstitialAdSettingsFormState extends State - with SingleTickerProviderStateMixin { - late TabController _tabController; - - /// Controllers for transitions before showing interstitial ads, mapped by user role. - /// These are used to manage text input for each role's interstitial ad frequency. - late final Map - _transitionsBeforeShowingInterstitialAdsControllers; - - @override - void initState() { - super.initState(); - _tabController = TabController( - length: AppUserRole.values.length, - vsync: this, - ); - _initializeControllers(); - } - - /// Initializes text editing controllers for each user role based on current - /// remote config values. - void _initializeControllers() { - final interstitialConfig = - widget.remoteConfig.adConfig.interstitialAdConfiguration; - _transitionsBeforeShowingInterstitialAdsControllers = { - for (final role in AppUserRole.values) - role: - TextEditingController( - text: _getTransitionsBeforeInterstitial( - interstitialConfig, - role, - ).toString(), - ) - ..selection = TextSelection.collapsed( - offset: _getTransitionsBeforeInterstitial( - interstitialConfig, - role, - ).toString().length, - ), - }; - } - - /// Updates text editing controllers when the widget's remote config changes. - /// This ensures the form fields reflect the latest configuration. - void _updateControllers() { - final interstitialConfig = - widget.remoteConfig.adConfig.interstitialAdConfiguration; - for (final role in AppUserRole.values) { - final newInterstitialValue = _getTransitionsBeforeInterstitial( - interstitialConfig, - role, - ).toString(); - if (_transitionsBeforeShowingInterstitialAdsControllers[role]?.text != - newInterstitialValue) { - _transitionsBeforeShowingInterstitialAdsControllers[role]?.text = - newInterstitialValue; - _transitionsBeforeShowingInterstitialAdsControllers[role]?.selection = - TextSelection.collapsed( - offset: newInterstitialValue.length, - ); - } - } - } - - @override - void didUpdateWidget(covariant InterstitialAdSettingsForm oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.remoteConfig.adConfig.interstitialAdConfiguration != - oldWidget.remoteConfig.adConfig.interstitialAdConfiguration) { - _updateControllers(); - } - } - - @override - void dispose() { - _tabController.dispose(); - for (final controller - in _transitionsBeforeShowingInterstitialAdsControllers.values) { - controller.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - final adConfig = widget.remoteConfig.adConfig; - final interstitialAdConfig = adConfig.interstitialAdConfiguration; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SwitchListTile( - title: Text(l10n.enableInterstitialAdsLabel), - value: interstitialAdConfig.enabled, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - interstitialAdConfiguration: interstitialAdConfig.copyWith( - enabled: value, - ), - ), - ), - ); - }, - ), - ExpansionTile( - title: Text(l10n.userRoleInterstitialFrequencyTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.userRoleInterstitialFrequencyDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - textAlign: TextAlign.start, - ), - const SizedBox(height: AppSpacing.lg), - Align( - alignment: AlignmentDirectional.centerStart, - child: SizedBox( - height: kTextTabBarHeight, - child: TabBar( - controller: _tabController, - tabAlignment: TabAlignment.start, - isScrollable: true, - tabs: AppUserRole.values - .map((role) => Tab(text: role.l10n(context))) - .toList(), - ), - ), - ), - const SizedBox(height: AppSpacing.lg), - SizedBox( - height: 250, - child: TabBarView( - controller: _tabController, - children: AppUserRole.values - .map( - (role) => _buildInterstitialRoleSpecificFields( - context, - l10n, - role, - interstitialAdConfig, - ), - ) - .toList(), - ), - ), - ], - ), - ], - ); - } - - /// Builds role-specific configuration fields for interstitial ad frequency. - /// - /// This widget displays an input field for `transitionsBeforeShowingInterstitialAds` - /// for a given [AppUserRole]. - Widget _buildInterstitialRoleSpecificFields( - BuildContext context, - AppLocalizations l10n, - AppUserRole role, - InterstitialAdConfiguration config, - ) { - final roleConfig = config.visibleTo[role]; - - return Column( - children: [ - SwitchListTile( - title: Text(l10n.visibleToRoleLabel(role.l10n(context))), - value: roleConfig != null, - onChanged: (value) { - final newVisibleTo = - Map.from( - config.visibleTo, - ); - if (value) { - // Default value when enabling for a role - newVisibleTo[role] = const InterstitialAdFrequencyConfig( - transitionsBeforeShowingInterstitialAds: 5, - ); - } else { - newVisibleTo.remove(role); - } - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: widget.remoteConfig.adConfig.copyWith( - interstitialAdConfiguration: config.copyWith( - visibleTo: newVisibleTo, - ), - ), - ), - ); - }, - ), - if (roleConfig != null) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.lg, - vertical: AppSpacing.sm, - ), - child: AppConfigIntField( - label: l10n.transitionsBeforeInterstitialAdsLabel, - description: l10n.transitionsBeforeInterstitialAdsDescription, - value: roleConfig.transitionsBeforeShowingInterstitialAds, - onChanged: (value) { - final newRoleConfig = roleConfig.copyWith( - transitionsBeforeShowingInterstitialAds: value, - ); - final newVisibleTo = - Map.from( - config.visibleTo, - )..[role] = newRoleConfig; - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: widget.remoteConfig.adConfig.copyWith( - interstitialAdConfiguration: config.copyWith( - visibleTo: newVisibleTo, - ), - ), - ), - ); - }, - controller: - _transitionsBeforeShowingInterstitialAdsControllers[role], - ), - ), - ], - ); - } - - /// Retrieves the number of transitions before showing an interstitial ad - /// for a specific role from the configuration. - int _getTransitionsBeforeInterstitial( - InterstitialAdConfiguration config, - AppUserRole role, - ) { - return config.visibleTo[role]?.transitionsBeforeShowingInterstitialAds ?? 0; - } -} diff --git a/lib/app_configuration/widgets/maintenance_config_form.dart b/lib/app_configuration/widgets/maintenance_config_form.dart new file mode 100644 index 00000000..834ae11d --- /dev/null +++ b/lib/app_configuration/widgets/maintenance_config_form.dart @@ -0,0 +1,61 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template maintenance_config_form} +/// A form widget for configuring application maintenance settings. +/// {@endtemplate} +class MaintenanceConfigForm extends StatelessWidget { + /// {@macro maintenance_config_form} + const MaintenanceConfigForm({ + required this.remoteConfig, + required this.onConfigChanged, + super.key, + }); + + /// The current [RemoteConfig] object. + final RemoteConfig remoteConfig; + + /// Callback to notify parent of changes to the [RemoteConfig]. + final ValueChanged onConfigChanged; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + final appConfig = remoteConfig.app; + final maintenanceConfig = appConfig.maintenance; + + return Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.maintenanceConfigDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + const SizedBox(height: AppSpacing.lg), + SwitchListTile( + title: Text(l10n.isUnderMaintenanceLabel), + subtitle: Text(l10n.isUnderMaintenanceDescription), + value: maintenanceConfig.isUnderMaintenance, + onChanged: (value) { + onConfigChanged( + remoteConfig.copyWith( + app: appConfig.copyWith( + maintenance: maintenanceConfig.copyWith( + isUnderMaintenance: value, + ), + ), + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/app_configuration/widgets/navigation_ad_settings_form.dart b/lib/app_configuration/widgets/navigation_ad_settings_form.dart new file mode 100644 index 00000000..9dc382bd --- /dev/null +++ b/lib/app_configuration/widgets/navigation_ad_settings_form.dart @@ -0,0 +1,319 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_config_form_fields.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template navigation_ad_settings_form} +/// A form widget for configuring navigation ad settings. +/// {@endtemplate} +class NavigationAdSettingsForm extends StatefulWidget { + /// {@macro navigation_ad_settings_form} + const NavigationAdSettingsForm({ + required this.remoteConfig, + required this.onConfigChanged, + super.key, + }); + + /// The current [RemoteConfig] object. + final RemoteConfig remoteConfig; + + /// Callback to notify parent of changes to the [RemoteConfig]. + final ValueChanged onConfigChanged; + + @override + State createState() => + _NavigationAdSettingsFormState(); +} + +class _NavigationAdSettingsFormState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + late final Map + _internalNavigationsControllers; + late final Map + _externalNavigationsControllers; + + @override + void initState() { + super.initState(); + _tabController = TabController( + length: AppUserRole.values.length, + vsync: this, + ); + _initializeControllers(); + } + + void _initializeControllers() { + final navAdConfig = + widget.remoteConfig.features.ads.navigationAdConfiguration; + _internalNavigationsControllers = { + for (final role in AppUserRole.values) + role: TextEditingController( + text: _getInternalNavigations(navAdConfig, role).toString(), + ), + }; + _externalNavigationsControllers = { + for (final role in AppUserRole.values) + role: TextEditingController( + text: _getExternalNavigations(navAdConfig, role).toString(), + ), + }; + } + + void _updateControllers() { + final navAdConfig = + widget.remoteConfig.features.ads.navigationAdConfiguration; + for (final role in AppUserRole.values) { + final newInternalValue = _getInternalNavigations( + navAdConfig, + role, + ).toString(); + if (_internalNavigationsControllers[role]?.text != newInternalValue) { + _internalNavigationsControllers[role]?.text = newInternalValue; + } + final newExternalValue = _getExternalNavigations( + navAdConfig, + role, + ).toString(); + if (_externalNavigationsControllers[role]?.text != newExternalValue) { + _externalNavigationsControllers[role]?.text = newExternalValue; + } + } + } + + @override + void didUpdateWidget(covariant NavigationAdSettingsForm oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.remoteConfig.features.ads.navigationAdConfiguration != + oldWidget.remoteConfig.features.ads.navigationAdConfiguration) { + _updateControllers(); + } + } + + @override + void dispose() { + _tabController.dispose(); + for (final c in _internalNavigationsControllers.values) { + c.dispose(); + } + for (final c in _externalNavigationsControllers.values) { + c.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + final features = widget.remoteConfig.features; + final adConfig = features.ads; + final navAdConfig = adConfig.navigationAdConfiguration; + + return ExpansionTile( + title: Text(l10n.navigationAdConfigTitle), + children: [ + SwitchListTile( + title: Text(l10n.enableNavigationAdsLabel), + value: navAdConfig.enabled, + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + features: features.copyWith( + ads: adConfig.copyWith( + navigationAdConfiguration: navAdConfig.copyWith( + enabled: value, + ), + ), + ), + ), + ); + }, + ), + ExpansionTile( + title: Text(l10n.navigationAdFrequencyTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.navigationAdFrequencyDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + textAlign: TextAlign.start, + ), + const SizedBox(height: AppSpacing.lg), + Align( + alignment: AlignmentDirectional.centerStart, + child: SizedBox( + height: kTextTabBarHeight, + child: TabBar( + controller: _tabController, + tabAlignment: TabAlignment.start, + isScrollable: true, + tabs: AppUserRole.values + .map((role) => Tab(text: role.l10n(context))) + .toList(), + ), + ), + ), + const SizedBox(height: AppSpacing.lg), + SizedBox( + height: 350, + child: TabBarView( + controller: _tabController, + children: AppUserRole.values + .map( + (role) => _buildRoleSpecificFields( + context, + l10n, + role, + navAdConfig, + ), + ) + .toList(), + ), + ), + ], + ), + ], + ); + } + + Widget _buildRoleSpecificFields( + BuildContext context, + AppLocalizations l10n, + AppUserRole role, + NavigationAdConfiguration config, + ) { + final roleConfig = config.visibleTo[role]; + + return SingleChildScrollView( + child: Column( + children: [ + SwitchListTile( + title: Text(l10n.visibleToRoleLabel(role.l10n(context))), + value: roleConfig != null, + onChanged: (value) { + final newVisibleTo = + Map.from( + config.visibleTo, + ); + if (value) { + newVisibleTo[role] = const NavigationAdFrequencyConfig( + internalNavigationsBeforeShowingInterstitialAd: 5, + externalNavigationsBeforeShowingInterstitialAd: 1, + ); + } else { + newVisibleTo.remove(role); + } + widget.onConfigChanged( + widget.remoteConfig.copyWith( + features: widget.remoteConfig.features.copyWith( + ads: widget.remoteConfig.features.ads.copyWith( + navigationAdConfiguration: config.copyWith( + visibleTo: newVisibleTo, + ), + ), + ), + ), + ); + }, + ), + if (roleConfig != null) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.sm, + ), + child: Column( + children: [ + AppConfigIntField( + label: l10n.internalNavigationsBeforeAdLabel, + description: l10n.internalNavigationsBeforeAdDescription, + value: roleConfig + .internalNavigationsBeforeShowingInterstitialAd, + onChanged: (value) { + final newRoleConfig = roleConfig.copyWith( + internalNavigationsBeforeShowingInterstitialAd: value, + ); + final newVisibleTo = + Map.from( + config.visibleTo, + )..[role] = newRoleConfig; + widget.onConfigChanged( + widget.remoteConfig.copyWith( + features: widget.remoteConfig.features.copyWith( + ads: widget.remoteConfig.features.ads.copyWith( + navigationAdConfiguration: config.copyWith( + visibleTo: newVisibleTo, + ), + ), + ), + ), + ); + }, + controller: _internalNavigationsControllers[role], + ), + AppConfigIntField( + label: l10n.externalNavigationsBeforeAdLabel, + description: l10n.externalNavigationsBeforeAdDescription, + value: roleConfig + .externalNavigationsBeforeShowingInterstitialAd, + onChanged: (value) { + final newRoleConfig = roleConfig.copyWith( + externalNavigationsBeforeShowingInterstitialAd: value, + ); + final newVisibleTo = + Map.from( + config.visibleTo, + )..[role] = newRoleConfig; + widget.onConfigChanged( + widget.remoteConfig.copyWith( + features: widget.remoteConfig.features.copyWith( + ads: widget.remoteConfig.features.ads.copyWith( + navigationAdConfiguration: config.copyWith( + visibleTo: newVisibleTo, + ), + ), + ), + ), + ); + }, + controller: _externalNavigationsControllers[role], + ), + ], + ), + ), + ], + ), + ); + } + + int _getInternalNavigations( + NavigationAdConfiguration config, + AppUserRole role, + ) { + return config + .visibleTo[role] + ?.internalNavigationsBeforeShowingInterstitialAd ?? + 0; + } + + int _getExternalNavigations( + NavigationAdConfiguration config, + AppUserRole role, + ) { + return config + .visibleTo[role] + ?.externalNavigationsBeforeShowingInterstitialAd ?? + 0; + } +} diff --git a/lib/app_configuration/widgets/push_notification_settings_form.dart b/lib/app_configuration/widgets/push_notification_settings_form.dart index f011fa95..8c418d14 100644 --- a/lib/app_configuration/widgets/push_notification_settings_form.dart +++ b/lib/app_configuration/widgets/push_notification_settings_form.dart @@ -26,7 +26,8 @@ class PushNotificationSettingsForm extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; - final pushConfig = remoteConfig.pushNotificationConfig; + final features = remoteConfig.features; + final pushConfig = features.pushNotifications; return SingleChildScrollView( padding: const EdgeInsets.all(AppSpacing.lg), @@ -40,7 +41,9 @@ class PushNotificationSettingsForm extends StatelessWidget { onChanged: (value) { onConfigChanged( remoteConfig.copyWith( - pushNotificationConfig: pushConfig.copyWith(enabled: value), + features: features.copyWith( + pushNotifications: pushConfig.copyWith(enabled: value), + ), ), ); }, @@ -90,8 +93,10 @@ class PushNotificationSettingsForm extends StatelessWidget { onSelectionChanged: (newSelection) { onConfigChanged( remoteConfig.copyWith( - pushNotificationConfig: pushConfig.copyWith( - primaryProvider: newSelection.first, + features: remoteConfig.features.copyWith( + pushNotifications: pushConfig.copyWith( + primaryProvider: newSelection.first, + ), ), ), ); @@ -140,8 +145,10 @@ class PushNotificationSettingsForm extends StatelessWidget { newDeliveryConfigs[type] = value; onConfigChanged( remoteConfig.copyWith( - pushNotificationConfig: pushConfig.copyWith( - deliveryConfigs: newDeliveryConfigs, + features: remoteConfig.features.copyWith( + pushNotifications: pushConfig.copyWith( + deliveryConfigs: newDeliveryConfigs, + ), ), ), ); diff --git a/lib/app_configuration/widgets/saved_filter_limits_form.dart b/lib/app_configuration/widgets/saved_filter_limits_form.dart index b5467566..ad926ffa 100644 --- a/lib/app_configuration/widgets/saved_filter_limits_form.dart +++ b/lib/app_configuration/widgets/saved_filter_limits_form.dart @@ -60,8 +60,7 @@ class _SavedFilterLimitsFormState extends State @override void didUpdateWidget(covariant SavedFilterLimitsForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.remoteConfig.userPreferenceConfig != - oldWidget.remoteConfig.userPreferenceConfig) { + if (widget.remoteConfig.user.limits != oldWidget.remoteConfig.user.limits) { _updateControllerValues(); } } @@ -80,7 +79,9 @@ class _SavedFilterLimitsFormState extends State if (widget.filterType == SavedFilterType.headline) { for (final type in PushNotificationSubscriptionDeliveryType.values) { final value = limits.notificationSubscriptions?[type] ?? 0; - _controllers[role]![type.name] = _createController(value.toString()); + _controllers[role]!['notification_${type.name}'] = _createController( + value.toString(), + ); } } } @@ -102,7 +103,10 @@ class _SavedFilterLimitsFormState extends State if (widget.filterType == SavedFilterType.headline) { for (final type in PushNotificationSubscriptionDeliveryType.values) { final value = limits.notificationSubscriptions?[type] ?? 0; - _updateControllerText(_controllers[role]![type.name]!, value); + _updateControllerText( + _controllers[role]!['notification_${type.name}']!, + value, + ); } } } @@ -131,35 +135,35 @@ class _SavedFilterLimitsFormState extends State /// Retrieves the correct [SavedFilterLimits] for a given role. SavedFilterLimits _getLimitsForRole(AppUserRole role) { - final config = widget.remoteConfig.userPreferenceConfig; + final limitsConfig = widget.remoteConfig.user.limits; final limitsMap = widget.filterType == SavedFilterType.headline - ? config.savedHeadlineFiltersLimit - : config.savedSourceFiltersLimit; + ? limitsConfig.savedHeadlineFilters + : limitsConfig.savedSourceFilters; return limitsMap[role]!; } /// Updates the remote config when a value changes. void _onValueChanged(AppUserRole role, String field, int value) { - final config = widget.remoteConfig.userPreferenceConfig; + final userConfig = widget.remoteConfig.user; + final limitsConfig = userConfig.limits; final isHeadline = widget.filterType == SavedFilterType.headline; - // Create a mutable copy of the role-to-limits map. + final currentLimitsMap = isHeadline + ? limitsConfig.savedHeadlineFilters + : limitsConfig.savedSourceFilters; + final newLimitsMap = Map.from( - isHeadline - ? config.savedHeadlineFiltersLimit - : config.savedSourceFiltersLimit, + currentLimitsMap, ); - // Get the current limits for the role and create a modified copy. final currentLimits = newLimitsMap[role]!; - final SavedFilterLimits newLimits; + SavedFilterLimits newLimits; if (field == 'total') { newLimits = currentLimits.copyWith(total: value); } else if (field == 'pinned') { newLimits = currentLimits.copyWith(pinned: value); } else { - // This must be a notification subscription change. final deliveryType = PushNotificationSubscriptionDeliveryType.values .byName(field); final newSubscriptions = @@ -172,18 +176,17 @@ class _SavedFilterLimitsFormState extends State ); } - // Update the map with the new limits for the role. newLimitsMap[role] = newLimits; - // Create the updated UserPreferenceConfig. - final newUserPreferenceConfig = isHeadline - ? config.copyWith(savedHeadlineFiltersLimit: newLimitsMap) - : config.copyWith(savedSourceFiltersLimit: newLimitsMap); + final newUserLimitsConfig = isHeadline + ? limitsConfig.copyWith(savedHeadlineFilters: newLimitsMap) + : limitsConfig.copyWith(savedSourceFilters: newLimitsMap); - // Notify the parent widget. widget.onConfigChanged( widget.remoteConfig.copyWith( - userPreferenceConfig: newUserPreferenceConfig, + user: userConfig.copyWith( + limits: newUserLimitsConfig, + ), ), ); } @@ -211,9 +214,7 @@ class _SavedFilterLimitsFormState extends State ), ), const SizedBox(height: AppSpacing.lg), - // TabBarView to display role-specific fields SizedBox( - // Adjust height based on whether notification fields are shown. height: isHeadlineFilter ? 400 : 250, child: TabBarView( controller: _tabController, @@ -250,7 +251,6 @@ class _SavedFilterLimitsFormState extends State ); } - /// Builds the list of input fields for notification subscription limits. List _buildNotificationFields( AppLocalizations l10n, AppUserRole role, @@ -259,11 +259,11 @@ class _SavedFilterLimitsFormState extends State return PushNotificationSubscriptionDeliveryType.values.map((type) { final value = limits.notificationSubscriptions?[type] ?? 0; return AppConfigIntField( - label: type.l10n(context), - description: l10n.notificationSubscriptionLimitDescription, + label: l10n.notificationSubscriptionLimitLabel, + description: type.l10n(context), value: value, onChanged: (newValue) => _onValueChanged(role, type.name, newValue), - controller: _controllers[role]![type.name], + controller: _controllers[role]!['notification_${type.name}'], ); }).toList(); } diff --git a/lib/app_configuration/widgets/saved_filter_limits_section.dart b/lib/app_configuration/widgets/saved_filter_limits_section.dart index 7c86bf83..69803edf 100644 --- a/lib/app_configuration/widgets/saved_filter_limits_section.dart +++ b/lib/app_configuration/widgets/saved_filter_limits_section.dart @@ -31,13 +31,18 @@ class SavedFilterLimitsSection extends StatefulWidget { _SavedFilterLimitsSectionState(); } -class _SavedFilterLimitsSectionState extends State - with SingleTickerProviderStateMixin { - /// Notifier for the index of the currently expanded top-level ExpansionTile. +class _SavedFilterLimitsSectionState extends State { + /// Notifier for the index of the currently expanded nested ExpansionTile. /// /// A value of `null` means no tile is expanded. final ValueNotifier _expandedTileIndex = ValueNotifier(null); + @override + void dispose() { + _expandedTileIndex.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; diff --git a/lib/app_configuration/widgets/update_config_form.dart b/lib/app_configuration/widgets/update_config_form.dart new file mode 100644 index 00000000..6ddc890c --- /dev/null +++ b/lib/app_configuration/widgets/update_config_form.dart @@ -0,0 +1,146 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_config_form_fields.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template update_config_form} +/// A form widget for configuring application update settings. +/// {@endtemplate} +class UpdateConfigForm extends StatefulWidget { + /// {@macro update_config_form} + const UpdateConfigForm({ + required this.remoteConfig, + required this.onConfigChanged, + super.key, + }); + + /// The current [RemoteConfig] object. + final RemoteConfig remoteConfig; + + /// Callback to notify parent of changes to the [RemoteConfig]. + final ValueChanged onConfigChanged; + + @override + State createState() => _UpdateConfigFormState(); +} + +class _UpdateConfigFormState extends State { + late final TextEditingController _latestVersionController; + late final TextEditingController _iosUrlController; + late final TextEditingController _androidUrlController; + + @override + void initState() { + super.initState(); + final updateConfig = widget.remoteConfig.app.update; + _latestVersionController = TextEditingController( + text: updateConfig.latestAppVersion, + ); + _iosUrlController = TextEditingController(text: updateConfig.iosUpdateUrl); + _androidUrlController = TextEditingController( + text: updateConfig.androidUpdateUrl, + ); + } + + @override + void didUpdateWidget(covariant UpdateConfigForm oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.remoteConfig.app.update != oldWidget.remoteConfig.app.update) { + final updateConfig = widget.remoteConfig.app.update; + _latestVersionController.text = updateConfig.latestAppVersion; + _iosUrlController.text = updateConfig.iosUpdateUrl; + _androidUrlController.text = updateConfig.androidUpdateUrl; + } + } + + @override + void dispose() { + _latestVersionController.dispose(); + _iosUrlController.dispose(); + _androidUrlController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + final appConfig = widget.remoteConfig.app; + final updateConfig = appConfig.update; + + return Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.updateConfigDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + const SizedBox(height: AppSpacing.lg), + AppConfigTextField( + label: l10n.latestAppVersionLabel, + description: l10n.latestAppVersionDescription, + value: updateConfig.latestAppVersion, + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + app: appConfig.copyWith( + update: updateConfig.copyWith(latestAppVersion: value), + ), + ), + ); + }, + controller: _latestVersionController, + ), + SwitchListTile( + title: Text(l10n.isLatestVersionOnlyLabel), + subtitle: Text(l10n.isLatestVersionOnlyDescription), + value: updateConfig.isLatestVersionOnly, + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + app: appConfig.copyWith( + update: updateConfig.copyWith(isLatestVersionOnly: value), + ), + ), + ); + }, + ), + AppConfigTextField( + label: l10n.iosUpdateUrlLabel, + description: l10n.iosUpdateUrlDescription, + value: updateConfig.iosUpdateUrl, + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + app: appConfig.copyWith( + update: updateConfig.copyWith(iosUpdateUrl: value), + ), + ), + ); + }, + controller: _iosUrlController, + ), + AppConfigTextField( + label: l10n.androidUpdateUrlLabel, + description: l10n.androidUpdateUrlDescription, + value: updateConfig.androidUpdateUrl, + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + app: appConfig.copyWith( + update: updateConfig.copyWith(androidUpdateUrl: value), + ), + ), + ); + }, + controller: _androidUrlController, + ), + ], + ), + ); + } +} diff --git a/lib/app_configuration/widgets/user_limits_config_form.dart b/lib/app_configuration/widgets/user_limits_config_form.dart new file mode 100644 index 00000000..6916eb09 --- /dev/null +++ b/lib/app_configuration/widgets/user_limits_config_form.dart @@ -0,0 +1,205 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_config_form_fields.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template user_limits_config_form} +/// A form widget for configuring user content limits based on role. +/// +/// This widget uses a [TabBar] to allow selection of an [AppUserRole] +/// and then renders input fields for that role's limits. +/// {@endtemplate} +class UserLimitsConfigForm extends StatefulWidget { + /// {@macro user_limits_config_form} + const UserLimitsConfigForm({ + required this.remoteConfig, + required this.onConfigChanged, + super.key, + }); + + /// The current [RemoteConfig] object. + final RemoteConfig remoteConfig; + + /// Callback to notify parent of changes to the [RemoteConfig]. + final ValueChanged onConfigChanged; + + @override + State createState() => _UserLimitsConfigFormState(); +} + +class _UserLimitsConfigFormState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + late final Map + _followedItemsLimitControllers; + late final Map + _savedHeadlinesLimitControllers; + + @override + void initState() { + super.initState(); + _tabController = TabController( + length: AppUserRole.values.length, + vsync: this, + ); + _initializeControllers(); + } + + @override + void didUpdateWidget(covariant UserLimitsConfigForm oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.remoteConfig.user.limits != oldWidget.remoteConfig.user.limits) { + _updateControllers(); + } + } + + void _initializeControllers() { + final limits = widget.remoteConfig.user.limits; + _followedItemsLimitControllers = { + for (final role in AppUserRole.values) + role: TextEditingController( + text: (limits.followedItems[role] ?? 0).toString(), + ), + }; + _savedHeadlinesLimitControllers = { + for (final role in AppUserRole.values) + role: TextEditingController( + text: (limits.savedHeadlines[role] ?? 0).toString(), + ), + }; + } + + void _updateControllers() { + final limits = widget.remoteConfig.user.limits; + for (final role in AppUserRole.values) { + final newFollowedItemsLimit = (limits.followedItems[role] ?? 0) + .toString(); + if (_followedItemsLimitControllers[role]?.text != newFollowedItemsLimit) { + _followedItemsLimitControllers[role]?.text = newFollowedItemsLimit; + } + + final newSavedHeadlinesLimit = (limits.savedHeadlines[role] ?? 0) + .toString(); + if (_savedHeadlinesLimitControllers[role]?.text != + newSavedHeadlinesLimit) { + _savedHeadlinesLimitControllers[role]?.text = newSavedHeadlinesLimit; + } + } + } + + @override + void dispose() { + _tabController.dispose(); + for (final c in _followedItemsLimitControllers.values) { + c.dispose(); + } + for (final c in _savedHeadlinesLimitControllers.values) { + c.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), + child: Text( + l10n.userContentLimitsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + ), + const SizedBox(height: AppSpacing.lg), + Align( + alignment: AlignmentDirectional.centerStart, + child: SizedBox( + height: kTextTabBarHeight, + child: TabBar( + controller: _tabController, + tabAlignment: TabAlignment.start, + isScrollable: true, + tabs: AppUserRole.values + .map((role) => Tab(text: role.l10n(context))) + .toList(), + ), + ), + ), + const SizedBox(height: AppSpacing.lg), + SizedBox( + height: 250, + child: TabBarView( + controller: _tabController, + children: AppUserRole.values + .map( + (role) => _buildRoleSpecificFields( + context, + l10n, + role, + widget.remoteConfig.user.limits, + ), + ) + .toList(), + ), + ), + ], + ); + } + + Widget _buildRoleSpecificFields( + BuildContext context, + AppLocalizations l10n, + AppUserRole role, + UserLimitsConfig limits, + ) { + return SingleChildScrollView( + child: Column( + children: [ + AppConfigIntField( + label: l10n.followedItemsLimitLabel, + description: l10n.followedItemsLimitDescription, + value: limits.followedItems[role] ?? 0, + onChanged: (value) { + final newLimits = Map.from(limits.followedItems) + ..[role] = value; + widget.onConfigChanged( + widget.remoteConfig.copyWith( + user: widget.remoteConfig.user.copyWith( + limits: limits.copyWith(followedItems: newLimits), + ), + ), + ); + }, + controller: _followedItemsLimitControllers[role], + ), + AppConfigIntField( + label: l10n.savedHeadlinesLimitLabel, + description: l10n.savedHeadlinesLimitDescription, + value: limits.savedHeadlines[role] ?? 0, + onChanged: (value) { + final newLimits = Map.from( + limits.savedHeadlines, + )..[role] = value; + widget.onConfigChanged( + widget.remoteConfig.copyWith( + user: widget.remoteConfig.user.copyWith( + limits: limits.copyWith(savedHeadlines: newLimits), + ), + ), + ); + }, + controller: _savedHeadlinesLimitControllers[role], + ), + ], + ), + ); + } +} diff --git a/lib/app_configuration/widgets/user_preference_limits_form.dart b/lib/app_configuration/widgets/user_preference_limits_form.dart index 03be00d0..516805b3 100644 --- a/lib/app_configuration/widgets/user_preference_limits_form.dart +++ b/lib/app_configuration/widgets/user_preference_limits_form.dart @@ -52,53 +52,44 @@ class _UserPreferenceLimitsFormState extends State @override void didUpdateWidget(covariant UserPreferenceLimitsForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.remoteConfig.userPreferenceConfig != - oldWidget.remoteConfig.userPreferenceConfig) { + if (widget.remoteConfig.user.limits != oldWidget.remoteConfig.user.limits) { _updateControllers(); } } void _initializeControllers() { + final limitsConfig = widget.remoteConfig.user.limits; _followedItemsLimitControllers = { for (final role in AppUserRole.values) role: TextEditingController( - text: _getFollowedItemsLimit( - widget.remoteConfig.userPreferenceConfig, - role, - ).toString(), + text: (limitsConfig.followedItems[role] ?? 0).toString(), ) ..selection = TextSelection.collapsed( - offset: _getFollowedItemsLimit( - widget.remoteConfig.userPreferenceConfig, - role, - ).toString().length, + offset: (limitsConfig.followedItems[role] ?? 0) + .toString() + .length, ), }; _savedHeadlinesLimitControllers = { for (final role in AppUserRole.values) role: TextEditingController( - text: _getSavedHeadlinesLimit( - widget.remoteConfig.userPreferenceConfig, - role, - ).toString(), + text: (limitsConfig.savedHeadlines[role] ?? 0).toString(), ) ..selection = TextSelection.collapsed( - offset: _getSavedHeadlinesLimit( - widget.remoteConfig.userPreferenceConfig, - role, - ).toString().length, + offset: (limitsConfig.savedHeadlines[role] ?? 0) + .toString() + .length, ), }; } void _updateControllers() { + final limitsConfig = widget.remoteConfig.user.limits; for (final role in AppUserRole.values) { - final newFollowedItemsLimit = _getFollowedItemsLimit( - widget.remoteConfig.userPreferenceConfig, - role, - ).toString(); + final newFollowedItemsLimit = (limitsConfig.followedItems[role] ?? 0) + .toString(); if (_followedItemsLimitControllers[role]?.text != newFollowedItemsLimit) { _followedItemsLimitControllers[role]?.text = newFollowedItemsLimit; _followedItemsLimitControllers[role]?.selection = @@ -107,10 +98,8 @@ class _UserPreferenceLimitsFormState extends State ); } - final newSavedHeadlinesLimit = _getSavedHeadlinesLimit( - widget.remoteConfig.userPreferenceConfig, - role, - ).toString(); + final newSavedHeadlinesLimit = (limitsConfig.savedHeadlines[role] ?? 0) + .toString(); if (_savedHeadlinesLimitControllers[role]?.text != newSavedHeadlinesLimit) { _savedHeadlinesLimitControllers[role]?.text = newSavedHeadlinesLimit; @@ -136,7 +125,7 @@ class _UserPreferenceLimitsFormState extends State @override Widget build(BuildContext context) { - final userPreferenceConfig = widget.remoteConfig.userPreferenceConfig; + final limitsConfig = widget.remoteConfig.user.limits; final l10n = AppLocalizationsX(context).l10n; return Column( @@ -168,7 +157,7 @@ class _UserPreferenceLimitsFormState extends State context, l10n, role, - userPreferenceConfig, + limitsConfig, ), ) .toList(), @@ -182,21 +171,21 @@ class _UserPreferenceLimitsFormState extends State BuildContext context, AppLocalizations l10n, AppUserRole role, - UserPreferenceConfig config, + UserLimitsConfig config, ) { return Column( children: [ AppConfigIntField( label: _getFollowedItemsLimitLabel(l10n, role), description: _getFollowedItemsLimitDescription(l10n, role), - value: _getFollowedItemsLimit(config, role), + value: config.followedItems[role] ?? 0, onChanged: (value) { + final newLimits = Map.from(config.followedItems); + newLimits[role] = value; widget.onConfigChanged( widget.remoteConfig.copyWith( - userPreferenceConfig: _updateFollowedItemsLimit( - config, - value, - role, + user: widget.remoteConfig.user.copyWith( + limits: config.copyWith(followedItems: newLimits), ), ), ); @@ -206,14 +195,14 @@ class _UserPreferenceLimitsFormState extends State AppConfigIntField( label: _getSavedHeadlinesLimitLabel(l10n, role), description: _getSavedHeadlinesLimitDescription(l10n, role), - value: _getSavedHeadlinesLimit(config, role), + value: config.savedHeadlines[role] ?? 0, onChanged: (value) { + final newLimits = Map.from(config.savedHeadlines); + newLimits[role] = value; widget.onConfigChanged( widget.remoteConfig.copyWith( - userPreferenceConfig: _updateSavedHeadlinesLimit( - config, - value, - role, + user: widget.remoteConfig.user.copyWith( + limits: config.copyWith(savedHeadlines: newLimits), ), ), ); @@ -273,54 +262,4 @@ class _UserPreferenceLimitsFormState extends State return l10n.premiumSavedHeadlinesLimitDescription; } } - - /// Retrieves the followed items limit for a given [AppUserRole] from the map. - int _getFollowedItemsLimit(UserPreferenceConfig config, AppUserRole role) { - // The '!' is safe as the model guarantees a value for every role. - return config.followedItemsLimit[role]!; - } - - /// Retrieves the saved headlines limit for a given [AppUserRole] from the map. - int _getSavedHeadlinesLimit(UserPreferenceConfig config, AppUserRole role) { - // The '!' is safe as the model guarantees a value for every role. - return config.savedHeadlinesLimit[role]!; - } - - /// Creates an updated [UserPreferenceConfig] with a new followed items limit - /// for a specific [AppUserRole]. - /// - /// This method creates a mutable copy of the existing map, updates the value - /// for the specified role, and then returns a new `UserPreferenceConfig` - /// with the updated map. - UserPreferenceConfig _updateFollowedItemsLimit( - UserPreferenceConfig config, - int value, - AppUserRole role, - ) { - // Create a mutable copy of the map to avoid modifying the original state. - final newLimits = Map.from(config.followedItemsLimit); - // Update the value for the specified role. - newLimits[role] = value; - // Return a new config object with the updated map. - return config.copyWith(followedItemsLimit: newLimits); - } - - /// Creates an updated [UserPreferenceConfig] with a new saved headlines limit - /// for a specific [AppUserRole]. - /// - /// This method creates a mutable copy of the existing map, updates the value - /// for the specified role, and then returns a new `UserPreferenceConfig` - /// with the updated map. - UserPreferenceConfig _updateSavedHeadlinesLimit( - UserPreferenceConfig config, - int value, - AppUserRole role, - ) { - // Create a mutable copy of the map to avoid modifying the original state. - final newLimits = Map.from(config.savedHeadlinesLimit); - // Update the value for the specified role. - newLimits[role] = value; - // Return a new config object with the updated map. - return config.copyWith(savedHeadlinesLimit: newLimits); - } } diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 6581e2f3..713b945e 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -59,7 +59,7 @@ Future bootstrap( DataClient topicsClient; DataClient sourcesClient; DataClient userContentPreferencesClient; - DataClient userAppSettingsClient; + DataClient appSettingsClient; DataClient remoteConfigClient; DataClient dashboardSummaryClient; DataClient countriesClient; @@ -90,10 +90,12 @@ Future bootstrap( getId: (i) => i.id, logger: Logger('DataInMemory'), ); - userAppSettingsClient = DataInMemory( + appSettingsClient = DataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, - logger: Logger('DataInMemory'), + logger: Logger( + 'DataInMemory', + ), ); remoteConfigClient = DataInMemory( toJson: (i) => i.toJson(), @@ -155,12 +157,12 @@ Future bootstrap( toJson: (prefs) => prefs.toJson(), logger: Logger('DataApi'), ); - userAppSettingsClient = DataApi( + appSettingsClient = DataApi( httpClient: httpClient, - modelName: 'user_app_settings', - fromJson: UserAppSettings.fromJson, + modelName: 'app_settings', + fromJson: AppSettings.fromJson, toJson: (settings) => settings.toJson(), - logger: Logger('DataApi'), + logger: Logger('DataApi'), ); remoteConfigClient = DataApi( httpClient: httpClient, @@ -213,8 +215,8 @@ Future bootstrap( DataRepository( dataClient: userContentPreferencesClient, ); - final userAppSettingsRepository = DataRepository( - dataClient: userAppSettingsClient, + final appSettingsRepository = DataRepository( + dataClient: appSettingsClient, ); final remoteConfigRepository = DataRepository( dataClient: remoteConfigClient, @@ -235,7 +237,7 @@ Future bootstrap( headlinesRepository: headlinesRepository, topicsRepository: topicsRepository, sourcesRepository: sourcesRepository, - userAppSettingsRepository: userAppSettingsRepository, + appSettingsRepository: appSettingsRepository, userContentPreferencesRepository: userContentPreferencesRepository, remoteConfigRepository: remoteConfigRepository, dashboardSummaryRepository: dashboardSummaryRepository, 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 c912cfa5..b1d6c998 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -17,7 +17,6 @@ class CreateHeadlineBloc }) : _headlinesRepository = headlinesRepository, super(const CreateHeadlineState()) { on(_onTitleChanged); - on(_onExcerptChanged); on(_onUrlChanged); on(_onImageUrlChanged); on(_onSourceChanged); @@ -39,13 +38,6 @@ class CreateHeadlineBloc emit(state.copyWith(title: event.title)); } - void _onExcerptChanged( - CreateHeadlineExcerptChanged event, - Emitter emit, - ) { - emit(state.copyWith(excerpt: event.excerpt)); - } - void _onUrlChanged( CreateHeadlineUrlChanged event, Emitter emit, @@ -99,7 +91,6 @@ class CreateHeadlineBloc final newHeadline = Headline( id: _uuid.v4(), title: state.title, - excerpt: state.excerpt, url: state.url, imageUrl: state.imageUrl, source: state.source!, @@ -141,7 +132,6 @@ class CreateHeadlineBloc final newHeadline = Headline( id: _uuid.v4(), title: state.title, - excerpt: state.excerpt, url: state.url, imageUrl: state.imageUrl, source: state.source!, diff --git a/lib/content_management/bloc/create_headline/create_headline_event.dart b/lib/content_management/bloc/create_headline/create_headline_event.dart index ddd08832..68d4299b 100644 --- a/lib/content_management/bloc/create_headline/create_headline_event.dart +++ b/lib/content_management/bloc/create_headline/create_headline_event.dart @@ -16,14 +16,6 @@ final class CreateHeadlineTitleChanged extends CreateHeadlineEvent { List get props => [title]; } -/// Event for when the headline's excerpt is changed. -final class CreateHeadlineExcerptChanged extends CreateHeadlineEvent { - const CreateHeadlineExcerptChanged(this.excerpt); - final String excerpt; - @override - List get props => [excerpt]; -} - /// Event for when the headline's URL is changed. final class CreateHeadlineUrlChanged extends CreateHeadlineEvent { const CreateHeadlineUrlChanged(this.url); 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 97c06d0c..e1d2cfbd 100644 --- a/lib/content_management/bloc/create_headline/create_headline_state.dart +++ b/lib/content_management/bloc/create_headline/create_headline_state.dart @@ -23,7 +23,6 @@ final class CreateHeadlineState extends Equatable { const CreateHeadlineState({ this.status = CreateHeadlineStatus.initial, this.title = '', - this.excerpt = '', this.url = '', this.imageUrl = '', this.source, @@ -36,7 +35,6 @@ final class CreateHeadlineState extends Equatable { final CreateHeadlineStatus status; final String title; - final String excerpt; final String url; final String imageUrl; final Source? source; @@ -49,18 +47,16 @@ final class CreateHeadlineState extends Equatable { /// Returns true if the form is valid and can be submitted. bool get isFormValid => title.isNotEmpty && - excerpt.isNotEmpty && url.isNotEmpty && imageUrl.isNotEmpty && source != null && topic != null && eventCountry != null && - !isBreaking; // If breaking, it must be published, not drafted. + !isBreaking; CreateHeadlineState copyWith({ CreateHeadlineStatus? status, String? title, - String? excerpt, String? url, String? imageUrl, ValueGetter? source, @@ -73,7 +69,6 @@ final class CreateHeadlineState extends Equatable { return CreateHeadlineState( status: status ?? this.status, title: title ?? this.title, - excerpt: excerpt ?? this.excerpt, url: url ?? this.url, imageUrl: imageUrl ?? this.imageUrl, source: source != null ? source() : this.source, @@ -89,7 +84,6 @@ final class CreateHeadlineState extends Equatable { List get props => [ status, title, - excerpt, url, imageUrl, source, 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 e89da9e8..5211f5db 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart @@ -22,7 +22,6 @@ class EditHeadlineBloc extends Bloc { ) { on(_onEditHeadlineLoaded); on(_onTitleChanged); - on(_onExcerptChanged); on(_onUrlChanged); on(_onImageUrlChanged); on(_onSourceChanged); @@ -47,7 +46,6 @@ class EditHeadlineBloc extends Bloc { state.copyWith( status: EditHeadlineStatus.initial, title: headline.title, - excerpt: headline.excerpt, url: headline.url, imageUrl: headline.imageUrl, source: () => headline.source, @@ -77,18 +75,6 @@ class EditHeadlineBloc extends Bloc { ); } - void _onExcerptChanged( - EditHeadlineExcerptChanged event, - Emitter emit, - ) { - emit( - state.copyWith( - excerpt: event.excerpt, - status: EditHeadlineStatus.initial, - ), - ); - } - void _onUrlChanged( EditHeadlineUrlChanged event, Emitter emit, @@ -168,7 +154,6 @@ class EditHeadlineBloc extends Bloc { ); final updatedHeadline = originalHeadline.copyWith( title: state.title, - excerpt: state.excerpt, url: state.url, imageUrl: state.imageUrl, source: state.source, @@ -213,7 +198,6 @@ class EditHeadlineBloc extends Bloc { ); final updatedHeadline = originalHeadline.copyWith( title: state.title, - excerpt: state.excerpt, url: state.url, imageUrl: state.imageUrl, source: state.source, diff --git a/lib/content_management/bloc/edit_headline/edit_headline_event.dart b/lib/content_management/bloc/edit_headline/edit_headline_event.dart index fa9b1a15..ee411a4a 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_event.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_event.dart @@ -21,14 +21,6 @@ final class EditHeadlineTitleChanged extends EditHeadlineEvent { List get props => [title]; } -/// Event for when the headline's excerpt is changed. -final class EditHeadlineExcerptChanged extends EditHeadlineEvent { - const EditHeadlineExcerptChanged(this.excerpt); - final String excerpt; - @override - List get props => [excerpt]; -} - /// Event for when the headline's URL is changed. final class EditHeadlineUrlChanged extends EditHeadlineEvent { const EditHeadlineUrlChanged(this.url); 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 87667319..e30680be 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_state.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_state.dart @@ -24,7 +24,6 @@ final class EditHeadlineState extends Equatable { required this.headlineId, this.status = EditHeadlineStatus.initial, this.title = '', - this.excerpt = '', this.url = '', this.imageUrl = '', this.source, @@ -38,7 +37,6 @@ final class EditHeadlineState extends Equatable { final EditHeadlineStatus status; final String headlineId; final String title; - final String excerpt; final String url; final String imageUrl; final Source? source; @@ -52,7 +50,6 @@ final class EditHeadlineState extends Equatable { bool get isFormValid => headlineId.isNotEmpty && title.isNotEmpty && - excerpt.isNotEmpty && url.isNotEmpty && imageUrl.isNotEmpty && source != null && @@ -66,7 +63,6 @@ final class EditHeadlineState extends Equatable { EditHeadlineStatus? status, String? headlineId, String? title, - String? excerpt, String? url, String? imageUrl, ValueGetter? source, @@ -80,7 +76,6 @@ final class EditHeadlineState extends Equatable { status: status ?? this.status, headlineId: headlineId ?? this.headlineId, title: title ?? this.title, - excerpt: excerpt ?? this.excerpt, url: url ?? this.url, imageUrl: imageUrl ?? this.imageUrl, source: source != null ? source() : this.source, @@ -97,7 +92,6 @@ final class EditHeadlineState extends Equatable { status, headlineId, title, - excerpt, url, imageUrl, source, diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index 0cdd7f39..794aa238 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -38,7 +38,6 @@ class _CreateHeadlineView extends StatefulWidget { class _CreateHeadlineViewState extends State<_CreateHeadlineView> { final _formKey = GlobalKey(); late final TextEditingController _titleController; - late final TextEditingController _excerptController; late final TextEditingController _urlController; late final TextEditingController _imageUrlController; @@ -47,7 +46,6 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { super.initState(); final state = context.read().state; _titleController = TextEditingController(text: state.title); - _excerptController = TextEditingController(text: state.excerpt); _urlController = TextEditingController(text: state.url); _imageUrlController = TextEditingController(text: state.imageUrl); } @@ -55,7 +53,6 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { @override void dispose() { _titleController.dispose(); - _excerptController.dispose(); _urlController.dispose(); _imageUrlController.dispose(); super.dispose(); @@ -164,18 +161,6 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { .add(CreateHeadlineTitleChanged(value)), ), const SizedBox(height: AppSpacing.lg), - TextFormField( - controller: _excerptController, - decoration: InputDecoration( - labelText: l10n.excerpt, - border: const OutlineInputBorder(), - ), - maxLines: 3, - onChanged: (value) => context - .read() - .add(CreateHeadlineExcerptChanged(value)), - ), - const SizedBox(height: AppSpacing.lg), TextFormField( controller: _urlController, decoration: InputDecoration( @@ -324,7 +309,6 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { bool _isSaveButtonEnabled(CreateHeadlineState state) { final allFieldsFilled = state.title.isNotEmpty && - state.excerpt.isNotEmpty && state.url.isNotEmpty && state.imageUrl.isNotEmpty && state.source != null && @@ -375,7 +359,7 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { context, l10n, ); - if (confirmBreaking != true) return; // If not confirmed, do nothing. + if (confirmBreaking != true) return; } // Dispatch the appropriate event based on user's choice. diff --git a/lib/content_management/view/edit_headline_page.dart b/lib/content_management/view/edit_headline_page.dart index 611ca305..72ed1934 100644 --- a/lib/content_management/view/edit_headline_page.dart +++ b/lib/content_management/view/edit_headline_page.dart @@ -44,7 +44,6 @@ class _EditHeadlineView extends StatefulWidget { class _EditHeadlineViewState extends State<_EditHeadlineView> { final _formKey = GlobalKey(); late final TextEditingController _titleController; - late final TextEditingController _excerptController; late final TextEditingController _urlController; late final TextEditingController _imageUrlController; @@ -52,7 +51,6 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { void initState() { super.initState(); _titleController = TextEditingController(); - _excerptController = TextEditingController(); _urlController = TextEditingController(); _imageUrlController = TextEditingController(); } @@ -60,7 +58,6 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { @override void dispose() { _titleController.dispose(); - _excerptController.dispose(); _urlController.dispose(); _imageUrlController.dispose(); super.dispose(); @@ -144,7 +141,6 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { // Update text controllers when data is loaded or changed if (state.status == EditHeadlineStatus.initial) { _titleController.text = state.title; - _excerptController.text = state.excerpt; _urlController.text = state.url; _imageUrlController.text = state.imageUrl; // No need to update a controller for `isBreaking` as it's a Switch. @@ -188,18 +184,6 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { .add(EditHeadlineTitleChanged(value)), ), const SizedBox(height: AppSpacing.lg), - TextFormField( - controller: _excerptController, - decoration: InputDecoration( - labelText: l10n.excerpt, - border: const OutlineInputBorder(), - ), - maxLines: 3, - onChanged: (value) => context - .read() - .add(EditHeadlineExcerptChanged(value)), - ), - const SizedBox(height: AppSpacing.lg), TextFormField( controller: _urlController, decoration: InputDecoration( diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e13c8776..f1fd3547 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2887,6 +2887,192 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'OneSignal'** String get pushNotificationProviderOneSignal; + + /// Tab title for global App settings. + /// + /// In en, this message translates to: + /// **'General'** + String get appTab; + + /// Tab title for Features settings (Ads, Notifications, etc.). + /// + /// In en, this message translates to: + /// **'Features'** + String get featuresTab; + + /// Tab title for User-specific settings and limits. + /// + /// In en, this message translates to: + /// **'User'** + String get userTab; + + /// Title for the Maintenance Mode configuration section. + /// + /// In en, this message translates to: + /// **'Maintenance Mode'** + String get maintenanceConfigTitle; + + /// Description for the Maintenance Mode configuration section. + /// + /// In en, this message translates to: + /// **'Enable to put the app into maintenance mode, preventing user access.'** + String get maintenanceConfigDescription; + + /// Title for the App Update configuration section. + /// + /// In en, this message translates to: + /// **'Update Settings'** + String get updateConfigTitle; + + /// Description for the App Update configuration section. + /// + /// In en, this message translates to: + /// **'Configure mandatory app updates for users.'** + String get updateConfigDescription; + + /// Title for the General App Settings configuration section. + /// + /// In en, this message translates to: + /// **'General App Settings'** + String get generalAppConfigTitle; + + /// Description for the General App Settings configuration section. + /// + /// In en, this message translates to: + /// **'Manage general application settings like Terms of Service and Privacy Policy URLs.'** + String get generalAppConfigDescription; + + /// Label for the Terms of Service URL input field. + /// + /// In en, this message translates to: + /// **'Terms of Service URL'** + String get termsOfServiceUrlLabel; + + /// Description for the Terms of Service URL input field. + /// + /// In en, this message translates to: + /// **'The URL for the application\'s Terms of Service page.'** + String get termsOfServiceUrlDescription; + + /// Label for the Privacy Policy URL input field. + /// + /// In en, this message translates to: + /// **'Privacy Policy URL'** + String get privacyPolicyUrlLabel; + + /// Description for the Privacy Policy URL input field. + /// + /// In en, this message translates to: + /// **'The URL for the application\'s Privacy Policy page.'** + String get privacyPolicyUrlDescription; + + /// Title for the Navigation (Interstitial) Ad Settings section. + /// + /// In en, this message translates to: + /// **'Navigation Ad Settings'** + String get navigationAdConfigTitle; + + /// Label for the switch to enable navigation ads. + /// + /// In en, this message translates to: + /// **'Enable Navigation Ads'** + String get enableNavigationAdsLabel; + + /// Title for the Navigation Ad Frequency settings section. + /// + /// In en, this message translates to: + /// **'Navigation Ad Frequency'** + String get navigationAdFrequencyTitle; + + /// Description for the Navigation Ad Frequency settings section. + /// + /// In en, this message translates to: + /// **'Configure how many transitions a user must make before an interstitial ad is shown, based on their role.'** + String get navigationAdFrequencyDescription; + + /// Label for the number of internal navigations before showing an ad. + /// + /// In en, this message translates to: + /// **'Internal Navigations Before Ad'** + String get internalNavigationsBeforeAdLabel; + + /// Description for the internal navigations before ad setting. + /// + /// In en, this message translates to: + /// **'The number of internal page-to-page navigations a user must make before an interstitial ad is displayed.'** + String get internalNavigationsBeforeAdDescription; + + /// Label for the number of external navigations before showing an ad. + /// + /// In en, this message translates to: + /// **'External Navigations Before Ad'** + String get externalNavigationsBeforeAdLabel; + + /// Description for the external navigations before ad setting. + /// + /// In en, this message translates to: + /// **'The number of external navigations a user must make before an interstitial ad is displayed.'** + String get externalNavigationsBeforeAdDescription; + + /// Label for the Native Ad ID input field. + /// + /// In en, this message translates to: + /// **'Native Ad ID'** + String get nativeAdIdLabel; + + /// Description for the Native Ad ID input field. + /// + /// In en, this message translates to: + /// **'The unit ID for native ads.'** + String get nativeAdIdDescription; + + /// Label for the Banner Ad ID input field. + /// + /// In en, this message translates to: + /// **'Banner Ad ID'** + String get bannerAdIdLabel; + + /// Description for the Banner Ad ID input field. + /// + /// In en, this message translates to: + /// **'The unit ID for banner ads.'** + String get bannerAdIdDescription; + + /// Label for the Interstitial Ad ID input field. + /// + /// In en, this message translates to: + /// **'Interstitial Ad ID'** + String get interstitialAdIdLabel; + + /// Description for the Interstitial Ad ID input field. + /// + /// In en, this message translates to: + /// **'The unit ID for interstitial ads.'** + String get interstitialAdIdDescription; + + /// Label for Saved Headlines Limit + /// + /// In en, this message translates to: + /// **'Saved Headlines Limit'** + String get savedHeadlinesLimitLabel; + + /// Description for Saved Headlines Limit + /// + /// In en, this message translates to: + /// **'Maximum number of headlines this user role can save.'** + String get savedHeadlinesLimitDescription; + + /// Title for the Application Update Management expansion tile. + /// + /// In en, this message translates to: + /// **'Application Update Management'** + String get appUpdateManagementTitle; + + /// Title for the Legal & General Information expansion tile. + /// + /// In en, this message translates to: + /// **'Legal & General Information'** + String get appLegalInformationTitle; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index bb30b7ce..e2099be5 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1545,4 +1545,108 @@ class AppLocalizationsAr extends AppLocalizations { @override String get pushNotificationProviderOneSignal => 'OneSignal'; + + @override + String get appTab => 'الإعدادات العامة'; + + @override + String get featuresTab => 'الميزات'; + + @override + String get userTab => 'المستخدم'; + + @override + String get maintenanceConfigTitle => 'وضع الصيانة'; + + @override + String get maintenanceConfigDescription => + 'تفعيل لوضع التطبيق في وضع الصيانة، مما يمنع وصول المستخدمين.'; + + @override + String get updateConfigTitle => 'إعدادات التحديث'; + + @override + String get updateConfigDescription => + 'تكوين تحديثات التطبيق الإلزامية للمستخدمين.'; + + @override + String get generalAppConfigTitle => 'إعدادات التطبيق العامة'; + + @override + String get generalAppConfigDescription => + 'إدارة الإعدادات العامة للتطبيق مثل روابط شروط الخدمة وسياسة الخصوصية.'; + + @override + String get termsOfServiceUrlLabel => 'رابط شروط الخدمة'; + + @override + String get termsOfServiceUrlDescription => + 'الرابط لصفحة شروط الخدمة للتطبيق.'; + + @override + String get privacyPolicyUrlLabel => 'رابط سياسة الخصوصية'; + + @override + String get privacyPolicyUrlDescription => + 'الرابط لصفحة سياسة الخصوصية للتطبيق.'; + + @override + String get navigationAdConfigTitle => 'إعدادات إعلانات التنقل'; + + @override + String get enableNavigationAdsLabel => 'تفعيل إعلانات التنقل'; + + @override + String get navigationAdFrequencyTitle => 'تكرار إعلانات التنقل'; + + @override + String get navigationAdFrequencyDescription => + 'تكوين عدد الانتقالات التي يجب أن يقوم بها المستخدم قبل عرض إعلان بيني، بناءً على دوره.'; + + @override + String get internalNavigationsBeforeAdLabel => + 'الانتقالات الداخلية قبل الإعلان'; + + @override + String get internalNavigationsBeforeAdDescription => + 'عدد الانتقالات الداخلية بين الصفحات التي يجب أن يقوم بها المستخدم قبل عرض إعلان بيني.'; + + @override + String get externalNavigationsBeforeAdLabel => + 'الانتقالات الخارجية قبل الإعلان'; + + @override + String get externalNavigationsBeforeAdDescription => + 'عدد الانتقالات الخارجية التي يجب أن يقوم بها المستخدم قبل عرض إعلان بيني.'; + + @override + String get nativeAdIdLabel => 'معرف الإعلان الأصلي'; + + @override + String get nativeAdIdDescription => 'معرف الوحدة للإعلانات الأصلية.'; + + @override + String get bannerAdIdLabel => 'معرف إعلان البانر'; + + @override + String get bannerAdIdDescription => 'معرف الوحدة لإعلانات البانر.'; + + @override + String get interstitialAdIdLabel => 'معرف الإعلان البيني'; + + @override + String get interstitialAdIdDescription => 'معرف الوحدة للإعلانات البينية.'; + + @override + String get savedHeadlinesLimitLabel => 'حد العناوين المحفوظة'; + + @override + String get savedHeadlinesLimitDescription => + 'الحد الأقصى لعدد العناوين التي يمكن لهذا الدور المستخدم حفظها.'; + + @override + String get appUpdateManagementTitle => 'إدارة تحديثات التطبيق'; + + @override + String get appLegalInformationTitle => 'المعلومات القانونية والعامة'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b4066573..7db3aeaf 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1548,4 +1548,108 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pushNotificationProviderOneSignal => 'OneSignal'; + + @override + String get appTab => 'General'; + + @override + String get featuresTab => 'Features'; + + @override + String get userTab => 'User'; + + @override + String get maintenanceConfigTitle => 'Maintenance Mode'; + + @override + String get maintenanceConfigDescription => + 'Enable to put the app into maintenance mode, preventing user access.'; + + @override + String get updateConfigTitle => 'Update Settings'; + + @override + String get updateConfigDescription => + 'Configure mandatory app updates for users.'; + + @override + String get generalAppConfigTitle => 'General App Settings'; + + @override + String get generalAppConfigDescription => + 'Manage general application settings like Terms of Service and Privacy Policy URLs.'; + + @override + String get termsOfServiceUrlLabel => 'Terms of Service URL'; + + @override + String get termsOfServiceUrlDescription => + 'The URL for the application\'s Terms of Service page.'; + + @override + String get privacyPolicyUrlLabel => 'Privacy Policy URL'; + + @override + String get privacyPolicyUrlDescription => + 'The URL for the application\'s Privacy Policy page.'; + + @override + String get navigationAdConfigTitle => 'Navigation Ad Settings'; + + @override + String get enableNavigationAdsLabel => 'Enable Navigation Ads'; + + @override + String get navigationAdFrequencyTitle => 'Navigation Ad Frequency'; + + @override + String get navigationAdFrequencyDescription => + 'Configure how many transitions a user must make before an interstitial ad is shown, based on their role.'; + + @override + String get internalNavigationsBeforeAdLabel => + 'Internal Navigations Before Ad'; + + @override + String get internalNavigationsBeforeAdDescription => + 'The number of internal page-to-page navigations a user must make before an interstitial ad is displayed.'; + + @override + String get externalNavigationsBeforeAdLabel => + 'External Navigations Before Ad'; + + @override + String get externalNavigationsBeforeAdDescription => + 'The number of external navigations a user must make before an interstitial ad is displayed.'; + + @override + String get nativeAdIdLabel => 'Native Ad ID'; + + @override + String get nativeAdIdDescription => 'The unit ID for native ads.'; + + @override + String get bannerAdIdLabel => 'Banner Ad ID'; + + @override + String get bannerAdIdDescription => 'The unit ID for banner ads.'; + + @override + String get interstitialAdIdLabel => 'Interstitial Ad ID'; + + @override + String get interstitialAdIdDescription => 'The unit ID for interstitial ads.'; + + @override + String get savedHeadlinesLimitLabel => 'Saved Headlines Limit'; + + @override + String get savedHeadlinesLimitDescription => + 'Maximum number of headlines this user role can save.'; + + @override + String get appUpdateManagementTitle => 'Application Update Management'; + + @override + String get appLegalInformationTitle => 'Legal & General Information'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index be004178..3659fbe2 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1801,8 +1801,7 @@ "subscriptionPremium": "مميز", "@subscriptionPremium": { "description": "حالة الاشتراك لمستخدم مميز" - } -, + }, "savedHeadlineFilterLimitsTitle": "حدود مرشحات العناوين المحفوظة", "@savedHeadlineFilterLimitsTitle": { "description": "عنوان قسم حدود مرشحات العناوين المحفوظة" @@ -1854,8 +1853,7 @@ "pushNotificationSubscriptionDeliveryTypeWeeklyRoundup": "حصاد الأسبوع", "@pushNotificationSubscriptionDeliveryTypeWeeklyRoundup": { "description": "تسمية لنوع توصيل إشعارات 'حصاد الأسبوع'" - } -, + }, "isBreakingNewsLabel": "وضع علامة 'خبر عاجل'", "@isBreakingNewsLabel": { "description": "تسمية مفتاح وضع علامة 'خبر عاجل' على العنوان" @@ -1947,5 +1945,129 @@ "pushNotificationProviderOneSignal": "OneSignal", "@pushNotificationProviderOneSignal": { "description": "تسمية مزود الإشعارات الفورية OneSignal" + }, + "appTab": "الإعدادات العامة", + "@appTab": { + "description": "عنوان تبويب إعدادات التطبيق العامة." + }, + "featuresTab": "الميزات", + "@featuresTab": { + "description": "عنوان تبويب إعدادات الميزات (الإعلانات، الإشعارات، إلخ)." + }, + "userTab": "المستخدم", + "@userTab": { + "description": "عنوان تبويب الإعدادات والحدود الخاصة بالمستخدم." + }, + "maintenanceConfigTitle": "وضع الصيانة", + "@maintenanceConfigTitle": { + "description": "عنوان قسم إعدادات وضع الصيانة." + }, + "maintenanceConfigDescription": "تفعيل لوضع التطبيق في وضع الصيانة، مما يمنع وصول المستخدمين.", + "@maintenanceConfigDescription": { + "description": "وصف قسم إعدادات وضع الصيانة." + }, + "updateConfigTitle": "إعدادات التحديث", + "@updateConfigTitle": { + "description": "عنوان قسم إعدادات تحديث التطبيق." + }, + "updateConfigDescription": "تكوين تحديثات التطبيق الإلزامية للمستخدمين.", + "@updateConfigDescription": { + "description": "وصف قسم إعدادات تحديث التطبيق." + }, + "generalAppConfigTitle": "إعدادات التطبيق العامة", + "@generalAppConfigTitle": { + "description": "عنوان قسم إعدادات التطبيق العامة." + }, + "generalAppConfigDescription": "إدارة الإعدادات العامة للتطبيق مثل روابط شروط الخدمة وسياسة الخصوصية.", + "@generalAppConfigDescription": { + "description": "وصف قسم إعدادات التطبيق العامة." + }, + "termsOfServiceUrlLabel": "رابط شروط الخدمة", + "@termsOfServiceUrlLabel": { + "description": "تسمية حقل إدخال رابط شروط الخدمة." + }, + "termsOfServiceUrlDescription": "الرابط لصفحة شروط الخدمة للتطبيق.", + "@termsOfServiceUrlDescription": { + "description": "وصف حقل إدخال رابط شروط الخدمة." + }, + "privacyPolicyUrlLabel": "رابط سياسة الخصوصية", + "@privacyPolicyUrlLabel": { + "description": "تسمية حقل إدخال رابط سياسة الخصوصية." + }, + "privacyPolicyUrlDescription": "الرابط لصفحة سياسة الخصوصية للتطبيق.", + "@privacyPolicyUrlDescription": { + "description": "وصف حقل إدخال رابط سياسة الخصوصية." + }, + "navigationAdConfigTitle": "إعدادات إعلانات التنقل", + "@navigationAdConfigTitle": { + "description": "عنوان قسم إعدادات إعلانات التنقل (البينية)." + }, + "enableNavigationAdsLabel": "تفعيل إعلانات التنقل", + "@enableNavigationAdsLabel": { + "description": "تسمية مفتاح تفعيل إعلانات التنقل." + }, + "navigationAdFrequencyTitle": "تكرار إعلانات التنقل", + "@navigationAdFrequencyTitle": { + "description": "عنوان قسم إعدادات تكرار إعلانات التنقل." + }, + "navigationAdFrequencyDescription": "تكوين عدد الانتقالات التي يجب أن يقوم بها المستخدم قبل عرض إعلان بيني، بناءً على دوره.", + "@navigationAdFrequencyDescription": { + "description": "وصف قسم إعدادات تكرار إعلانات التنقل." + }, + "internalNavigationsBeforeAdLabel": "الانتقالات الداخلية قبل الإعلان", + "@internalNavigationsBeforeAdLabel": { + "description": "تسمية عدد الانتقالات الداخلية قبل عرض الإعلان." + }, + "internalNavigationsBeforeAdDescription": "عدد الانتقالات الداخلية بين الصفحات التي يجب أن يقوم بها المستخدم قبل عرض إعلان بيني.", + "@internalNavigationsBeforeAdDescription": { + "description": "وصف إعداد الانتقالات الداخلية قبل الإعلان." + }, + "externalNavigationsBeforeAdLabel": "الانتقالات الخارجية قبل الإعلان", + "@externalNavigationsBeforeAdLabel": { + "description": "تسمية عدد الانتقالات الخارجية قبل عرض الإعلان." + }, + "externalNavigationsBeforeAdDescription": "عدد الانتقالات الخارجية التي يجب أن يقوم بها المستخدم قبل عرض إعلان بيني.", + "@externalNavigationsBeforeAdDescription": { + "description": "وصف إعداد الانتقالات الخارجية قبل الإعلان." + }, + "nativeAdIdLabel": "معرف الإعلان الأصلي", + "@nativeAdIdLabel": { + "description": "تسمية حقل إدخال معرف الإعلان الأصلي." + }, + "nativeAdIdDescription": "معرف الوحدة للإعلانات الأصلية.", + "@nativeAdIdDescription": { + "description": "وصف حقل إدخال معرف الإعلان الأصلي." + }, + "bannerAdIdLabel": "معرف إعلان البانر", + "@bannerAdIdLabel": { + "description": "تسمية حقل إدخال معرف إعلان البانر." + }, + "bannerAdIdDescription": "معرف الوحدة لإعلانات البانر.", + "@bannerAdIdDescription": { + "description": "وصف حقل إدخال معرف إعلان البانر." + }, + "interstitialAdIdLabel": "معرف الإعلان البيني", + "@interstitialAdIdLabel": { + "description": "تسمية حقل إدخال معرف الإعلان البيني." + }, + "interstitialAdIdDescription": "معرف الوحدة للإعلانات البينية.", + "@interstitialAdIdDescription": { + "description": "وصف حقل إدخال معرف الإعلان البيني." + }, + "savedHeadlinesLimitLabel": "حد العناوين المحفوظة", + "@savedHeadlinesLimitLabel": { + "description": "تسمية حد العناوين المحفوظة" + }, + "savedHeadlinesLimitDescription": "الحد الأقصى لعدد العناوين التي يمكن لهذا الدور المستخدم حفظها.", + "@savedHeadlinesLimitDescription": { + "description": "وصف حد العناوين المحفوظة" + }, + "appUpdateManagementTitle": "إدارة تحديثات التطبيق", + "@appUpdateManagementTitle": { + "description": "عنوان قسم إدارة تحديثات التطبيق القابل للتوسيع." + }, + "appLegalInformationTitle": "المعلومات القانونية والعامة", + "@appLegalInformationTitle": { + "description": "عنوان قسم المعلومات القانونية والعامة القابل للتوسيع." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 643aef4b..31c693d7 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1941,5 +1941,129 @@ "pushNotificationProviderOneSignal": "OneSignal", "@pushNotificationProviderOneSignal": { "description": "Label for the OneSignal push notification provider" + }, + "appTab": "General", + "@appTab": { + "description": "Tab title for global App settings." + }, + "featuresTab": "Features", + "@featuresTab": { + "description": "Tab title for Features settings (Ads, Notifications, etc.)." + }, + "userTab": "User", + "@userTab": { + "description": "Tab title for User-specific settings and limits." + }, + "maintenanceConfigTitle": "Maintenance Mode", + "@maintenanceConfigTitle": { + "description": "Title for the Maintenance Mode configuration section." + }, + "maintenanceConfigDescription": "Enable to put the app into maintenance mode, preventing user access.", + "@maintenanceConfigDescription": { + "description": "Description for the Maintenance Mode configuration section." + }, + "updateConfigTitle": "Update Settings", + "@updateConfigTitle": { + "description": "Title for the App Update configuration section." + }, + "updateConfigDescription": "Configure mandatory app updates for users.", + "@updateConfigDescription": { + "description": "Description for the App Update configuration section." + }, + "generalAppConfigTitle": "General App Settings", + "@generalAppConfigTitle": { + "description": "Title for the General App Settings configuration section." + }, + "generalAppConfigDescription": "Manage general application settings like Terms of Service and Privacy Policy URLs.", + "@generalAppConfigDescription": { + "description": "Description for the General App Settings configuration section." + }, + "termsOfServiceUrlLabel": "Terms of Service URL", + "@termsOfServiceUrlLabel": { + "description": "Label for the Terms of Service URL input field." + }, + "termsOfServiceUrlDescription": "The URL for the application's Terms of Service page.", + "@termsOfServiceUrlDescription": { + "description": "Description for the Terms of Service URL input field." + }, + "privacyPolicyUrlLabel": "Privacy Policy URL", + "@privacyPolicyUrlLabel": { + "description": "Label for the Privacy Policy URL input field." + }, + "privacyPolicyUrlDescription": "The URL for the application's Privacy Policy page.", + "@privacyPolicyUrlDescription": { + "description": "Description for the Privacy Policy URL input field." + }, + "navigationAdConfigTitle": "Navigation Ad Settings", + "@navigationAdConfigTitle": { + "description": "Title for the Navigation (Interstitial) Ad Settings section." + }, + "enableNavigationAdsLabel": "Enable Navigation Ads", + "@enableNavigationAdsLabel": { + "description": "Label for the switch to enable navigation ads." + }, + "navigationAdFrequencyTitle": "Navigation Ad Frequency", + "@navigationAdFrequencyTitle": { + "description": "Title for the Navigation Ad Frequency settings section." + }, + "navigationAdFrequencyDescription": "Configure how many transitions a user must make before an interstitial ad is shown, based on their role.", + "@navigationAdFrequencyDescription": { + "description": "Description for the Navigation Ad Frequency settings section." + }, + "internalNavigationsBeforeAdLabel": "Internal Navigations Before Ad", + "@internalNavigationsBeforeAdLabel": { + "description": "Label for the number of internal navigations before showing an ad." + }, + "internalNavigationsBeforeAdDescription": "The number of internal page-to-page navigations a user must make before an interstitial ad is displayed.", + "@internalNavigationsBeforeAdDescription": { + "description": "Description for the internal navigations before ad setting." + }, + "externalNavigationsBeforeAdLabel": "External Navigations Before Ad", + "@externalNavigationsBeforeAdLabel": { + "description": "Label for the number of external navigations before showing an ad." + }, + "externalNavigationsBeforeAdDescription": "The number of external navigations a user must make before an interstitial ad is displayed.", + "@externalNavigationsBeforeAdDescription": { + "description": "Description for the external navigations before ad setting." + }, + "nativeAdIdLabel": "Native Ad ID", + "@nativeAdIdLabel": { + "description": "Label for the Native Ad ID input field." + }, + "nativeAdIdDescription": "The unit ID for native ads.", + "@nativeAdIdDescription": { + "description": "Description for the Native Ad ID input field." + }, + "bannerAdIdLabel": "Banner Ad ID", + "@bannerAdIdLabel": { + "description": "Label for the Banner Ad ID input field." + }, + "bannerAdIdDescription": "The unit ID for banner ads.", + "@bannerAdIdDescription": { + "description": "Description for the Banner Ad ID input field." + }, + "interstitialAdIdLabel": "Interstitial Ad ID", + "@interstitialAdIdLabel": { + "description": "Label for the Interstitial Ad ID input field." + }, + "interstitialAdIdDescription": "The unit ID for interstitial ads.", + "@interstitialAdIdDescription": { + "description": "Description for the Interstitial Ad ID input field." + }, + "savedHeadlinesLimitLabel": "Saved Headlines Limit", + "@savedHeadlinesLimitLabel": { + "description": "Label for Saved Headlines Limit" + }, + "savedHeadlinesLimitDescription": "Maximum number of headlines this user role can save.", + "@savedHeadlinesLimitDescription": { + "description": "Description for Saved Headlines Limit" + }, + "appUpdateManagementTitle": "Application Update Management", + "@appUpdateManagementTitle": { + "description": "Title for the Application Update Management expansion tile." + }, + "appLegalInformationTitle": "Legal & General Information", + "@appLegalInformationTitle": { + "description": "Title for the Legal & General Information expansion tile." } } \ No newline at end of file diff --git a/lib/router/router.dart b/lib/router/router.dart index 9ef24c32..49bb22dc 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -1,5 +1,5 @@ import 'package:auth_repository/auth_repository.dart'; -import 'package:core/core.dart' hide AppStatus; +import 'package:core/core.dart'; import 'package:data_repository/data_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/settings/bloc/settings_bloc.dart b/lib/settings/bloc/settings_bloc.dart index 56044942..8e68db67 100644 --- a/lib/settings/bloc/settings_bloc.dart +++ b/lib/settings/bloc/settings_bloc.dart @@ -10,8 +10,8 @@ part 'settings_state.dart'; class SettingsBloc extends Bloc { SettingsBloc({ - required DataRepository userAppSettingsRepository, - }) : _userAppSettingsRepository = userAppSettingsRepository, + required DataRepository appSettingsRepository, + }) : _appSettingsRepository = appSettingsRepository, super(const SettingsInitial()) { on(_onSettingsLoaded); on(_onSettingsBaseThemeChanged); @@ -22,22 +22,22 @@ class SettingsBloc extends Bloc { on(_onSettingsLanguageChanged); } - final DataRepository _userAppSettingsRepository; + final DataRepository _appSettingsRepository; Future _onSettingsLoaded( SettingsLoaded event, Emitter emit, ) async { - emit(SettingsLoadInProgress(userAppSettings: state.userAppSettings)); + emit(SettingsLoadInProgress(appSettings: state.appSettings)); try { - final userAppSettings = await _userAppSettingsRepository.read( + final appSettings = await _appSettingsRepository.read( id: event.userId!, ); - emit(SettingsLoadSuccess(userAppSettings: userAppSettings)); + emit(SettingsLoadSuccess(appSettings: appSettings)); } on NotFoundException { // If settings are not found, create default settings for the user. // This ensures that a user always has a valid settings object. - final defaultSettings = UserAppSettings( + final defaultSettings = AppSettings( id: event.userId!, displaySettings: const DisplaySettings( baseTheme: AppBaseTheme.system, @@ -52,45 +52,44 @@ class SettingsBloc extends Bloc { 'Default language "en" not found in language fixtures.', ), ), - feedPreferences: const FeedDisplayPreferences( - headlineDensity: HeadlineDensity.standard, - headlineImageStyle: HeadlineImageStyle.largeThumbnail, - showSourceInHeadlineFeed: true, - showPublishDateInHeadlineFeed: true, + feedSettings: const FeedSettings( + feedItemDensity: FeedItemDensity.standard, + feedItemImageStyle: FeedItemImageStyle.largeThumbnail, + feedItemClickBehavior: FeedItemClickBehavior.defaultBehavior, ), ); - await _userAppSettingsRepository.create(item: defaultSettings); - emit(SettingsLoadSuccess(userAppSettings: defaultSettings)); + await _appSettingsRepository.create(item: defaultSettings); + emit(SettingsLoadSuccess(appSettings: defaultSettings)); } on HttpException catch (e) { - emit(SettingsLoadFailure(e, userAppSettings: state.userAppSettings)); + emit(SettingsLoadFailure(e, appSettings: state.appSettings)); } catch (e) { emit( SettingsLoadFailure( UnknownException('An unexpected error occurred: $e'), - userAppSettings: state.userAppSettings, + appSettings: state.appSettings, ), ); } } Future _updateSettings( - UserAppSettings updatedSettings, + AppSettings updatedSettings, Emitter emit, ) async { - emit(SettingsUpdateInProgress(userAppSettings: updatedSettings)); + emit(SettingsUpdateInProgress(appSettings: updatedSettings)); try { - final result = await _userAppSettingsRepository.update( + final result = await _appSettingsRepository.update( id: updatedSettings.id, item: updatedSettings, ); - emit(SettingsUpdateSuccess(userAppSettings: result)); + emit(SettingsUpdateSuccess(appSettings: result)); } on HttpException catch (e) { - emit(SettingsUpdateFailure(e, userAppSettings: state.userAppSettings)); + emit(SettingsUpdateFailure(e, appSettings: state.appSettings)); } catch (e) { emit( SettingsUpdateFailure( UnknownException('An unexpected error occurred: $e'), - userAppSettings: state.userAppSettings, + appSettings: state.appSettings, ), ); } @@ -100,7 +99,7 @@ class SettingsBloc extends Bloc { SettingsBaseThemeChanged event, Emitter emit, ) async { - final currentSettings = state.userAppSettings; + final currentSettings = state.appSettings; if (currentSettings != null) { final updatedSettings = currentSettings.copyWith( displaySettings: currentSettings.displaySettings.copyWith( @@ -115,7 +114,7 @@ class SettingsBloc extends Bloc { SettingsAccentThemeChanged event, Emitter emit, ) async { - final currentSettings = state.userAppSettings; + final currentSettings = state.appSettings; if (currentSettings != null) { final updatedSettings = currentSettings.copyWith( displaySettings: currentSettings.displaySettings.copyWith( @@ -130,7 +129,7 @@ class SettingsBloc extends Bloc { SettingsFontFamilyChanged event, Emitter emit, ) async { - final currentSettings = state.userAppSettings; + final currentSettings = state.appSettings; if (currentSettings != null) { final updatedSettings = currentSettings.copyWith( displaySettings: currentSettings.displaySettings.copyWith( @@ -145,7 +144,7 @@ class SettingsBloc extends Bloc { SettingsTextScaleFactorChanged event, Emitter emit, ) async { - final currentSettings = state.userAppSettings; + final currentSettings = state.appSettings; if (currentSettings != null) { final updatedSettings = currentSettings.copyWith( displaySettings: currentSettings.displaySettings.copyWith( @@ -160,7 +159,7 @@ class SettingsBloc extends Bloc { SettingsFontWeightChanged event, Emitter emit, ) async { - final currentSettings = state.userAppSettings; + final currentSettings = state.appSettings; if (currentSettings != null) { final updatedSettings = currentSettings.copyWith( displaySettings: currentSettings.displaySettings.copyWith( @@ -175,7 +174,7 @@ class SettingsBloc extends Bloc { SettingsLanguageChanged event, Emitter emit, ) async { - final currentSettings = state.userAppSettings; + final currentSettings = state.appSettings; if (currentSettings != null) { final updatedSettings = currentSettings.copyWith( language: event.language, diff --git a/lib/settings/bloc/settings_state.dart b/lib/settings/bloc/settings_state.dart index 73ab964d..036a69b0 100644 --- a/lib/settings/bloc/settings_state.dart +++ b/lib/settings/bloc/settings_state.dart @@ -1,13 +1,13 @@ part of 'settings_bloc.dart'; sealed class SettingsState extends Equatable { - const SettingsState({this.userAppSettings}); + const SettingsState({this.appSettings}); /// The current user application settings. Null if not loaded or unauthenticated. - final UserAppSettings? userAppSettings; + final AppSettings? appSettings; @override - List get props => [userAppSettings]; + List get props => [appSettings]; } /// {@template settings_initial} @@ -15,7 +15,7 @@ sealed class SettingsState extends Equatable { /// {@endtemplate} final class SettingsInitial extends SettingsState { /// {@macro settings_initial} - const SettingsInitial({super.userAppSettings}); + const SettingsInitial({super.appSettings}); } /// {@template settings_load_in_progress} @@ -23,7 +23,7 @@ final class SettingsInitial extends SettingsState { /// {@endtemplate} final class SettingsLoadInProgress extends SettingsState { /// {@macro settings_load_in_progress} - const SettingsLoadInProgress({super.userAppSettings}); + const SettingsLoadInProgress({super.appSettings}); } /// {@template settings_load_success} @@ -31,7 +31,7 @@ final class SettingsLoadInProgress extends SettingsState { /// {@endtemplate} final class SettingsLoadSuccess extends SettingsState { /// {@macro settings_load_success} - const SettingsLoadSuccess({required super.userAppSettings}); + const SettingsLoadSuccess({required super.appSettings}); } /// {@template settings_load_failure} @@ -39,13 +39,13 @@ final class SettingsLoadSuccess extends SettingsState { /// {@endtemplate} final class SettingsLoadFailure extends SettingsState { /// {@macro settings_load_failure} - const SettingsLoadFailure(this.exception, {super.userAppSettings}); + const SettingsLoadFailure(this.exception, {super.appSettings}); /// The error exception describing the failure. final HttpException exception; @override - List get props => [exception, userAppSettings]; + List get props => [exception, appSettings]; } /// {@template settings_update_in_progress} @@ -53,7 +53,7 @@ final class SettingsLoadFailure extends SettingsState { /// {@endtemplate} final class SettingsUpdateInProgress extends SettingsState { /// {@macro settings_update_in_progress} - const SettingsUpdateInProgress({required super.userAppSettings}); + const SettingsUpdateInProgress({required super.appSettings}); } /// {@template settings_update_success} @@ -61,7 +61,7 @@ final class SettingsUpdateInProgress extends SettingsState { /// {@endtemplate} final class SettingsUpdateSuccess extends SettingsState { /// {@macro settings_update_success} - const SettingsUpdateSuccess({required super.userAppSettings}); + const SettingsUpdateSuccess({required super.appSettings}); } /// {@template settings_update_failure} @@ -69,11 +69,11 @@ final class SettingsUpdateSuccess extends SettingsState { /// {@endtemplate} final class SettingsUpdateFailure extends SettingsState { /// {@macro settings_update_failure} - const SettingsUpdateFailure(this.exception, {super.userAppSettings}); + const SettingsUpdateFailure(this.exception, {super.appSettings}); /// The error exception describing the failure. final HttpException exception; @override - List get props => [exception, userAppSettings]; + List get props => [exception, appSettings]; } diff --git a/lib/settings/view/settings_page.dart b/lib/settings/view/settings_page.dart index 44f140f2..4b45475c 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -20,8 +20,7 @@ class SettingsPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => SettingsBloc( - userAppSettingsRepository: context - .read>(), + appSettingsRepository: context.read>(), )..add(SettingsLoaded(userId: context.read().state.user?.id)), child: const _SettingsView(), ); @@ -86,9 +85,11 @@ class _SettingsViewState extends State<_SettingsView> { SnackBar(content: Text(l10n.settingsSavedSuccessfully)), ); // Trigger AppBloc to reload settings for immediate UI update - if (state.userAppSettings != null) { + if (state.appSettings != null) { context.read().add( - AppUserAppSettingsChanged(state.userAppSettings!), + AppUserAppSettingsChanged( + state.appSettings!, + ), ); } } else if (state is SettingsUpdateFailure) { @@ -102,8 +103,7 @@ class _SettingsViewState extends State<_SettingsView> { } }, builder: (context, state) { - if (state.userAppSettings == null && - state is! SettingsLoadInProgress) { + if (state.appSettings == null && state is! SettingsLoadInProgress) { // If settings are null and not loading, try to load them context.read().add( SettingsLoaded(userId: context.read().state.user?.id), @@ -127,8 +127,8 @@ class _SettingsViewState extends State<_SettingsView> { ); }, ); - } else if (state.userAppSettings != null) { - final userAppSettings = state.userAppSettings!; + } else if (state.appSettings != null) { + final appSettings = state.appSettings!; return ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ @@ -159,7 +159,7 @@ class _SettingsViewState extends State<_SettingsView> { title: l10n.baseThemeLabel, description: l10n.baseThemeDescription, child: DropdownButton( - value: userAppSettings.displaySettings.baseTheme, + value: appSettings.displaySettings.baseTheme, onChanged: (value) { if (value != null) { context.read().add( @@ -185,7 +185,7 @@ class _SettingsViewState extends State<_SettingsView> { title: l10n.accentThemeLabel, description: l10n.accentThemeDescription, child: DropdownButton( - value: userAppSettings.displaySettings.accentTheme, + value: appSettings.displaySettings.accentTheme, onChanged: (value) { if (value != null) { context.read().add( @@ -218,7 +218,7 @@ class _SettingsViewState extends State<_SettingsView> { title: l10n.fontFamilyLabel, description: l10n.fontFamilyDescription, child: DropdownButton( - value: userAppSettings.displaySettings.fontFamily, + value: appSettings.displaySettings.fontFamily, onChanged: (value) { if (value != null) { context.read().add( @@ -242,8 +242,7 @@ class _SettingsViewState extends State<_SettingsView> { title: l10n.textScaleFactorLabel, description: l10n.textScaleFactorDescription, child: DropdownButton( - value: - userAppSettings.displaySettings.textScaleFactor, + value: appSettings.displaySettings.textScaleFactor, onChanged: (value) { if (value != null) { context.read().add( @@ -269,7 +268,7 @@ class _SettingsViewState extends State<_SettingsView> { title: l10n.fontWeightLabel, description: l10n.fontWeightDescription, child: DropdownButton( - value: userAppSettings.displaySettings.fontWeight, + value: appSettings.displaySettings.fontWeight, onChanged: (value) { if (value != null) { context.read().add( @@ -316,7 +315,7 @@ class _SettingsViewState extends State<_SettingsView> { horizontal: AppSpacing.xxl, ), child: _LanguageSelectionList( - currentLanguage: userAppSettings.language, + currentLanguage: appSettings.language, l10n: l10n, ), ), diff --git a/lib/shared/extensions/extensions.dart b/lib/shared/extensions/extensions.dart index bac9865f..a5956785 100644 --- a/lib/shared/extensions/extensions.dart +++ b/lib/shared/extensions/extensions.dart @@ -5,7 +5,6 @@ export 'banner_ad_shape_l10n.dart'; export 'content_status_l10n.dart'; export 'dashboard_user_role_l10n.dart'; export 'feed_decorator_type_l10n.dart'; -export 'in_article_ad_slot_type_l10n.dart'; export 'push_notification_provider_l10n.dart'; export 'push_notification_subscription_delivery_type_l10n.dart'; export 'source_type_l10n.dart'; diff --git a/lib/shared/extensions/in_article_ad_slot_type_l10n.dart b/lib/shared/extensions/in_article_ad_slot_type_l10n.dart deleted file mode 100644 index 9729a26b..00000000 --- a/lib/shared/extensions/in_article_ad_slot_type_l10n.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:core/core.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; - -/// {@template in_article_ad_slot_type_l10n} -/// Extension on [InArticleAdSlotType] to provide localized string representations. -/// {@endtemplate} -extension InArticleAdSlotTypeL10n on InArticleAdSlotType { - /// Returns the localized name for an [InArticleAdSlotType]. - String l10n(BuildContext context) { - final l10n = context.l10n; - switch (this) { - case InArticleAdSlotType.aboveArticleContinueReadingButton: - return l10n.inArticleAdSlotTypeAboveArticleContinueReadingButton; - case InArticleAdSlotType.belowArticleContinueReadingButton: - return l10n.inArticleAdSlotTypeBelowArticleContinueReadingButton; - } - } -} diff --git a/lib/shared/widgets/selection_page/bloc/searchable_selection_bloc.dart b/lib/shared/widgets/selection_page/bloc/searchable_selection_bloc.dart index f27e100c..da32674f 100644 --- a/lib/shared/widgets/selection_page/bloc/searchable_selection_bloc.dart +++ b/lib/shared/widgets/selection_page/bloc/searchable_selection_bloc.dart @@ -99,7 +99,9 @@ class SearchableSelectionBloc final response = await (_arguments.repository!).readAll( filter: finalFilter, sort: _arguments.sortOptions, - pagination: PaginationOptions(limit: _arguments.limit), + pagination: const PaginationOptions( + limit: 20, + ), // Do not lower it below 20 for the initial fetch, if the list items did not reach the bottom of the screen, the infinity scrolling will not function. ); fetchedItems = response.items; diff --git a/pubspec.lock b/pubspec.lock index e0782b92..489f590e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,8 +89,8 @@ packages: dependency: "direct main" description: path: "." - ref: "064c4387b3f7df835565c41c918dc2d80dd2f49a" - resolved-ref: "064c4387b3f7df835565c41c918dc2d80dd2f49a" + ref: "3779a8b1dbd8450d524574cf5376b7cc2ed514e7" + resolved-ref: "3779a8b1dbd8450d524574cf5376b7cc2ed514e7" url: "https://github.com/flutter-news-app-full-source-code/core.git" source: git version: "1.3.1" diff --git a/pubspec.yaml b/pubspec.yaml index 222f4faa..10824d60 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,7 +94,7 @@ dependency_overrides: core: git: url: https://github.com/flutter-news-app-full-source-code/core.git - ref: 064c4387b3f7df835565c41c918dc2d80dd2f49a + ref: 3779a8b1dbd8450d524574cf5376b7cc2ed514e7 http_client: git: url: https://github.com/flutter-news-app-full-source-code/http-client.git