diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index 34cb31ad..0d183a26 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -1,8 +1,8 @@ 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/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/system_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'; @@ -65,7 +65,7 @@ class _AppConfigurationPageState extends State tabAlignment: TabAlignment.start, isScrollable: true, tabs: [ - Tab(text: l10n.appTab), + Tab(text: l10n.systemTab), Tab(text: l10n.featuresTab), Tab(text: l10n.userTab), ], @@ -132,7 +132,7 @@ class _AppConfigurationPageState extends State return TabBarView( controller: _tabController, children: [ - AppConfigurationTab( + SystemConfigurationTab( remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( diff --git a/lib/app_configuration/view/tabs/app_configuration_tab.dart b/lib/app_configuration/view/tabs/app_configuration_tab.dart deleted file mode 100644 index fd5d81f9..00000000 --- a/lib/app_configuration/view/tabs/app_configuration_tab.dart +++ /dev/null @@ -1,119 +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/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/features_configuration_tab.dart b/lib/app_configuration/view/tabs/features_configuration_tab.dart index fac84584..18a17739 100644 --- a/lib/app_configuration/view/tabs/features_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -8,6 +8,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuratio 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:flutter_news_app_web_dashboard_full_source_code/shared/extensions/feed_item_click_behavior_l10n.dart'; import 'package:ui_kit/ui_kit.dart'; /// {@template features_configuration_tab} @@ -66,6 +67,12 @@ class _FeaturesConfigurationTabState extends State { _expandedTileIndex.value = isExpanded ? tileIndex : null; }, initiallyExpanded: expandedIndex == tileIndex, + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, children: [ AdConfigForm( remoteConfig: widget.remoteConfig, @@ -104,6 +111,12 @@ class _FeaturesConfigurationTabState extends State { _expandedTileIndex.value = isExpanded ? tileIndex : null; }, initiallyExpanded: expandedIndex == tileIndex, + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, children: [ PushNotificationSettingsForm( remoteConfig: widget.remoteConfig, @@ -115,54 +128,118 @@ class _FeaturesConfigurationTabState extends State { ), const SizedBox(height: AppSpacing.lg), - // Feed Decorators + // Feed ValueListenableBuilder( valueListenable: _expandedTileIndex, builder: (context, expandedIndex, child) { const tileIndex = 2; return ExpansionTile( - key: ValueKey('feedDecoratorsTile_$expandedIndex'), - title: Text(l10n.feedDecoratorsTitle), + key: ValueKey('feedTile_$expandedIndex'), + title: Text(l10n.feedTab), + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, 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: [ - Text( - l10n.feedDecoratorsDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), + ExpansionTile( + title: Text(l10n.feedItemClickBehaviorTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.feedItemClickBehaviorDescription, + 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: SegmentedButton( + segments: FeedItemClickBehavior.values + .where( + (b) => b != FeedItemClickBehavior.defaultBehavior, + ) + .map( + (behavior) => + ButtonSegment( + value: behavior, + label: Text(behavior.l10n(context)), + ), + ) + .toList(), + selected: { + widget.remoteConfig.features.feed.itemClickBehavior, + }, + onSelectionChanged: (newSelection) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + features: widget.remoteConfig.features.copyWith( + feed: widget.remoteConfig.features.feed + .copyWith( + itemClickBehavior: newSelection.first, + ), + ), + ), + ); + }, + ), + ), + ], ), const SizedBox(height: AppSpacing.lg), - for (final decoratorType in FeedDecoratorType.values) - Padding( - padding: const EdgeInsets.only(bottom: AppSpacing.md), - child: ExpansionTile( - title: Text(decoratorType.l10n(context)), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.xl, - top: AppSpacing.md, - bottom: AppSpacing.md, + ExpansionTile( + title: Text(l10n.feedDecoratorsTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.feedDecoratorsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - FeedDecoratorForm( - decoratorType: decoratorType, - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), - ], ), - ), + const SizedBox(height: AppSpacing.lg), + for (final decoratorType in FeedDecoratorType.values) + Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.md), + child: ExpansionTile( + title: Text(decoratorType.l10n(context)), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.xl, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + FeedDecoratorForm( + decoratorType: decoratorType, + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ), + ), + ], + ), ], ); }, diff --git a/lib/app_configuration/view/tabs/system_configuration_tab.dart b/lib/app_configuration/view/tabs/system_configuration_tab.dart new file mode 100644 index 00000000..bb275f45 --- /dev/null +++ b/lib/app_configuration/view/tabs/system_configuration_tab.dart @@ -0,0 +1,149 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_urls_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 system_configuration_tab} +/// A widget representing the "System" 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 SystemConfigurationTab extends StatefulWidget { + /// {@macro system_configuration_tab} + const SystemConfigurationTab({ + 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() => _SystemConfigurationTabState(); +} + +class _SystemConfigurationTabState 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: [ + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 0; + return ExpansionTile( + key: ValueKey('appStatusAndUpdatesTile_$expandedIndex'), + title: Text(l10n.appStatusAndUpdatesTitle), + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + ExpansionTile( + title: Text(l10n.maintenanceModeTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + SwitchListTile( + title: Text(l10n.isUnderMaintenanceLabel), + subtitle: Text(l10n.isUnderMaintenanceDescription), + value: widget + .remoteConfig + .app + .maintenance + .isUnderMaintenance, + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + app: widget.remoteConfig.app.copyWith( + maintenance: widget.remoteConfig.app.maintenance + .copyWith(isUnderMaintenance: value), + ), + ), + ); + }, + ), + ], + ), + const SizedBox(height: AppSpacing.lg), + ExpansionTile( + title: Text(l10n.appUpdateManagementTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + UpdateConfigForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ), + ], + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 1; + return ExpansionTile( + key: ValueKey('appUrlsTile_$expandedIndex'), + title: Text(l10n.appUrlsTitle), + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppUrlsForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ); + }, + ), + ], + ); + } +} diff --git a/lib/app_configuration/view/tabs/user_configuration_tab.dart b/lib/app_configuration/view/tabs/user_configuration_tab.dart index 6a28176b..058d5ccb 100644 --- a/lib/app_configuration/view/tabs/user_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/user_configuration_tab.dart @@ -47,45 +47,54 @@ class _UserConfigurationTabState extends State { 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), + key: ValueKey('userLimitsTile_$expandedIndex'), + title: Text(l10n.userLimitsTitle), onExpansionChanged: (isExpanded) { _expandedTileIndex.value = isExpanded ? tileIndex : null; }, initiallyExpanded: expandedIndex == tileIndex, + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, children: [ - UserLimitsConfigForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, + ExpansionTile( + title: Text(l10n.userContentLimitsTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + 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, + const SizedBox(height: AppSpacing.lg), + ExpansionTile( + title: Text(l10n.savedFeedFilterLimitsTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + SavedFilterLimitsSection( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], ), ], ); diff --git a/lib/app_configuration/widgets/ad_platform_config_form.dart b/lib/app_configuration/widgets/ad_platform_config_form.dart index 475dd00d..ff776fd5 100644 --- a/lib/app_configuration/widgets/ad_platform_config_form.dart +++ b/lib/app_configuration/widgets/ad_platform_config_form.dart @@ -27,15 +27,30 @@ class AdPlatformConfigForm extends StatefulWidget { State createState() => _AdPlatformConfigFormState(); } -class _AdPlatformConfigFormState extends State { +class _AdPlatformConfigFormState extends State + with SingleTickerProviderStateMixin { late AdPlatformType _selectedPlatform; late Map> _platformAdIdentifierControllers; + late final TabController _tabController; @override void initState() { super.initState(); _selectedPlatform = widget.remoteConfig.features.ads.primaryAdPlatform; + _tabController = TabController( + length: AdPlatformType.values.length, + vsync: this, + ); + _tabController.index = AdPlatformType.values.indexOf(_selectedPlatform); + + _tabController.addListener(() { + if (!_tabController.indexIsChanging) { + setState( + () => _selectedPlatform = AdPlatformType.values[_tabController.index], + ); + } + }); _initializeControllers(); } @@ -117,6 +132,7 @@ class _AdPlatformConfigFormState extends State { @override void dispose() { + _tabController.dispose(); for (final platformControllers in _platformAdIdentifierControllers.values) { for (final controller in platformControllers.values) { controller.dispose(); @@ -133,7 +149,6 @@ class _AdPlatformConfigFormState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Primary Ad Platform Selection ExpansionTile( title: Text(l10n.primaryAdPlatformTitle), childrenPadding: const EdgeInsetsDirectional.only( @@ -174,6 +189,9 @@ class _AdPlatformConfigFormState extends State { onSelectionChanged: (newSelection) { setState(() { _selectedPlatform = newSelection.first; + _tabController.index = AdPlatformType.values.indexOf( + _selectedPlatform, + ); }); widget.onConfigChanged( widget.remoteConfig.copyWith( @@ -190,8 +208,6 @@ class _AdPlatformConfigFormState extends State { ], ), const SizedBox(height: AppSpacing.lg), - - // Ad Unit Identifiers ExpansionTile( title: Text(l10n.adUnitIdentifiersTitle), childrenPadding: const EdgeInsetsDirectional.only( @@ -201,6 +217,24 @@ class _AdPlatformConfigFormState extends State { ), expandedCrossAxisAlignment: CrossAxisAlignment.start, children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: SizedBox( + height: kTextTabBarHeight, + child: TabBar( + controller: _tabController, + tabAlignment: TabAlignment.start, + isScrollable: true, + tabs: AdPlatformType.values + .where( + (type) => type != AdPlatformType.demo, + ) + .map((platform) => Tab(text: platform.l10n(context))) + .toList(), + ), + ), + ), + const SizedBox(height: AppSpacing.lg), Text( l10n.adUnitIdentifiersDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( @@ -209,15 +243,27 @@ class _AdPlatformConfigFormState extends State { textAlign: TextAlign.start, ), const SizedBox(height: AppSpacing.lg), - _buildAdUnitIdentifierFields( - context, - l10n, - _selectedPlatform, - adConfig, + SizedBox( + height: 300, // Adjust height as needed for the content + child: TabBarView( + controller: _tabController, + children: AdPlatformType.values + .where( + (type) => type != AdPlatformType.demo, + ) + .map( + (platform) => _buildAdUnitIdentifierFields( + context, + l10n, + platform, + adConfig, + ), + ) + .toList(), + ), ), ], ), - const SizedBox(height: AppSpacing.lg), ], ); } @@ -265,31 +311,35 @@ class _AdPlatformConfigFormState extends State { ); } - return Column( - children: [ - AppConfigTextField( - label: l10n.nativeAdIdLabel, - description: l10n.nativeAdIdDescription, - value: platformIdentifiers.nativeAdId, - onChanged: (value) => updatePlatformIdentifiers('nativeAdId', value), - controller: controllers['nativeAdId'], - ), - AppConfigTextField( - label: l10n.bannerAdIdLabel, - description: l10n.bannerAdIdDescription, - value: platformIdentifiers.bannerAdId, - onChanged: (value) => updatePlatformIdentifiers('bannerAdId', value), - controller: controllers['bannerAdId'], - ), - AppConfigTextField( - label: l10n.interstitialAdIdLabel, - description: l10n.interstitialAdIdDescription, - value: platformIdentifiers.interstitialAdId, - onChanged: (value) => - updatePlatformIdentifiers('interstitialAdId', value), - controller: controllers['interstitialAdId'], - ), - ], + return SingleChildScrollView( + child: Column( + children: [ + AppConfigTextField( + label: l10n.nativeAdIdLabel, + description: l10n.nativeAdIdDescription, + value: platformIdentifiers.nativeAdId, + onChanged: (value) => + updatePlatformIdentifiers('nativeAdId', value), + controller: controllers['nativeAdId'], + ), + AppConfigTextField( + label: l10n.bannerAdIdLabel, + description: l10n.bannerAdIdDescription, + value: platformIdentifiers.bannerAdId, + onChanged: (value) => + updatePlatformIdentifiers('bannerAdId', value), + controller: controllers['bannerAdId'], + ), + AppConfigTextField( + label: l10n.interstitialAdIdLabel, + description: l10n.interstitialAdIdDescription, + value: platformIdentifiers.interstitialAdId, + onChanged: (value) => + updatePlatformIdentifiers('interstitialAdId', value), + controller: controllers['interstitialAdId'], + ), + ], + ), ); } } diff --git a/lib/app_configuration/widgets/app_urls_form.dart b/lib/app_configuration/widgets/app_urls_form.dart new file mode 100644 index 00000000..14a77375 --- /dev/null +++ b/lib/app_configuration/widgets/app_urls_form.dart @@ -0,0 +1,131 @@ +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 app_urls_form} +/// A form widget for configuring application URLs. +/// +/// This form manages settings like Terms of Service and Privacy Policy URLs. +/// {@endtemplate} +class AppUrlsForm extends StatefulWidget { + /// {@macro app_urls_form} + const AppUrlsForm({ + 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() => _AppUrlsFormState(); +} + +class _AppUrlsFormState extends State { + late final TextEditingController _termsUrlController; + late final TextEditingController _privacyUrlController; + + @override + void initState() { + super.initState(); + final generalConfig = widget.remoteConfig.app.general; + _termsUrlController = _createController(generalConfig.termsOfServiceUrl); + _privacyUrlController = _createController(generalConfig.privacyPolicyUrl); + } + + TextEditingController _createController(String text) { + return TextEditingController(text: text) + ..selection = TextSelection.collapsed(offset: text.length); + } + + void _updateControllerText(TextEditingController controller, String text) { + if (controller.text != text) { + controller + ..text = text + ..selection = TextSelection.collapsed(offset: text.length); + } + } + + @override + void didUpdateWidget(covariant AppUrlsForm oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.remoteConfig.app.general != oldWidget.remoteConfig.app.general) { + final generalConfig = widget.remoteConfig.app.general; + _updateControllerText( + _termsUrlController, + generalConfig.termsOfServiceUrl, + ); + _updateControllerText( + _privacyUrlController, + generalConfig.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 Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.appUrlsDescription, + 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/feed_ad_settings_form.dart b/lib/app_configuration/widgets/feed_ad_settings_form.dart index 26629b3a..d0c53081 100644 --- a/lib/app_configuration/widgets/feed_ad_settings_form.dart +++ b/lib/app_configuration/widgets/feed_ad_settings_form.dart @@ -138,6 +138,12 @@ class _FeedAdSettingsFormState extends State return ExpansionTile( title: Text(l10n.feedAdSettingsTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile( title: Text(l10n.enableFeedAdsLabel), diff --git a/lib/app_configuration/widgets/feed_decorator_form.dart b/lib/app_configuration/widgets/feed_decorator_form.dart index fae03eb4..7d334fd7 100644 --- a/lib/app_configuration/widgets/feed_decorator_form.dart +++ b/lib/app_configuration/widgets/feed_decorator_form.dart @@ -283,41 +283,43 @@ class _FeedDecoratorFormState extends State : null, ), if (roleConfig != null) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.lg, - vertical: AppSpacing.sm, - ), - child: AppConfigIntField( - label: l10n.daysBetweenViewsLabel, - description: l10n.daysBetweenViewsDescription, - value: roleConfig.daysBetweenViews, - onChanged: (value) { - final newRoleConfig = roleConfig.copyWith( - daysBetweenViews: value, - ); - final newVisibleTo = - Map.from( - decoratorConfig.visibleTo, - )..[role] = newRoleConfig; - final newDecoratorConfig = decoratorConfig.copyWith( - visibleTo: newVisibleTo, - ); - final newDecorators = - Map.from( - widget.remoteConfig.features.feed.decorators, - )..[widget.decoratorType] = newDecoratorConfig; - widget.onConfigChanged( - widget.remoteConfig.copyWith( - features: widget.remoteConfig.features.copyWith( - feed: widget.remoteConfig.features.feed.copyWith( - decorators: newDecorators, + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.sm, + ), + child: AppConfigIntField( + label: l10n.daysBetweenViewsLabel, + description: l10n.daysBetweenViewsDescription, + value: roleConfig.daysBetweenViews, + onChanged: (value) { + final newRoleConfig = roleConfig.copyWith( + daysBetweenViews: value, + ); + final newVisibleTo = + Map.from( + decoratorConfig.visibleTo, + )..[role] = newRoleConfig; + final newDecoratorConfig = decoratorConfig.copyWith( + visibleTo: newVisibleTo, + ); + final newDecorators = + Map.from( + widget.remoteConfig.features.feed.decorators, + )..[widget.decoratorType] = newDecoratorConfig; + widget.onConfigChanged( + widget.remoteConfig.copyWith( + features: widget.remoteConfig.features.copyWith( + feed: widget.remoteConfig.features.feed.copyWith( + decorators: newDecorators, + ), ), ), - ), - ); - }, - controller: _roleControllers[role], + ); + }, + controller: _roleControllers[role], + ), ), ), ], diff --git a/lib/app_configuration/widgets/general_app_config_form.dart b/lib/app_configuration/widgets/general_app_config_form.dart deleted file mode 100644 index a0304987..00000000 --- a/lib/app_configuration/widgets/general_app_config_form.dart +++ /dev/null @@ -1,119 +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_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/navigation_ad_settings_form.dart b/lib/app_configuration/widgets/navigation_ad_settings_form.dart index 9dc382bd..93c22258 100644 --- a/lib/app_configuration/widgets/navigation_ad_settings_form.dart +++ b/lib/app_configuration/widgets/navigation_ad_settings_form.dart @@ -115,6 +115,12 @@ class _NavigationAdSettingsFormState extends State return ExpansionTile( title: Text(l10n.navigationAdConfigTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile( title: Text(l10n.enableNavigationAdsLabel), diff --git a/lib/app_configuration/widgets/push_notification_settings_form.dart b/lib/app_configuration/widgets/push_notification_settings_form.dart index 8c418d14..38db1155 100644 --- a/lib/app_configuration/widgets/push_notification_settings_form.dart +++ b/lib/app_configuration/widgets/push_notification_settings_form.dart @@ -29,31 +29,28 @@ class PushNotificationSettingsForm extends StatelessWidget { final features = remoteConfig.features; final pushConfig = features.pushNotifications; - return SingleChildScrollView( - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SwitchListTile( - title: Text(l10n.pushNotificationSystemStatusTitle), - subtitle: Text(l10n.pushNotificationSystemStatusDescription), - value: pushConfig.enabled, - onChanged: (value) { - onConfigChanged( - remoteConfig.copyWith( - features: features.copyWith( - pushNotifications: pushConfig.copyWith(enabled: value), - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SwitchListTile( + title: Text(l10n.pushNotificationSystemStatusTitle), + subtitle: Text(l10n.pushNotificationSystemStatusDescription), + value: pushConfig.enabled, + onChanged: (value) { + onConfigChanged( + remoteConfig.copyWith( + features: features.copyWith( + pushNotifications: pushConfig.copyWith(enabled: value), ), - ); - }, - ), - const SizedBox(height: AppSpacing.lg), - _buildPrimaryProviderSection(context, l10n, pushConfig), - const SizedBox(height: AppSpacing.lg), - _buildDeliveryTypesSection(context, l10n, pushConfig), - ], - ), + ), + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + _buildPrimaryProviderSection(context, l10n, pushConfig), + const SizedBox(height: AppSpacing.lg), + _buildDeliveryTypesSection(context, l10n, pushConfig), + ], ); } diff --git a/lib/app_configuration/widgets/saved_filter_limits_form.dart b/lib/app_configuration/widgets/saved_filter_limits_form.dart index ad926ffa..46c47429 100644 --- a/lib/app_configuration/widgets/saved_filter_limits_form.dart +++ b/lib/app_configuration/widgets/saved_filter_limits_form.dart @@ -41,7 +41,7 @@ class SavedFilterLimitsForm extends StatefulWidget { } class _SavedFilterLimitsFormState extends State - with SingleTickerProviderStateMixin { + with TickerProviderStateMixin { late TabController _tabController; // A nested map to hold controllers: Role -> Field -> Controller @@ -54,6 +54,10 @@ class _SavedFilterLimitsFormState extends State length: AppUserRole.values.length, vsync: this, ); + _notificationTabController = TabController( + length: PushNotificationSubscriptionDeliveryType.values.length, + vsync: this, + ); _initializeControllers(); } @@ -125,6 +129,7 @@ class _SavedFilterLimitsFormState extends State @override void dispose() { _tabController.dispose(); + _notificationTabController.dispose(); for (final roleControllers in _controllers.values) { for (final controller in roleControllers.values) { controller.dispose(); @@ -215,12 +220,16 @@ class _SavedFilterLimitsFormState extends State ), const SizedBox(height: AppSpacing.lg), SizedBox( - height: isHeadlineFilter ? 400 : 250, + height: isHeadlineFilter ? 500 : 250, child: TabBarView( controller: _tabController, children: AppUserRole.values.map((role) { final limits = _getLimitsForRole(role); return SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.sm, + ), child: Column( children: [ AppConfigIntField( @@ -240,7 +249,7 @@ class _SavedFilterLimitsFormState extends State controller: _controllers[role]!['pinned'], ), if (isHeadlineFilter) - ..._buildNotificationFields(l10n, role, limits), + _buildNotificationFields(l10n, role, limits), ], ), ); @@ -251,20 +260,79 @@ class _SavedFilterLimitsFormState extends State ); } - List _buildNotificationFields( + String _getNotificationDescription( + BuildContext context, + PushNotificationSubscriptionDeliveryType type, + ) { + final l10n = AppLocalizationsX(context).l10n; + switch (type) { + case PushNotificationSubscriptionDeliveryType.breakingOnly: + return l10n.notificationSubscriptionBreakingOnlyDescription; + case PushNotificationSubscriptionDeliveryType.dailyDigest: + return l10n.notificationSubscriptionDailyDigestDescription; + case PushNotificationSubscriptionDeliveryType.weeklyRoundup: + return l10n.notificationSubscriptionWeeklyRoundupDescription; + } + } + + late final TabController _notificationTabController; + + Widget _buildNotificationFields( AppLocalizations l10n, AppUserRole role, SavedFilterLimits limits, ) { - return PushNotificationSubscriptionDeliveryType.values.map((type) { - final value = limits.notificationSubscriptions?[type] ?? 0; - return AppConfigIntField( - label: l10n.notificationSubscriptionLimitLabel, - description: type.l10n(context), - value: value, - onChanged: (newValue) => _onValueChanged(role, type.name, newValue), - controller: _controllers[role]!['notification_${type.name}'], - ); - }).toList(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: AppSpacing.lg), + Text( + l10n.notificationSubscriptionLimitLabel, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: AppSpacing.xs), + Text( + l10n.notificationSubscriptionLimitDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + const SizedBox(height: AppSpacing.md), + Align( + alignment: AlignmentDirectional.centerStart, + child: SizedBox( + height: kTextTabBarHeight, + child: TabBar( + controller: _notificationTabController, + tabAlignment: TabAlignment.start, + isScrollable: true, + tabs: PushNotificationSubscriptionDeliveryType.values + .map((type) => Tab(text: type.l10n(context))) + .toList(), + ), + ), + ), + const SizedBox(height: AppSpacing.lg), + SizedBox( + height: 150, + child: TabBarView( + controller: _notificationTabController, + children: PushNotificationSubscriptionDeliveryType.values.map( + (type) { + final value = limits.notificationSubscriptions?[type] ?? 0; + return AppConfigIntField( + label: type.l10n(context), + description: _getNotificationDescription(context, type), + value: value, + onChanged: (newValue) => + _onValueChanged(role, type.name, newValue), + 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 69803edf..6ef236ed 100644 --- a/lib/app_configuration/widgets/saved_filter_limits_section.dart +++ b/lib/app_configuration/widgets/saved_filter_limits_section.dart @@ -47,13 +47,13 @@ class _SavedFilterLimitsSectionState extends State { Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; - return Column( - children: [ - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 0; - return ExpansionTile( + return ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 0; + return Column( + children: [ + ExpansionTile( key: ValueKey('savedHeadlineFilterLimitsTile_$expandedIndex'), title: Text(l10n.savedHeadlineFilterLimitsTitle), childrenPadding: const EdgeInsetsDirectional.only( @@ -82,47 +82,47 @@ class _SavedFilterLimitsSectionState extends State { filterType: SavedFilterType.headline, ), ], - ); - }, - ), - const SizedBox(height: AppSpacing.lg), - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 1; - return ExpansionTile( - key: ValueKey('savedSourceFilterLimitsTile_$expandedIndex'), - title: Text(l10n.savedSourceFilterLimitsTitle), - 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: [ - Text( - l10n.savedSourceFilterLimitsDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), + ), + const SizedBox(height: AppSpacing.lg), + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 1; + return ExpansionTile( + key: ValueKey('savedSourceFilterLimitsTile_$expandedIndex'), + title: Text(l10n.savedSourceFilterLimitsTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, ), - ), - const SizedBox(height: AppSpacing.lg), - SavedFilterLimitsForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - filterType: SavedFilterType.source, - ), - ], - ); - }, - ), - ], + expandedCrossAxisAlignment: CrossAxisAlignment.start, + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + children: [ + Text( + l10n.savedSourceFilterLimitsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), + ), + const SizedBox(height: AppSpacing.lg), + SavedFilterLimitsForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + filterType: SavedFilterType.source, + ), + ], + ); + }, + ), + ], + ); + }, ); } } diff --git a/lib/app_configuration/widgets/update_config_form.dart b/lib/app_configuration/widgets/update_config_form.dart index 6ddc890c..5b46975e 100644 --- a/lib/app_configuration/widgets/update_config_form.dart +++ b/lib/app_configuration/widgets/update_config_form.dart @@ -34,13 +34,22 @@ class _UpdateConfigFormState extends State { 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, - ); + _latestVersionController = _createController(updateConfig.latestAppVersion); + _iosUrlController = _createController(updateConfig.iosUpdateUrl); + _androidUrlController = _createController(updateConfig.androidUpdateUrl); + } + + TextEditingController _createController(String text) { + return TextEditingController(text: text) + ..selection = TextSelection.collapsed(offset: text.length); + } + + void _updateControllerText(TextEditingController controller, String text) { + if (controller.text != text) { + controller + ..text = text + ..selection = TextSelection.collapsed(offset: text.length); + } } @override @@ -48,9 +57,15 @@ class _UpdateConfigFormState extends State { 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; + _updateControllerText( + _latestVersionController, + updateConfig.latestAppVersion, + ); + _updateControllerText(_iosUrlController, updateConfig.iosUpdateUrl); + _updateControllerText( + _androidUrlController, + updateConfig.androidUpdateUrl, + ); } } @@ -68,79 +83,84 @@ class _UpdateConfigFormState extends State { 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), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SwitchListTile( + title: Text(l10n.enableForcedUpdatesLabel), + subtitle: Text(l10n.enableForcedUpdatesDescription), + value: updateConfig.isLatestVersionOnly, + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + app: appConfig.copyWith( + update: updateConfig.copyWith(isLatestVersionOnly: value), + ), + ), + ); + }, + ), + if (updateConfig.isLatestVersionOnly) + Padding( + padding: const EdgeInsets.only( + left: AppSpacing.lg, + top: AppSpacing.md, ), - ), - 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), - ), + child: Column( + children: [ + 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, ), - ); - }, - 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.iosUpdateUrlLabel, - description: l10n.iosUpdateUrlDescription, - value: updateConfig.iosUpdateUrl, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - app: appConfig.copyWith( - update: updateConfig.copyWith(iosUpdateUrl: value), - ), + 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, ), - ); - }, - 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 index 6916eb09..afb99d3d 100644 --- a/lib/app_configuration/widgets/user_limits_config_form.dart +++ b/lib/app_configuration/widgets/user_limits_config_form.dart @@ -109,13 +109,10 @@ class _UserLimitsConfigFormState extends State 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), - ), + Text( + l10n.userContentLimitsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), ), const SizedBox(height: AppSpacing.lg), diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f1fd3547..2d935214 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -284,11 +284,11 @@ abstract class AppLocalizations { /// **'Advertisements'** String get advertisementsTab; - /// Tab title for General settings + /// Tab title for system settings /// /// In en, this message translates to: - /// **'General'** - String get generalTab; + /// **'System'** + String get systemTab; /// Title for the User Content Limits section /// @@ -1523,7 +1523,7 @@ abstract class AppLocalizations { /// Description for the 'is under maintenance' switch /// /// In en, this message translates to: - /// **'Toggle to put the app in maintenance mode, preventing user access.'** + /// **'Toggle to put the mobile app in maintenance mode, preventing user access.'** String get isUnderMaintenanceDescription; /// Label for the 'is latest version only' switch @@ -1937,7 +1937,7 @@ abstract class AppLocalizations { /// Title for the Primary Ad Platform Selection section /// /// In en, this message translates to: - /// **'Primary Ad Platform Selection'** + /// **'Primary Ad Platform'** String get primaryAdPlatformTitle; /// Description for the Primary Ad Platform Selection section @@ -3068,11 +3068,83 @@ abstract class AppLocalizations { /// **'Application Update Management'** String get appUpdateManagementTitle; - /// Title for the Legal & General Information expansion tile. + /// Title for the feed item click behavior setting. + /// + /// In en, this message translates to: + /// **'Feed Item Click Behavior'** + String get feedItemClickBehaviorTitle; + + /// Description for the feed item click behavior setting. + /// + /// In en, this message translates to: + /// **'Select which browser opens when a user taps on a headline in the feed.'** + String get feedItemClickBehaviorDescription; + + /// Option for opening links in the app's internal browser. + /// + /// In en, this message translates to: + /// **'In-App Browser'** + String get feedItemClickBehaviorInternalNavigation; + + /// Option for opening links in the device's default system browser. + /// + /// In en, this message translates to: + /// **'System Browser'** + String get feedItemClickBehaviorExternalNavigation; + + /// Title for the User Limits expansion tile. + /// + /// In en, this message translates to: + /// **'User Limits'** + String get userLimitsTitle; + + /// Description for the breaking news notification subscription limit. + /// + /// In en, this message translates to: + /// **'Limit for subscriptions that send immediate alerts for matching headlines.'** + String get notificationSubscriptionBreakingOnlyDescription; + + /// Description for the daily digest notification subscription limit. + /// + /// In en, this message translates to: + /// **'Limit for subscriptions that send a daily summary of matching headlines.'** + String get notificationSubscriptionDailyDigestDescription; + + /// Description for the weekly roundup notification subscription limit. + /// + /// In en, this message translates to: + /// **'Limit for subscriptions that send a weekly summary of matching headlines.'** + String get notificationSubscriptionWeeklyRoundupDescription; + + /// Title for the App Status & Updates expansion tile. + /// + /// In en, this message translates to: + /// **'App Status & Updates'** + String get appStatusAndUpdatesTitle; + + /// Label for the switch to enable forced updates. + /// + /// In en, this message translates to: + /// **'Enable Forced Updates'** + String get enableForcedUpdatesLabel; + + /// Description for the switch to enable forced updates. + /// + /// In en, this message translates to: + /// **'When enabled, you can specify a minimum required version for the mobile app.'** + String get enableForcedUpdatesDescription; + + /// Title for the Application URLs expansion tile. + /// + /// In en, this message translates to: + /// **'Application URLs'** + String get appUrlsTitle; + + /// Description for the Application URLs expansion tile. /// /// In en, this message translates to: - /// **'Legal & General Information'** - String get appLegalInformationTitle; + /// **'Manage external and internal URLs used within the application.'** + String get appUrlsDescription; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index e2099be5..ef4c2407 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -115,7 +115,7 @@ class AppLocalizationsAr extends AppLocalizations { String get advertisementsTab => 'الإعلانات'; @override - String get generalTab => 'عام'; + String get systemTab => 'النظام'; @override String get userContentLimitsTitle => 'حدود المحتوى'; @@ -800,7 +800,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get isUnderMaintenanceDescription => - 'تبديل لوضع التطبيق في وضع الصيانة، مما يمنع وصول المستخدمين.'; + 'تبديل لوضع تطبيق الجوال في وضع الصيانة، مما يمنع وصول المستخدمين.'; @override String get isLatestVersionOnlyLabel => 'فرض أحدث إصدار فقط'; @@ -840,7 +840,7 @@ class AppLocalizationsAr extends AppLocalizations { 'عدد مرات ظهور الإعلان لهذا الدور المستخدم (على سبيل المثال، قيمة 5 تعني أنه يمكن وضع إعلان بعد كل 5 عناصر إخبارية).'; @override - String get savedFeedFilterLimitsTitle => 'حد المرشحات المحفوظة'; + String get savedFeedFilterLimitsTitle => 'حدود المرشحات المحفوظة'; @override String get savedFeedFilterLimitsDescription => @@ -1023,7 +1023,7 @@ class AppLocalizationsAr extends AppLocalizations { String get adPlatformConfigurationTitle => 'إعدادات منصة الإعلان'; @override - String get primaryAdPlatformTitle => 'اختيار منصة الإعلانات الأساسية'; + String get primaryAdPlatformTitle => 'منصة الإعلانات الأساسية'; @override String get primaryAdPlatformDescription => @@ -1648,5 +1648,47 @@ class AppLocalizationsAr extends AppLocalizations { String get appUpdateManagementTitle => 'إدارة تحديثات التطبيق'; @override - String get appLegalInformationTitle => 'المعلومات القانونية والعامة'; + String get feedItemClickBehaviorTitle => 'سلوك النقر على عنصر الموجز'; + + @override + String get feedItemClickBehaviorDescription => + 'اختر المتصفح الذي يفتح عند نقر المستخدم على عنوان في الموجز.'; + + @override + String get feedItemClickBehaviorInternalNavigation => 'متصفح داخل التطبيق'; + + @override + String get feedItemClickBehaviorExternalNavigation => 'متصفح النظام'; + + @override + String get userLimitsTitle => 'حدود المستخدم'; + + @override + String get notificationSubscriptionBreakingOnlyDescription => + 'حد الاشتراكات التي ترسل تنبيهات فورية للأخبار العاجلة للعناوين المطابقة.'; + + @override + String get notificationSubscriptionDailyDigestDescription => + 'حد الاشتراكات التي ترسل ملخصًا يوميًا للعناوين المطابقة.'; + + @override + String get notificationSubscriptionWeeklyRoundupDescription => + 'حد الاشتراكات التي ترسل ملخصًا أسبوعيًا للعناوين المطابقة.'; + + @override + String get appStatusAndUpdatesTitle => 'حالة التطبيق والتحديثات'; + + @override + String get enableForcedUpdatesLabel => 'تفعيل التحديثات الإجبارية'; + + @override + String get enableForcedUpdatesDescription => + 'عند التفعيل، يمكنك تحديد الحد الأدنى للإصدار المطلوب لتطبيق الجوال.'; + + @override + String get appUrlsTitle => 'روابط التطبيق'; + + @override + String get appUrlsDescription => + 'إدارة الروابط الخارجية والداخلية المستخدمة داخل التطبيق.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 7db3aeaf..279785d4 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -114,7 +114,7 @@ class AppLocalizationsEn extends AppLocalizations { String get advertisementsTab => 'Advertisements'; @override - String get generalTab => 'General'; + String get systemTab => 'System'; @override String get userContentLimitsTitle => 'User Content Limits'; @@ -798,7 +798,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get isUnderMaintenanceDescription => - 'Toggle to put the app in maintenance mode, preventing user access.'; + 'Toggle to put the mobile app in maintenance mode, preventing user access.'; @override String get isLatestVersionOnlyLabel => 'Force Latest Version Only'; @@ -1021,7 +1021,7 @@ class AppLocalizationsEn extends AppLocalizations { String get adPlatformConfigurationTitle => 'Ad Platform Configuration'; @override - String get primaryAdPlatformTitle => 'Primary Ad Platform Selection'; + String get primaryAdPlatformTitle => 'Primary Ad Platform'; @override String get primaryAdPlatformDescription => @@ -1651,5 +1651,47 @@ class AppLocalizationsEn extends AppLocalizations { String get appUpdateManagementTitle => 'Application Update Management'; @override - String get appLegalInformationTitle => 'Legal & General Information'; + String get feedItemClickBehaviorTitle => 'Feed Item Click Behavior'; + + @override + String get feedItemClickBehaviorDescription => + 'Select which browser opens when a user taps on a headline in the feed.'; + + @override + String get feedItemClickBehaviorInternalNavigation => 'In-App Browser'; + + @override + String get feedItemClickBehaviorExternalNavigation => 'System Browser'; + + @override + String get userLimitsTitle => 'User Limits'; + + @override + String get notificationSubscriptionBreakingOnlyDescription => + 'Limit for subscriptions that send immediate alerts for matching headlines.'; + + @override + String get notificationSubscriptionDailyDigestDescription => + 'Limit for subscriptions that send a daily summary of matching headlines.'; + + @override + String get notificationSubscriptionWeeklyRoundupDescription => + 'Limit for subscriptions that send a weekly summary of matching headlines.'; + + @override + String get appStatusAndUpdatesTitle => 'App Status & Updates'; + + @override + String get enableForcedUpdatesLabel => 'Enable Forced Updates'; + + @override + String get enableForcedUpdatesDescription => + 'When enabled, you can specify a minimum required version for the mobile app.'; + + @override + String get appUrlsTitle => 'Application URLs'; + + @override + String get appUrlsDescription => + 'Manage external and internal URLs used within the application.'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 3659fbe2..deb7c638 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -139,9 +139,9 @@ "@advertisementsTab": { "description": "عنوان تبويب إعدادات الإعلانات" }, - "generalTab": "عام", - "@generalTab": { - "description": "عنوان تبويب الإعدادات العامة" + "systemTab": "النظام", + "@systemTab": { + "description": "عنوان تبويب لإعدادات النظام" }, "userContentLimitsTitle": "حدود المحتوى", "@userContentLimitsTitle": { @@ -986,7 +986,7 @@ "@isUnderMaintenanceLabel": { "description": "تسمية مفتاح 'تحت الصيانة'" }, - "isUnderMaintenanceDescription": "تبديل لوضع التطبيق في وضع الصيانة، مما يمنع وصول المستخدمين.", + "isUnderMaintenanceDescription": "تبديل لوضع تطبيق الجوال في وضع الصيانة، مما يمنع وصول المستخدمين.", "@isUnderMaintenanceDescription": { "description": "وصف مفتاح 'تحت الصيانة'" }, @@ -1038,7 +1038,7 @@ "@adFrequencyDescription": { "description": "وصف تكرار الإعلان" }, - "savedFeedFilterLimitsTitle": "حد المرشحات المحفوظة", + "savedFeedFilterLimitsTitle": "حدود المرشحات المحفوظة", "@savedFeedFilterLimitsTitle": { "description": "وصف لحد مرشحات موجز الأخبار المحفوظة" }, @@ -1282,7 +1282,7 @@ "@adPlatformConfigurationTitle": { "description": "عنوان قسم إعدادات منصة الإعلانات" }, - "primaryAdPlatformTitle": "اختيار منصة الإعلانات الأساسية", + "primaryAdPlatformTitle": "منصة الإعلانات الأساسية", "@primaryAdPlatformTitle": { "description": "عنوان قسم اختيار منصة الإعلانات الأساسية" }, @@ -2066,8 +2066,56 @@ "@appUpdateManagementTitle": { "description": "عنوان قسم إدارة تحديثات التطبيق القابل للتوسيع." }, - "appLegalInformationTitle": "المعلومات القانونية والعامة", - "@appLegalInformationTitle": { - "description": "عنوان قسم المعلومات القانونية والعامة القابل للتوسيع." + "feedItemClickBehaviorTitle": "سلوك النقر على عنصر الموجز", + "@feedItemClickBehaviorTitle": { + "description": "عنوان إعداد سلوك النقر على عنصر الموجز." + }, + "feedItemClickBehaviorDescription": "اختر المتصفح الذي يفتح عند نقر المستخدم على عنوان في الموجز.", + "@feedItemClickBehaviorDescription": { + "description": "وصف إعداد سلوك النقر على عنصر الموجز." + }, + "feedItemClickBehaviorInternalNavigation": "متصفح داخل التطبيق", + "@feedItemClickBehaviorInternalNavigation": { + "description": "خيار لفتح الروابط في المتصفح الداخلي للتطبيق." + }, + "feedItemClickBehaviorExternalNavigation": "متصفح النظام", + "@feedItemClickBehaviorExternalNavigation": { + "description": "خيار لفتح الروابط في متصفح النظام الافتراضي للجهاز." + }, + "userLimitsTitle": "حدود المستخدم", + "@userLimitsTitle": { + "description": "عنوان قسم حدود المستخدم القابل للتوسيع." + }, + "notificationSubscriptionBreakingOnlyDescription": "حد الاشتراكات التي ترسل تنبيهات فورية للأخبار العاجلة للعناوين المطابقة.", + "@notificationSubscriptionBreakingOnlyDescription": { + "description": "وصف لحد اشتراك إشعارات الأخبار العاجلة." + }, + "notificationSubscriptionDailyDigestDescription": "حد الاشتراكات التي ترسل ملخصًا يوميًا للعناوين المطابقة.", + "@notificationSubscriptionDailyDigestDescription": { + "description": "وصف لحد اشتراك إشعارات الملخص اليومي." + }, + "notificationSubscriptionWeeklyRoundupDescription": "حد الاشتراكات التي ترسل ملخصًا أسبوعيًا للعناوين المطابقة.", + "@notificationSubscriptionWeeklyRoundupDescription": { + "description": "وصف لحد اشتراك إشعارات الحصاد الأسبوعي." + }, + "appStatusAndUpdatesTitle": "حالة التطبيق والتحديثات", + "@appStatusAndUpdatesTitle": { + "description": "عنوان قسم حالة التطبيق والتحديثات القابل للتوسيع." + }, + "enableForcedUpdatesLabel": "تفعيل التحديثات الإجبارية", + "@enableForcedUpdatesLabel": { + "description": "تسمية مفتاح تفعيل التحديثات الإجبارية." + }, + "enableForcedUpdatesDescription": "عند التفعيل، يمكنك تحديد الحد الأدنى للإصدار المطلوب لتطبيق الجوال.", + "@enableForcedUpdatesDescription": { + "description": "وصف مفتاح تفعيل التحديثات الإجبارية." + }, + "appUrlsTitle": "روابط التطبيق", + "@appUrlsTitle": { + "description": "عنوان قسم روابط التطبيق القابل للتوسيع." + }, + "appUrlsDescription": "إدارة الروابط الخارجية والداخلية المستخدمة داخل التطبيق.", + "@appUrlsDescription": { + "description": "وصف قسم روابط التطبيق القابل للتوسيع." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 31c693d7..10e5a602 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -139,9 +139,9 @@ "@advertisementsTab": { "description": "Tab title for Advertisements settings" }, - "generalTab": "General", - "@generalTab": { - "description": "Tab title for General settings" + "systemTab": "System", + "@systemTab": { + "description": "Tab title for system settings" }, "userContentLimitsTitle": "User Content Limits", "@userContentLimitsTitle": { @@ -990,7 +990,7 @@ "@isUnderMaintenanceLabel": { "description": "Label for the 'is under maintenance' switch" }, - "isUnderMaintenanceDescription": "Toggle to put the app in maintenance mode, preventing user access.", + "isUnderMaintenanceDescription": "Toggle to put the mobile app in maintenance mode, preventing user access.", "@isUnderMaintenanceDescription": { "description": "Description for the 'is under maintenance' switch" }, @@ -1282,7 +1282,7 @@ "@adPlatformConfigurationTitle": { "description": "Title for the Ad Platform Configuration section" }, - "primaryAdPlatformTitle": "Primary Ad Platform Selection", + "primaryAdPlatformTitle": "Primary Ad Platform", "@primaryAdPlatformTitle": { "description": "Title for the Primary Ad Platform Selection section" }, @@ -2062,8 +2062,56 @@ "@appUpdateManagementTitle": { "description": "Title for the Application Update Management expansion tile." }, - "appLegalInformationTitle": "Legal & General Information", - "@appLegalInformationTitle": { - "description": "Title for the Legal & General Information expansion tile." + "feedItemClickBehaviorTitle": "Feed Item Click Behavior", + "@feedItemClickBehaviorTitle": { + "description": "Title for the feed item click behavior setting." + }, + "feedItemClickBehaviorDescription": "Select which browser opens when a user taps on a headline in the feed.", + "@feedItemClickBehaviorDescription": { + "description": "Description for the feed item click behavior setting." + }, + "feedItemClickBehaviorInternalNavigation": "In-App Browser", + "@feedItemClickBehaviorInternalNavigation": { + "description": "Option for opening links in the app's internal browser." + }, + "feedItemClickBehaviorExternalNavigation": "System Browser", + "@feedItemClickBehaviorExternalNavigation": { + "description": "Option for opening links in the device's default system browser." + }, + "userLimitsTitle": "User Limits", + "@userLimitsTitle": { + "description": "Title for the User Limits expansion tile." + }, + "notificationSubscriptionBreakingOnlyDescription": "Limit for subscriptions that send immediate alerts for matching headlines.", + "@notificationSubscriptionBreakingOnlyDescription": { + "description": "Description for the breaking news notification subscription limit." + }, + "notificationSubscriptionDailyDigestDescription": "Limit for subscriptions that send a daily summary of matching headlines.", + "@notificationSubscriptionDailyDigestDescription": { + "description": "Description for the daily digest notification subscription limit." + }, + "notificationSubscriptionWeeklyRoundupDescription": "Limit for subscriptions that send a weekly summary of matching headlines.", + "@notificationSubscriptionWeeklyRoundupDescription": { + "description": "Description for the weekly roundup notification subscription limit." + }, + "appStatusAndUpdatesTitle": "App Status & Updates", + "@appStatusAndUpdatesTitle": { + "description": "Title for the App Status & Updates expansion tile." + }, + "enableForcedUpdatesLabel": "Enable Forced Updates", + "@enableForcedUpdatesLabel": { + "description": "Label for the switch to enable forced updates." + }, + "enableForcedUpdatesDescription": "When enabled, you can specify a minimum required version for the mobile app.", + "@enableForcedUpdatesDescription": { + "description": "Description for the switch to enable forced updates." + }, + "appUrlsTitle": "Application URLs", + "@appUrlsTitle": { + "description": "Title for the Application URLs expansion tile." + }, + "appUrlsDescription": "Manage external and internal URLs used within the application.", + "@appUrlsDescription": { + "description": "Description for the Application URLs expansion tile." } } \ No newline at end of file diff --git a/lib/shared/extensions/feed_item_click_behavior_l10n.dart b/lib/shared/extensions/feed_item_click_behavior_l10n.dart new file mode 100644 index 00000000..8802976e --- /dev/null +++ b/lib/shared/extensions/feed_item_click_behavior_l10n.dart @@ -0,0 +1,20 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +/// Extension to localize the [FeedItemClickBehavior] enum. +extension FeedItemClickBehaviorL10n on FeedItemClickBehavior { + /// Returns the localized string representation of the + /// [FeedItemClickBehavior]. + String l10n(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + switch (this) { + case FeedItemClickBehavior.internalNavigation: + return l10n.feedItemClickBehaviorInternalNavigation; + case FeedItemClickBehavior.externalNavigation: + return l10n.feedItemClickBehaviorExternalNavigation; + case FeedItemClickBehavior.defaultBehavior: + return ''; // Mobile client related, should not be displayed in the dashboard UI. + } + } +}