From c177e386d81d2b85b96b639652da0597d9deb361 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 12:06:47 +0100 Subject: [PATCH 01/46] build(deps): update core dependency - Update core dependency to latest version - Update ref in pubspec.yaml and pubspec.lock --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e0782b92..adc60b77 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,8 +89,8 @@ packages: dependency: "direct main" description: path: "." - ref: "064c4387b3f7df835565c41c918dc2d80dd2f49a" - resolved-ref: "064c4387b3f7df835565c41c918dc2d80dd2f49a" + ref: c0c41c069b885f0c16d1b134269aa06861634186 + resolved-ref: c0c41c069b885f0c16d1b134269aa06861634186 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..0eff3f61 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: c0c41c069b885f0c16d1b134269aa06861634186 http_client: git: url: https://github.com/flutter-news-app-full-source-code/http-client.git From 9736ff4f4f014b66cb86b6c8950ba460ee4d7eab Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:25:15 +0100 Subject: [PATCH 02/46] chore: delete absolete files --- .../advertisements_configuration_tab.dart | 188 ------------ .../view/tabs/feed_configuration_tab.dart | 212 ------------- .../view/tabs/general_configuration_tab.dart | 198 ------------- .../widgets/article_ad_settings_form.dart | 278 ----------------- .../interstitial_ad_settings_form.dart | 279 ------------------ 5 files changed, 1155 deletions(-) delete mode 100644 lib/app_configuration/view/tabs/advertisements_configuration_tab.dart delete mode 100644 lib/app_configuration/view/tabs/feed_configuration_tab.dart delete mode 100644 lib/app_configuration/view/tabs/general_configuration_tab.dart delete mode 100644 lib/app_configuration/widgets/article_ad_settings_form.dart delete mode 100644 lib/app_configuration/widgets/interstitial_ad_settings_form.dart 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/feed_configuration_tab.dart b/lib/app_configuration/view/tabs/feed_configuration_tab.dart deleted file mode 100644 index 35e8c186..00000000 --- a/lib/app_configuration/view/tabs/feed_configuration_tab.dart +++ /dev/null @@ -1,212 +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/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/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. -/// -/// This tab allows configuration of user content limits and feed decorators. -/// {@endtemplate} -class FeedConfigurationTab extends StatefulWidget { - /// {@macro feed_configuration_tab} - const FeedConfigurationTab({ - 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() => _FeedConfigurationTabState(); -} - -class _FeedConfigurationTabState 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 User Content Limits - 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, - 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), - ), - ), - const SizedBox(height: AppSpacing.lg), - UserPreferenceLimitsForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), - ], - ); - }, - ), - const SizedBox(height: AppSpacing.lg), - // New Top-level ExpansionTile for User Preset Limits - 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, - 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( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), - ], - ); - }, - ), - const SizedBox(height: AppSpacing.lg), - // New Top-level ExpansionTile for Feed Decorators - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 2; - return ExpansionTile( - key: ValueKey('feedDecoratorsTile_$expandedIndex'), - title: Text(l10n.feedDecoratorsTitle), - 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), - ), - ), - 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), - 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.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, - ), - ), - ), - 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/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/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; - } -} From a6d19f7def7e092faf2f30ec01cf1262ad29f0ae Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:27:06 +0100 Subject: [PATCH 03/46] feat(config): overhaul AppConfigurationPage with new tab structure [WHAT]: Refactors the `AppConfigurationPage` to implement the new three-tab structure: "App", "Features", and "User". [WHY]: This is the central step in aligning the UI with the new `RemoteConfig` model. It replaces the old structure with a new one that directly reflects the hierarchical `app`, `features`, and `user` properties of the model, improving clarity and maintainability. [HOW]: The `TabController` length is changed to 3. The `TabBar` is updated with the new, admin-centric localized tab titles. The `TabBarView` is updated to render the three new tab widgets: `AppConfigurationTab`, `FeaturesConfigurationTab`, and `UserConfigurationTab`, passing the `remoteConfig` and `onConfigChanged` callback to each. --- .../view/app_configuration_page.dart | 66 ++++++++----------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index ff746207..cff0c0ca 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), ], ), ), @@ -84,16 +82,16 @@ class _AppConfigurationPageState extends State content: Text( l10n.appConfigSaveSuccessMessage, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onPrimary, - ), + color: Theme.of(context).colorScheme.onPrimary, + ), ), backgroundColor: Theme.of(context).colorScheme.primary, ), ); // Clear the showSaveSuccess flag after showing the snackbar context.read().add( - const AppConfigurationFieldChanged(), - ); + const AppConfigurationFieldChanged(), + ); } else if (state.status == AppConfigurationStatus.failure && state.exception != null) { ScaffoldMessenger.of(context) @@ -103,8 +101,8 @@ class _AppConfigurationPageState extends State content: Text( state.exception!.toFriendlyMessage(context), style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onError, - ), + color: Theme.of(context).colorScheme.onError, + ), ), backgroundColor: Theme.of(context).colorScheme.error, ), @@ -124,8 +122,8 @@ class _AppConfigurationPageState extends State exception: state.exception!, onRetry: () { context.read().add( - const AppConfigurationLoaded(), - ); + const AppConfigurationLoaded(), + ); }, ); } else if (state.status == AppConfigurationStatus.success && @@ -134,36 +132,28 @@ class _AppConfigurationPageState extends State return TabBarView( controller: _tabController, children: [ - GeneralConfigurationTab( + AppConfigurationTab( remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged(remoteConfig: newConfig), - ); - }, - ), - FeedConfigurationTab( - remoteConfig: remoteConfig, - onConfigChanged: (newConfig) { - context.read().add( - AppConfigurationFieldChanged(remoteConfig: newConfig), - ); + AppConfigurationFieldChanged(remoteConfig: newConfig), + ); }, ), - AdvertisementsConfigurationTab( + FeaturesConfigurationTab( remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged(remoteConfig: newConfig), - ); + AppConfigurationFieldChanged(remoteConfig: newConfig), + ); }, ), - PushNotificationSettingsForm( + UserConfigurationTab( remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged(remoteConfig: newConfig), - ); + AppConfigurationFieldChanged(remoteConfig: newConfig), + ); }, ), ], @@ -199,8 +189,8 @@ class _AppConfigurationPageState extends State ? () { // Discard changes: revert to original config context.read().add( - const AppConfigurationDiscarded(), - ); + const AppConfigurationDiscarded(), + ); } : null, child: Text(AppLocalizationsX(context).l10n.discardChangesButton), @@ -214,8 +204,8 @@ class _AppConfigurationPageState extends State confirmed && remoteConfig != null) { context.read().add( - AppConfigurationUpdated(remoteConfig), - ); + AppConfigurationUpdated(remoteConfig), + ); } } : null, From e7cfcc2aafd3b4e494781a7acd68fb5b89868e78 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:29:22 +0100 Subject: [PATCH 04/46] feat(config): create new model-driven tab views [WHAT]: Creates three new tab view widgets: `AppConfigurationTab`, `FeaturesConfigurationTab`, and `UserConfigurationTab`. [WHY]: These widgets form the new top-level UI structure for the App Configuration page, directly corresponding to the `app`, `features`, and `user` properties of the new `RemoteConfig` model. This decomposition is essential for creating a maintainable, model-driven UI. [HOW]: Each file defines a `StatelessWidget` that accepts the `RemoteConfig` model and an `onConfigChanged` callback. They will serve as containers for the various configuration forms, organized into `ExpansionTile` widgets that reflect the nested model structure. --- .../view/tabs/app_configuration_tab.dart | 122 ++++++++++++ .../view/tabs/features_configuration_tab.dart | 175 ++++++++++++++++++ .../view/tabs/user_configuration_tab.dart | 97 ++++++++++ 3 files changed, 394 insertions(+) create mode 100644 lib/app_configuration/view/tabs/app_configuration_tab.dart create mode 100644 lib/app_configuration/view/tabs/features_configuration_tab.dart create mode 100644 lib/app_configuration/view/tabs/user_configuration_tab.dart 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..e29a55a1 --- /dev/null +++ b/lib/app_configuration/view/tabs/app_configuration_tab.dart @@ -0,0 +1,122 @@ +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/maintenance_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; + + return ListView( + padding: const EdgeInsets.all(AppSpacing.lg), + children: [ + // Maintenance Config + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 0; + return ExpansionTile( + key: ValueKey('maintenanceConfigTile_$expandedIndex'), + title: Text(l10n.maintenanceConfigTitle), + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + children: [ + MaintenanceConfigForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ); + }, + ), + 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.updateConfigTitle), + 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.generalAppConfigTitle), + 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 new file mode 100644 index 00000000..88dc09de --- /dev/null +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -0,0 +1,175 @@ +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/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 features_configuration_tab} +/// A widget representing the "Features" tab in the App Configuration page. +/// +/// This tab allows configuration of user-facing features like ads, +/// push notifications, and feed settings. +/// {@endtemplate} +class FeaturesConfigurationTab extends StatefulWidget { + /// {@macro features_configuration_tab} + const FeaturesConfigurationTab({ + 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() => + _FeaturesConfigurationTabState(); +} + +class _FeaturesConfigurationTabState 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 features = widget.remoteConfig.features; + + return ListView( + padding: const EdgeInsets.all(AppSpacing.lg), + children: [ + // Advertisements + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 0; + return ExpansionTile( + key: ValueKey('advertisementsTile_$expandedIndex'), + title: Text(l10n.advertisementsTab), + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + children: [ + 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), + NavigationAdSettingsForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + + // Push Notifications + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 1; + return ExpansionTile( + key: ValueKey('pushNotificationsTile_$expandedIndex'), + title: Text(l10n.notificationsTab), + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + children: [ + PushNotificationSettingsForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + + // Feed Decorators + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 2; + return ExpansionTile( + key: ValueKey('feedDecoratorsTile_$expandedIndex'), + title: Text(l10n.feedDecoratorsTitle), + 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), + ), + ), + 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/user_configuration_tab.dart b/lib/app_configuration/view/tabs/user_configuration_tab.dart new file mode 100644 index 00000000..066d1568 --- /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: const ValueKey('savedFilterLimitsTile_'), + title: Text(l10n.savedFeedFilterLimitsTitle), + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + children: [ + SavedFilterLimitsSection( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ); + }, + ), + ], + ); + } +} From f153ad663f7bf1eb1649dc20873e094d58cbf23b Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:30:35 +0100 Subject: [PATCH 05/46] feat(config): create new forms for app and navigation ad settings [WHAT]: Creates new form widgets for `MaintenanceConfig`, `UpdateConfig`, `GeneralAppConfig`, and `NavigationAdConfiguration`. [WHY]: These new widgets are required to manage the settings for the corresponding new models in the `RemoteConfig` structure. They encapsulate the form logic for these specific configuration sections, making the UI modular and easier to manage. [HOW]: Each form is a `StatefulWidget` that manages its own `TextEditingController`s and binds to the respective sub-model within `RemoteConfig`. They use the shared `AppConfigTextField` and `SwitchListTile` widgets to ensure UI consistency and propagate changes up via the `onConfigChanged` callback. --- .../widgets/general_app_config_form.dart | 119 +++++++ .../widgets/maintenance_config_form.dart | 61 ++++ .../widgets/navigation_ad_settings_form.dart | 308 ++++++++++++++++++ .../widgets/update_config_form.dart | 144 ++++++++ 4 files changed, 632 insertions(+) create mode 100644 lib/app_configuration/widgets/general_app_config_form.dart create mode 100644 lib/app_configuration/widgets/maintenance_config_form.dart create mode 100644 lib/app_configuration/widgets/navigation_ad_settings_form.dart create mode 100644 lib/app_configuration/widgets/update_config_form.dart 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..c77169b8 --- /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/maintenance_config_form.dart b/lib/app_configuration/widgets/maintenance_config_form.dart new file mode 100644 index 00000000..5cb9a5f4 --- /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..905551cd --- /dev/null +++ b/lib/app_configuration/widgets/navigation_ad_settings_form.dart @@ -0,0 +1,308 @@ +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 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/update_config_form.dart b/lib/app_configuration/widgets/update_config_form.dart new file mode 100644 index 00000000..1ce2f75d --- /dev/null +++ b/lib/app_configuration/widgets/update_config_form.dart @@ -0,0 +1,144 @@ +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, + ), + ], + ), + ); + } +} From 4b0e77554b645949a334d14ce186ce909cacf195 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:31:54 +0100 Subject: [PATCH 06/46] feat(config): create UserLimitsConfigForm for simple user limits [WHAT]: Creates the `UserLimitsConfigForm` widget. [WHY]: This widget replaces the old `UserPreferenceLimitsForm` and is responsible for managing the simple, role-based integer limits (`followedItems`, `savedHeadlines`) found in the new `UserLimitsConfig` model. [HOW]: The widget uses a `TabBar` to switch between user roles (`Guest`, `Standard`, `Premium`). For each role, it displays `AppConfigIntField` widgets bound to the corresponding values in the `remoteConfig.user.limits` map, ensuring a clear and organized UI for managing role-based limits. --- .../widgets/user_limits_config_form.dart | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 lib/app_configuration/widgets/user_limits_config_form.dart 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..6f114b92 --- /dev/null +++ b/lib/app_configuration/widgets/user_limits_config_form.dart @@ -0,0 +1,197 @@ +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: [ + 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 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], + ), + ], + ); + } +} From 168daf3d4ae8de97f2c266e19f1b2158a07ccfae Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:33:11 +0100 Subject: [PATCH 07/46] refactor(config): align ad and platform forms with new AdConfig model [WHAT]: Refactors `AdConfigForm` and `AdPlatformConfigForm` to align with the new `AdConfig` and `AdPlatformIdentifiers` models. [WHY]: The old forms were based on a different model structure. This refactoring is necessary to correctly bind the UI to the new, simplified ad configuration models. `AdConfigForm` now only handles the global `enabled` switch. `AdPlatformConfigForm` is updated to manage the new generic ad IDs (`nativeAdId`, `bannerAdId`, `interstitialAdId`). [HOW]: In `AdConfigForm`, the logic is simplified to a single `SwitchListTile` bound to `remoteConfig.features.ads.enabled`. In `AdPlatformConfigForm`, the `_buildAdUnitIdentifierFields` method is rewritten to create `AppConfigTextField`s for the three new generic ad IDs, and the `onChanged` logic is updated to use the new `AdPlatformIdentifiers.copyWith` method. --- .../widgets/ad_config_form.dart | 20 +- .../widgets/ad_platform_config_form.dart | 266 +++++------------- 2 files changed, 80 insertions(+), 206 deletions(-) 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..abd213e2 100644 --- a/lib/app_configuration/widgets/ad_platform_config_form.dart +++ b/lib/app_configuration/widgets/ad_platform_config_form.dart @@ -30,19 +30,20 @@ class AdPlatformConfigForm extends StatefulWidget { class _AdPlatformConfigFormState extends State { late AdPlatformType _selectedPlatform; late Map> - _platformAdIdentifierControllers; + _platformAdIdentifierControllers; @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,19 @@ class _AdPlatformConfigFormState extends State { _platformAdIdentifierControllers = { for (final platform in AdPlatformType.values) platform: { - 'feedNativeAdId': TextEditingController( - text: - widget - .remoteConfig - .adConfig - .platformAdIdentifiers[platform] - ?.feedNativeAdId ?? + 'nativeAdId': TextEditingController( + text: widget.remoteConfig.features.ads + .platformAdIdentifiers[platform]?.nativeAdId ?? '', ), - 'feedBannerAdId': TextEditingController( - text: - widget - .remoteConfig - .adConfig - .platformAdIdentifiers[platform] - ?.feedBannerAdId ?? + 'bannerAdId': TextEditingController( + text: widget.remoteConfig.features.ads + .platformAdIdentifiers[platform]?.bannerAdId ?? '', ), - 'feedToArticleInterstitialAdId': TextEditingController( - text: - widget - .remoteConfig - .adConfig - .platformAdIdentifiers[platform] - ?.feedToArticleInterstitialAdId ?? - '', - ), - 'inArticleNativeAdId': TextEditingController( - text: - widget - .remoteConfig - .adConfig - .platformAdIdentifiers[platform] - ?.inArticleNativeAdId ?? - '', - ), - 'inArticleBannerAdId': TextEditingController( - text: - widget - .remoteConfig - .adConfig - .platformAdIdentifiers[platform] - ?.inArticleBannerAdId ?? + 'interstitialAdId': TextEditingController( + text: widget.remoteConfig.features.ads + .platformAdIdentifiers[platform]?.interstitialAdId ?? '', ), }, @@ -102,95 +73,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 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 identifiers = + widget.remoteConfig.features.ads.platformAdIdentifiers[platform]; - 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 nativeAdId = identifiers?.nativeAdId ?? ''; + if (_platformAdIdentifierControllers[platform]!['nativeAdId']?.text != + nativeAdId) { + _platformAdIdentifierControllers[platform]!['nativeAdId']?.text = + nativeAdId; } - 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 +113,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,10 +131,8 @@ 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, ), const SizedBox(height: AppSpacing.lg), @@ -259,8 +162,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,10 +189,8 @@ 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, ), const SizedBox(height: AppSpacing.lg), @@ -315,38 +218,29 @@ 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; + config.platformAdIdentifiers, + )..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 +249,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'], + label: l10n.nativeAdIdLabel, + description: l10n.nativeAdIdDescription, + value: platformIdentifiers.nativeAdId, + onChanged: (value) => updatePlatformIdentifiers('nativeAdId', value), + controller: controllers['nativeAdId'], ), AppConfigTextField( - label: l10n.feedBannerAdIdLabel, - description: l10n.feedBannerAdIdDescription, - value: platformIdentifiers.feedBannerAdId, - onChanged: (value) => - updatePlatformIdentifiers('feedBannerAdId', value), - controller: controllers['feedBannerAdId'], - ), - 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'], ), ], ); From 5eeb023073ba2f1e7a24c2fff5bee81b7ec56673 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:34:27 +0100 Subject: [PATCH 08/46] refactor(config): align feed ad and decorator forms with new models [WHAT]: Refactors `FeedAdSettingsForm` and `FeedDecoratorForm` to use the new `FeaturesConfig` model paths. [WHY]: These forms need to be updated to read from and write to the correct locations within the new `RemoteConfig.features` structure. [HOW]: The `remoteConfig` access path is updated in both widgets. For `FeedAdSettingsForm`, it now accesses `remoteConfig.features.ads.feedAdConfiguration`. For `FeedDecoratorForm`, it now accesses `remoteConfig.features.feed.decorators`. The internal `copyWith` logic is updated to correctly rebuild the nested state object. --- .../widgets/feed_ad_settings_form.dart | 111 +++++++++--------- .../widgets/feed_decorator_form.dart | 99 ++++++++-------- 2 files changed, 110 insertions(+), 100 deletions(-) diff --git a/lib/app_configuration/widgets/feed_ad_settings_form.dart b/lib/app_configuration/widgets/feed_ad_settings_form.dart index 301cf7e5..98e34a0f 100644 --- a/lib/app_configuration/widgets/feed_ad_settings_form.dart +++ b/lib/app_configuration/widgets/feed_ad_settings_form.dart @@ -39,7 +39,7 @@ class _FeedAdSettingsFormState extends State /// Controllers for ad placement interval fields, mapped by user role. /// These are used to manage text input for each role's ad placement interval. late final Map - _adPlacementIntervalControllers; + _adPlacementIntervalControllers; @override void initState() { @@ -54,36 +54,32 @@ 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: - TextEditingController( - text: _getAdFrequency(feedAdConfig, role).toString(), - ) - ..selection = TextSelection.collapsed( - offset: _getAdFrequency(feedAdConfig, role).toString().length, - ), + role: TextEditingController( + text: _getAdFrequency(feedAdConfig, role).toString(), + )..selection = TextSelection.collapsed( + offset: _getAdFrequency(feedAdConfig, role).toString().length, + ), }; _adPlacementIntervalControllers = { for (final role in AppUserRole.values) - role: - TextEditingController( - text: _getAdPlacementInterval(feedAdConfig, role).toString(), - ) - ..selection = TextSelection.collapsed( - offset: _getAdPlacementInterval( - feedAdConfig, - role, - ).toString().length, - ), + role: TextEditingController( + text: _getAdPlacementInterval(feedAdConfig, role).toString(), + )..selection = TextSelection.collapsed( + offset: + _getAdPlacementInterval(feedAdConfig, 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 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) { @@ -93,17 +89,15 @@ class _FeedAdSettingsFormState extends State ); } - final newPlacementIntervalValue = _getAdPlacementInterval( - feedAdConfig, - role, - ).toString(); + final newPlacementIntervalValue = + _getAdPlacementInterval(feedAdConfig, role).toString(); if (_adPlacementIntervalControllers[role]?.text != newPlacementIntervalValue) { _adPlacementIntervalControllers[role]?.text = newPlacementIntervalValue; _adPlacementIntervalControllers[role]?.selection = TextSelection.collapsed( - offset: newPlacementIntervalValue.length, - ); + offset: newPlacementIntervalValue.length, + ); } } } @@ -111,8 +105,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 +126,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 +139,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), + ), ), ), ); @@ -164,8 +161,8 @@ class _FeedAdSettingsFormState extends State Text( l10n.feedAdTypeSelectionDescription, 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, ), const SizedBox(height: AppSpacing.lg), @@ -192,9 +189,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, + ), ), ), ), @@ -217,8 +216,8 @@ class _FeedAdSettingsFormState extends State Text( l10n.userRoleFrequencySettingsDescription, 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, ), const SizedBox(height: AppSpacing.lg), @@ -291,9 +290,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, + ), ), ), ), @@ -318,13 +319,15 @@ class _FeedAdSettingsFormState extends State ); final newVisibleTo = Map.from( - config.visibleTo, - )..[role] = newRoleConfig; + config.visibleTo, + )..[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, + ), ), ), ), @@ -342,13 +345,15 @@ class _FeedAdSettingsFormState extends State ); final newVisibleTo = Map.from( - config.visibleTo, - )..[role] = newRoleConfig; + config.visibleTo, + )..[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..2468cbcc 100644 --- a/lib/app_configuration/widgets/feed_decorator_form.dart +++ b/lib/app_configuration/widgets/feed_decorator_form.dart @@ -50,45 +50,38 @@ 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]!; - _itemsToDisplayController = - TextEditingController( - text: decoratorConfig.itemsToDisplay?.toString() ?? '', - ) - ..selection = TextSelection.collapsed( - offset: decoratorConfig.itemsToDisplay?.toString().length ?? 0, - ); + widget.remoteConfig.features.feed.decorators[widget.decoratorType]!; + _itemsToDisplayController = TextEditingController( + text: decoratorConfig.itemsToDisplay?.toString() ?? '', + )..selection = TextSelection.collapsed( + offset: decoratorConfig.itemsToDisplay?.toString().length ?? 0, + ); _roleControllers = { for (final role in AppUserRole.values) - role: - TextEditingController( - text: - decoratorConfig.visibleTo[role]?.daysBetweenViews - .toString() ?? - '', - ) - ..selection = TextSelection.collapsed( - offset: - decoratorConfig.visibleTo[role]?.daysBetweenViews - .toString() - .length ?? - 0, - ), + role: TextEditingController( + text: + decoratorConfig.visibleTo[role]?.daysBetweenViews.toString() ?? + '', + )..selection = TextSelection.collapsed( + offset: decoratorConfig + .visibleTo[role]?.daysBetweenViews.toString().length ?? + 0, + ), }; } 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 +139,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 +152,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, - )..[widget.decoratorType] = newDecoratorConfig; + decorators, + )..[widget.decoratorType] = newDecoratorConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( - feedDecoratorConfig: newFeedDecoratorConfig, + features: features.copyWith( + feed: feed.copyWith(decorators: newDecorators), + ), ), ); }, @@ -177,13 +174,15 @@ class _FeedDecoratorFormState extends State final newDecoratorConfig = decoratorConfig.copyWith( itemsToDisplay: value, ); - final newFeedDecoratorConfig = + final newDecorators = Map.from( - widget.remoteConfig.feedDecoratorConfig, - )..[widget.decoratorType] = newDecoratorConfig; + decorators, + )..[widget.decoratorType] = newDecoratorConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( - feedDecoratorConfig: newFeedDecoratorConfig, + features: features.copyWith( + feed: feed.copyWith(decorators: newDecorators), + ), ), ); }, @@ -248,8 +247,8 @@ class _FeedDecoratorFormState extends State ? (value) { final newVisibleTo = Map.from( - decoratorConfig.visibleTo, - ); + decoratorConfig.visibleTo, + ); if (value ?? false) { newVisibleTo[role] = const FeedDecoratorRoleConfig( daysBetweenViews: 7, @@ -260,13 +259,16 @@ class _FeedDecoratorFormState extends State final newDecoratorConfig = decoratorConfig.copyWith( visibleTo: newVisibleTo, ); - final newFeedDecoratorConfig = + final newDecorators = Map.from( - widget.remoteConfig.feedDecoratorConfig, - )..[widget.decoratorType] = newDecoratorConfig; + 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), + ), ), ); } @@ -288,18 +290,21 @@ class _FeedDecoratorFormState extends State ); final newVisibleTo = Map.from( - decoratorConfig.visibleTo, - )..[role] = newRoleConfig; + decoratorConfig.visibleTo, + )..[role] = newRoleConfig; final newDecoratorConfig = decoratorConfig.copyWith( visibleTo: newVisibleTo, ); - final newFeedDecoratorConfig = + final newDecorators = Map.from( - widget.remoteConfig.feedDecoratorConfig, - )..[widget.decoratorType] = newDecoratorConfig; + 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), + ), ), ); }, From 061e759182b3f4dfa6dd04c0a0c1c7a21782856d Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:35:22 +0100 Subject: [PATCH 09/46] refactor(config): align PushNotificationSettingsForm with new model [WHAT]: Refactors the `PushNotificationSettingsForm` to align with the new `PushNotificationConfig` model. [WHY]: The form's data binding needs to be updated to point to the new location of the push notification configuration within the main `RemoteConfig` state object. [HOW]: The widget is updated to access its configuration from `remoteConfig.features.pushNotifications`. The `onChanged` callbacks are updated to use the correct `copyWith` path: `remoteConfig.copyWith(features: features.copyWith(pushNotifications: ...))`. --- .../push_notification_settings_form.dart | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/app_configuration/widgets/push_notification_settings_form.dart b/lib/app_configuration/widgets/push_notification_settings_form.dart index f011fa95..ac6bad6d 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), + ), ), ); }, @@ -71,8 +74,8 @@ class PushNotificationSettingsForm extends StatelessWidget { Text( l10n.pushNotificationPrimaryProviderDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), Align( @@ -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, + ), ), ), ); @@ -119,8 +124,8 @@ class PushNotificationSettingsForm extends StatelessWidget { Text( l10n.pushNotificationDeliveryTypesDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), Column( @@ -131,17 +136,16 @@ class PushNotificationSettingsForm extends StatelessWidget { value: pushConfig.deliveryConfigs[type] ?? false, onChanged: (value) { final newDeliveryConfigs = - Map< - PushNotificationSubscriptionDeliveryType, - bool - >.from( - pushConfig.deliveryConfigs, - ); + Map.from( + pushConfig.deliveryConfigs, + ); newDeliveryConfigs[type] = value; onConfigChanged( remoteConfig.copyWith( - pushNotificationConfig: pushConfig.copyWith( - deliveryConfigs: newDeliveryConfigs, + features: remoteConfig.features.copyWith( + pushNotifications: pushConfig.copyWith( + deliveryConfigs: newDeliveryConfigs, + ), ), ), ); From 71347daee39f88d2369c8079434644146f471b43 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:36:20 +0100 Subject: [PATCH 10/46] refactor(config): align saved filter limit forms with UserConfig model [WHAT]: Refactors `SavedFilterLimitsSection` and `SavedFilterLimitsForm` to bind to the new `UserLimitsConfig` model. [WHY]: These widgets were previously bound to a different part of the configuration. They must be updated to read from and write to the `savedHeadlineFilters` and `savedSourceFilters` maps within `remoteConfig.user.limits`. [HOW]: In `SavedFilterLimitsSection`, the `ExpansionTile`s are updated to pass the correct `SavedFilterLimitsForm` widgets. In `SavedFilterLimitsForm`, the `_getLimitsForRole` and `_onValueChanged` methods are completely rewritten to access and update the `UserLimitsConfig` model via `remoteConfig.user.limits`, using the correct nested `copyWith` logic. --- .../widgets/saved_filter_limits_form.dart | 65 +++++++++---------- .../widgets/saved_filter_limits_section.dart | 23 +++---- 2 files changed, 41 insertions(+), 47 deletions(-) diff --git a/lib/app_configuration/widgets/saved_filter_limits_form.dart b/lib/app_configuration/widgets/saved_filter_limits_form.dart index b5467566..21ce3d7c 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(); } } @@ -73,9 +72,8 @@ class _SavedFilterLimitsFormState extends State final limits = _getLimitsForRole(role); _controllers[role]!['total'] = _createController(limits.total.toString()); - _controllers[role]!['pinned'] = _createController( - limits.pinned.toString(), - ); + _controllers[role]!['pinned'] = + _createController(limits.pinned.toString()); if (widget.filterType == SavedFilterType.headline) { for (final type in PushNotificationSubscriptionDeliveryType.values) { @@ -102,7 +100,7 @@ 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,59 +129,57 @@ 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 newLimitsMap = Map.from( - isHeadline - ? config.savedHeadlineFiltersLimit - : config.savedSourceFiltersLimit, - ); + final currentLimitsMap = isHeadline + ? limitsConfig.savedHeadlineFilters + : limitsConfig.savedSourceFilters; + + final newLimitsMap = + Map.from(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 deliveryType = + PushNotificationSubscriptionDeliveryType.values.byName(field); final newSubscriptions = Map.from( - currentLimits.notificationSubscriptions ?? {}, - ); + currentLimits.notificationSubscriptions ?? {}, + ); newSubscriptions[deliveryType] = value; newLimits = currentLimits.copyWith( notificationSubscriptions: newSubscriptions, ); } - // 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 +207,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 +244,6 @@ class _SavedFilterLimitsFormState extends State ); } - /// Builds the list of input fields for notification subscription limits. List _buildNotificationFields( AppLocalizations l10n, AppUserRole role, @@ -259,11 +252,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..45741f8a 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; @@ -65,10 +70,8 @@ class _SavedFilterLimitsSectionState extends State Text( l10n.savedHeadlineFilterLimitsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), SavedFilterLimitsForm( @@ -102,10 +105,8 @@ class _SavedFilterLimitsSectionState extends State Text( l10n.savedSourceFilterLimitsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), SavedFilterLimitsForm( From 86a2a5b6d027f6aa3935b313c87c7e760b4c0129 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:36:34 +0100 Subject: [PATCH 11/46] fix(router): remove unnecessary hide directive - Remove 'hide AppStatus' directive from core package import - This change simplifies the import statement and removes confusion about unused hidden elements --- lib/router/router.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'; From 9ea81228656abf4fc70bc67541a5a84aa789f284 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:36:55 +0100 Subject: [PATCH 12/46] feat(l10n): add new settings tab and configuration labels - Add app-wide settings tab and labels - Introduce new sections for maintenance mode, updates, and general app settings - Include navigation ad settings and frequency configuration - Add labels for ad unit IDs and saved headlines limit - Ensure new labels are included in both English and Arabic ARB files --- lib/l10n/arb/app_ar.arb | 122 ++++++++++++++++++++++++++++++++++++++-- lib/l10n/arb/app_en.arb | 116 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 4 deletions(-) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index be004178..7b70d92f 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,121 @@ "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": "وصف حد العناوين المحفوظة" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 643aef4b..301e96fd 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1941,5 +1941,121 @@ "pushNotificationProviderOneSignal": "OneSignal", "@pushNotificationProviderOneSignal": { "description": "Label for the OneSignal push notification provider" + }, + "appTab": "App", + "@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" } } \ No newline at end of file From 6bc6b6a5482f568bdc6bad6b2c65cdcf8ba85070 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:37:09 +0100 Subject: [PATCH 13/46] build(l10n): generate --- lib/l10n/app_localizations.dart | 174 +++++++++++++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 98 ++++++++++++++++ lib/l10n/app_localizations_en.dart | 98 ++++++++++++++++ 3 files changed, 370 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e13c8776..06c30a0c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2887,6 +2887,180 @@ 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: + /// **'App'** + 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; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index bb30b7ce..0011c9a2 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1545,4 +1545,102 @@ 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 => + 'الحد الأقصى لعدد العناوين التي يمكن لهذا الدور المستخدم حفظها.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b4066573..d331541a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1548,4 +1548,102 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pushNotificationProviderOneSignal => 'OneSignal'; + + @override + String get appTab => 'App'; + + @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.'; } From 2272641a11a9e4adc2a5406e43a3465cc0f402b4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:50:02 +0100 Subject: [PATCH 14/46] chore: delete file --- .../in_article_ad_slot_type_l10n.dart | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 lib/shared/extensions/in_article_ad_slot_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; - } - } -} From c28c29e11270db40582c520d7a33094fdc747190 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:50:16 +0100 Subject: [PATCH 15/46] fix(app_configuration): update ExpansionTile key to ensure uniqueness - Modify the key of ExpansionTile in UserConfigurationTab - Change from a constant ValueKey to a unique ValueKey based on expandedIndex - This ensures that each ExpansionTile has a unique key, improving the widget's ability to rebuild correctly when its state changes --- lib/app_configuration/view/tabs/user_configuration_tab.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app_configuration/view/tabs/user_configuration_tab.dart b/lib/app_configuration/view/tabs/user_configuration_tab.dart index 066d1568..6a28176b 100644 --- a/lib/app_configuration/view/tabs/user_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/user_configuration_tab.dart @@ -76,7 +76,7 @@ class _UserConfigurationTabState extends State { builder: (context, expandedIndex, child) { const tileIndex = 1; return ExpansionTile( - key: const ValueKey('savedFilterLimitsTile_'), + key: ValueKey('savedFilterLimitsTile_$expandedIndex'), title: Text(l10n.savedFeedFilterLimitsTitle), onExpansionChanged: (isExpanded) { _expandedTileIndex.value = isExpanded ? tileIndex : null; From 1c99175a897c680240d8e6630931c687ebe4604c Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:50:31 +0100 Subject: [PATCH 16/46] feat(app_configuration): wrap navigation ad settings form in SingleChildScrollView This change improves the layout by making the navigation ad settings form scrollable, which helps to prevent overflow errors on smaller screens or when the keyboard is displayed. --- .../widgets/navigation_ad_settings_form.dart | 172 +++++++++--------- 1 file changed, 87 insertions(+), 85 deletions(-) diff --git a/lib/app_configuration/widgets/navigation_ad_settings_form.dart b/lib/app_configuration/widgets/navigation_ad_settings_form.dart index 905551cd..bbe4b5c2 100644 --- a/lib/app_configuration/widgets/navigation_ad_settings_form.dart +++ b/lib/app_configuration/widgets/navigation_ad_settings_form.dart @@ -188,103 +188,105 @@ class _NavigationAdSettingsFormState extends State ) { 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) { - newVisibleTo[role] = const NavigationAdFrequencyConfig( - internalNavigationsBeforeShowingInterstitialAd: 5, - externalNavigationsBeforeShowingInterstitialAd: 1, + return SingleChildScrollView( + child: Column( + children: [ + SwitchListTile( + title: Text(l10n.visibleToRoleLabel(role.l10n(context))), + value: roleConfig != null, + onChanged: (value) { + final newVisibleTo = + Map.from( + config.visibleTo, ); - } 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 (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, ), - ); - }, - ), - 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, + 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: _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], - ), - ], + ); + }, + controller: _externalNavigationsControllers[role], + ), + ], + ), ), - ), - ], + ], + ), ); } From 09c1f5930db28982b35107d26318332c876e95c4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 16:50:39 +0100 Subject: [PATCH 17/46] fix(app_configuration): wrap role limit fields in SingleChildScrollView - Add SingleChildScrollView to wrap the column of AppConfigIntField widgets - This change allows the content to be scrollable and avoid overflow issues - Improve user interface usability on smaller screens or when keyboard is open --- .../widgets/user_limits_config_form.dart | 85 ++++++++++--------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/lib/app_configuration/widgets/user_limits_config_form.dart b/lib/app_configuration/widgets/user_limits_config_form.dart index 6f114b92..b9ef2631 100644 --- a/lib/app_configuration/widgets/user_limits_config_form.dart +++ b/lib/app_configuration/widgets/user_limits_config_form.dart @@ -107,11 +107,14 @@ class _UserLimitsConfigFormState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - l10n.userContentLimitsDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + 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( @@ -155,43 +158,45 @@ class _UserLimitsConfigFormState extends State AppUserRole role, UserLimitsConfig limits, ) { - return 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), + 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: _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], - ), - ], + ); + }, + controller: _savedHeadlinesLimitControllers[role], + ), + ], + ), ); } } From c7a3f3f178a5d8a8e28deb87325245fdf7ed5aa9 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 17:01:19 +0100 Subject: [PATCH 18/46] refactor(app_configuration): simplify user preference limits management - Remove unnecessary helper methods and direct map access - Consolidate limits update logic into more concise functions - Rename variables and methods to better reflect their purpose - Improve code readability and maintainability --- .../widgets/user_preference_limits_form.dart | 147 +++++------------- 1 file changed, 41 insertions(+), 106 deletions(-) diff --git a/lib/app_configuration/widgets/user_preference_limits_form.dart b/lib/app_configuration/widgets/user_preference_limits_form.dart index 03be00d0..897d0c00 100644 --- a/lib/app_configuration/widgets/user_preference_limits_form.dart +++ b/lib/app_configuration/widgets/user_preference_limits_form.dart @@ -35,9 +35,9 @@ class _UserPreferenceLimitsFormState extends State with SingleTickerProviderStateMixin { late TabController _tabController; late final Map - _followedItemsLimitControllers; + _followedItemsLimitControllers; late final Map - _savedHeadlinesLimitControllers; + _savedHeadlinesLimitControllers; @override void initState() { @@ -52,72 +52,55 @@ 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(), - ) - ..selection = TextSelection.collapsed( - offset: _getFollowedItemsLimit( - widget.remoteConfig.userPreferenceConfig, - role, - ).toString().length, - ), + role: TextEditingController( + text: (limitsConfig.followedItems[role] ?? 0).toString(), + )..selection = TextSelection.collapsed( + offset: (limitsConfig.followedItems[role] ?? 0).toString().length, + ), }; _savedHeadlinesLimitControllers = { for (final role in AppUserRole.values) - role: - TextEditingController( - text: _getSavedHeadlinesLimit( - widget.remoteConfig.userPreferenceConfig, - role, - ).toString(), - ) - ..selection = TextSelection.collapsed( - offset: _getSavedHeadlinesLimit( - widget.remoteConfig.userPreferenceConfig, - role, - ).toString().length, - ), + role: TextEditingController( + text: (limitsConfig.savedHeadlines[role] ?? 0).toString(), + )..selection = TextSelection.collapsed( + 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(); - if (_followedItemsLimitControllers[role]?.text != newFollowedItemsLimit) { + final newFollowedItemsLimit = + (limitsConfig.followedItems[role] ?? 0).toString(); + if (_followedItemsLimitControllers[role]?.text != + newFollowedItemsLimit) { _followedItemsLimitControllers[role]?.text = newFollowedItemsLimit; _followedItemsLimitControllers[role]?.selection = TextSelection.collapsed( - offset: newFollowedItemsLimit.length, - ); + offset: newFollowedItemsLimit.length, + ); } - final newSavedHeadlinesLimit = _getSavedHeadlinesLimit( - widget.remoteConfig.userPreferenceConfig, - role, - ).toString(); + final newSavedHeadlinesLimit = + (limitsConfig.savedHeadlines[role] ?? 0).toString(); if (_savedHeadlinesLimitControllers[role]?.text != newSavedHeadlinesLimit) { _savedHeadlinesLimitControllers[role]?.text = newSavedHeadlinesLimit; _savedHeadlinesLimitControllers[role]?.selection = TextSelection.collapsed( - offset: newSavedHeadlinesLimit.length, - ); + offset: newSavedHeadlinesLimit.length, + ); } } } @@ -136,7 +119,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 +151,7 @@ class _UserPreferenceLimitsFormState extends State context, l10n, role, - userPreferenceConfig, + limitsConfig, ), ) .toList(), @@ -182,21 +165,22 @@ 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 +190,15 @@ 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 +258,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); - } } From 0fbd114e30383dc2f4e2810695e4eeb6b52d1294 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 17:01:27 +0100 Subject: [PATCH 19/46] fix(app_configuration): update notification subscription controller keys - Change controller keys for notification subscriptions from 'type.name' to 'notification_type.name' - Update code to reflect new keys in both initialization and value update - Improve readability and maintainability of the code --- .../widgets/saved_filter_limits_form.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/app_configuration/widgets/saved_filter_limits_form.dart b/lib/app_configuration/widgets/saved_filter_limits_form.dart index 21ce3d7c..5ece689d 100644 --- a/lib/app_configuration/widgets/saved_filter_limits_form.dart +++ b/lib/app_configuration/widgets/saved_filter_limits_form.dart @@ -78,7 +78,8 @@ 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()); } } } @@ -100,7 +101,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]!['notification_${type.name}']!, value); + _updateControllerText( + _controllers[role]!['notification_${type.name}']!, + value, + ); } } } From 35c7569349c39d12aee479f71f24c3325739fa2e Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 17:01:45 +0100 Subject: [PATCH 20/46] refactor(extensions): remove unused in_article_ad_slot_type_l10n export - Removed the export statement for 'in_article_ad_slot_type_l10n.dart' from the extensions.dart file. - This change helps to clean up unused code and reduce potential clutter in the project. --- lib/shared/extensions/extensions.dart | 1 - 1 file changed, 1 deletion(-) 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'; From 45e93da83e38c2745ccb28b42ab256c880b69a6a Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 17:11:40 +0100 Subject: [PATCH 21/46] refactor(app): rename UserAppSettings to AppSettings and adjust related classes - Rename UserAppSettings to AppSettings - Update class references in AppBloc, AppEvent, and AppState - Modify property names and method calls to use the new AppSettings class - Adjust variable names to match --- lib/app/bloc/app_bloc.dart | 33 +++++++++++++++++---------------- lib/app/bloc/app_event.dart | 6 +++--- lib/app/bloc/app_state.dart | 15 ++++++++------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index a287481b..252814b2 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -17,7 +17,7 @@ part 'app_state.dart'; class AppBloc extends Bloc { AppBloc({ required AuthRepository authenticationRepository, - required DataRepository userAppSettingsRepository, + required DataRepository userAppSettingsRepository, required DataRepository appConfigRepository, required local_config.AppEnvironment environment, Logger? logger, @@ -36,7 +36,7 @@ class AppBloc extends Bloc { } final AuthRepository _authenticationRepository; - final DataRepository _userAppSettingsRepository; + final DataRepository _userAppSettingsRepository; final DataRepository _appConfigRepository; final Logger _logger; late final StreamSubscription _userSubscription; @@ -68,16 +68,17 @@ 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 _userAppSettingsRepository.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( + // Corrected class name id: user.id, displaySettings: const DisplaySettings( baseTheme: AppBaseTheme.system, @@ -92,15 +93,15 @@ 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( + // Corrected class name and properties + feedItemDensity: FeedItemDensity.standard, + feedItemImageStyle: FeedItemImageStyle.largeThumbnail, + feedItemClickBehavior: FeedItemClickBehavior.internalNavigation, ), ); await _userAppSettingsRepository.create(item: defaultSettings); - emit(state.copyWith(userAppSettings: 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..1aaf074c 100644 --- a/lib/app/bloc/app_event.dart +++ b/lib/app/bloc/app_event.dart @@ -29,11 +29,11 @@ class AppLogoutRequested extends AppEvent { /// {@endtemplate} 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; + 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..77598ec0 100644 --- a/lib/app/bloc/app_state.dart +++ b/lib/app/bloc/app_state.dart @@ -25,7 +25,7 @@ class AppState extends Equatable { this.status = AppStatus.initial, this.user, this.environment, - this.userAppSettings, + this.appSettings, }); /// The current authentication status of the application. @@ -38,24 +38,25 @@ class AppState extends Equatable { final local_config.AppEnvironment? environment; /// The current user application settings. Null if not loaded or unauthenticated. - final UserAppSettings? userAppSettings; + final AppSettings? appSettings; /// Creates a copy of the current state with updated values. AppState copyWith({ AppStatus? status, User? user, local_config.AppEnvironment? environment, - UserAppSettings? userAppSettings, + AppSettings? appSettings, bool clearEnvironment = false, - bool clearUserAppSettings = false, + bool clearAppSettings = false, }) { return AppState( status: status ?? this.status, user: user ?? this.user, environment: clearEnvironment ? null : environment ?? this.environment, - userAppSettings: clearUserAppSettings + appSettings: + clearAppSettings // Corrected property name ? null - : userAppSettings ?? this.userAppSettings, + : appSettings ?? this.appSettings, ); } @@ -64,6 +65,6 @@ class AppState extends Equatable { status, user, environment, - userAppSettings, + appSettings, ]; } From e8ab82ee2553600bb2ecda50c48afc089615de17 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 17:12:10 +0100 Subject: [PATCH 22/46] style: format --- lib/app/config/app_config.dart | 2 +- .../view/app_configuration_page.dart | 36 +++++----- .../view/tabs/features_configuration_tab.dart | 9 ++- .../widgets/ad_platform_config_form.dart | 57 ++++++++++----- .../widgets/feed_ad_settings_form.dart | 62 +++++++++-------- .../widgets/feed_decorator_form.dart | 69 +++++++++++-------- .../widgets/general_app_config_form.dart | 4 +- .../widgets/maintenance_config_form.dart | 4 +- .../widgets/navigation_ad_settings_form.dart | 49 +++++++------ .../push_notification_settings_form.dart | 17 +++-- .../widgets/saved_filter_limits_form.dart | 23 ++++--- .../widgets/saved_filter_limits_section.dart | 12 ++-- .../widgets/update_config_form.dart | 14 ++-- .../widgets/user_limits_config_form.dart | 19 ++--- .../widgets/user_preference_limits_form.dart | 60 ++++++++-------- .../create_headline_state.dart | 2 +- .../view/create_headline_page.dart | 2 +- 17 files changed, 251 insertions(+), 190 deletions(-) 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_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index cff0c0ca..34cb31ad 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -82,16 +82,16 @@ class _AppConfigurationPageState extends State content: Text( l10n.appConfigSaveSuccessMessage, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onPrimary, - ), + color: Theme.of(context).colorScheme.onPrimary, + ), ), backgroundColor: Theme.of(context).colorScheme.primary, ), ); // Clear the showSaveSuccess flag after showing the snackbar context.read().add( - const AppConfigurationFieldChanged(), - ); + const AppConfigurationFieldChanged(), + ); } else if (state.status == AppConfigurationStatus.failure && state.exception != null) { ScaffoldMessenger.of(context) @@ -101,8 +101,8 @@ class _AppConfigurationPageState extends State content: Text( state.exception!.toFriendlyMessage(context), style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onError, - ), + color: Theme.of(context).colorScheme.onError, + ), ), backgroundColor: Theme.of(context).colorScheme.error, ), @@ -122,8 +122,8 @@ class _AppConfigurationPageState extends State exception: state.exception!, onRetry: () { context.read().add( - const AppConfigurationLoaded(), - ); + const AppConfigurationLoaded(), + ); }, ); } else if (state.status == AppConfigurationStatus.success && @@ -136,24 +136,24 @@ class _AppConfigurationPageState extends State remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged(remoteConfig: newConfig), - ); + AppConfigurationFieldChanged(remoteConfig: newConfig), + ); }, ), FeaturesConfigurationTab( remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged(remoteConfig: newConfig), - ); + AppConfigurationFieldChanged(remoteConfig: newConfig), + ); }, ), UserConfigurationTab( remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged(remoteConfig: newConfig), - ); + AppConfigurationFieldChanged(remoteConfig: newConfig), + ); }, ), ], @@ -189,8 +189,8 @@ class _AppConfigurationPageState extends State ? () { // Discard changes: revert to original config context.read().add( - const AppConfigurationDiscarded(), - ); + const AppConfigurationDiscarded(), + ); } : null, child: Text(AppLocalizationsX(context).l10n.discardChangesButton), @@ -204,8 +204,8 @@ class _AppConfigurationPageState extends State confirmed && remoteConfig != null) { context.read().add( - AppConfigurationUpdated(remoteConfig), - ); + AppConfigurationUpdated(remoteConfig), + ); } } : null, diff --git a/lib/app_configuration/view/tabs/features_configuration_tab.dart b/lib/app_configuration/view/tabs/features_configuration_tab.dart index 88dc09de..d2e9945b 100644 --- a/lib/app_configuration/view/tabs/features_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -138,11 +138,10 @@ class _FeaturesConfigurationTabState extends State { Text( l10n.feedDecoratorsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), for (final decoratorType in FeedDecoratorType.values) diff --git a/lib/app_configuration/widgets/ad_platform_config_form.dart b/lib/app_configuration/widgets/ad_platform_config_form.dart index abd213e2..475dd00d 100644 --- a/lib/app_configuration/widgets/ad_platform_config_form.dart +++ b/lib/app_configuration/widgets/ad_platform_config_form.dart @@ -30,7 +30,7 @@ class AdPlatformConfigForm extends StatefulWidget { class _AdPlatformConfigFormState extends State { late AdPlatformType _selectedPlatform; late Map> - _platformAdIdentifierControllers; + _platformAdIdentifierControllers; @override void initState() { @@ -53,18 +53,33 @@ class _AdPlatformConfigFormState extends State { for (final platform in AdPlatformType.values) platform: { 'nativeAdId': TextEditingController( - text: widget.remoteConfig.features.ads - .platformAdIdentifiers[platform]?.nativeAdId ?? + text: + widget + .remoteConfig + .features + .ads + .platformAdIdentifiers[platform] + ?.nativeAdId ?? '', ), 'bannerAdId': TextEditingController( - text: widget.remoteConfig.features.ads - .platformAdIdentifiers[platform]?.bannerAdId ?? + text: + widget + .remoteConfig + .features + .ads + .platformAdIdentifiers[platform] + ?.bannerAdId ?? '', ), 'interstitialAdId': TextEditingController( - text: widget.remoteConfig.features.ads - .platformAdIdentifiers[platform]?.interstitialAdId ?? + text: + widget + .remoteConfig + .features + .ads + .platformAdIdentifiers[platform] + ?.interstitialAdId ?? '', ), }, @@ -131,8 +146,8 @@ 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, ), const SizedBox(height: AppSpacing.lg), @@ -189,8 +204,8 @@ 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, ), const SizedBox(height: AppSpacing.lg), @@ -219,8 +234,12 @@ class _AdPlatformConfigFormState extends State { void updatePlatformIdentifiers(String key, String? value) { final newIdentifiers = platformIdentifiers.copyWith( - nativeAdId: key == 'nativeAdId' ? value : platformIdentifiers.nativeAdId, - bannerAdId: key == 'bannerAdId' ? value : platformIdentifiers.bannerAdId, + nativeAdId: key == 'nativeAdId' + ? value + : platformIdentifiers.nativeAdId, + bannerAdId: key == 'bannerAdId' + ? value + : platformIdentifiers.bannerAdId, interstitialAdId: key == 'interstitialAdId' ? value : platformIdentifiers.interstitialAdId, @@ -228,12 +247,12 @@ class _AdPlatformConfigFormState extends State { final newPlatformAdIdentifiers = Map.from( - config.platformAdIdentifiers, - )..update( - platform, - (_) => newIdentifiers, - ifAbsent: () => newIdentifiers, - ); + config.platformAdIdentifiers, + )..update( + platform, + (_) => newIdentifiers, + ifAbsent: () => newIdentifiers, + ); widget.onConfigChanged( widget.remoteConfig.copyWith( diff --git a/lib/app_configuration/widgets/feed_ad_settings_form.dart b/lib/app_configuration/widgets/feed_ad_settings_form.dart index 98e34a0f..26629b3a 100644 --- a/lib/app_configuration/widgets/feed_ad_settings_form.dart +++ b/lib/app_configuration/widgets/feed_ad_settings_form.dart @@ -39,7 +39,7 @@ class _FeedAdSettingsFormState extends State /// Controllers for ad placement interval fields, mapped by user role. /// These are used to manage text input for each role's ad placement interval. late final Map - _adPlacementIntervalControllers; + _adPlacementIntervalControllers; @override void initState() { @@ -54,32 +54,36 @@ class _FeedAdSettingsFormState extends State /// Initializes text editing controllers for each user role based on current /// remote config values. void _initializeControllers() { - final feedAdConfig = - widget.remoteConfig.features.ads.feedAdConfiguration; + final feedAdConfig = widget.remoteConfig.features.ads.feedAdConfiguration; _adFrequencyControllers = { for (final role in AppUserRole.values) - role: TextEditingController( - text: _getAdFrequency(feedAdConfig, role).toString(), - )..selection = TextSelection.collapsed( - offset: _getAdFrequency(feedAdConfig, role).toString().length, - ), + role: + TextEditingController( + text: _getAdFrequency(feedAdConfig, role).toString(), + ) + ..selection = TextSelection.collapsed( + offset: _getAdFrequency(feedAdConfig, role).toString().length, + ), }; _adPlacementIntervalControllers = { for (final role in AppUserRole.values) - role: TextEditingController( - text: _getAdPlacementInterval(feedAdConfig, role).toString(), - )..selection = TextSelection.collapsed( - offset: - _getAdPlacementInterval(feedAdConfig, role).toString().length, - ), + role: + TextEditingController( + text: _getAdPlacementInterval(feedAdConfig, role).toString(), + ) + ..selection = TextSelection.collapsed( + offset: _getAdPlacementInterval( + feedAdConfig, + 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 feedAdConfig = - widget.remoteConfig.features.ads.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) { @@ -89,15 +93,17 @@ class _FeedAdSettingsFormState extends State ); } - final newPlacementIntervalValue = - _getAdPlacementInterval(feedAdConfig, role).toString(); + final newPlacementIntervalValue = _getAdPlacementInterval( + feedAdConfig, + role, + ).toString(); if (_adPlacementIntervalControllers[role]?.text != newPlacementIntervalValue) { _adPlacementIntervalControllers[role]?.text = newPlacementIntervalValue; _adPlacementIntervalControllers[role]?.selection = TextSelection.collapsed( - offset: newPlacementIntervalValue.length, - ); + offset: newPlacementIntervalValue.length, + ); } } } @@ -161,8 +167,8 @@ class _FeedAdSettingsFormState extends State Text( l10n.feedAdTypeSelectionDescription, 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, ), const SizedBox(height: AppSpacing.lg), @@ -216,8 +222,8 @@ class _FeedAdSettingsFormState extends State Text( l10n.userRoleFrequencySettingsDescription, 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, ), const SizedBox(height: AppSpacing.lg), @@ -319,8 +325,8 @@ class _FeedAdSettingsFormState extends State ); final newVisibleTo = Map.from( - config.visibleTo, - )..[role] = newRoleConfig; + config.visibleTo, + )..[role] = newRoleConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( features: widget.remoteConfig.features.copyWith( @@ -345,8 +351,8 @@ class _FeedAdSettingsFormState extends State ); final newVisibleTo = Map.from( - config.visibleTo, - )..[role] = newRoleConfig; + config.visibleTo, + )..[role] = newRoleConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( features: widget.remoteConfig.features.copyWith( diff --git a/lib/app_configuration/widgets/feed_decorator_form.dart b/lib/app_configuration/widgets/feed_decorator_form.dart index 2468cbcc..fae03eb4 100644 --- a/lib/app_configuration/widgets/feed_decorator_form.dart +++ b/lib/app_configuration/widgets/feed_decorator_form.dart @@ -59,23 +59,30 @@ class _FeedDecoratorFormState extends State void _initializeControllers() { final decoratorConfig = widget.remoteConfig.features.feed.decorators[widget.decoratorType]!; - _itemsToDisplayController = TextEditingController( - text: decoratorConfig.itemsToDisplay?.toString() ?? '', - )..selection = TextSelection.collapsed( - offset: decoratorConfig.itemsToDisplay?.toString().length ?? 0, - ); + _itemsToDisplayController = + TextEditingController( + text: decoratorConfig.itemsToDisplay?.toString() ?? '', + ) + ..selection = TextSelection.collapsed( + offset: decoratorConfig.itemsToDisplay?.toString().length ?? 0, + ); _roleControllers = { for (final role in AppUserRole.values) - role: TextEditingController( - text: - decoratorConfig.visibleTo[role]?.daysBetweenViews.toString() ?? - '', - )..selection = TextSelection.collapsed( - offset: decoratorConfig - .visibleTo[role]?.daysBetweenViews.toString().length ?? - 0, - ), + role: + TextEditingController( + text: + decoratorConfig.visibleTo[role]?.daysBetweenViews + .toString() ?? + '', + ) + ..selection = TextSelection.collapsed( + offset: + decoratorConfig.visibleTo[role]?.daysBetweenViews + .toString() + .length ?? + 0, + ), }; } @@ -154,8 +161,8 @@ class _FeedDecoratorFormState extends State final newDecoratorConfig = decoratorConfig.copyWith(enabled: value); final newDecorators = Map.from( - decorators, - )..[widget.decoratorType] = newDecoratorConfig; + decorators, + )..[widget.decoratorType] = newDecoratorConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( features: features.copyWith( @@ -176,8 +183,8 @@ class _FeedDecoratorFormState extends State ); final newDecorators = Map.from( - decorators, - )..[widget.decoratorType] = newDecoratorConfig; + decorators, + )..[widget.decoratorType] = newDecoratorConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( features: features.copyWith( @@ -247,8 +254,8 @@ class _FeedDecoratorFormState extends State ? (value) { final newVisibleTo = Map.from( - decoratorConfig.visibleTo, - ); + decoratorConfig.visibleTo, + ); if (value ?? false) { newVisibleTo[role] = const FeedDecoratorRoleConfig( daysBetweenViews: 7, @@ -261,13 +268,14 @@ class _FeedDecoratorFormState extends State ); final newDecorators = Map.from( - widget.remoteConfig.features.feed.decorators, - )..[widget.decoratorType] = newDecoratorConfig; + 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), + feed: widget.remoteConfig.features.feed.copyWith( + decorators: newDecorators, + ), ), ), ); @@ -290,20 +298,21 @@ class _FeedDecoratorFormState extends State ); final newVisibleTo = Map.from( - decoratorConfig.visibleTo, - )..[role] = newRoleConfig; + decoratorConfig.visibleTo, + )..[role] = newRoleConfig; final newDecoratorConfig = decoratorConfig.copyWith( visibleTo: newVisibleTo, ); final newDecorators = Map.from( - widget.remoteConfig.features.feed.decorators, - )..[widget.decoratorType] = newDecoratorConfig; + 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), + 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 index c77169b8..a0304987 100644 --- a/lib/app_configuration/widgets/general_app_config_form.dart +++ b/lib/app_configuration/widgets/general_app_config_form.dart @@ -74,8 +74,8 @@ class _GeneralAppConfigFormState extends State { Text( l10n.generalAppConfigDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), AppConfigTextField( diff --git a/lib/app_configuration/widgets/maintenance_config_form.dart b/lib/app_configuration/widgets/maintenance_config_form.dart index 5cb9a5f4..834ae11d 100644 --- a/lib/app_configuration/widgets/maintenance_config_form.dart +++ b/lib/app_configuration/widgets/maintenance_config_form.dart @@ -34,8 +34,8 @@ class MaintenanceConfigForm extends StatelessWidget { Text( l10n.maintenanceConfigDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), SwitchListTile( diff --git a/lib/app_configuration/widgets/navigation_ad_settings_form.dart b/lib/app_configuration/widgets/navigation_ad_settings_form.dart index bbe4b5c2..9dc382bd 100644 --- a/lib/app_configuration/widgets/navigation_ad_settings_form.dart +++ b/lib/app_configuration/widgets/navigation_ad_settings_form.dart @@ -33,9 +33,9 @@ class _NavigationAdSettingsFormState extends State late TabController _tabController; late final Map - _internalNavigationsControllers; + _internalNavigationsControllers; late final Map - _externalNavigationsControllers; + _externalNavigationsControllers; @override void initState() { @@ -68,11 +68,17 @@ class _NavigationAdSettingsFormState extends State final navAdConfig = widget.remoteConfig.features.ads.navigationAdConfiguration; for (final role in AppUserRole.values) { - final newInternalValue = _getInternalNavigations(navAdConfig, role).toString(); + final newInternalValue = _getInternalNavigations( + navAdConfig, + role, + ).toString(); if (_internalNavigationsControllers[role]?.text != newInternalValue) { _internalNavigationsControllers[role]?.text = newInternalValue; } - final newExternalValue = _getExternalNavigations(navAdConfig, role).toString(); + final newExternalValue = _getExternalNavigations( + navAdConfig, + role, + ).toString(); if (_externalNavigationsControllers[role]?.text != newExternalValue) { _externalNavigationsControllers[role]?.text = newExternalValue; } @@ -118,8 +124,9 @@ class _NavigationAdSettingsFormState extends State widget.remoteConfig.copyWith( features: features.copyWith( ads: adConfig.copyWith( - navigationAdConfiguration: - navAdConfig.copyWith(enabled: value), + navigationAdConfiguration: navAdConfig.copyWith( + enabled: value, + ), ), ), ), @@ -138,8 +145,8 @@ class _NavigationAdSettingsFormState extends State Text( l10n.navigationAdFrequencyDescription, 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, ), const SizedBox(height: AppSpacing.lg), @@ -197,8 +204,8 @@ class _NavigationAdSettingsFormState extends State onChanged: (value) { final newVisibleTo = Map.from( - config.visibleTo, - ); + config.visibleTo, + ); if (value) { newVisibleTo[role] = const NavigationAdFrequencyConfig( internalNavigationsBeforeShowingInterstitialAd: 5, @@ -231,16 +238,16 @@ class _NavigationAdSettingsFormState extends State AppConfigIntField( label: l10n.internalNavigationsBeforeAdLabel, description: l10n.internalNavigationsBeforeAdDescription, - value: - roleConfig.internalNavigationsBeforeShowingInterstitialAd, + value: roleConfig + .internalNavigationsBeforeShowingInterstitialAd, onChanged: (value) { final newRoleConfig = roleConfig.copyWith( internalNavigationsBeforeShowingInterstitialAd: value, ); final newVisibleTo = Map.from( - config.visibleTo, - )..[role] = newRoleConfig; + config.visibleTo, + )..[role] = newRoleConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( features: widget.remoteConfig.features.copyWith( @@ -258,16 +265,16 @@ class _NavigationAdSettingsFormState extends State AppConfigIntField( label: l10n.externalNavigationsBeforeAdLabel, description: l10n.externalNavigationsBeforeAdDescription, - value: - roleConfig.externalNavigationsBeforeShowingInterstitialAd, + value: roleConfig + .externalNavigationsBeforeShowingInterstitialAd, onChanged: (value) { final newRoleConfig = roleConfig.copyWith( externalNavigationsBeforeShowingInterstitialAd: value, ); final newVisibleTo = Map.from( - config.visibleTo, - )..[role] = newRoleConfig; + config.visibleTo, + )..[role] = newRoleConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( features: widget.remoteConfig.features.copyWith( @@ -295,7 +302,8 @@ class _NavigationAdSettingsFormState extends State AppUserRole role, ) { return config - .visibleTo[role]?.internalNavigationsBeforeShowingInterstitialAd ?? + .visibleTo[role] + ?.internalNavigationsBeforeShowingInterstitialAd ?? 0; } @@ -304,7 +312,8 @@ class _NavigationAdSettingsFormState extends State AppUserRole role, ) { return config - .visibleTo[role]?.externalNavigationsBeforeShowingInterstitialAd ?? + .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 ac6bad6d..8c418d14 100644 --- a/lib/app_configuration/widgets/push_notification_settings_form.dart +++ b/lib/app_configuration/widgets/push_notification_settings_form.dart @@ -74,8 +74,8 @@ class PushNotificationSettingsForm extends StatelessWidget { Text( l10n.pushNotificationPrimaryProviderDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), Align( @@ -124,8 +124,8 @@ class PushNotificationSettingsForm extends StatelessWidget { Text( l10n.pushNotificationDeliveryTypesDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), Column( @@ -136,9 +136,12 @@ class PushNotificationSettingsForm extends StatelessWidget { value: pushConfig.deliveryConfigs[type] ?? false, onChanged: (value) { final newDeliveryConfigs = - Map.from( - pushConfig.deliveryConfigs, - ); + Map< + PushNotificationSubscriptionDeliveryType, + bool + >.from( + pushConfig.deliveryConfigs, + ); newDeliveryConfigs[type] = value; onConfigChanged( remoteConfig.copyWith( diff --git a/lib/app_configuration/widgets/saved_filter_limits_form.dart b/lib/app_configuration/widgets/saved_filter_limits_form.dart index 5ece689d..ad926ffa 100644 --- a/lib/app_configuration/widgets/saved_filter_limits_form.dart +++ b/lib/app_configuration/widgets/saved_filter_limits_form.dart @@ -72,14 +72,16 @@ class _SavedFilterLimitsFormState extends State final limits = _getLimitsForRole(role); _controllers[role]!['total'] = _createController(limits.total.toString()); - _controllers[role]!['pinned'] = - _createController(limits.pinned.toString()); + _controllers[role]!['pinned'] = _createController( + limits.pinned.toString(), + ); if (widget.filterType == SavedFilterType.headline) { for (final type in PushNotificationSubscriptionDeliveryType.values) { final value = limits.notificationSubscriptions?[type] ?? 0; - _controllers[role]!['notification_${type.name}'] = - _createController(value.toString()); + _controllers[role]!['notification_${type.name}'] = _createController( + value.toString(), + ); } } } @@ -150,8 +152,9 @@ class _SavedFilterLimitsFormState extends State ? limitsConfig.savedHeadlineFilters : limitsConfig.savedSourceFilters; - final newLimitsMap = - Map.from(currentLimitsMap); + final newLimitsMap = Map.from( + currentLimitsMap, + ); final currentLimits = newLimitsMap[role]!; SavedFilterLimits newLimits; @@ -161,12 +164,12 @@ class _SavedFilterLimitsFormState extends State } else if (field == 'pinned') { newLimits = currentLimits.copyWith(pinned: value); } else { - final deliveryType = - PushNotificationSubscriptionDeliveryType.values.byName(field); + final deliveryType = PushNotificationSubscriptionDeliveryType.values + .byName(field); final newSubscriptions = Map.from( - currentLimits.notificationSubscriptions ?? {}, - ); + currentLimits.notificationSubscriptions ?? {}, + ); newSubscriptions[deliveryType] = value; newLimits = currentLimits.copyWith( notificationSubscriptions: newSubscriptions, diff --git a/lib/app_configuration/widgets/saved_filter_limits_section.dart b/lib/app_configuration/widgets/saved_filter_limits_section.dart index 45741f8a..69803edf 100644 --- a/lib/app_configuration/widgets/saved_filter_limits_section.dart +++ b/lib/app_configuration/widgets/saved_filter_limits_section.dart @@ -70,8 +70,10 @@ class _SavedFilterLimitsSectionState extends State { Text( l10n.savedHeadlineFilterLimitsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), SavedFilterLimitsForm( @@ -105,8 +107,10 @@ class _SavedFilterLimitsSectionState extends State { Text( l10n.savedSourceFilterLimitsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), SavedFilterLimitsForm( diff --git a/lib/app_configuration/widgets/update_config_form.dart b/lib/app_configuration/widgets/update_config_form.dart index 1ce2f75d..6ddc890c 100644 --- a/lib/app_configuration/widgets/update_config_form.dart +++ b/lib/app_configuration/widgets/update_config_form.dart @@ -34,11 +34,13 @@ class _UpdateConfigFormState extends State { void initState() { super.initState(); final updateConfig = widget.remoteConfig.app.update; - _latestVersionController = - TextEditingController(text: updateConfig.latestAppVersion); + _latestVersionController = TextEditingController( + text: updateConfig.latestAppVersion, + ); _iosUrlController = TextEditingController(text: updateConfig.iosUpdateUrl); - _androidUrlController = - TextEditingController(text: updateConfig.androidUpdateUrl); + _androidUrlController = TextEditingController( + text: updateConfig.androidUpdateUrl, + ); } @override @@ -74,8 +76,8 @@ class _UpdateConfigFormState extends State { Text( l10n.updateConfigDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), AppConfigTextField( diff --git a/lib/app_configuration/widgets/user_limits_config_form.dart b/lib/app_configuration/widgets/user_limits_config_form.dart index b9ef2631..6916eb09 100644 --- a/lib/app_configuration/widgets/user_limits_config_form.dart +++ b/lib/app_configuration/widgets/user_limits_config_form.dart @@ -34,9 +34,9 @@ class _UserLimitsConfigFormState extends State with SingleTickerProviderStateMixin { late TabController _tabController; late final Map - _followedItemsLimitControllers; + _followedItemsLimitControllers; late final Map - _savedHeadlinesLimitControllers; + _savedHeadlinesLimitControllers; @override void initState() { @@ -75,12 +75,14 @@ class _UserLimitsConfigFormState extends State void _updateControllers() { final limits = widget.remoteConfig.user.limits; for (final role in AppUserRole.values) { - final newFollowedItemsLimit = (limits.followedItems[role] ?? 0).toString(); + final newFollowedItemsLimit = (limits.followedItems[role] ?? 0) + .toString(); if (_followedItemsLimitControllers[role]?.text != newFollowedItemsLimit) { _followedItemsLimitControllers[role]?.text = newFollowedItemsLimit; } - final newSavedHeadlinesLimit = (limits.savedHeadlines[role] ?? 0).toString(); + final newSavedHeadlinesLimit = (limits.savedHeadlines[role] ?? 0) + .toString(); if (_savedHeadlinesLimitControllers[role]?.text != newSavedHeadlinesLimit) { _savedHeadlinesLimitControllers[role]?.text = newSavedHeadlinesLimit; @@ -112,8 +114,8 @@ class _UserLimitsConfigFormState extends State child: Text( l10n.userContentLimitsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), ), const SizedBox(height: AppSpacing.lg), @@ -183,8 +185,9 @@ class _UserLimitsConfigFormState extends State description: l10n.savedHeadlinesLimitDescription, value: limits.savedHeadlines[role] ?? 0, onChanged: (value) { - final newLimits = Map.from(limits.savedHeadlines) - ..[role] = value; + final newLimits = Map.from( + limits.savedHeadlines, + )..[role] = value; widget.onConfigChanged( widget.remoteConfig.copyWith( user: widget.remoteConfig.user.copyWith( diff --git a/lib/app_configuration/widgets/user_preference_limits_form.dart b/lib/app_configuration/widgets/user_preference_limits_form.dart index 897d0c00..516805b3 100644 --- a/lib/app_configuration/widgets/user_preference_limits_form.dart +++ b/lib/app_configuration/widgets/user_preference_limits_form.dart @@ -35,9 +35,9 @@ class _UserPreferenceLimitsFormState extends State with SingleTickerProviderStateMixin { late TabController _tabController; late final Map - _followedItemsLimitControllers; + _followedItemsLimitControllers; late final Map - _savedHeadlinesLimitControllers; + _savedHeadlinesLimitControllers; @override void initState() { @@ -52,8 +52,7 @@ class _UserPreferenceLimitsFormState extends State @override void didUpdateWidget(covariant UserPreferenceLimitsForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.remoteConfig.user.limits != - oldWidget.remoteConfig.user.limits) { + if (widget.remoteConfig.user.limits != oldWidget.remoteConfig.user.limits) { _updateControllers(); } } @@ -62,45 +61,52 @@ class _UserPreferenceLimitsFormState extends State final limitsConfig = widget.remoteConfig.user.limits; _followedItemsLimitControllers = { for (final role in AppUserRole.values) - role: TextEditingController( - text: (limitsConfig.followedItems[role] ?? 0).toString(), - )..selection = TextSelection.collapsed( - offset: (limitsConfig.followedItems[role] ?? 0).toString().length, - ), + role: + TextEditingController( + text: (limitsConfig.followedItems[role] ?? 0).toString(), + ) + ..selection = TextSelection.collapsed( + offset: (limitsConfig.followedItems[role] ?? 0) + .toString() + .length, + ), }; _savedHeadlinesLimitControllers = { for (final role in AppUserRole.values) - role: TextEditingController( - text: (limitsConfig.savedHeadlines[role] ?? 0).toString(), - )..selection = TextSelection.collapsed( - offset: (limitsConfig.savedHeadlines[role] ?? 0).toString().length, - ), + role: + TextEditingController( + text: (limitsConfig.savedHeadlines[role] ?? 0).toString(), + ) + ..selection = TextSelection.collapsed( + offset: (limitsConfig.savedHeadlines[role] ?? 0) + .toString() + .length, + ), }; } void _updateControllers() { final limitsConfig = widget.remoteConfig.user.limits; for (final role in AppUserRole.values) { - final newFollowedItemsLimit = - (limitsConfig.followedItems[role] ?? 0).toString(); - if (_followedItemsLimitControllers[role]?.text != - newFollowedItemsLimit) { + final newFollowedItemsLimit = (limitsConfig.followedItems[role] ?? 0) + .toString(); + if (_followedItemsLimitControllers[role]?.text != newFollowedItemsLimit) { _followedItemsLimitControllers[role]?.text = newFollowedItemsLimit; _followedItemsLimitControllers[role]?.selection = TextSelection.collapsed( - offset: newFollowedItemsLimit.length, - ); + offset: newFollowedItemsLimit.length, + ); } - final newSavedHeadlinesLimit = - (limitsConfig.savedHeadlines[role] ?? 0).toString(); + final newSavedHeadlinesLimit = (limitsConfig.savedHeadlines[role] ?? 0) + .toString(); if (_savedHeadlinesLimitControllers[role]?.text != newSavedHeadlinesLimit) { _savedHeadlinesLimitControllers[role]?.text = newSavedHeadlinesLimit; _savedHeadlinesLimitControllers[role]?.selection = TextSelection.collapsed( - offset: newSavedHeadlinesLimit.length, - ); + offset: newSavedHeadlinesLimit.length, + ); } } } @@ -174,8 +180,7 @@ class _UserPreferenceLimitsFormState extends State description: _getFollowedItemsLimitDescription(l10n, role), value: config.followedItems[role] ?? 0, onChanged: (value) { - final newLimits = - Map.from(config.followedItems); + final newLimits = Map.from(config.followedItems); newLimits[role] = value; widget.onConfigChanged( widget.remoteConfig.copyWith( @@ -192,8 +197,7 @@ class _UserPreferenceLimitsFormState extends State description: _getSavedHeadlinesLimitDescription(l10n, role), value: config.savedHeadlines[role] ?? 0, onChanged: (value) { - final newLimits = - Map.from(config.savedHeadlines); + final newLimits = Map.from(config.savedHeadlines); newLimits[role] = value; widget.onConfigChanged( widget.remoteConfig.copyWith( 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..c1f33844 100644 --- a/lib/content_management/bloc/create_headline/create_headline_state.dart +++ b/lib/content_management/bloc/create_headline/create_headline_state.dart @@ -55,7 +55,7 @@ final class CreateHeadlineState extends Equatable { source != null && topic != null && eventCountry != null && - !isBreaking; // If breaking, it must be published, not drafted. + !isBreaking; CreateHeadlineState copyWith({ CreateHeadlineStatus? status, diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index 0cdd7f39..b365ff41 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -375,7 +375,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. From f77f0d4e5b29fe44298f622b95facfe922bacc15 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:36:56 +0100 Subject: [PATCH 23/46] feat(bootstrap): sync with core package AppSettings and Headline models This commit updates the bootstrap file to align with changes in the core package. Specifically, `UserAppSettings` has been renamed to `AppSettings`, and the `Headline` model no longer includes an `excerpt` field. The `DataClient` and `DataRepository` instantiations have been updated to use `AppSettings`, and the `headlinesFixturesData` is now mapped to remove the `excerpt` field to prevent compilation errors. --- lib/bootstrap.dart | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 6581e2f3..05308878 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; // Changed from UserAppSettings DataClient remoteConfigClient; DataClient dashboardSummaryClient; DataClient countriesClient; @@ -90,10 +90,10 @@ Future bootstrap( getId: (i) => i.id, logger: Logger('DataInMemory'), ); - userAppSettingsClient = DataInMemory( + appSettingsClient = DataInMemory( // Changed from UserAppSettings toJson: (i) => i.toJson(), getId: (i) => i.id, - logger: Logger('DataInMemory'), + logger: Logger('DataInMemory'), // Changed from UserAppSettings ); remoteConfigClient = DataInMemory( toJson: (i) => i.toJson(), @@ -155,12 +155,12 @@ Future bootstrap( toJson: (prefs) => prefs.toJson(), logger: Logger('DataApi'), ); - userAppSettingsClient = DataApi( + appSettingsClient = DataApi( // Changed from UserAppSettings httpClient: httpClient, - modelName: 'user_app_settings', - fromJson: UserAppSettings.fromJson, + modelName: 'app_settings', // Changed from user_app_settings + fromJson: AppSettings.fromJson, // Changed from UserAppSettings.fromJson toJson: (settings) => settings.toJson(), - logger: Logger('DataApi'), + logger: Logger('DataApi'), // Changed from UserAppSettings ); remoteConfigClient = DataApi( httpClient: httpClient, @@ -213,8 +213,8 @@ Future bootstrap( DataRepository( dataClient: userContentPreferencesClient, ); - final userAppSettingsRepository = DataRepository( - dataClient: userAppSettingsClient, + final appSettingsRepository = DataRepository( // Changed from UserAppSettings + dataClient: appSettingsClient, // Changed from UserAppSettings ); final remoteConfigRepository = DataRepository( dataClient: remoteConfigClient, @@ -235,7 +235,7 @@ Future bootstrap( headlinesRepository: headlinesRepository, topicsRepository: topicsRepository, sourcesRepository: sourcesRepository, - userAppSettingsRepository: userAppSettingsRepository, + appSettingsRepository: appSettingsRepository, userContentPreferencesRepository: userContentPreferencesRepository, remoteConfigRepository: remoteConfigRepository, dashboardSummaryRepository: dashboardSummaryRepository, From f112536f21c61af1989b9404f896a2439201c621 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:39:10 +0100 Subject: [PATCH 24/46] feat(app): update AppView for AppSettings and FeedItemClickBehavior This commit modifies the `App` widget and its state to use the new `AppSettings` model instead of `UserAppSettings`. It also updates the theme logic to correctly handle the `FeedItemClickBehavior` enum, replacing the deprecated `internalNavigation` with `defaultBehavior` to ensure compatibility with the updated core package. --- lib/app/view/app.dart | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) 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, From ac393adca215d63f95709155b1948b8c329689e4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:39:37 +0100 Subject: [PATCH 25/46] refactor(data): update AppSettings references and improve code formatting - Change DataInMemory to DataInMemory - Update logger name in appSettingsClient initialization - Modify DataApi to use 'app_settings' instead of 'user_app_settings' - Update DataRepository reference - Improve code formatting in data client initializations --- lib/bootstrap.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 05308878..018d4246 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -59,7 +59,7 @@ Future bootstrap( DataClient topicsClient; DataClient sourcesClient; DataClient userContentPreferencesClient; - DataClient appSettingsClient; // Changed from UserAppSettings + DataClient appSettingsClient; DataClient remoteConfigClient; DataClient dashboardSummaryClient; DataClient countriesClient; @@ -90,10 +90,13 @@ Future bootstrap( getId: (i) => i.id, logger: Logger('DataInMemory'), ); - appSettingsClient = DataInMemory( // Changed from UserAppSettings + appSettingsClient = DataInMemory( + // Changed from UserAppSettings toJson: (i) => i.toJson(), getId: (i) => i.id, - logger: Logger('DataInMemory'), // Changed from UserAppSettings + logger: Logger( + 'DataInMemory', + ), // Changed from UserAppSettings ); remoteConfigClient = DataInMemory( toJson: (i) => i.toJson(), @@ -155,7 +158,8 @@ Future bootstrap( toJson: (prefs) => prefs.toJson(), logger: Logger('DataApi'), ); - appSettingsClient = DataApi( // Changed from UserAppSettings + appSettingsClient = DataApi( + // Changed from UserAppSettings httpClient: httpClient, modelName: 'app_settings', // Changed from user_app_settings fromJson: AppSettings.fromJson, // Changed from UserAppSettings.fromJson @@ -213,7 +217,8 @@ Future bootstrap( DataRepository( dataClient: userContentPreferencesClient, ); - final appSettingsRepository = DataRepository( // Changed from UserAppSettings + final appSettingsRepository = DataRepository( + // Changed from UserAppSettings dataClient: appSettingsClient, // Changed from UserAppSettings ); final remoteConfigRepository = DataRepository( From b42d9b9f234a7678099737f642868d678e31b874 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:40:20 +0100 Subject: [PATCH 26/46] feat(app_bloc): update AppBloc for AppSettings and FeedSettings models This commit refactors the `AppBloc` to align with the updated core package models. `UserAppSettings` is replaced by `AppSettings`, and `FeedDisplayPreferences` is replaced by `FeedSettings`. The default settings creation logic is adjusted to use the new `FeedSettings` properties, including `feedItemDensity`, `feedItemImageStyle`, and `feedItemClickBehavior`, while removing obsolete fields. --- lib/app/bloc/app_bloc.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index 252814b2..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,7 +70,7 @@ class AppBloc extends Bloc { // If user is authenticated, load their app settings if (status == AppStatus.authenticated && user != null) { try { - final appSettings = await _userAppSettingsRepository.read( + final appSettings = await _appSettingsRepository.read( id: user.id, ); emit(state.copyWith(appSettings: appSettings)); @@ -78,7 +80,6 @@ class AppBloc extends Bloc { 'User app settings not found for user ${user.id}. Creating default.', ); final defaultSettings = AppSettings( - // Corrected class name id: user.id, displaySettings: const DisplaySettings( baseTheme: AppBaseTheme.system, @@ -94,13 +95,12 @@ class AppBloc extends Bloc { ), ), feedSettings: const FeedSettings( - // Corrected class name and properties feedItemDensity: FeedItemDensity.standard, feedItemImageStyle: FeedItemImageStyle.largeThumbnail, - feedItemClickBehavior: FeedItemClickBehavior.internalNavigation, + feedItemClickBehavior: FeedItemClickBehavior.defaultBehavior, ), ); - await _userAppSettingsRepository.create(item: defaultSettings); + await _appSettingsRepository.create(item: defaultSettings); emit(state.copyWith(appSettings: defaultSettings)); } on HttpException catch (e, s) { // Handle HTTP exceptions during settings load From f0d3b1420dc96d8d6cc38c27da344a4435d43e13 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:40:47 +0100 Subject: [PATCH 27/46] refactor(app_event): update AppUserAppSettingsChanged event type This commit updates the `AppUserAppSettingsChanged` event to use the `AppSettings` model instead of the deprecated `UserAppSettings` model, ensuring type consistency across the application's global state management. --- lib/app/bloc/app_event.dart | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/app/bloc/app_event.dart b/lib/app/bloc/app_event.dart index 1aaf074c..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,22 +16,15 @@ 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.appSettings); - /// The updated user application settings. + /// The new user app settings. final AppSettings appSettings; @override From 374cf4924b47ea885fb2b65e8e36479fd243c971 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:41:36 +0100 Subject: [PATCH 28/46] refactor(app_state): update AppState to use AppSettings model This commit modifies the `AppState` to hold an `AppSettings` object instead of `UserAppSettings`, aligning with the core package model changes. The `copyWith` method and `props` list are updated accordingly. --- lib/app/bloc/app_state.dart | 46 +++++++++---------------------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/lib/app/bloc/app_state.dart b/lib/app/bloc/app_state.dart index 77598ec0..d13d4a18 100644 --- a/lib/app/bloc/app_state.dart +++ b/lib/app/bloc/app_state.dart @@ -1,70 +1,46 @@ 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({ - this.status = AppStatus.initial, + required this.environment, this.status = AppStatus.initial, this.user, - this.environment, this.appSettings, }); - /// The current authentication status of the application. final AppStatus status; - - /// The current user details. Null if unauthenticated. final User? user; - - /// 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 AppSettings? appSettings; + final local_config.AppEnvironment environment; - /// Creates a copy of the current state with updated values. AppState copyWith({ AppStatus? status, User? user, - local_config.AppEnvironment? environment, AppSettings? appSettings, - bool clearEnvironment = false, bool clearAppSettings = false, }) { return AppState( status: status ?? this.status, user: user ?? this.user, - environment: clearEnvironment ? null : environment ?? this.environment, - appSettings: - clearAppSettings // Corrected property name - ? null - : appSettings ?? this.appSettings, + appSettings: clearAppSettings ? null : appSettings ?? this.appSettings, + environment: environment, ); } @override - List get props => [ - status, - user, - environment, - appSettings, - ]; + List get props => [status, user, appSettings, environment]; } From 440d2f0577e3677df26b3948caed9408c7e2da04 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:42:01 +0100 Subject: [PATCH 29/46] refactor(create_headline_state): remove excerpt field This commit removes the `excerpt` field from `CreateHeadlineState` and updates the `isFormValid` getter, `copyWith` method, and `props` list. This change reflects the removal of the `excerpt` field from the `Headline` model in the core package. --- .../bloc/create_headline/create_headline_state.dart | 6 ------ 1 file changed, 6 deletions(-) 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 c1f33844..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,7 +47,6 @@ 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 && @@ -60,7 +57,6 @@ final class CreateHeadlineState extends Equatable { 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, From 0f9aec4369bb0a4776f8043b0144c825cfc064c6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:42:43 +0100 Subject: [PATCH 30/46] refactor(create_headline_event): remove CreateHeadlineExcerptChanged event This commit removes the `CreateHeadlineExcerptChanged` event as the `excerpt` field is no longer part of the `Headline` model in the core package. --- .../bloc/create_headline/create_headline_event.dart | 8 -------- 1 file changed, 8 deletions(-) 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); From ebb856c0ae98f3cb101b9bd1ca42a2e5070e875a Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:44:38 +0100 Subject: [PATCH 31/46] refactor(create_headline_bloc): remove excerpt handling logic This commit removes all logic related to the `excerpt` field from the `CreateHeadlineBloc`, including its event handler and its usage when constructing new `Headline` objects, to align with the updated `Headline` model in the core package. --- .../bloc/create_headline/create_headline_bloc.dart | 10 ---------- 1 file changed, 10 deletions(-) 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!, From 9e45c314163a4f39460f8e3d6fd8f4050489f76f Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:45:41 +0100 Subject: [PATCH 32/46] refactor(create_headline_page): remove excerpt TextFormField and logic This commit removes the `TextFormField` for the `excerpt` field and its corresponding controller and validation logic from `CreateHeadlinePage`. This change is necessary because the `excerpt` field has been removed from the `Headline` model in the core package. --- .../view/create_headline_page.dart | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index b365ff41..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 && From db50eb0bdcdd5f0e026582394bb816f0108c5ee7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:46:12 +0100 Subject: [PATCH 33/46] refactor(edit_headline_state): remove excerpt field This commit removes the `excerpt` field from `EditHeadlineState` and updates the `isFormValid` getter, `copyWith` method, and `props` list. This change reflects the removal of the `excerpt` field from the `Headline` model in the core package. --- .../bloc/edit_headline/edit_headline_state.dart | 6 ------ 1 file changed, 6 deletions(-) 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, From 29057c26084fa4ef9f272dd823034acd231d33cc Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:46:42 +0100 Subject: [PATCH 34/46] refactor(edit_headline_event): remove EditHeadlineExcerptChanged event This commit removes the `EditHeadlineExcerptChanged` event as the `excerpt` field is no longer part of the `Headline` model in the core package. --- .../bloc/edit_headline/edit_headline_event.dart | 8 -------- 1 file changed, 8 deletions(-) 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); From f8cbb5368a0442b2582f6862ccc0b045433a5d5c Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:47:18 +0100 Subject: [PATCH 35/46] refactor(edit_headline_bloc): remove excerpt handling logic This commit removes all logic related to the `excerpt` field from the `EditHeadlineBloc`, including its event handler, its population during loading, and its usage when updating `Headline` objects, to align with the updated `Headline` model in the core package. --- .../bloc/edit_headline/edit_headline_bloc.dart | 16 ---------------- 1 file changed, 16 deletions(-) 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, From c7badff4d7cea7944d5f71da1b9d41ec1fbd053d Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:48:02 +0100 Subject: [PATCH 36/46] refactor(edit_headline_page): remove excerpt TextFormField and logic This commit removes the `TextFormField` for the `excerpt` field and its corresponding controller and state update logic from `EditHeadlinePage`. This change is necessary because the `excerpt` field has been removed from the `Headline` model in the core package. --- .../view/edit_headline_page.dart | 16 ---------------- 1 file changed, 16 deletions(-) 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( From 8cf31b54b0115d438e5e0cbc10f80c5d0d2d225c Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:48:34 +0100 Subject: [PATCH 37/46] feat(settings_bloc): update SettingsBloc for AppSettings and FeedSettings This commit refactors the `SettingsBloc` to align with the updated core package models. `UserAppSettings` is replaced by `AppSettings`, and `FeedDisplayPreferences` is replaced by `FeedSettings`. The default settings creation logic is adjusted to use the new `FeedSettings` properties, including `feedItemDensity`, `feedItemImageStyle`, and `feedItemClickBehavior`, while removing obsolete fields. --- lib/settings/bloc/settings_bloc.dart | 64 ++++++++++++++++------------ 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/lib/settings/bloc/settings_bloc.dart b/lib/settings/bloc/settings_bloc.dart index 56044942..bc51a54c 100644 --- a/lib/settings/bloc/settings_bloc.dart +++ b/lib/settings/bloc/settings_bloc.dart @@ -10,8 +10,10 @@ part 'settings_state.dart'; class SettingsBloc extends Bloc { SettingsBloc({ - required DataRepository userAppSettingsRepository, - }) : _userAppSettingsRepository = userAppSettingsRepository, + required DataRepository + appSettingsRepository, // Changed from UserAppSettings + }) : _appSettingsRepository = + appSettingsRepository, // Changed from UserAppSettings super(const SettingsInitial()) { on(_onSettingsLoaded); on(_onSettingsBaseThemeChanged); @@ -22,22 +24,24 @@ 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( + // Changed from userAppSettingsRepository 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( + // Changed from UserAppSettings id: event.userId!, displaySettings: const DisplaySettings( baseTheme: AppBaseTheme.system, @@ -52,45 +56,49 @@ 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( + // Changed from FeedDisplayPreferences + feedItemDensity: + FeedItemDensity.standard, // Changed from headlineDensity + feedItemImageStyle: FeedItemImageStyle + .largeThumbnail, // Changed from headlineImageStyle + feedItemClickBehavior: + FeedItemClickBehavior.defaultBehavior, // Added new field ), ); - 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, // Changed from userAppSettings ), ); } } Future _updateSettings( - UserAppSettings updatedSettings, + AppSettings updatedSettings, // Changed from UserAppSettings Emitter emit, ) async { - emit(SettingsUpdateInProgress(userAppSettings: updatedSettings)); + emit(SettingsUpdateInProgress(appSettings: updatedSettings)); try { - final result = await _userAppSettingsRepository.update( + final result = await _appSettingsRepository.update( + // Changed from userAppSettingsRepository 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, // Changed from userAppSettings ), ); } @@ -100,7 +108,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 +123,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 +138,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 +153,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 +168,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 +183,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, From 0df8dd7d93808183b6d335bf4d7359193e76037b Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:48:58 +0100 Subject: [PATCH 38/46] refactor(settings_state): update SettingsState to use AppSettings model This commit modifies the `SettingsState` to hold an `AppSettings` object instead of `UserAppSettings`, aligning with the core package model changes. The `props` list is updated accordingly. --- lib/settings/bloc/settings_state.dart | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) 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]; } From bf18995b88bd7ab98554a7aab2faa3d7981c6ba5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:49:48 +0100 Subject: [PATCH 39/46] feat(settings_page): update SettingsPage for AppSettings model This commit updates the `SettingsPage` to use the `AppSettings` model instead of `UserAppSettings` for its `SettingsBloc` and `AppBloc` interactions, ensuring type consistency with the updated core package. --- lib/settings/view/settings_page.dart | 34 ++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/settings/view/settings_page.dart b/lib/settings/view/settings_page.dart index 44f140f2..edfe2812 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -20,8 +20,11 @@ class SettingsPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => SettingsBloc( - userAppSettingsRepository: context - .read>(), + appSettingsRepository: + context + .read< + DataRepository + >(), )..add(SettingsLoaded(userId: context.read().state.user?.id)), child: const _SettingsView(), ); @@ -86,9 +89,12 @@ 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,7 +108,7 @@ class _SettingsViewState extends State<_SettingsView> { } }, builder: (context, state) { - if (state.userAppSettings == null && + if (state.appSettings == null && state is! SettingsLoadInProgress) { // If settings are null and not loading, try to load them context.read().add( @@ -127,8 +133,9 @@ 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 +166,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 +192,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 +225,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 +249,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 +275,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 +322,7 @@ class _SettingsViewState extends State<_SettingsView> { horizontal: AppSpacing.xxl, ), child: _LanguageSelectionList( - currentLanguage: userAppSettings.language, + currentLanguage: appSettings.language, l10n: l10n, ), ), From c5e4eb6a6f664e1bca7381bbfde95776ad36bb66 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:51:27 +0100 Subject: [PATCH 40/46] style: format --- lib/app/bloc/app_state.dart | 3 ++- lib/bootstrap.dart | 13 +++++-------- lib/settings/bloc/settings_bloc.dart | 25 ++++++++----------------- lib/settings/view/settings_page.dart | 13 +++---------- 4 files changed, 18 insertions(+), 36 deletions(-) diff --git a/lib/app/bloc/app_state.dart b/lib/app/bloc/app_state.dart index d13d4a18..af3898d3 100644 --- a/lib/app/bloc/app_state.dart +++ b/lib/app/bloc/app_state.dart @@ -17,7 +17,8 @@ enum AppStatus { final class AppState extends Equatable { const AppState({ - required this.environment, this.status = AppStatus.initial, + required this.environment, + this.status = AppStatus.initial, this.user, this.appSettings, }); diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 018d4246..713b945e 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -91,12 +91,11 @@ Future bootstrap( logger: Logger('DataInMemory'), ); appSettingsClient = DataInMemory( - // Changed from UserAppSettings toJson: (i) => i.toJson(), getId: (i) => i.id, logger: Logger( 'DataInMemory', - ), // Changed from UserAppSettings + ), ); remoteConfigClient = DataInMemory( toJson: (i) => i.toJson(), @@ -159,12 +158,11 @@ Future bootstrap( logger: Logger('DataApi'), ); appSettingsClient = DataApi( - // Changed from UserAppSettings httpClient: httpClient, - modelName: 'app_settings', // Changed from user_app_settings - fromJson: AppSettings.fromJson, // Changed from UserAppSettings.fromJson + modelName: 'app_settings', + fromJson: AppSettings.fromJson, toJson: (settings) => settings.toJson(), - logger: Logger('DataApi'), // Changed from UserAppSettings + logger: Logger('DataApi'), ); remoteConfigClient = DataApi( httpClient: httpClient, @@ -218,8 +216,7 @@ Future bootstrap( dataClient: userContentPreferencesClient, ); final appSettingsRepository = DataRepository( - // Changed from UserAppSettings - dataClient: appSettingsClient, // Changed from UserAppSettings + dataClient: appSettingsClient, ); final remoteConfigRepository = DataRepository( dataClient: remoteConfigClient, diff --git a/lib/settings/bloc/settings_bloc.dart b/lib/settings/bloc/settings_bloc.dart index bc51a54c..8e68db67 100644 --- a/lib/settings/bloc/settings_bloc.dart +++ b/lib/settings/bloc/settings_bloc.dart @@ -10,10 +10,8 @@ part 'settings_state.dart'; class SettingsBloc extends Bloc { SettingsBloc({ - required DataRepository - appSettingsRepository, // Changed from UserAppSettings - }) : _appSettingsRepository = - appSettingsRepository, // Changed from UserAppSettings + required DataRepository appSettingsRepository, + }) : _appSettingsRepository = appSettingsRepository, super(const SettingsInitial()) { on(_onSettingsLoaded); on(_onSettingsBaseThemeChanged); @@ -33,7 +31,6 @@ class SettingsBloc extends Bloc { emit(SettingsLoadInProgress(appSettings: state.appSettings)); try { final appSettings = await _appSettingsRepository.read( - // Changed from userAppSettingsRepository id: event.userId!, ); emit(SettingsLoadSuccess(appSettings: appSettings)); @@ -41,7 +38,6 @@ class SettingsBloc extends Bloc { // If settings are not found, create default settings for the user. // This ensures that a user always has a valid settings object. final defaultSettings = AppSettings( - // Changed from UserAppSettings id: event.userId!, displaySettings: const DisplaySettings( baseTheme: AppBaseTheme.system, @@ -57,13 +53,9 @@ class SettingsBloc extends Bloc { ), ), feedSettings: const FeedSettings( - // Changed from FeedDisplayPreferences - feedItemDensity: - FeedItemDensity.standard, // Changed from headlineDensity - feedItemImageStyle: FeedItemImageStyle - .largeThumbnail, // Changed from headlineImageStyle - feedItemClickBehavior: - FeedItemClickBehavior.defaultBehavior, // Added new field + feedItemDensity: FeedItemDensity.standard, + feedItemImageStyle: FeedItemImageStyle.largeThumbnail, + feedItemClickBehavior: FeedItemClickBehavior.defaultBehavior, ), ); await _appSettingsRepository.create(item: defaultSettings); @@ -74,20 +66,19 @@ class SettingsBloc extends Bloc { emit( SettingsLoadFailure( UnknownException('An unexpected error occurred: $e'), - appSettings: state.appSettings, // Changed from userAppSettings + appSettings: state.appSettings, ), ); } } Future _updateSettings( - AppSettings updatedSettings, // Changed from UserAppSettings + AppSettings updatedSettings, Emitter emit, ) async { emit(SettingsUpdateInProgress(appSettings: updatedSettings)); try { final result = await _appSettingsRepository.update( - // Changed from userAppSettingsRepository id: updatedSettings.id, item: updatedSettings, ); @@ -98,7 +89,7 @@ class SettingsBloc extends Bloc { emit( SettingsUpdateFailure( UnknownException('An unexpected error occurred: $e'), - appSettings: state.appSettings, // Changed from userAppSettings + appSettings: state.appSettings, ), ); } diff --git a/lib/settings/view/settings_page.dart b/lib/settings/view/settings_page.dart index edfe2812..4b45475c 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -20,11 +20,7 @@ class SettingsPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => SettingsBloc( - appSettingsRepository: - context - .read< - DataRepository - >(), + appSettingsRepository: context.read>(), )..add(SettingsLoaded(userId: context.read().state.user?.id)), child: const _SettingsView(), ); @@ -90,11 +86,10 @@ class _SettingsViewState extends State<_SettingsView> { ); // Trigger AppBloc to reload settings for immediate UI update if (state.appSettings != null) { - context.read().add( AppUserAppSettingsChanged( state.appSettings!, - ), + ), ); } } else if (state is SettingsUpdateFailure) { @@ -108,8 +103,7 @@ class _SettingsViewState extends State<_SettingsView> { } }, builder: (context, state) { - if (state.appSettings == 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), @@ -134,7 +128,6 @@ class _SettingsViewState extends State<_SettingsView> { }, ); } else if (state.appSettings != null) { - final appSettings = state.appSettings!; return ListView( padding: const EdgeInsets.all(AppSpacing.lg), From 31c3530b9c245aa29d65f8fac6714c84d2ca0f3d Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 18:52:13 +0100 Subject: [PATCH 41/46] refactor(app_configuration): remove unused features variable - Remove unused variable 'features' in FeaturesConfigurationTab build method - This change simplifies the code by removing an unnecessary variable assignment --- .../view/tabs/features_configuration_tab.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/app_configuration/view/tabs/features_configuration_tab.dart b/lib/app_configuration/view/tabs/features_configuration_tab.dart index d2e9945b..1088344c 100644 --- a/lib/app_configuration/view/tabs/features_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -50,8 +50,7 @@ class _FeaturesConfigurationTabState extends State { @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; - final features = widget.remoteConfig.features; - + return ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ From 053eed98c669ac82960ad03732091ea4c6fb8012 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 19:21:01 +0100 Subject: [PATCH 42/46] fix(shared): set fixed pagination limit for initial item fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set pagination limit to 20 for initial fetch to ensure proper infinite scrolling functionality - Added comment提醒 do not lower the limit below 20 for the initial fetch --- .../selection_page/bloc/searchable_selection_bloc.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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..ec4bc557 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 nto 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; From 5f02ff42435e2bc79802f889e4757bd8a55b71ff Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 20:14:04 +0100 Subject: [PATCH 43/46] chore(deps): update core package - Update core package reference from c0c41c0 to 3779a8b - This update ensures the project uses the latest version of the core package --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index adc60b77..489f590e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,8 +89,8 @@ packages: dependency: "direct main" description: path: "." - ref: c0c41c069b885f0c16d1b134269aa06861634186 - resolved-ref: c0c41c069b885f0c16d1b134269aa06861634186 + 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 0eff3f61..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: c0c41c069b885f0c16d1b134269aa06861634186 + ref: 3779a8b1dbd8450d524574cf5376b7cc2ed514e7 http_client: git: url: https://github.com/flutter-news-app-full-source-code/http-client.git From 852e94d0c11b240a9d98d16e4a6ffedf77ac9818 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 20:34:47 +0100 Subject: [PATCH 44/46] refactor(app_configuration): simplify maintenance config and update UI strings - Replace MaintenanceConfigForm with a simple SwitchListTile for better maintainability - Update Arabic and English localization strings for app update management and legal information - Remove unused import of MaintenanceConfigForm - Adjust expansion tile titles to use new localized strings --- .../view/tabs/app_configuration_tab.dart | 37 +++++++++---------- lib/l10n/app_localizations.dart | 14 ++++++- lib/l10n/app_localizations_ar.dart | 8 +++- lib/l10n/app_localizations_en.dart | 8 +++- lib/l10n/arb/app_ar.arb | 10 ++++- lib/l10n/arb/app_en.arb | 10 ++++- 6 files changed, 62 insertions(+), 25 deletions(-) diff --git a/lib/app_configuration/view/tabs/app_configuration_tab.dart b/lib/app_configuration/view/tabs/app_configuration_tab.dart index e29a55a1..fd5d81f9 100644 --- a/lib/app_configuration/view/tabs/app_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/app_configuration_tab.dart @@ -1,7 +1,6 @@ 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/maintenance_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'; @@ -45,28 +44,26 @@ class _AppConfigurationTabState extends State { @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 - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 0; - return ExpansionTile( - key: ValueKey('maintenanceConfigTile_$expandedIndex'), - title: Text(l10n.maintenanceConfigTitle), - onExpansionChanged: (isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; - }, - initiallyExpanded: expandedIndex == tileIndex, - children: [ - MaintenanceConfigForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, + // 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, + ), ), - ], + ), ); }, ), @@ -79,7 +76,7 @@ class _AppConfigurationTabState extends State { const tileIndex = 1; return ExpansionTile( key: ValueKey('updateConfigTile_$expandedIndex'), - title: Text(l10n.updateConfigTitle), + title: Text(l10n.appUpdateManagementTitle), onExpansionChanged: (isExpanded) { _expandedTileIndex.value = isExpanded ? tileIndex : null; }, @@ -102,7 +99,7 @@ class _AppConfigurationTabState extends State { const tileIndex = 2; return ExpansionTile( key: ValueKey('generalAppConfigTile_$expandedIndex'), - title: Text(l10n.generalAppConfigTitle), + title: Text(l10n.appLegalInformationTitle), onExpansionChanged: (isExpanded) { _expandedTileIndex.value = isExpanded ? tileIndex : null; }, diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 06c30a0c..f1fd3547 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2891,7 +2891,7 @@ abstract class AppLocalizations { /// Tab title for global App settings. /// /// In en, this message translates to: - /// **'App'** + /// **'General'** String get appTab; /// Tab title for Features settings (Ads, Notifications, etc.). @@ -3061,6 +3061,18 @@ abstract class AppLocalizations { /// 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 0011c9a2..e2099be5 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1547,7 +1547,7 @@ class AppLocalizationsAr extends AppLocalizations { String get pushNotificationProviderOneSignal => 'OneSignal'; @override - String get appTab => 'التطبيق'; + String get appTab => 'الإعدادات العامة'; @override String get featuresTab => 'الميزات'; @@ -1643,4 +1643,10 @@ class AppLocalizationsAr extends AppLocalizations { @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 d331541a..7db3aeaf 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1550,7 +1550,7 @@ class AppLocalizationsEn extends AppLocalizations { String get pushNotificationProviderOneSignal => 'OneSignal'; @override - String get appTab => 'App'; + String get appTab => 'General'; @override String get featuresTab => 'Features'; @@ -1646,4 +1646,10 @@ class AppLocalizationsEn extends AppLocalizations { @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 7b70d92f..3659fbe2 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1946,7 +1946,7 @@ "@pushNotificationProviderOneSignal": { "description": "تسمية مزود الإشعارات الفورية OneSignal" }, - "appTab": "التطبيق", + "appTab": "الإعدادات العامة", "@appTab": { "description": "عنوان تبويب إعدادات التطبيق العامة." }, @@ -2061,5 +2061,13 @@ "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 301e96fd..31c693d7 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1942,7 +1942,7 @@ "@pushNotificationProviderOneSignal": { "description": "Label for the OneSignal push notification provider" }, - "appTab": "App", + "appTab": "General", "@appTab": { "description": "Tab title for global App settings." }, @@ -2057,5 +2057,13 @@ "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 From aae7dc198f83d45d205de87157eccea95ff3a5d6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 20:35:16 +0100 Subject: [PATCH 45/46] style: format --- lib/app_configuration/view/tabs/features_configuration_tab.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app_configuration/view/tabs/features_configuration_tab.dart b/lib/app_configuration/view/tabs/features_configuration_tab.dart index 1088344c..fac84584 100644 --- a/lib/app_configuration/view/tabs/features_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -50,7 +50,7 @@ class _FeaturesConfigurationTabState extends State { @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; - + return ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ From efe2ddaccd9eda2c02915b6c789b6c5c38801cb2 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 25 Nov 2025 20:40:41 +0100 Subject: [PATCH 46/46] style: correct typo in comment - Change 'Do nto' to 'Do not' in a comment within the SearchableSelectionBloc class --- .../widgets/selection_page/bloc/searchable_selection_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ec4bc557..da32674f 100644 --- a/lib/shared/widgets/selection_page/bloc/searchable_selection_bloc.dart +++ b/lib/shared/widgets/selection_page/bloc/searchable_selection_bloc.dart @@ -101,7 +101,7 @@ class SearchableSelectionBloc sort: _arguments.sortOptions, pagination: const PaginationOptions( limit: 20, - ), // Do nto 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. + ), // 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;