From d4bc4bf076485fa80d1cd50d91e7ee0d1d8079a0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 10:43:25 +0100 Subject: [PATCH 01/17] chore(deps): update dependencies - Update core.git resolved-ref from ed5aa8b4444d6e4627ae6ca9fb28058009e7ffd7 to 36be72f1b7bd22236eb0cdf97e5f2545c7a47d34 - Update pinput package from 5.0.1 to 5.0.2 --- pubspec.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index ac693011..bce32611 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -90,7 +90,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: ed5aa8b4444d6e4627ae6ca9fb28058009e7ffd7 + resolved-ref: "36be72f1b7bd22236eb0cdf97e5f2545c7a47d34" url: "https://github.com/flutter-news-app-full-source-code/core.git" source: git version: "0.0.0" @@ -440,10 +440,10 @@ packages: dependency: "direct main" description: name: pinput - sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a" + sha256: c41f42ee301505ae2375ec32871c985d3717bf8aee845620465b286e0140aad2 url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.2" platform: dependency: transitive description: From 11050bd76f6b54f92a139c537fd73eacc1f3cb61 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 10:43:38 +0100 Subject: [PATCH 02/17] feat(l10n): add AR and EN translations for interstitial ad settings - Add Arabic and English translations for new ad settings features - Reorganize interstitial ad settings structure in both languages - Introduce global ad enable/disable switch - Include feed-to-article interstitial ad ID and descriptions - Update user role interstitial frequency descriptions - Add transitions before interstitial ads labels and descriptions --- lib/l10n/app_localizations.dart | 78 +++++++++++++++++++++--------- lib/l10n/app_localizations_ar.dart | 47 ++++++++++++------ lib/l10n/app_localizations_en.dart | 47 ++++++++++++------ lib/l10n/arb/app_ar.arb | 52 ++++++++++++++------ lib/l10n/arb/app_en.arb | 52 ++++++++++++++------ 5 files changed, 192 insertions(+), 84 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 08d1d22c..4394269b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2012,30 +2012,6 @@ abstract class AppLocalizations { /// **'Choose the default type of ads to display within articles (Native or Banner).'** String get defaultInArticleAdTypeSelectionDescription; - /// Title for the Interstitial Ad Settings section - /// - /// In en, this message translates to: - /// **'Interstitial Ad Settings'** - String get interstitialAdSettingsTitle; - - /// Label for the enable interstitial ads switch - /// - /// In en, this message translates to: - /// **'Enable Interstitial Ads'** - String get enableInterstitialAdsLabel; - - /// Title for the User Role Interstitial Frequency section - /// - /// In en, this message translates to: - /// **'User Role Interstitial Frequency'** - String get userRoleInterstitialFrequencyTitle; - - /// Description for the User Role Interstitial Frequency section - /// - /// In en, this message translates to: - /// **'Configure how often interstitial ads are shown based on user roles.'** - String get userRoleInterstitialFrequencyDescription; - /// Title for the In-Article Ad Slot Placements section /// /// In en, this message translates to: @@ -2317,6 +2293,60 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Copy ID'** String get copyId; + + /// Label for the switch to enable or disable all ads globally. + /// + /// In en, this message translates to: + /// **'Enable Ads'** + String get enableGlobalAdsLabel; + + /// Label for the ad unit ID for interstitial ads shown when transitioning from feed to article. + /// + /// In en, this message translates to: + /// **'Feed to Article Interstitial Ad ID'** + String get feedToArticleInterstitialAdIdLabel; + + /// Description for the ad unit ID for interstitial ads shown when transitioning from feed to article. + /// + /// In en, this message translates to: + /// **'The ad unit ID for interstitial ads displayed when a user navigates from a feed to an article.'** + String get feedToArticleInterstitialAdIdDescription; + + /// Title for the section configuring global interstitial ad settings. + /// + /// In en, this message translates to: + /// **'Interstitial Ad Settings'** + String get interstitialAdSettingsTitle; + + /// Label for the switch to enable or disable interstitial ads. + /// + /// In en, this message translates to: + /// **'Enable Interstitial Ads'** + String get enableInterstitialAdsLabel; + + /// Title for the section configuring interstitial ad frequency based on user roles. + /// + /// In en, this message translates to: + /// **'Interstitial Ad Frequency by User Role'** + String get userRoleInterstitialFrequencyTitle; + + /// Description for the interstitial ad frequency settings by user role. + /// + /// 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 userRoleInterstitialFrequencyDescription; + + /// Label for the number of transitions a user must make before an interstitial ad is shown. + /// + /// In en, this message translates to: + /// **'Transitions Before Interstitial Ads'** + String get transitionsBeforeInterstitialAdsLabel; + + /// Description for the number of transitions a user must make before an interstitial ad is shown. + /// + /// In en, this message translates to: + /// **'The number of transitions (e.g., opening articles) a user must make before an interstitial ad is displayed.'** + String get transitionsBeforeInterstitialAdsDescription; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index d0ef4cbb..055dd7d9 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1067,20 +1067,6 @@ class AppLocalizationsAr extends AppLocalizations { String get defaultInArticleAdTypeSelectionDescription => 'اختر نوع الإعلانات الافتراضي الذي سيُعرض داخل المقالات (أصلي أو بانر).'; - @override - String get interstitialAdSettingsTitle => 'إعدادات الإعلانات البينية'; - - @override - String get enableInterstitialAdsLabel => 'تفعيل الإعلانات البينية'; - - @override - String get userRoleInterstitialFrequencyTitle => - 'تكرار الإعلانات البينية حسب دور المستخدم'; - - @override - String get userRoleInterstitialFrequencyDescription => - 'تكوين عدد مرات عرض الإعلانات البينية بناءً على أدوار المستخدمين.'; - @override String get inArticleAdSlotPlacementsTitle => 'مواضع الإعلانات داخل المقال'; @@ -1238,4 +1224,37 @@ class AppLocalizationsAr extends AppLocalizations { @override String get copyId => 'نسخ المعرف'; + + @override + String get enableGlobalAdsLabel => 'تفعيل الإعلانات'; + + @override + String get feedToArticleInterstitialAdIdLabel => + 'معرف الإعلان البيني للانتقال من الموجز إلى المقال'; + + @override + String get feedToArticleInterstitialAdIdDescription => + 'معرف الوحدة الإعلانية للإعلانات البينية التي تظهر عندما ينتقل المستخدم من الموجز إلى المقال.'; + + @override + String get interstitialAdSettingsTitle => 'إعدادات الإعلانات البينية'; + + @override + String get enableInterstitialAdsLabel => 'تفعيل الإعلانات البينية'; + + @override + String get userRoleInterstitialFrequencyTitle => + 'تكرار الإعلانات البينية حسب دور المستخدم'; + + @override + String get userRoleInterstitialFrequencyDescription => + 'تكوين عدد الانتقالات التي يجب أن يقوم بها المستخدم قبل عرض إعلان بيني، بناءً على دوره.'; + + @override + String get transitionsBeforeInterstitialAdsLabel => + 'الانتقالات قبل الإعلانات البينية'; + + @override + String get transitionsBeforeInterstitialAdsDescription => + 'عدد الانتقالات (مثل فتح المقالات) التي يجب أن يقوم بها المستخدم قبل عرض إعلان بيني.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index ece34a45..fb9ac631 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1065,20 +1065,6 @@ class AppLocalizationsEn extends AppLocalizations { String get defaultInArticleAdTypeSelectionDescription => 'Choose the default type of ads to display within articles (Native or Banner).'; - @override - String get interstitialAdSettingsTitle => 'Interstitial Ad Settings'; - - @override - String get enableInterstitialAdsLabel => 'Enable Interstitial Ads'; - - @override - String get userRoleInterstitialFrequencyTitle => - 'User Role Interstitial Frequency'; - - @override - String get userRoleInterstitialFrequencyDescription => - 'Configure how often interstitial ads are shown based on user roles.'; - @override String get inArticleAdSlotPlacementsTitle => 'In-Article Ad Slot Placements'; @@ -1240,4 +1226,37 @@ class AppLocalizationsEn extends AppLocalizations { @override String get copyId => 'Copy ID'; + + @override + String get enableGlobalAdsLabel => 'Enable Ads'; + + @override + String get feedToArticleInterstitialAdIdLabel => + 'Feed to Article Interstitial Ad ID'; + + @override + String get feedToArticleInterstitialAdIdDescription => + 'The ad unit ID for interstitial ads displayed when a user navigates from a feed to an article.'; + + @override + String get interstitialAdSettingsTitle => 'Interstitial Ad Settings'; + + @override + String get enableInterstitialAdsLabel => 'Enable Interstitial Ads'; + + @override + String get userRoleInterstitialFrequencyTitle => + 'Interstitial Ad Frequency by User Role'; + + @override + String get userRoleInterstitialFrequencyDescription => + 'Configure how many transitions a user must make before an interstitial ad is shown, based on their role.'; + + @override + String get transitionsBeforeInterstitialAdsLabel => + 'Transitions Before Interstitial Ads'; + + @override + String get transitionsBeforeInterstitialAdsDescription => + 'The number of transitions (e.g., opening articles) a user must make before an interstitial ad is displayed.'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index bc084c40..0d105b4b 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1338,22 +1338,6 @@ "@defaultInArticleAdTypeSelectionDescription": { "description": "وصف قسم اختيار نوع الإعلان الافتراضي داخل المقال" }, - "interstitialAdSettingsTitle": "إعدادات الإعلانات البينية", - "@interstitialAdSettingsTitle": { - "description": "عنوان قسم إعدادات الإعلانات البينية" - }, - "enableInterstitialAdsLabel": "تفعيل الإعلانات البينية", - "@enableInterstitialAdsLabel": { - "description": "تسمية مفتاح تفعيل الإعلانات البينية" - }, - "userRoleInterstitialFrequencyTitle": "تكرار الإعلانات البينية حسب دور المستخدم", - "@userRoleInterstitialFrequencyTitle": { - "description": "عنوان قسم تكرار الإعلانات البينية حسب دور المستخدم" - }, - "userRoleInterstitialFrequencyDescription": "تكوين عدد مرات عرض الإعلانات البينية بناءً على أدوار المستخدمين.", - "@userRoleInterstitialFrequencyDescription": { - "description": "وصف قسم تكرار الإعلانات البينية حسب دور المستخدم" - }, "inArticleAdSlotPlacementsTitle": "مواضع الإعلانات داخل المقال", "@inArticleAdSlotPlacementsTitle": { "description": "عنوان قسم مواضع فتحات الإعلانات داخل المقال" @@ -1553,5 +1537,41 @@ "copyId": "نسخ المعرف", "@copyId": { "description": "Tooltip for the copy ID button." + }, + "enableGlobalAdsLabel": "تفعيل الإعلانات", + "@enableGlobalAdsLabel": { + "description": "Label for the switch to enable or disable all ads globally." + }, + "feedToArticleInterstitialAdIdLabel": "معرف الإعلان البيني للانتقال من الموجز إلى المقال", + "@feedToArticleInterstitialAdIdLabel": { + "description": "Label for the ad unit ID for interstitial ads shown when transitioning from feed to article." + }, + "feedToArticleInterstitialAdIdDescription": "معرف الوحدة الإعلانية للإعلانات البينية التي تظهر عندما ينتقل المستخدم من الموجز إلى المقال.", + "@feedToArticleInterstitialAdIdDescription": { + "description": "Description for the ad unit ID for interstitial ads shown when transitioning from feed to article." + }, + "interstitialAdSettingsTitle": "إعدادات الإعلانات البينية", + "@interstitialAdSettingsTitle": { + "description": "Title for the section configuring global interstitial ad settings." + }, + "enableInterstitialAdsLabel": "تفعيل الإعلانات البينية", + "@enableInterstitialAdsLabel": { + "description": "Label for the switch to enable or disable interstitial ads." + }, + "userRoleInterstitialFrequencyTitle": "تكرار الإعلانات البينية حسب دور المستخدم", + "@userRoleInterstitialFrequencyTitle": { + "description": "Title for the section configuring interstitial ad frequency based on user roles." + }, + "userRoleInterstitialFrequencyDescription": "تكوين عدد الانتقالات التي يجب أن يقوم بها المستخدم قبل عرض إعلان بيني، بناءً على دوره.", + "@userRoleInterstitialFrequencyDescription": { + "description": "Description for the interstitial ad frequency settings by user role." + }, + "transitionsBeforeInterstitialAdsLabel": "الانتقالات قبل الإعلانات البينية", + "@transitionsBeforeInterstitialAdsLabel": { + "description": "Label for the number of transitions a user must make before an interstitial ad is shown." + }, + "transitionsBeforeInterstitialAdsDescription": "عدد الانتقالات (مثل فتح المقالات) التي يجب أن يقوم بها المستخدم قبل عرض إعلان بيني.", + "@transitionsBeforeInterstitialAdsDescription": { + "description": "Description for the number of transitions a user must make before an interstitial ad is shown." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 06c17f1e..fb451705 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1334,22 +1334,6 @@ "@defaultInArticleAdTypeSelectionDescription": { "description": "Description for the Default In-Article Ad Type Selection section" }, - "interstitialAdSettingsTitle": "Interstitial Ad Settings", - "@interstitialAdSettingsTitle": { - "description": "Title for the Interstitial Ad Settings section" - }, - "enableInterstitialAdsLabel": "Enable Interstitial Ads", - "@enableInterstitialAdsLabel": { - "description": "Label for the enable interstitial ads switch" - }, - "userRoleInterstitialFrequencyTitle": "User Role Interstitial Frequency", - "@userRoleInterstitialFrequencyTitle": { - "description": "Title for the User Role Interstitial Frequency section" - }, - "userRoleInterstitialFrequencyDescription": "Configure how often interstitial ads are shown based on user roles.", - "@userRoleInterstitialFrequencyDescription": { - "description": "Description for the User Role Interstitial Frequency section" - }, "inArticleAdSlotPlacementsTitle": "In-Article Ad Slot Placements", "@inArticleAdSlotPlacementsTitle": { "description": "Title for the In-Article Ad Slot Placements section" @@ -1549,5 +1533,41 @@ "copyId": "Copy ID", "@copyId": { "description": "Tooltip for the copy ID button." + }, + "enableGlobalAdsLabel": "Enable Ads", + "@enableGlobalAdsLabel": { + "description": "Label for the switch to enable or disable all ads globally." + }, + "feedToArticleInterstitialAdIdLabel": "Feed to Article Interstitial Ad ID", + "@feedToArticleInterstitialAdIdLabel": { + "description": "Label for the ad unit ID for interstitial ads shown when transitioning from feed to article." + }, + "feedToArticleInterstitialAdIdDescription": "The ad unit ID for interstitial ads displayed when a user navigates from a feed to an article.", + "@feedToArticleInterstitialAdIdDescription": { + "description": "Description for the ad unit ID for interstitial ads shown when transitioning from feed to article." + }, + "interstitialAdSettingsTitle": "Interstitial Ad Settings", + "@interstitialAdSettingsTitle": { + "description": "Title for the section configuring global interstitial ad settings." + }, + "enableInterstitialAdsLabel": "Enable Interstitial Ads", + "@enableInterstitialAdsLabel": { + "description": "Label for the switch to enable or disable interstitial ads." + }, + "userRoleInterstitialFrequencyTitle": "Interstitial Ad Frequency by User Role", + "@userRoleInterstitialFrequencyTitle": { + "description": "Title for the section configuring interstitial ad frequency based on user roles." + }, + "userRoleInterstitialFrequencyDescription": "Configure how many transitions a user must make before an interstitial ad is shown, based on their role.", + "@userRoleInterstitialFrequencyDescription": { + "description": "Description for the interstitial ad frequency settings by user role." + }, + "transitionsBeforeInterstitialAdsLabel": "Transitions Before Interstitial Ads", + "@transitionsBeforeInterstitialAdsLabel": { + "description": "Label for the number of transitions a user must make before an interstitial ad is shown." + }, + "transitionsBeforeInterstitialAdsDescription": "The number of transitions (e.g., opening articles) a user must make before an interstitial ad is displayed.", + "@transitionsBeforeInterstitialAdsDescription": { + "description": "Description for the number of transitions a user must make before an interstitial ad is shown." } } \ No newline at end of file From c036cfafabd4a07585673023496b7dc1345c76c5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 10:44:04 +0100 Subject: [PATCH 03/17] feat(app_configuration): add interstitial ad settings form - Implement InterstitialAdSettingsForm widget for configuring global interstitial ad settings - Add functionality to handle different user roles and their ad frequency settings - Include enable/disable toggle for interstitial ads - Implement dynamic form fields for transitions before showing interstitial ads --- .../interstitial_ad_settings_form.dart | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 lib/app_configuration/widgets/interstitial_ad_settings_form.dart diff --git a/lib/app_configuration/widgets/interstitial_ad_settings_form.dart b/lib/app_configuration/widgets/interstitial_ad_settings_form.dart new file mode 100644 index 00000000..a08c5cc0 --- /dev/null +++ b/lib/app_configuration/widgets/interstitial_ad_settings_form.dart @@ -0,0 +1,272 @@ +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; + late final Map + _transitionsBeforeShowingInterstitialAdsControllers; + + @override + void initState() { + super.initState(); + _tabController = TabController( + length: AppUserRole.values.length, + vsync: this, + ); + _initializeControllers(); + } + + @override + void didUpdateWidget(covariant InterstitialAdSettingsForm oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.remoteConfig.adConfig.interstitialAdConfiguration != + oldWidget.remoteConfig.adConfig.interstitialAdConfiguration) { + _updateControllers(); + } + } + + 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, + ), + }; + } + + 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 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 AbsorbPointer( + absorbing: !adConfig.enabled, + child: Opacity( + opacity: adConfig.enabled ? 1.0 : 0.5, + child: 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, // Fixed height for TabBarView within a ListView + child: TabBarView( + controller: _tabController, + children: AppUserRole.values + .map( + (role) => _buildInterstitialRoleSpecificFields( + context, + l10n, + role, + interstitialAdConfig, + ), + ) + .toList(), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildInterstitialRoleSpecificFields( + BuildContext context, + AppLocalizations l10n, + AppUserRole role, + InterstitialAdConfiguration config, + ) { + return Column( + children: [ + AppConfigIntField( + label: l10n.transitionsBeforeInterstitialAdsLabel, + description: l10n.transitionsBeforeInterstitialAdsDescription, + value: _getTransitionsBeforeInterstitial(config, role), + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: widget.remoteConfig.adConfig.copyWith( + interstitialAdConfiguration: + _updateTransitionsBeforeInterstitial( + config, + value, + role, + ), + ), + ), + ); + }, + controller: _transitionsBeforeShowingInterstitialAdsControllers[role], + ), + ], + ); + } + + int _getTransitionsBeforeInterstitial( + InterstitialAdConfiguration config, + AppUserRole role, + ) { + switch (role) { + case AppUserRole.guestUser: + return config + .feedInterstitialAdFrequencyConfig + .guestTransitionsBeforeShowingInterstitialAds; + case AppUserRole.standardUser: + return config + .feedInterstitialAdFrequencyConfig + .standardUserTransitionsBeforeShowingInterstitialAds; + case AppUserRole.premiumUser: + return config + .feedInterstitialAdFrequencyConfig + .premiumUserTransitionsBeforeShowingInterstitialAds; + } + } + + InterstitialAdConfiguration _updateTransitionsBeforeInterstitial( + InterstitialAdConfiguration config, + int value, + AppUserRole role, + ) { + final currentFrequencyConfig = config.feedInterstitialAdFrequencyConfig; + + InterstitialAdFrequencyConfig newFrequencyConfig; + + switch (role) { + case AppUserRole.guestUser: + newFrequencyConfig = currentFrequencyConfig.copyWith( + guestTransitionsBeforeShowingInterstitialAds: value, + ); + case AppUserRole.standardUser: + newFrequencyConfig = currentFrequencyConfig.copyWith( + standardUserTransitionsBeforeShowingInterstitialAds: value, + ); + case AppUserRole.premiumUser: + newFrequencyConfig = currentFrequencyConfig.copyWith( + premiumUserTransitionsBeforeShowingInterstitialAds: value, + ); + } + + return config.copyWith( + feedInterstitialAdFrequencyConfig: newFrequencyConfig, + ); + } +} From a46b1df65c6007d6c3aa509bf8548bd4f9113e79 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 10:44:26 +0100 Subject: [PATCH 04/17] refactor(ad): improve ad configuration form usability - Rename 'articleInterstitialAdId' to 'feedToArticleInterstitialAdId' - Add AbsorbPointer and Opacity to gray out form when ads are disabled - Adjust padding and alignment for better visual hierarchy - Reorganize widgets for improved code structure and readability --- .../widgets/ad_platform_config_form.dart | 252 +++++++++--------- 1 file changed, 131 insertions(+), 121 deletions(-) diff --git a/lib/app_configuration/widgets/ad_platform_config_form.dart b/lib/app_configuration/widgets/ad_platform_config_form.dart index fce3ab2f..0a553d25 100644 --- a/lib/app_configuration/widgets/ad_platform_config_form.dart +++ b/lib/app_configuration/widgets/ad_platform_config_form.dart @@ -70,13 +70,13 @@ class _AdPlatformConfigFormState extends State { ?.feedBannerAdId ?? '', ), - 'articleInterstitialAdId': TextEditingController( + 'feedToArticleInterstitialAdId': TextEditingController( text: widget .remoteConfig .adConfig .platformAdIdentifiers[platform] - ?.articleInterstitialAdId ?? + ?.feedToArticleInterstitialAdId ?? '', ), 'inArticleNativeAdId': TextEditingController( @@ -137,22 +137,22 @@ class _AdPlatformConfigFormState extends State { ); } - final articleInterstitialAdId = + final feedToArticleInterstitialAdId = widget .remoteConfig .adConfig .platformAdIdentifiers[platform] - ?.articleInterstitialAdId ?? + ?.feedToArticleInterstitialAdId ?? ''; - if (_platformAdIdentifierControllers[platform]!['articleInterstitialAdId'] + if (_platformAdIdentifierControllers[platform]!['feedToArticleInterstitialAdId'] ?.text != - articleInterstitialAdId) { - _platformAdIdentifierControllers[platform]!['articleInterstitialAdId'] + feedToArticleInterstitialAdId) { + _platformAdIdentifierControllers[platform]!['feedToArticleInterstitialAdId'] ?.text = - articleInterstitialAdId; - _platformAdIdentifierControllers[platform]!['articleInterstitialAdId'] + feedToArticleInterstitialAdId; + _platformAdIdentifierControllers[platform]!['feedToArticleInterstitialAdId'] ?.selection = TextSelection.collapsed( - offset: articleInterstitialAdId.length, + offset: feedToArticleInterstitialAdId.length, ); } @@ -211,124 +211,134 @@ class _AdPlatformConfigFormState extends State { final l10n = AppLocalizationsX(context).l10n; final adConfig = widget.remoteConfig.adConfig; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Primary Ad Platform Selection - ExpansionTile( - title: Text(l10n.primaryAdPlatformTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, // Adjusted padding for hierarchy - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: - CrossAxisAlignment.start, // Align content to start + return AbsorbPointer( + absorbing: !adConfig.enabled, + child: Opacity( + opacity: adConfig.enabled ? 1.0 : 0.5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - l10n.primaryAdPlatformDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + // Primary Ad Platform Selection + ExpansionTile( + title: Text(l10n.primaryAdPlatformTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, // Adjusted padding for hierarchy + top: AppSpacing.md, + bottom: AppSpacing.md, ), - textAlign: TextAlign.start, // Ensure text aligns to start - ), - const SizedBox(height: AppSpacing.lg), - Align( - alignment: AlignmentDirectional.centerStart, - child: SegmentedButton( - style: SegmentedButton.styleFrom( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.zero, + expandedCrossAxisAlignment: + CrossAxisAlignment.start, // Align content to start + children: [ + Text( + l10n.primaryAdPlatformDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), ), + textAlign: TextAlign.start, // Ensure text aligns to start ), - segments: AdPlatformType.values - .map( - (platform) => ButtonSegment( - value: platform, - label: Text(platform.name), - ), - ) - .toList(), - selected: {_selectedPlatform}, - onSelectionChanged: (newSelection) { - setState(() { - _selectedPlatform = newSelection.first; - }); - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - primaryAdPlatform: newSelection.first, + const SizedBox(height: AppSpacing.lg), + Align( + alignment: AlignmentDirectional.centerStart, + child: SegmentedButton( + style: SegmentedButton.styleFrom( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.zero, ), ), - ); - }, - ), + segments: AdPlatformType.values + .map( + (platform) => ButtonSegment( + value: platform, + label: Text(platform.name), + ), + ) + .toList(), + selected: {_selectedPlatform}, + onSelectionChanged: (newSelection) { + setState(() { + _selectedPlatform = newSelection.first; + }); + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: adConfig.copyWith( + primaryAdPlatform: newSelection.first, + ), + ), + ); + }, + ), + ), + ], ), - ], - ), - const SizedBox(height: AppSpacing.lg), + const SizedBox(height: AppSpacing.lg), - // Ad Unit Identifiers - ExpansionTile( - title: Text(l10n.adUnitIdentifiersTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, // Adjusted padding for hierarchy - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: - CrossAxisAlignment.start, // Align content to start - children: [ - Text( - l10n.adUnitIdentifiersDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + // Ad Unit Identifiers + ExpansionTile( + title: Text(l10n.adUnitIdentifiersTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, // Adjusted padding for hierarchy + top: AppSpacing.md, + bottom: AppSpacing.md, ), - textAlign: TextAlign.start, // Ensure text aligns to start + expandedCrossAxisAlignment: + CrossAxisAlignment.start, // Align content to start + children: [ + Text( + l10n.adUnitIdentifiersDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), + textAlign: TextAlign.start, // Ensure text aligns to start + ), + const SizedBox(height: AppSpacing.lg), + _buildAdUnitIdentifierFields( + context, + l10n, + _selectedPlatform, + adConfig, + ), + ], ), const SizedBox(height: AppSpacing.lg), - _buildAdUnitIdentifierFields( - context, - l10n, - _selectedPlatform, - adConfig, - ), - ], - ), - const SizedBox(height: AppSpacing.lg), - // Local Ad Management - if (_selectedPlatform == AdPlatformType.local) - ExpansionTile( - title: Text(l10n.localAdManagementTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, // Adjusted padding for hierarchy - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: - CrossAxisAlignment.start, // Align content to start - children: [ - Text( - l10n.localAdManagementDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - textAlign: TextAlign.start, // Ensure text aligns to start - ), - const SizedBox(height: AppSpacing.lg), - Center( - child: ElevatedButton( - onPressed: () => - context.goNamed(Routes.localAdsManagementName), - child: Text(l10n.manageLocalAdsButton), + // Local Ad Management + if (_selectedPlatform == AdPlatformType.local) + ExpansionTile( + title: Text(l10n.localAdManagementTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, // Adjusted padding for hierarchy + top: AppSpacing.md, + bottom: AppSpacing.md, ), + expandedCrossAxisAlignment: + CrossAxisAlignment.start, // Align content to start + children: [ + Text( + l10n.localAdManagementDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), + textAlign: TextAlign.start, // Ensure text aligns to start + ), + const SizedBox(height: AppSpacing.lg), + Center( + child: ElevatedButton( + onPressed: () => + context.goNamed(Routes.localAdsManagementName), + child: Text(l10n.manageLocalAdsButton), + ), + ), + ], ), - ], - ), - ], + ], + ), + ), ); } @@ -350,9 +360,9 @@ class _AdPlatformConfigFormState extends State { newIdentifiers = platformIdentifiers.copyWith(feedNativeAdId: value); case 'feedBannerAdId': newIdentifiers = platformIdentifiers.copyWith(feedBannerAdId: value); - case 'articleInterstitialAdId': + case 'feedToArticleInterstitialAdId': newIdentifiers = platformIdentifiers.copyWith( - articleInterstitialAdId: value, + feedToArticleInterstitialAdId: value, ); case 'inArticleNativeAdId': newIdentifiers = platformIdentifiers.copyWith( @@ -399,12 +409,12 @@ class _AdPlatformConfigFormState extends State { controller: controllers['feedBannerAdId'], ), AppConfigTextField( - label: l10n.articleInterstitialAdIdLabel, - description: l10n.articleInterstitialAdIdDescription, - value: platformIdentifiers.articleInterstitialAdId, + label: l10n.feedToArticleInterstitialAdIdLabel, + description: l10n.feedToArticleInterstitialAdIdDescription, + value: platformIdentifiers.feedToArticleInterstitialAdId, onChanged: (value) => - updatePlatformIdentifiers('articleInterstitialAdId', value), - controller: controllers['articleInterstitialAdId'], + updatePlatformIdentifiers('feedToArticleInterstitialAdId', value), + controller: controllers['feedToArticleInterstitialAdId'], ), AppConfigTextField( label: l10n.inArticleNativeAdIdLabel, From 1a5207fb6ce004f6109b3c9eceb628fc84bbb83d Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 10:44:40 +0100 Subject: [PATCH 05/17] refactor(app_configuration): simplify article ad settings form - Remove unused imports and code related to user roles and interstitial ads - Wrap form content with AbsorbPointer and Opacity widgets - Reorganize UI elements and spacing - Update switch label and logic for enabling article ads - Combine in-article ad slot placements into a single ExpansionTile --- .../widgets/article_ad_settings_form.dart | 396 ++++-------------- 1 file changed, 84 insertions(+), 312 deletions(-) diff --git a/lib/app_configuration/widgets/article_ad_settings_form.dart b/lib/app_configuration/widgets/article_ad_settings_form.dart index 48cbcd0b..c9201002 100644 --- a/lib/app_configuration/widgets/article_ad_settings_form.dart +++ b/lib/app_configuration/widgets/article_ad_settings_form.dart @@ -1,9 +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/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:flutter_news_app_web_dashboard_full_source_code/shared/extensions/in_article_ad_slot_type_l10n.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -30,78 +27,14 @@ class ArticleAdSettingsForm extends StatefulWidget { class _ArticleAdSettingsFormState extends State with SingleTickerProviderStateMixin { - late TabController _tabController; - late final Map - _articlesToReadBeforeShowingInterstitialAdsControllers; - @override void initState() { super.initState(); - _tabController = TabController( - length: AppUserRole.values.length, - vsync: this, - ); - _initializeControllers(); } @override void didUpdateWidget(covariant ArticleAdSettingsForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.remoteConfig.adConfig.articleAdConfiguration != - oldWidget.remoteConfig.adConfig.articleAdConfiguration) { - _updateControllers(); - } - } - - void _initializeControllers() { - final articleAdConfig = widget.remoteConfig.adConfig.articleAdConfiguration; - final interstitialConfig = articleAdConfig.interstitialAdConfiguration; - _articlesToReadBeforeShowingInterstitialAdsControllers = { - for (final role in AppUserRole.values) - role: - TextEditingController( - text: _getArticlesBeforeInterstitial( - interstitialConfig, - role, - ).toString(), - ) - ..selection = TextSelection.collapsed( - offset: _getArticlesBeforeInterstitial( - interstitialConfig, - role, - ).toString().length, - ), - }; - } - - void _updateControllers() { - final articleAdConfig = widget.remoteConfig.adConfig.articleAdConfiguration; - final interstitialConfig = articleAdConfig.interstitialAdConfiguration; - for (final role in AppUserRole.values) { - final newInterstitialValue = _getArticlesBeforeInterstitial( - interstitialConfig, - role, - ).toString(); - if (_articlesToReadBeforeShowingInterstitialAdsControllers[role]?.text != - newInterstitialValue) { - _articlesToReadBeforeShowingInterstitialAdsControllers[role]?.text = - newInterstitialValue; - _articlesToReadBeforeShowingInterstitialAdsControllers[role] - ?.selection = TextSelection.collapsed( - offset: newInterstitialValue.length, - ); - } - } - } - - @override - void dispose() { - _tabController.dispose(); - for (final controller - in _articlesToReadBeforeShowingInterstitialAdsControllers.values) { - controller.dispose(); - } - super.dispose(); } @override @@ -110,111 +43,33 @@ class _ArticleAdSettingsFormState extends State 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.defaultInArticleAdTypeSelectionTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, // Adjusted padding for hierarchy - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: - CrossAxisAlignment.start, // Align content to start - children: [ - Text( - l10n.defaultInArticleAdTypeSelectionDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), - textAlign: TextAlign.start, // Ensure text aligns to start - ), - const SizedBox(height: AppSpacing.lg), - Align( - alignment: AlignmentDirectional.centerStart, - child: SegmentedButton( - style: SegmentedButton.styleFrom( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.zero, - ), - ), - segments: AdType.values - .where( - (type) => type == AdType.native || type == AdType.banner, - ) - .map( - (type) => ButtonSegment( - value: type, - label: Text(type.name), - ), - ) - .toList(), - selected: {articleAdConfig.defaultInArticleAdType}, - onSelectionChanged: (newSelection) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - articleAdConfiguration: articleAdConfig.copyWith( - defaultInArticleAdType: newSelection.first, - ), - ), - ), - ); - }, - ), - ), - ], - ), - const SizedBox(height: AppSpacing.lg), - ExpansionTile( - title: Text(l10n.interstitialAdSettingsTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, // Adjusted padding for hierarchy - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: - CrossAxisAlignment.start, // Align content to start + return AbsorbPointer( + absorbing: !adConfig.enabled, + child: Opacity( + opacity: adConfig.enabled ? 1.0 : 0.5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile( - title: Text(l10n.enableInterstitialAdsLabel), - value: articleAdConfig.interstitialAdConfiguration.enabled, + title: Text(l10n.enableArticleAdsLabel), + value: articleAdConfig.enabled, onChanged: (value) { widget.onConfigChanged( widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( articleAdConfiguration: articleAdConfig.copyWith( - interstitialAdConfiguration: articleAdConfig - .interstitialAdConfiguration - .copyWith(enabled: value), + enabled: value, ), ), ), ); }, ), + const SizedBox(height: AppSpacing.lg), ExpansionTile( - title: Text(l10n.userRoleInterstitialFrequencyTitle), + title: Text(l10n.defaultInArticleAdTypeSelectionTitle), childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing - .xl, // Further adjusted padding for nested hierarchy + start: AppSpacing.lg, // Adjusted padding for hierarchy top: AppSpacing.md, bottom: AppSpacing.md, ), @@ -222,7 +77,7 @@ class _ArticleAdSettingsFormState extends State CrossAxisAlignment.start, // Align content to start children: [ Text( - l10n.userRoleInterstitialFrequencyDescription, + l10n.defaultInArticleAdTypeSelectionDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of( context, @@ -231,176 +86,93 @@ class _ArticleAdSettingsFormState extends State textAlign: TextAlign.start, // Ensure text aligns to start ), const SizedBox(height: AppSpacing.lg), - // Replaced SegmentedButton with TabBar for role selection 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(), + child: SegmentedButton( + style: SegmentedButton.styleFrom( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.zero, + ), ), - ), - ), - const SizedBox(height: AppSpacing.lg), - // TabBarView to display role-specific fields - SizedBox( - height: 250, // Fixed height for TabBarView within a ListView - child: TabBarView( - controller: _tabController, - children: AppUserRole.values + segments: AdType.values + .where( + (type) => + type == AdType.native || type == AdType.banner, + ) .map( - (role) => _buildInterstitialRoleSpecificFields( - context, - l10n, - role, - articleAdConfig.interstitialAdConfiguration, + (type) => ButtonSegment( + value: type, + label: Text(type.name), ), ) .toList(), + selected: {articleAdConfig.defaultInArticleAdType}, + onSelectionChanged: (newSelection) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: adConfig.copyWith( + articleAdConfiguration: articleAdConfig.copyWith( + defaultInArticleAdType: newSelection.first, + ), + ), + ), + ); + }, ), ), ], ), - ], - ), - const SizedBox(height: AppSpacing.lg), - ExpansionTile( - title: Text(l10n.inArticleAdSlotPlacementsTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, // Adjusted padding for hierarchy - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: - CrossAxisAlignment.start, // Align content to start - children: [ - Text( - l10n.inArticleAdSlotPlacementsDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), - textAlign: TextAlign.start, // Ensure text aligns to start - ), const SizedBox(height: AppSpacing.lg), - ...articleAdConfig.inArticleAdSlotConfigurations.map( - (slotConfig) => SwitchListTile( - title: Text(slotConfig.slotType.l10n(context)), - value: slotConfig.enabled, - onChanged: (value) { - final updatedSlots = articleAdConfig - .inArticleAdSlotConfigurations - .map( - (e) => e.slotType == slotConfig.slotType - ? e.copyWith(enabled: value) - : e, - ) - .toList(); - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - articleAdConfiguration: articleAdConfig.copyWith( - inArticleAdSlotConfigurations: updatedSlots, - ), - ), - ), - ); - }, + ExpansionTile( + title: Text(l10n.inArticleAdSlotPlacementsTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, // Adjusted padding for hierarchy + top: AppSpacing.md, + bottom: AppSpacing.md, ), - ), - ], - ), - ], - ); - } - - Widget _buildInterstitialRoleSpecificFields( - BuildContext context, - AppLocalizations l10n, - AppUserRole role, - ArticleInterstitialAdConfiguration config, - ) { - return Column( - children: [ - AppConfigIntField( - label: l10n.articlesBeforeInterstitialAdsLabel, - description: l10n.articlesBeforeInterstitialAdsDescription, - value: _getArticlesBeforeInterstitial(config, role), - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: widget.remoteConfig.adConfig.copyWith( - articleAdConfiguration: widget - .remoteConfig - .adConfig - .articleAdConfiguration - .copyWith( - interstitialAdConfiguration: - _updateArticlesBeforeInterstitial( - config, - value, - role, + expandedCrossAxisAlignment: + CrossAxisAlignment.start, // Align content to start + children: [ + Text( + l10n.inArticleAdSlotPlacementsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), + textAlign: TextAlign.start, // Ensure text aligns to start + ), + const SizedBox(height: AppSpacing.lg), + ...articleAdConfig.inArticleAdSlotConfigurations.map( + (slotConfig) => SwitchListTile( + title: Text(slotConfig.slotType.l10n(context)), + value: slotConfig.enabled, + onChanged: (value) { + final updatedSlots = articleAdConfig + .inArticleAdSlotConfigurations + .map( + (e) => e.slotType == slotConfig.slotType + ? e.copyWith(enabled: value) + : e, + ) + .toList(); + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: adConfig.copyWith( + articleAdConfiguration: articleAdConfig.copyWith( + inArticleAdSlotConfigurations: updatedSlots, ), - ), + ), + ), + ); + }, + ), ), - ), - ); - }, - controller: - _articlesToReadBeforeShowingInterstitialAdsControllers[role], + ], + ), + ], ), - ], + ), ); } - - int _getArticlesBeforeInterstitial( - ArticleInterstitialAdConfiguration config, - AppUserRole role, - ) { - switch (role) { - case AppUserRole.guestUser: - return config - .frequencyConfig - .guestArticlesToReadBeforeShowingInterstitialAds; - case AppUserRole.standardUser: - return config - .frequencyConfig - .standardUserArticlesToReadBeforeShowingInterstitialAds; - case AppUserRole.premiumUser: - return config - .frequencyConfig - .premiumUserArticlesToReadBeforeShowingInterstitialAds; - } - } - - ArticleInterstitialAdConfiguration _updateArticlesBeforeInterstitial( - ArticleInterstitialAdConfiguration config, - int value, - AppUserRole role, - ) { - final currentFrequencyConfig = config.frequencyConfig; - - ArticleInterstitialAdFrequencyConfig newFrequencyConfig; - - switch (role) { - case AppUserRole.guestUser: - newFrequencyConfig = currentFrequencyConfig.copyWith( - guestArticlesToReadBeforeShowingInterstitialAds: value, - ); - case AppUserRole.standardUser: - newFrequencyConfig = currentFrequencyConfig.copyWith( - standardUserArticlesToReadBeforeShowingInterstitialAds: value, - ); - case AppUserRole.premiumUser: - newFrequencyConfig = currentFrequencyConfig.copyWith( - premiumUserArticlesToReadBeforeShowingInterstitialAds: value, - ); - } - - return config.copyWith(frequencyConfig: newFrequencyConfig); - } } From c74796611869b781ff73554604973ddea4755028 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 10:44:50 +0100 Subject: [PATCH 06/17] refactor(ad): simplify ad configuration form - Remove articles before interstitial ad settings - Add global ad enable/disable toggle - Gray out settings when ads are disabled - Reorganize layout for better clarity --- .../widgets/ad_config_form.dart | 204 +++++------------- 1 file changed, 58 insertions(+), 146 deletions(-) diff --git a/lib/app_configuration/widgets/ad_config_form.dart b/lib/app_configuration/widgets/ad_config_form.dart index eb0569af..e8b687fd 100644 --- a/lib/app_configuration/widgets/ad_config_form.dart +++ b/lib/app_configuration/widgets/ad_config_form.dart @@ -36,8 +36,6 @@ class _AdConfigFormState extends State late final Map _adFrequencyControllers; late final Map _adPlacementIntervalControllers; - late final Map - _articlesToReadBeforeShowingInterstitialAdsControllers; @override void initState() { @@ -82,19 +80,6 @@ class _AdConfigFormState extends State ).toString().length, ), }; - _articlesToReadBeforeShowingInterstitialAdsControllers = { - for (final role in AppUserRole.values) - role: - TextEditingController( - text: _getArticlesBeforeInterstitial(adConfig, role).toString(), - ) - ..selection = TextSelection.collapsed( - offset: _getArticlesBeforeInterstitial( - adConfig, - role, - ).toString().length, - ), - }; } void _updateControllers() { @@ -120,20 +105,6 @@ class _AdConfigFormState extends State offset: newPlacementIntervalValue.length, ); } - - final newInterstitialValue = _getArticlesBeforeInterstitial( - adConfig, - role, - ).toString(); - if (_articlesToReadBeforeShowingInterstitialAdsControllers[role]?.text != - newInterstitialValue) { - _articlesToReadBeforeShowingInterstitialAdsControllers[role]?.text = - newInterstitialValue; - _articlesToReadBeforeShowingInterstitialAdsControllers[role] - ?.selection = TextSelection.collapsed( - offset: newInterstitialValue.length, - ); - } } } @@ -146,10 +117,6 @@ class _AdConfigFormState extends State for (final controller in _adPlacementIntervalControllers.values) { controller.dispose(); } - for (final controller - in _articlesToReadBeforeShowingInterstitialAdsControllers.values) { - controller.dispose(); - } super.dispose(); } @@ -161,44 +128,67 @@ class _AdConfigFormState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - l10n.adSettingsDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), - ), - const SizedBox(height: AppSpacing.lg), - // Replaced SegmentedButton with TabBar for role selection - 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(), - ), - ), + SwitchListTile( + title: Text(l10n.enableGlobalAdsLabel), + value: adConfig.enabled, + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: adConfig.copyWith(enabled: value), + ), + ); + }, ), const SizedBox(height: AppSpacing.lg), - // TabBarView to display role-specific fields - SizedBox( - height: 400, // Fixed height for TabBarView within a ListView - child: TabBarView( - controller: _tabController, - children: AppUserRole.values - .map( - (role) => _buildRoleSpecificFields( - context, - l10n, - role, - adConfig, + AbsorbPointer( + absorbing: !adConfig.enabled, + child: Opacity( + opacity: adConfig.enabled ? 1.0 : 0.5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.adSettingsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), ), - ) - .toList(), + ), + 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: 400, + child: TabBarView( + controller: _tabController, + children: AppUserRole.values + .map( + (role) => _buildRoleSpecificFields( + context, + l10n, + role, + adConfig, + ), + ) + .toList(), + ), + ), + ], + ), ), ), ], @@ -239,24 +229,6 @@ class _AdConfigFormState extends State }, controller: _adPlacementIntervalControllers[role], ), - AppConfigIntField( - label: l10n.articlesBeforeInterstitialAdsLabel, - description: l10n.articlesBeforeInterstitialAdsDescription, - value: _getArticlesBeforeInterstitial(config, role), - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: _updateArticlesBeforeInterstitial( - config, - value, - role, - ), - ), - ); - }, - controller: - _articlesToReadBeforeShowingInterstitialAdsControllers[role], - ), ], ); } @@ -295,29 +267,6 @@ class _AdConfigFormState extends State } } - int _getArticlesBeforeInterstitial(AdConfig config, AppUserRole role) { - switch (role) { - case AppUserRole.guestUser: - return config - .articleAdConfiguration - .interstitialAdConfiguration - .frequencyConfig - .guestArticlesToReadBeforeShowingInterstitialAds; - case AppUserRole.standardUser: - return config - .articleAdConfiguration - .interstitialAdConfiguration - .frequencyConfig - .standardUserArticlesToReadBeforeShowingInterstitialAds; - case AppUserRole.premiumUser: - return config - .articleAdConfiguration - .interstitialAdConfiguration - .frequencyConfig - .premiumUserArticlesToReadBeforeShowingInterstitialAds; - } - } - AdConfig _updateAdFrequency(AdConfig config, int value, AppUserRole role) { switch (role) { case AppUserRole.guestUser: @@ -373,41 +322,4 @@ class _AdConfigFormState extends State ); } } - - AdConfig _updateArticlesBeforeInterstitial( - AdConfig config, - int value, - AppUserRole role, - ) { - final currentFrequencyConfig = config - .articleAdConfiguration - .interstitialAdConfiguration - .frequencyConfig; - - ArticleInterstitialAdFrequencyConfig newFrequencyConfig; - - switch (role) { - case AppUserRole.guestUser: - newFrequencyConfig = currentFrequencyConfig.copyWith( - guestArticlesToReadBeforeShowingInterstitialAds: value, - ); - case AppUserRole.standardUser: - newFrequencyConfig = currentFrequencyConfig.copyWith( - standardUserArticlesToReadBeforeShowingInterstitialAds: value, - ); - case AppUserRole.premiumUser: - newFrequencyConfig = currentFrequencyConfig.copyWith( - premiumUserArticlesToReadBeforeShowingInterstitialAds: value, - ); - } - - return config.copyWith( - articleAdConfiguration: config.articleAdConfiguration.copyWith( - interstitialAdConfiguration: config - .articleAdConfiguration - .interstitialAdConfiguration - .copyWith(frequencyConfig: newFrequencyConfig), - ), - ); - } } From 3c018b372eeb85b9cbb511ce810f432e7608a2e0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 10:45:00 +0100 Subject: [PATCH 07/17] feat(app_configuration): add global ad switch and interstitial ad settings - Add AdConfigForm for global ad configuration - Include InterstitialAdSettingsForm in the ad settings section - Disable expansion of ad platform configurations if global ads are off - Adjust the layout to accommodate the new global ad switch --- .../advertisements_configuration_tab.dart | 69 +++++++++++++++---- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/lib/app_configuration/view/tabs/advertisements_configuration_tab.dart b/lib/app_configuration/view/tabs/advertisements_configuration_tab.dart index bac8c7a4..0ab9b674 100644 --- a/lib/app_configuration/view/tabs/advertisements_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/advertisements_configuration_tab.dart @@ -1,8 +1,10 @@ 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'; @@ -46,10 +48,17 @@ class _AdvertisementsConfigurationTabState @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, @@ -64,10 +73,12 @@ class _AdvertisementsConfigurationTabState bottom: AppSpacing.md, ), expandedCrossAxisAlignment: CrossAxisAlignment.start, - onExpansionChanged: (isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; - }, - initiallyExpanded: expandedIndex == tileIndex, + onExpansionChanged: adConfig.enabled + ? (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + } + : null, // Disable expansion if global ads are off + initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled, children: [ AdPlatformConfigForm( remoteConfig: widget.remoteConfig, @@ -92,10 +103,12 @@ class _AdvertisementsConfigurationTabState bottom: AppSpacing.md, ), expandedCrossAxisAlignment: CrossAxisAlignment.start, - onExpansionChanged: (isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; - }, - initiallyExpanded: expandedIndex == tileIndex, + onExpansionChanged: adConfig.enabled + ? (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + } + : null, // Disable expansion if global ads are off + initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled, children: [ FeedAdSettingsForm( remoteConfig: widget.remoteConfig, @@ -120,10 +133,12 @@ class _AdvertisementsConfigurationTabState bottom: AppSpacing.md, ), expandedCrossAxisAlignment: CrossAxisAlignment.start, - onExpansionChanged: (isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; - }, - initiallyExpanded: expandedIndex == tileIndex, + onExpansionChanged: adConfig.enabled + ? (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + } + : null, // Disable expansion if global ads are off + initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled, children: [ ArticleAdSettingsForm( remoteConfig: widget.remoteConfig, @@ -133,6 +148,36 @@ class _AdvertisementsConfigurationTabState ); }, ), + 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, // Disable expansion if global ads are off + initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled, + children: [ + InterstitialAdSettingsForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ); + }, + ), ], ); } From 44c4768e9545ec33aaabcca710b2153ee1f01fd0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 10:57:42 +0100 Subject: [PATCH 08/17] refactor(ad_config_form): remove duplicate code and imports - Remove duplicate imports - Remove duplicate code for role-specific fields - Reduce complexity by removing unnecessary methods --- .../widgets/ad_config_form.dart | 150 ------------------ 1 file changed, 150 deletions(-) diff --git a/lib/app_configuration/widgets/ad_config_form.dart b/lib/app_configuration/widgets/ad_config_form.dart index e8b687fd..2ddd61d5 100644 --- a/lib/app_configuration/widgets/ad_config_form.dart +++ b/lib/app_configuration/widgets/ad_config_form.dart @@ -1,10 +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/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 ad_config_form} /// A form widget for configuring ad settings based on user role. @@ -139,96 +135,6 @@ class _AdConfigFormState extends State ); }, ), - const SizedBox(height: AppSpacing.lg), - AbsorbPointer( - absorbing: !adConfig.enabled, - child: Opacity( - opacity: adConfig.enabled ? 1.0 : 0.5, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.adSettingsDescription, - 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: 400, - child: TabBarView( - controller: _tabController, - children: AppUserRole.values - .map( - (role) => _buildRoleSpecificFields( - context, - l10n, - role, - adConfig, - ), - ) - .toList(), - ), - ), - ], - ), - ), - ), - ], - ); - } - - Widget _buildRoleSpecificFields( - BuildContext context, - AppLocalizations l10n, - AppUserRole role, - AdConfig config, - ) { - return Column( - children: [ - AppConfigIntField( - label: l10n.adFrequencyLabel, - description: l10n.adFrequencyDescription, - value: _getAdFrequency(config, role), - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: _updateAdFrequency(config, value, role), - ), - ); - }, - controller: _adFrequencyControllers[role], - ), - AppConfigIntField( - label: l10n.adPlacementIntervalLabel, - description: l10n.adPlacementIntervalDescription, - value: _getAdPlacementInterval(config, role), - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: _updateAdPlacementInterval(config, value, role), - ), - ); - }, - controller: _adPlacementIntervalControllers[role], - ), ], ); } @@ -266,60 +172,4 @@ class _AdConfigFormState extends State .premiumAdPlacementInterval; } } - - AdConfig _updateAdFrequency(AdConfig config, int value, AppUserRole role) { - switch (role) { - case AppUserRole.guestUser: - return config.copyWith( - feedAdConfiguration: config.feedAdConfiguration.copyWith( - frequencyConfig: config.feedAdConfiguration.frequencyConfig - .copyWith(guestAdFrequency: value), - ), - ); - case AppUserRole.standardUser: - return config.copyWith( - feedAdConfiguration: config.feedAdConfiguration.copyWith( - frequencyConfig: config.feedAdConfiguration.frequencyConfig - .copyWith(authenticatedAdFrequency: value), - ), - ); - case AppUserRole.premiumUser: - return config.copyWith( - feedAdConfiguration: config.feedAdConfiguration.copyWith( - frequencyConfig: config.feedAdConfiguration.frequencyConfig - .copyWith(premiumAdFrequency: value), - ), - ); - } - } - - AdConfig _updateAdPlacementInterval( - AdConfig config, - int value, - AppUserRole role, - ) { - switch (role) { - case AppUserRole.guestUser: - return config.copyWith( - feedAdConfiguration: config.feedAdConfiguration.copyWith( - frequencyConfig: config.feedAdConfiguration.frequencyConfig - .copyWith(guestAdPlacementInterval: value), - ), - ); - case AppUserRole.standardUser: - return config.copyWith( - feedAdConfiguration: config.feedAdConfiguration.copyWith( - frequencyConfig: config.feedAdConfiguration.frequencyConfig - .copyWith(authenticatedAdPlacementInterval: value), - ), - ); - case AppUserRole.premiumUser: - return config.copyWith( - feedAdConfiguration: config.feedAdConfiguration.copyWith( - frequencyConfig: config.feedAdConfiguration.frequencyConfig - .copyWith(premiumAdPlacementInterval: value), - ), - ); - } - } } From 7e091964249628e40de368831bd1db68060e9a9d Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 11:26:58 +0100 Subject: [PATCH 09/17] feat(l10n): add Arabic and English keys for ad platform types - Add Arabic and English translations for AdMob and Local ad platform names - Introduce new localization keys for ad platform types --- lib/l10n/app_localizations.dart | 12 ++++++++++++ lib/l10n/app_localizations_ar.dart | 6 ++++++ lib/l10n/app_localizations_en.dart | 6 ++++++ lib/l10n/arb/app_ar.arb | 8 ++++++++ lib/l10n/arb/app_en.arb | 8 ++++++++ 5 files changed, 40 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 4394269b..4eb06129 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2347,6 +2347,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'The number of transitions (e.g., opening articles) a user must make before an interstitial ad is displayed.'** String get transitionsBeforeInterstitialAdsDescription; + + /// The name of the AdMob ad platform. + /// + /// In en, this message translates to: + /// **'AdMob'** + String get adPlatformTypeAdmob; + + /// The name of the Local ad platform for custom ads. + /// + /// In en, this message translates to: + /// **'Local'** + String get adPlatformTypeLocal; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 055dd7d9..366fccbb 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1257,4 +1257,10 @@ class AppLocalizationsAr extends AppLocalizations { @override String get transitionsBeforeInterstitialAdsDescription => 'عدد الانتقالات (مثل فتح المقالات) التي يجب أن يقوم بها المستخدم قبل عرض إعلان بيني.'; + + @override + String get adPlatformTypeAdmob => 'أدموب'; + + @override + String get adPlatformTypeLocal => 'محلي'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index fb9ac631..d3a8b32e 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1259,4 +1259,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get transitionsBeforeInterstitialAdsDescription => 'The number of transitions (e.g., opening articles) a user must make before an interstitial ad is displayed.'; + + @override + String get adPlatformTypeAdmob => 'AdMob'; + + @override + String get adPlatformTypeLocal => 'Local'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 0d105b4b..e0eac73c 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1573,5 +1573,13 @@ "transitionsBeforeInterstitialAdsDescription": "عدد الانتقالات (مثل فتح المقالات) التي يجب أن يقوم بها المستخدم قبل عرض إعلان بيني.", "@transitionsBeforeInterstitialAdsDescription": { "description": "Description for the number of transitions a user must make before an interstitial ad is shown." + }, + "adPlatformTypeAdmob": "أدموب", + "@adPlatformTypeAdmob": { + "description": "اسم منصة إعلانات أدموب." + }, + "adPlatformTypeLocal": "محلي", + "@adPlatformTypeLocal": { + "description": "اسم منصة الإعلانات المحلية للإعلانات المخصصة." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index fb451705..958213a1 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1569,5 +1569,13 @@ "transitionsBeforeInterstitialAdsDescription": "The number of transitions (e.g., opening articles) a user must make before an interstitial ad is displayed.", "@transitionsBeforeInterstitialAdsDescription": { "description": "Description for the number of transitions a user must make before an interstitial ad is shown." + }, + "adPlatformTypeAdmob": "AdMob", + "@adPlatformTypeAdmob": { + "description": "The name of the AdMob ad platform." + }, + "adPlatformTypeLocal": "Local", + "@adPlatformTypeLocal": { + "description": "The name of the Local ad platform for custom ads." } } \ No newline at end of file From eda21240b438af2f957a7e9bf37250eba3efcaa6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 11:27:06 +0100 Subject: [PATCH 10/17] feat(l10n): add extension for localized AdPlatformType strings - Create new extension on AdPlatformType for localized string representations - Add cases for AdPlatformType.admob and AdPlatformType.local - Import necessary packages and use context.l10n for localization --- .../extensions/ad_platform_type_l10n.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 lib/shared/extensions/ad_platform_type_l10n.dart diff --git a/lib/shared/extensions/ad_platform_type_l10n.dart b/lib/shared/extensions/ad_platform_type_l10n.dart new file mode 100644 index 00000000..88744066 --- /dev/null +++ b/lib/shared/extensions/ad_platform_type_l10n.dart @@ -0,0 +1,17 @@ +import 'package:core/core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +/// Extension on [AdPlatformType] to provide localized string representations. +extension AdPlatformTypeL10n on AdPlatformType { + /// Returns a localized string for the [AdPlatformType]. + String l10n(BuildContext context) { + final l10n = context.l10n; + switch (this) { + case AdPlatformType.admob: + return l10n.adPlatformTypeAdmob; + case AdPlatformType.local: + return l10n.adPlatformTypeLocal; + } + } +} From 9f9be2d6c1dc41c76d328e2206ad7ea67fc23696 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 11:27:19 +0100 Subject: [PATCH 11/17] feat(app_configuration): localize ad platform type names - Import AdPlatformTypeL10n extension - Replace platform name with localized version in AdPlatformConfigForm --- lib/app_configuration/widgets/ad_platform_config_form.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/app_configuration/widgets/ad_platform_config_form.dart b/lib/app_configuration/widgets/ad_platform_config_form.dart index 0a553d25..04b9c1b2 100644 --- a/lib/app_configuration/widgets/ad_platform_config_form.dart +++ b/lib/app_configuration/widgets/ad_platform_config_form.dart @@ -4,6 +4,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuratio 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/router/routes.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/ad_platform_type_l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -251,7 +252,7 @@ class _AdPlatformConfigFormState extends State { .map( (platform) => ButtonSegment( value: platform, - label: Text(platform.name), + label: Text(platform.l10n(context)), ), ) .toList(), From a7278117c905bfdce15d3523d4e78d78e2b92b09 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 11:27:30 +0100 Subject: [PATCH 12/17] fix(app_configuration): disable ad platform tiles correctly when ads are off - Add 'enabled' property to ExpansionTile to completely disable tiles when ads are off - This change ensures that ad platform tiles are not just non-expandable, but also disabled - The fix is applied to all ad platform tiles: AdPlatformConfig, FeedAdSettings, ArticleAdSettings, and InterstitialAdSettings --- .../view/tabs/advertisements_configuration_tab.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/app_configuration/view/tabs/advertisements_configuration_tab.dart b/lib/app_configuration/view/tabs/advertisements_configuration_tab.dart index 0ab9b674..12d494a7 100644 --- a/lib/app_configuration/view/tabs/advertisements_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/advertisements_configuration_tab.dart @@ -77,8 +77,9 @@ class _AdvertisementsConfigurationTabState ? (isExpanded) { _expandedTileIndex.value = isExpanded ? tileIndex : null; } - : null, // Disable expansion if global ads are off + : null, initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled, + enabled: adConfig.enabled, // Disable the tile itself children: [ AdPlatformConfigForm( remoteConfig: widget.remoteConfig, @@ -107,8 +108,9 @@ class _AdvertisementsConfigurationTabState ? (isExpanded) { _expandedTileIndex.value = isExpanded ? tileIndex : null; } - : null, // Disable expansion if global ads are off + : null, initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled, + enabled: adConfig.enabled, // Disable the tile itself children: [ FeedAdSettingsForm( remoteConfig: widget.remoteConfig, @@ -137,8 +139,9 @@ class _AdvertisementsConfigurationTabState ? (isExpanded) { _expandedTileIndex.value = isExpanded ? tileIndex : null; } - : null, // Disable expansion if global ads are off + : null, initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled, + enabled: adConfig.enabled, // Disable the tile itself children: [ ArticleAdSettingsForm( remoteConfig: widget.remoteConfig, @@ -167,8 +170,9 @@ class _AdvertisementsConfigurationTabState ? (isExpanded) { _expandedTileIndex.value = isExpanded ? tileIndex : null; } - : null, // Disable expansion if global ads are off + : null, initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled, + enabled: adConfig.enabled, // Disable the tile itself children: [ InterstitialAdSettingsForm( remoteConfig: widget.remoteConfig, From ae3897a5a31a2ff9e22dc3965681120abca1ef20 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 11:34:18 +0100 Subject: [PATCH 13/17] chore: misc --- lib/router/router.dart | 4 ++-- lib/shared/extensions/extensions.dart | 2 ++ lib/shared/shared.dart | 1 + lib/shared/widgets/searchable_selection_input.dart | 4 ++-- .../selection_page/bloc/searchable_selection_bloc.dart | 2 +- .../selection_page/bloc/searchable_selection_event.dart | 0 .../selection_page/bloc/searchable_selection_state.dart | 0 .../selection_page/searchable_selection_page.dart | 4 ++-- .../selection_page/selection_page_arguments.dart | 2 +- lib/shared/widgets/widgets.dart | 1 + 10 files changed, 12 insertions(+), 8 deletions(-) rename lib/shared/{ => widgets}/selection_page/bloc/searchable_selection_bloc.dart (99%) rename lib/shared/{ => widgets}/selection_page/bloc/searchable_selection_event.dart (100%) rename lib/shared/{ => widgets}/selection_page/bloc/searchable_selection_state.dart (100%) rename lib/shared/{ => widgets}/selection_page/searchable_selection_page.dart (98%) rename lib/shared/{ => widgets}/selection_page/selection_page_arguments.dart (98%) create mode 100644 lib/shared/widgets/widgets.dart diff --git a/lib/router/router.dart b/lib/router/router.dart index 88b93ff7..c9079e31 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -33,8 +33,8 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_manage import 'package:flutter_news_app_web_dashboard_full_source_code/overview/view/overview_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/settings/view/settings_page.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/selection_page/searchable_selection_page.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/selection_page/selection_page_arguments.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/selection_page/searchable_selection_page.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/selection_page/selection_page_arguments.dart'; import 'package:go_router/go_router.dart'; /// Creates and configures the GoRouter instance for the application. diff --git a/lib/shared/extensions/extensions.dart b/lib/shared/extensions/extensions.dart index 651f7726..556f6dfc 100644 --- a/lib/shared/extensions/extensions.dart +++ b/lib/shared/extensions/extensions.dart @@ -1,6 +1,8 @@ +export 'ad_platform_type_l10n.dart'; export 'app_user_role_l10n.dart'; export 'content_status_l10n.dart'; export 'feed_decorator_type_l10n.dart'; export 'in_article_ad_slot_type_l10n.dart'; +export 'local_ad_to_ad_type.dart'; export 'source_type_l10n.dart'; export 'string_truncate.dart'; diff --git a/lib/shared/shared.dart b/lib/shared/shared.dart index 1ab43f92..204e1e23 100644 --- a/lib/shared/shared.dart +++ b/lib/shared/shared.dart @@ -1,3 +1,4 @@ export 'constants/constants.dart'; export 'extensions/extensions.dart'; export 'services/services.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/shared/widgets/searchable_selection_input.dart b/lib/shared/widgets/searchable_selection_input.dart index b5e262ee..92cbb35f 100644 --- a/lib/shared/widgets/searchable_selection_input.dart +++ b/lib/shared/widgets/searchable_selection_input.dart @@ -3,9 +3,9 @@ import 'package:data_repository/data_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/selection_page/searchable_selection_page.dart' +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/selection_page/searchable_selection_page.dart' show SearchableSelectionPage; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/selection_page/selection_page_arguments.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/selection_page/selection_page_arguments.dart'; import 'package:go_router/go_router.dart'; /// {@template searchable_selection_input} diff --git a/lib/shared/selection_page/bloc/searchable_selection_bloc.dart b/lib/shared/widgets/selection_page/bloc/searchable_selection_bloc.dart similarity index 99% rename from lib/shared/selection_page/bloc/searchable_selection_bloc.dart rename to lib/shared/widgets/selection_page/bloc/searchable_selection_bloc.dart index 6095625d..e90c9d75 100644 --- a/lib/shared/selection_page/bloc/searchable_selection_bloc.dart +++ b/lib/shared/widgets/selection_page/bloc/searchable_selection_bloc.dart @@ -6,7 +6,7 @@ import 'package:core/core.dart'; import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/selection_page/selection_page_arguments.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/selection_page/selection_page_arguments.dart'; import 'package:rxdart/rxdart.dart'; part 'searchable_selection_event.dart'; diff --git a/lib/shared/selection_page/bloc/searchable_selection_event.dart b/lib/shared/widgets/selection_page/bloc/searchable_selection_event.dart similarity index 100% rename from lib/shared/selection_page/bloc/searchable_selection_event.dart rename to lib/shared/widgets/selection_page/bloc/searchable_selection_event.dart diff --git a/lib/shared/selection_page/bloc/searchable_selection_state.dart b/lib/shared/widgets/selection_page/bloc/searchable_selection_state.dart similarity index 100% rename from lib/shared/selection_page/bloc/searchable_selection_state.dart rename to lib/shared/widgets/selection_page/bloc/searchable_selection_state.dart diff --git a/lib/shared/selection_page/searchable_selection_page.dart b/lib/shared/widgets/selection_page/searchable_selection_page.dart similarity index 98% rename from lib/shared/selection_page/searchable_selection_page.dart rename to lib/shared/widgets/selection_page/searchable_selection_page.dart index 4b326823..12007af6 100644 --- a/lib/shared/selection_page/searchable_selection_page.dart +++ b/lib/shared/widgets/selection_page/searchable_selection_page.dart @@ -2,8 +2,8 @@ import 'package:data_repository/data_repository.dart' show DataRepository; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/selection_page/bloc/searchable_selection_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/selection_page/selection_page_arguments.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/selection_page/bloc/searchable_selection_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/selection_page/selection_page_arguments.dart'; import 'package:ui_kit/ui_kit.dart'; /// {@template searchable_selection_page} diff --git a/lib/shared/selection_page/selection_page_arguments.dart b/lib/shared/widgets/selection_page/selection_page_arguments.dart similarity index 98% rename from lib/shared/selection_page/selection_page_arguments.dart rename to lib/shared/widgets/selection_page/selection_page_arguments.dart index f2e75d67..34813b39 100644 --- a/lib/shared/selection_page/selection_page_arguments.dart +++ b/lib/shared/widgets/selection_page/selection_page_arguments.dart @@ -2,7 +2,7 @@ import 'package:core/core.dart'; import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/selection_page/searchable_selection_page.dart' +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/selection_page/searchable_selection_page.dart' show SearchableSelectionPage; /// {@template selection_page_arguments} diff --git a/lib/shared/widgets/widgets.dart b/lib/shared/widgets/widgets.dart new file mode 100644 index 00000000..effa3f4f --- /dev/null +++ b/lib/shared/widgets/widgets.dart @@ -0,0 +1 @@ +export 'searchable_selection_input.dart'; From e5446ca29cdb882f7b437f0c555fb8649094a7a5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 11:42:49 +0100 Subject: [PATCH 14/17] refactor(app_configuration): remove AbsorbPointer and Opacity widgets - Remove AbsorbPointer and Opacity widgets to improve performance - Simplify the layout by using a single Column widget - Maintain the existing functionality and UI structure --- .../interstitial_ad_settings_form.dart | 132 +++++++++--------- 1 file changed, 63 insertions(+), 69 deletions(-) diff --git a/lib/app_configuration/widgets/interstitial_ad_settings_form.dart b/lib/app_configuration/widgets/interstitial_ad_settings_form.dart index a08c5cc0..401353d1 100644 --- a/lib/app_configuration/widgets/interstitial_ad_settings_form.dart +++ b/lib/app_configuration/widgets/interstitial_ad_settings_form.dart @@ -110,82 +110,76 @@ class _InterstitialAdSettingsFormState extends State final adConfig = widget.remoteConfig.adConfig; final interstitialAdConfig = adConfig.interstitialAdConfiguration; - return AbsorbPointer( - absorbing: !adConfig.enabled, - child: Opacity( - opacity: adConfig.enabled ? 1.0 : 0.5, - child: 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, + 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), + ), ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.userRoleInterstitialFrequencyDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( + ); + }, + ), + 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, // Fixed height for TabBarView within a ListView - child: TabBarView( - controller: _tabController, - children: AppUserRole.values - .map( - (role) => _buildInterstitialRoleSpecificFields( - context, - l10n, - role, - interstitialAdConfig, - ), - ) - .toList(), - ), + ), + 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, // Fixed height for TabBarView within a ListView + child: TabBarView( + controller: _tabController, + children: AppUserRole.values + .map( + (role) => _buildInterstitialRoleSpecificFields( + context, + l10n, + role, + interstitialAdConfig, + ), + ) + .toList(), + ), ), ], ), - ), + ], ); } From 5acba585e6de9cb64d936a8e6bb0746253fbe393 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 11:43:02 +0100 Subject: [PATCH 15/17] refactor(app_configuration): remove unnecessary AbsorbPointer and Opacity widgets - Removed AbsorbPointer and Opacity widgets that were used to disable the form - Simplified the layout by directly returning a Column widget - This change improves the code structure and may enhance performance slightly --- .../widgets/article_ad_settings_form.dart | 179 +++++++++--------- 1 file changed, 86 insertions(+), 93 deletions(-) diff --git a/lib/app_configuration/widgets/article_ad_settings_form.dart b/lib/app_configuration/widgets/article_ad_settings_form.dart index c9201002..5bc57587 100644 --- a/lib/app_configuration/widgets/article_ad_settings_form.dart +++ b/lib/app_configuration/widgets/article_ad_settings_form.dart @@ -43,107 +43,102 @@ class _ArticleAdSettingsFormState extends State final adConfig = widget.remoteConfig.adConfig; final articleAdConfig = adConfig.articleAdConfiguration; - return AbsorbPointer( - absorbing: !adConfig.enabled, - child: Opacity( - opacity: adConfig.enabled ? 1.0 : 0.5, - child: 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( + 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.defaultInArticleAdTypeSelectionTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, // Adjusted padding for hierarchy - top: AppSpacing.md, - bottom: AppSpacing.md, + ), ), - expandedCrossAxisAlignment: - CrossAxisAlignment.start, // Align content to start - children: [ - Text( - l10n.defaultInArticleAdTypeSelectionDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + ExpansionTile( + title: Text(l10n.defaultInArticleAdTypeSelectionTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, // Adjusted padding for hierarchy + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: + CrossAxisAlignment.start, // Align content to start + children: [ + Text( + l10n.defaultInArticleAdTypeSelectionDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( context, ).colorScheme.onSurface.withOpacity(0.7), + ), + textAlign: TextAlign.start, // Ensure text aligns to start + ), + const SizedBox(height: AppSpacing.lg), + Align( + alignment: AlignmentDirectional.centerStart, + child: SegmentedButton( + style: SegmentedButton.styleFrom( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.zero, ), - textAlign: TextAlign.start, // Ensure text aligns to start ), - const SizedBox(height: AppSpacing.lg), - Align( - alignment: AlignmentDirectional.centerStart, - child: SegmentedButton( - style: SegmentedButton.styleFrom( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.zero, + segments: AdType.values + .where( + (type) => type == AdType.native || type == AdType.banner, + ) + .map( + (type) => ButtonSegment( + value: type, + label: Text(type.name), ), - ), - segments: AdType.values - .where( - (type) => - type == AdType.native || type == AdType.banner, - ) - .map( - (type) => ButtonSegment( - value: type, - label: Text(type.name), - ), - ) - .toList(), - selected: {articleAdConfig.defaultInArticleAdType}, - onSelectionChanged: (newSelection) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - articleAdConfiguration: articleAdConfig.copyWith( + ) + .toList(), + selected: {articleAdConfig.defaultInArticleAdType}, + onSelectionChanged: (newSelection) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: adConfig.copyWith( + articleAdConfiguration: articleAdConfig.copyWith( defaultInArticleAdType: newSelection.first, ), - ), - ), - ); - }, - ), - ), - ], - ), - const SizedBox(height: AppSpacing.lg), - ExpansionTile( - title: Text(l10n.inArticleAdSlotPlacementsTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, // Adjusted padding for hierarchy - top: AppSpacing.md, - bottom: AppSpacing.md, + ), + ), + ); + }, ), - expandedCrossAxisAlignment: - CrossAxisAlignment.start, // Align content to start - children: [ - Text( - l10n.inArticleAdSlotPlacementsDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( + ), + ], + ), + const SizedBox(height: AppSpacing.lg), + ExpansionTile( + title: Text(l10n.inArticleAdSlotPlacementsTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, // Adjusted padding for hierarchy + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: + CrossAxisAlignment.start, // Align content to start + children: [ + Text( + l10n.inArticleAdSlotPlacementsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( context, ).colorScheme.onSurface.withOpacity(0.7), - ), - textAlign: TextAlign.start, // Ensure text aligns to start - ), - const SizedBox(height: AppSpacing.lg), - ...articleAdConfig.inArticleAdSlotConfigurations.map( + ), + textAlign: TextAlign.start, // Ensure text aligns to start + ), + const SizedBox(height: AppSpacing.lg), + ...articleAdConfig.inArticleAdSlotConfigurations.map( (slotConfig) => SwitchListTile( title: Text(slotConfig.slotType.l10n(context)), value: slotConfig.enabled, @@ -160,19 +155,17 @@ class _ArticleAdSettingsFormState extends State widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( articleAdConfiguration: articleAdConfig.copyWith( - inArticleAdSlotConfigurations: updatedSlots, - ), + inArticleAdSlotConfigurations: updatedSlots, + ), ), ), ); }, ), ), - ], - ), ], ), - ), + ], ); } } From dbb48cbfb4bf7f2eebdf83655380d48542e7516c Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 11:43:16 +0100 Subject: [PATCH 16/17] refactor(app_configuration): remove ad platform config form disablement - Remove AbsorbPointer and Opacity widgets that were used to disable the form - Keep the form structure and content intact - Update commit message to reflect the changes made to the code --- .../widgets/ad_platform_config_form.dart | 222 +++++++++--------- 1 file changed, 108 insertions(+), 114 deletions(-) diff --git a/lib/app_configuration/widgets/ad_platform_config_form.dart b/lib/app_configuration/widgets/ad_platform_config_form.dart index 04b9c1b2..2f60ff20 100644 --- a/lib/app_configuration/widgets/ad_platform_config_form.dart +++ b/lib/app_configuration/widgets/ad_platform_config_form.dart @@ -212,134 +212,128 @@ class _AdPlatformConfigFormState extends State { final l10n = AppLocalizationsX(context).l10n; final adConfig = widget.remoteConfig.adConfig; - return AbsorbPointer( - absorbing: !adConfig.enabled, - child: Opacity( - opacity: adConfig.enabled ? 1.0 : 0.5, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Primary Ad Platform Selection + ExpansionTile( + title: Text(l10n.primaryAdPlatformTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, // Adjusted padding for hierarchy + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: + CrossAxisAlignment.start, // Align content to start children: [ - // Primary Ad Platform Selection - ExpansionTile( - title: Text(l10n.primaryAdPlatformTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, // Adjusted padding for hierarchy - top: AppSpacing.md, - bottom: AppSpacing.md, + Text( + l10n.primaryAdPlatformDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), ), - expandedCrossAxisAlignment: - CrossAxisAlignment.start, // Align content to start - children: [ - Text( - l10n.primaryAdPlatformDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), + textAlign: TextAlign.start, // Ensure text aligns to start + ), + const SizedBox(height: AppSpacing.lg), + Align( + alignment: AlignmentDirectional.centerStart, + child: SegmentedButton( + style: SegmentedButton.styleFrom( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.zero, ), - textAlign: TextAlign.start, // Ensure text aligns to start ), - const SizedBox(height: AppSpacing.lg), - Align( - alignment: AlignmentDirectional.centerStart, - child: SegmentedButton( - style: SegmentedButton.styleFrom( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.zero, + segments: AdPlatformType.values + .map( + (platform) => ButtonSegment( + value: platform, + label: Text(platform.l10n(context)), + ), + ) + .toList(), + selected: {_selectedPlatform}, + onSelectionChanged: (newSelection) { + setState(() { + _selectedPlatform = newSelection.first; + }); + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: adConfig.copyWith( + primaryAdPlatform: newSelection.first, ), ), - segments: AdPlatformType.values - .map( - (platform) => ButtonSegment( - value: platform, - label: Text(platform.l10n(context)), - ), - ) - .toList(), - selected: {_selectedPlatform}, - onSelectionChanged: (newSelection) { - setState(() { - _selectedPlatform = newSelection.first; - }); - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - primaryAdPlatform: newSelection.first, - ), - ), - ); - }, - ), - ), - ], + ); + }, + ), ), - const SizedBox(height: AppSpacing.lg), + ], + ), + const SizedBox(height: AppSpacing.lg), - // Ad Unit Identifiers - ExpansionTile( - title: Text(l10n.adUnitIdentifiersTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, // Adjusted padding for hierarchy - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: - CrossAxisAlignment.start, // Align content to start - children: [ - Text( - l10n.adUnitIdentifiersDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - textAlign: TextAlign.start, // Ensure text aligns to start - ), - const SizedBox(height: AppSpacing.lg), - _buildAdUnitIdentifierFields( + // Ad Unit Identifiers + ExpansionTile( + title: Text(l10n.adUnitIdentifiersTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, // Adjusted padding for hierarchy + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: + CrossAxisAlignment.start, // Align content to start + children: [ + Text( + l10n.adUnitIdentifiersDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( context, - l10n, - _selectedPlatform, - adConfig, - ), - ], + ).colorScheme.onSurface.withOpacity(0.7), + ), + textAlign: TextAlign.start, // Ensure text aligns to start ), const SizedBox(height: AppSpacing.lg), + _buildAdUnitIdentifierFields( + context, + l10n, + _selectedPlatform, + adConfig, + ), + ], + ), + const SizedBox(height: AppSpacing.lg), - // Local Ad Management - if (_selectedPlatform == AdPlatformType.local) - ExpansionTile( - title: Text(l10n.localAdManagementTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, // Adjusted padding for hierarchy - top: AppSpacing.md, - bottom: AppSpacing.md, + // Local Ad Management + if (_selectedPlatform == AdPlatformType.local) + ExpansionTile( + title: Text(l10n.localAdManagementTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, // Adjusted padding for hierarchy + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: + CrossAxisAlignment.start, // Align content to start + children: [ + Text( + l10n.localAdManagementDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), ), - expandedCrossAxisAlignment: - CrossAxisAlignment.start, // Align content to start - children: [ - Text( - l10n.localAdManagementDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - textAlign: TextAlign.start, // Ensure text aligns to start - ), - const SizedBox(height: AppSpacing.lg), - Center( - child: ElevatedButton( - onPressed: () => - context.goNamed(Routes.localAdsManagementName), - child: Text(l10n.manageLocalAdsButton), - ), - ), - ], + textAlign: TextAlign.start, // Ensure text aligns to start ), - ], - ), - ), + const SizedBox(height: AppSpacing.lg), + Center( + child: ElevatedButton( + onPressed: () => + context.goNamed(Routes.localAdsManagementName), + child: Text(l10n.manageLocalAdsButton), + ), + ), + ], + ), + ], ); } From 56296bdec44154cb4c69879f98110d84ce5fe35f Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 4 Sep 2025 11:44:09 +0100 Subject: [PATCH 17/17] lint: misc --- .../widgets/article_ad_settings_form.dart | 62 +++++++++---------- .../interstitial_ad_settings_form.dart | 9 +-- .../view/create_headline_page.dart | 1 - .../view/create_source_page.dart | 1 - .../view/create_topic_page.dart | 1 - .../view/edit_headline_page.dart | 1 - .../view/edit_source_page.dart | 1 - .../view/edit_topic_page.dart | 1 - .../view/archived_local_ads_page.dart | 1 - 9 files changed, 36 insertions(+), 42 deletions(-) diff --git a/lib/app_configuration/widgets/article_ad_settings_form.dart b/lib/app_configuration/widgets/article_ad_settings_form.dart index 5bc57587..78b51521 100644 --- a/lib/app_configuration/widgets/article_ad_settings_form.dart +++ b/lib/app_configuration/widgets/article_ad_settings_form.dart @@ -54,8 +54,8 @@ class _ArticleAdSettingsFormState extends State widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( articleAdConfiguration: articleAdConfig.copyWith( - enabled: value, - ), + enabled: value, + ), ), ), ); @@ -76,8 +76,8 @@ class _ArticleAdSettingsFormState extends State l10n.defaultInArticleAdTypeSelectionDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), + context, + ).colorScheme.onSurface.withOpacity(0.7), ), textAlign: TextAlign.start, // Ensure text aligns to start ), @@ -107,8 +107,8 @@ class _ArticleAdSettingsFormState extends State widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( articleAdConfiguration: articleAdConfig.copyWith( - defaultInArticleAdType: newSelection.first, - ), + defaultInArticleAdType: newSelection.first, + ), ), ), ); @@ -132,37 +132,37 @@ class _ArticleAdSettingsFormState extends State l10n.inArticleAdSlotPlacementsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), + context, + ).colorScheme.onSurface.withOpacity(0.7), ), textAlign: TextAlign.start, // Ensure text aligns to start ), const SizedBox(height: AppSpacing.lg), ...articleAdConfig.inArticleAdSlotConfigurations.map( - (slotConfig) => SwitchListTile( - title: Text(slotConfig.slotType.l10n(context)), - value: slotConfig.enabled, - onChanged: (value) { - final updatedSlots = articleAdConfig - .inArticleAdSlotConfigurations - .map( - (e) => e.slotType == slotConfig.slotType - ? e.copyWith(enabled: value) - : e, - ) - .toList(); - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - articleAdConfiguration: articleAdConfig.copyWith( - inArticleAdSlotConfigurations: updatedSlots, - ), - ), + (slotConfig) => SwitchListTile( + title: Text(slotConfig.slotType.l10n(context)), + value: slotConfig.enabled, + onChanged: (value) { + final updatedSlots = articleAdConfig + .inArticleAdSlotConfigurations + .map( + (e) => e.slotType == slotConfig.slotType + ? e.copyWith(enabled: value) + : e, + ) + .toList(); + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: adConfig.copyWith( + articleAdConfiguration: articleAdConfig.copyWith( + inArticleAdSlotConfigurations: updatedSlots, ), - ); - }, - ), - ), + ), + ), + ); + }, + ), + ), ], ), ], diff --git a/lib/app_configuration/widgets/interstitial_ad_settings_form.dart b/lib/app_configuration/widgets/interstitial_ad_settings_form.dart index 401353d1..82ba5764 100644 --- a/lib/app_configuration/widgets/interstitial_ad_settings_form.dart +++ b/lib/app_configuration/widgets/interstitial_ad_settings_form.dart @@ -120,8 +120,9 @@ class _InterstitialAdSettingsFormState extends State widget.onConfigChanged( widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( - interstitialAdConfiguration: interstitialAdConfig - .copyWith(enabled: value), + interstitialAdConfiguration: interstitialAdConfig.copyWith( + enabled: value, + ), ), ), ); @@ -140,8 +141,8 @@ class _InterstitialAdSettingsFormState extends State l10n.userRoleInterstitialFrequencyDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), + context, + ).colorScheme.onSurface.withOpacity(0.7), ), textAlign: TextAlign.start, ), diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index 3a907d03..31fbe9ef 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -5,7 +5,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/create_headline/create_headline_bloc.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/shared.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/searchable_selection_input.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_kit/ui_kit.dart'; diff --git a/lib/content_management/view/create_source_page.dart b/lib/content_management/view/create_source_page.dart index 436cc53d..e885511d 100644 --- a/lib/content_management/view/create_source_page.dart +++ b/lib/content_management/view/create_source_page.dart @@ -5,7 +5,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/create_source/create_source_bloc.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/shared.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/searchable_selection_input.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_kit/ui_kit.dart'; diff --git a/lib/content_management/view/create_topic_page.dart b/lib/content_management/view/create_topic_page.dart index a27cddcc..b83a51bc 100644 --- a/lib/content_management/view/create_topic_page.dart +++ b/lib/content_management/view/create_topic_page.dart @@ -6,7 +6,6 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/content_manageme import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/create_topic/create_topic_bloc.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/shared.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/searchable_selection_input.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_kit/ui_kit.dart'; diff --git a/lib/content_management/view/edit_headline_page.dart b/lib/content_management/view/edit_headline_page.dart index 2bbfae5a..fd0a3904 100644 --- a/lib/content_management/view/edit_headline_page.dart +++ b/lib/content_management/view/edit_headline_page.dart @@ -5,7 +5,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/edit_headline/edit_headline_bloc.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/shared.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/searchable_selection_input.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_kit/ui_kit.dart'; diff --git a/lib/content_management/view/edit_source_page.dart b/lib/content_management/view/edit_source_page.dart index bef572cf..425e7fda 100644 --- a/lib/content_management/view/edit_source_page.dart +++ b/lib/content_management/view/edit_source_page.dart @@ -5,7 +5,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/edit_source/edit_source_bloc.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/shared.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/searchable_selection_input.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_kit/ui_kit.dart'; diff --git a/lib/content_management/view/edit_topic_page.dart b/lib/content_management/view/edit_topic_page.dart index bd290395..2087d555 100644 --- a/lib/content_management/view/edit_topic_page.dart +++ b/lib/content_management/view/edit_topic_page.dart @@ -6,7 +6,6 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/content_manageme import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/edit_topic/edit_topic_bloc.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/shared.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/searchable_selection_input.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_kit/ui_kit.dart'; diff --git a/lib/local_ads_management/view/archived_local_ads_page.dart b/lib/local_ads_management/view/archived_local_ads_page.dart index 3c196cfa..533c9be1 100644 --- a/lib/local_ads_management/view/archived_local_ads_page.dart +++ b/lib/local_ads_management/view/archived_local_ads_page.dart @@ -12,7 +12,6 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_manage RestoreLocalAdRequested, UndoDeleteLocalAdRequested; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/local_ad_to_ad_type.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart';