From 3fdbbd2cf5a1648330399f1ceb33946639fc4002 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 09:44:41 +0100 Subject: [PATCH 01/24] build(dependencies): update core dependency to latest commit - Update core dependency from v1.4.0 to latest commit (a8a1714) - This ensures the project uses the most recent version of the core package --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 1d16e69c..c32fe080 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,8 +89,8 @@ packages: dependency: "direct main" description: path: "." - ref: "v1.4.0" - resolved-ref: d26d4bfecf4c93cdcdda91dbc06c0a71f272b3f0 + ref: a8a1714fe24ad0944fde8e38e6ed3af831568e75 + resolved-ref: a8a1714fe24ad0944fde8e38e6ed3af831568e75 url: "https://github.com/flutter-news-app-full-source-code/core.git" source: git version: "1.4.0" diff --git a/pubspec.yaml b/pubspec.yaml index 590d74af..4567ee0e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,7 +94,7 @@ dependency_overrides: core: git: url: https://github.com/flutter-news-app-full-source-code/core.git - ref: v1.4.0 + ref: a8a1714fe24ad0944fde8e38e6ed3af831568e75 http_client: git: url: https://github.com/flutter-news-app-full-source-code/http-client.git From 36818ca3a0b91e4944e0f244ce59e945cbd25348 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 10:06:17 +0100 Subject: [PATCH 02/24] feat(l10n): add analytics related strings for English and Arabic - Add new strings for analytics settings in both English and Arabic - Include translations for analytics tab, provider, events, and sampling rates - Cover descriptions for system status, event configuration, and provider selection - Introduce labels for Firebase and Mixpanel analytics providers --- lib/l10n/arb/app_ar.arb | 49 +++++++++++++++++++++++++++++++++++++++++ lib/l10n/arb/app_en.arb | 49 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 6844f8ee..2fe63ab8 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2991,5 +2991,54 @@ "followUpActionsDescription": "تكوين ما يحدث بعد استجابة المستخدم لموجه الاستمتاع، مثل طلب مراجعة المتجر.", "@followUpActionsDescription": { "description": "وصف لإعدادات إجراءات المتابعة." + }, + "analyticsTab": "التحليلات", + "@analyticsTab": { + "description": "عنوان تبويب إعدادات التحليلات" + }, + "analyticsDescription": "تكوين مزود التحليلات، وتسجيل الأحداث، ومعدلات أخذ العينات.", + "@analyticsDescription": { + "description": "وصف لقسم التحليلات القابل للتوسيع" + }, + "analyticsSystemStatusTitle": "تفعيل نظام التحليلات", + "@analyticsSystemStatusTitle": { + "description": "عنوان قسم حالة نظام التحليلات" + }, + "analyticsSystemStatusDescription": "مفتاح رئيسي لتفعيل أو تعطيل جميع عمليات تتبع التحليلات.", + "@analyticsSystemStatusDescription": { + "description": "وصف قسم حالة نظام التحليلات" + }, + "analyticsProviderTitle": "المزود النشط", + "@analyticsProviderTitle": { + "description": "عنوان قسم مزود التحليلات" + }, + "analyticsProviderDescription": "اختر مزود خدمة التحليلات الأساسي.", + "@analyticsProviderDescription": { + "description": "وصف قسم مزود التحليلات" + }, + "analyticsEventsTitle": "تكوين الأحداث", + "@analyticsEventsTitle": { + "description": "عنوان قسم تكوين الأحداث" + }, + "analyticsEventsDescription": "ضبط دقيق لتسجيل أحداث محددة. قم بتعطيل الأحداث المزعجة أو تعديل معدلات أخذ العينات.", + "@analyticsEventsDescription": { + "description": "وصف قسم تكوين الأحداث" + }, + "samplingRateLabel": "معدل أخذ العينات: {rate}%", + "@samplingRateLabel": { + "description": "تسمية لشريط تمرير معدل أخذ العينات", + "placeholders": { + "rate": { + "type": "int" + } + } + }, + "analyticsProviderFirebase": "Firebase", + "@analyticsProviderFirebase": { + "description": "تسمية مزود التحليلات Firebase" + }, + "analyticsProviderMixpanel": "Mixpanel", + "@analyticsProviderMixpanel": { + "description": "تسمية مزود التحليلات Mixpanel" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 97d630c6..f63e47da 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2987,5 +2987,54 @@ "followUpActionsDescription": "Configure what happens after a user responds to the enjoyment prompt, such as requesting a store review.", "@followUpActionsDescription": { "description": "Description for the follow-up actions settings." + }, + "analyticsTab": "Analytics", + "@analyticsTab": { + "description": "Tab title for Analytics settings" + }, + "analyticsDescription": "Configure analytics provider, event logging, and sampling rates.", + "@analyticsDescription": { + "description": "Description for the Analytics expansion tile" + }, + "analyticsSystemStatusTitle": "Enable Analytics System", + "@analyticsSystemStatusTitle": { + "description": "Title for the Analytics System status section" + }, + "analyticsSystemStatusDescription": "Master switch to enable or disable all analytics tracking.", + "@analyticsSystemStatusDescription": { + "description": "Description for the Analytics System status section" + }, + "analyticsProviderTitle": "Active Provider", + "@analyticsProviderTitle": { + "description": "Title for the Analytics Provider section" + }, + "analyticsProviderDescription": "Select the primary analytics service provider.", + "@analyticsProviderDescription": { + "description": "Description for the Analytics Provider section" + }, + "analyticsEventsTitle": "Event Configuration", + "@analyticsEventsTitle": { + "description": "Title for the Event Configuration section" + }, + "analyticsEventsDescription": "Fine-tune logging for specific events. Disable noisy events or adjust sampling rates.", + "@analyticsEventsDescription": { + "description": "Description for the Event Configuration section" + }, + "samplingRateLabel": "Sampling Rate: {rate}%", + "@samplingRateLabel": { + "description": "Label for the sampling rate slider", + "placeholders": { + "rate": { + "type": "int" + } + } + }, + "analyticsProviderFirebase": "Firebase", + "@analyticsProviderFirebase": { + "description": "Label for Firebase analytics provider" + }, + "analyticsProviderMixpanel": "Mixpanel", + "@analyticsProviderMixpanel": { + "description": "Label for Mixpanel analytics provider" } } \ No newline at end of file From 2bb4dcd4fbdeb7ed4c8c49a03a0c339c853ba460 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 10:06:31 +0100 Subject: [PATCH 03/24] build(l10n): sync --- lib/l10n/app_localizations.dart | 66 ++++++++++++++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 39 ++++++++++++++++++ lib/l10n/app_localizations_en.dart | 39 ++++++++++++++++++ 3 files changed, 144 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index dd4abc09..f605650d 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4327,6 +4327,72 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Configure what happens after a user responds to the enjoyment prompt, such as requesting a store review.'** String get followUpActionsDescription; + + /// Tab title for Analytics settings + /// + /// In en, this message translates to: + /// **'Analytics'** + String get analyticsTab; + + /// Description for the Analytics expansion tile + /// + /// In en, this message translates to: + /// **'Configure analytics provider, event logging, and sampling rates.'** + String get analyticsDescription; + + /// Title for the Analytics System status section + /// + /// In en, this message translates to: + /// **'Enable Analytics System'** + String get analyticsSystemStatusTitle; + + /// Description for the Analytics System status section + /// + /// In en, this message translates to: + /// **'Master switch to enable or disable all analytics tracking.'** + String get analyticsSystemStatusDescription; + + /// Title for the Analytics Provider section + /// + /// In en, this message translates to: + /// **'Active Provider'** + String get analyticsProviderTitle; + + /// Description for the Analytics Provider section + /// + /// In en, this message translates to: + /// **'Select the primary analytics service provider.'** + String get analyticsProviderDescription; + + /// Title for the Event Configuration section + /// + /// In en, this message translates to: + /// **'Event Configuration'** + String get analyticsEventsTitle; + + /// Description for the Event Configuration section + /// + /// In en, this message translates to: + /// **'Fine-tune logging for specific events. Disable noisy events or adjust sampling rates.'** + String get analyticsEventsDescription; + + /// Label for the sampling rate slider + /// + /// In en, this message translates to: + /// **'Sampling Rate: {rate}%'** + String samplingRateLabel(int rate); + + /// Label for Firebase analytics provider + /// + /// In en, this message translates to: + /// **'Firebase'** + String get analyticsProviderFirebase; + + /// Label for Mixpanel analytics provider + /// + /// In en, this message translates to: + /// **'Mixpanel'** + String get analyticsProviderMixpanel; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index cab4677f..88cd0adf 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -2361,4 +2361,43 @@ class AppLocalizationsAr extends AppLocalizations { @override String get followUpActionsDescription => 'تكوين ما يحدث بعد استجابة المستخدم لموجه الاستمتاع، مثل طلب مراجعة المتجر.'; + + @override + String get analyticsTab => 'التحليلات'; + + @override + String get analyticsDescription => + 'تكوين مزود التحليلات، وتسجيل الأحداث، ومعدلات أخذ العينات.'; + + @override + String get analyticsSystemStatusTitle => 'تفعيل نظام التحليلات'; + + @override + String get analyticsSystemStatusDescription => + 'مفتاح رئيسي لتفعيل أو تعطيل جميع عمليات تتبع التحليلات.'; + + @override + String get analyticsProviderTitle => 'المزود النشط'; + + @override + String get analyticsProviderDescription => + 'اختر مزود خدمة التحليلات الأساسي.'; + + @override + String get analyticsEventsTitle => 'تكوين الأحداث'; + + @override + String get analyticsEventsDescription => + 'ضبط دقيق لتسجيل أحداث محددة. قم بتعطيل الأحداث المزعجة أو تعديل معدلات أخذ العينات.'; + + @override + String samplingRateLabel(int rate) { + return 'معدل أخذ العينات: $rate%'; + } + + @override + String get analyticsProviderFirebase => 'Firebase'; + + @override + String get analyticsProviderMixpanel => 'Mixpanel'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index f7234eca..39801969 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2367,4 +2367,43 @@ class AppLocalizationsEn extends AppLocalizations { @override String get followUpActionsDescription => 'Configure what happens after a user responds to the enjoyment prompt, such as requesting a store review.'; + + @override + String get analyticsTab => 'Analytics'; + + @override + String get analyticsDescription => + 'Configure analytics provider, event logging, and sampling rates.'; + + @override + String get analyticsSystemStatusTitle => 'Enable Analytics System'; + + @override + String get analyticsSystemStatusDescription => + 'Master switch to enable or disable all analytics tracking.'; + + @override + String get analyticsProviderTitle => 'Active Provider'; + + @override + String get analyticsProviderDescription => + 'Select the primary analytics service provider.'; + + @override + String get analyticsEventsTitle => 'Event Configuration'; + + @override + String get analyticsEventsDescription => + 'Fine-tune logging for specific events. Disable noisy events or adjust sampling rates.'; + + @override + String samplingRateLabel(int rate) { + return 'Sampling Rate: $rate%'; + } + + @override + String get analyticsProviderFirebase => 'Firebase'; + + @override + String get analyticsProviderMixpanel => 'Mixpanel'; } From e121d04adcf41f1a445b9d33005bec14d267719b Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 10:07:18 +0100 Subject: [PATCH 04/24] feat(app_configuration): add analytics config form widget - Implement AnalyticsConfigForm widget for configuring analytics settings - Add provider selection using SegmentedButton - Implement events configuration with CheckboxListTile and sampling rate slider - Localize all strings using AppLocalizations - Ensure proper theming and spacing using ui_kit and core packages --- .../widgets/analytics_config_form.dart | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 lib/app_configuration/widgets/analytics_config_form.dart diff --git a/lib/app_configuration/widgets/analytics_config_form.dart b/lib/app_configuration/widgets/analytics_config_form.dart new file mode 100644 index 00000000..e82ae1d9 --- /dev/null +++ b/lib/app_configuration/widgets/analytics_config_form.dart @@ -0,0 +1,226 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template analytics_config_form} +/// A form widget for configuring analytics settings. +/// {@endtemplate} +class AnalyticsConfigForm extends StatelessWidget { + /// {@macro analytics_config_form} + const AnalyticsConfigForm({ + required this.remoteConfig, + required this.onConfigChanged, + super.key, + }); + + /// The current [RemoteConfig] object. + final RemoteConfig remoteConfig; + + /// Callback to notify parent of changes to the [RemoteConfig]. + final ValueChanged onConfigChanged; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + final features = remoteConfig.features; + final analyticsConfig = features.analytics; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SwitchListTile( + title: Text(l10n.analyticsSystemStatusTitle), + subtitle: Text(l10n.analyticsSystemStatusDescription), + value: analyticsConfig.enabled, + onChanged: (value) { + onConfigChanged( + remoteConfig.copyWith( + features: features.copyWith( + analytics: analyticsConfig.copyWith(enabled: value), + ), + ), + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + _buildProviderSection(context, l10n, analyticsConfig), + const SizedBox(height: AppSpacing.lg), + _buildEventsSection(context, l10n, analyticsConfig), + ], + ); + } + + Widget _buildProviderSection( + BuildContext context, + AppLocalizations l10n, + AnalyticsConfig config, + ) { + return ExpansionTile( + title: Text(l10n.analyticsProviderTitle), + subtitle: Text( + l10n.analyticsProviderDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: SegmentedButton( + segments: + AnalyticsProvider.values + .where((provider) => provider != AnalyticsProvider.demo) + .map((provider) { + return ButtonSegment( + value: provider, + label: Text(_getProviderLabel(l10n, provider)), + ); + }) + .toList(), + selected: {config.activeProvider}, + onSelectionChanged: (newSelection) { + onConfigChanged( + remoteConfig.copyWith( + features: remoteConfig.features.copyWith( + analytics: config.copyWith( + activeProvider: newSelection.first, + ), + ), + ), + ); + }, + ), + ), + ], + ); + } + + Widget _buildEventsSection( + BuildContext context, + AppLocalizations l10n, + AnalyticsConfig config, + ) { + return ExpansionTile( + title: Text(l10n.analyticsEventsTitle), + subtitle: Text( + l10n.analyticsEventsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + // We use a Column here instead of ListView because it's inside a scrollable parent + Column( + children: + AnalyticsEvent.values.map((event) { + final isEnabled = !config.disabledEvents.contains(event); + final samplingRate = config.eventSamplingRates[event] ?? 1.0; + + return Column( + children: [ + CheckboxListTile( + title: Text(event.name), + value: isEnabled, + onChanged: (value) { + final newDisabledEvents = Set.from( + config.disabledEvents, + ); + if (value == true) { + newDisabledEvents.remove(event); + } else { + newDisabledEvents.add(event); + } + onConfigChanged( + remoteConfig.copyWith( + features: remoteConfig.features.copyWith( + analytics: config.copyWith( + disabledEvents: newDisabledEvents, + ), + ), + ), + ); + }, + ), + if (isEnabled) + Padding( + padding: const EdgeInsetsDirectional.only( + start: AppSpacing.xxl, + end: AppSpacing.md, + bottom: AppSpacing.sm, + ), + child: Row( + children: [ + Text( + l10n.samplingRateLabel( + (samplingRate * 100).toInt(), + ), + style: Theme.of(context).textTheme.bodySmall, + ), + Expanded( + child: Slider( + value: samplingRate, + min: 0.0, + max: 1.0, + divisions: 20, + label: '${(samplingRate * 100).toInt()}%', + onChanged: (value) { + final newSamplingRates = + Map.from( + config.eventSamplingRates, + ); + // If value is 1.0, we can remove it from the map to save space/default + if (value == 1.0) { + newSamplingRates.remove(event); + } else { + newSamplingRates[event] = value; + } + onConfigChanged( + remoteConfig.copyWith( + features: remoteConfig.features.copyWith( + analytics: config.copyWith( + eventSamplingRates: newSamplingRates, + ), + ), + ), + ); + }, + ), + ), + ], + ), + ), + const Divider(height: 1), + ], + ); + }).toList(), + ), + ], + ); + } + + String _getProviderLabel(AppLocalizations l10n, AnalyticsProvider provider) { + switch (provider) { + case AnalyticsProvider.firebase: + return l10n.analyticsProviderFirebase; + case AnalyticsProvider.mixpanel: + return l10n.analyticsProviderMixpanel; + case AnalyticsProvider.demo: + // Fallback, though filtered out in UI + return 'Demo'; + } + } +} From 9374afd99bbedfb126eab4657c070ed083e001a3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 10:07:29 +0100 Subject: [PATCH 05/24] feat(app_configuration): add analytics configuration tab - Insert AnalyticsConfigForm and its corresponding ExpansionTile - Update existing ExpansionTile indices to maintain correct order - Add analytics-related strings for localization --- .../view/tabs/features_configuration_tab.dart | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/lib/app_configuration/view/tabs/features_configuration_tab.dart b/lib/app_configuration/view/tabs/features_configuration_tab.dart index f6f68a6d..84a1592e 100644 --- a/lib/app_configuration/view/tabs/features_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -2,6 +2,7 @@ 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/analytics_config_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/community_config_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_ad_settings_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_decorator_form.dart'; @@ -178,11 +179,54 @@ class _FeaturesConfigurationTabState extends State { ), const SizedBox(height: AppSpacing.lg), - // Feed + // Analytics ValueListenableBuilder( valueListenable: _expandedTileIndex, builder: (context, expandedIndex, child) { const tileIndex = 2; + return ExpansionTile( + leading: Icon( + Icons.analytics_outlined, + color: Theme.of(context).colorScheme.onSurface.withOpacity( + 0.7, + ), + ), + key: ValueKey('analyticsTile_$expandedIndex'), + title: Text(l10n.analyticsTab), + subtitle: Text( + l10n.analyticsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), + ), + onExpansionChanged: (bool isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.xxl, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnalyticsConfigForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + + // Feed + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 3; return ExpansionTile( leading: Icon( Icons.dynamic_feed_outlined, @@ -321,7 +365,7 @@ class _FeaturesConfigurationTabState extends State { ValueListenableBuilder( valueListenable: _expandedTileIndex, builder: (context, expandedIndex, child) { - const tileIndex = 3; + const tileIndex = 4; return ExpansionTile( leading: Icon( Icons.groups_outlined, From 7c655a7ec47f4de280ce1d9af49b581f14283218 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 10:16:17 +0100 Subject: [PATCH 06/24] chore: feature overhaul --- lib/app/view/app.dart | 6 - .../widgets/analytics_config_form.dart | 6 +- lib/overview/bloc/overview_bloc.dart | 86 ------ lib/overview/bloc/overview_event.dart | 17 -- lib/overview/bloc/overview_state.dart | 48 --- lib/overview/view/overview_page.dart | 274 +----------------- 6 files changed, 6 insertions(+), 431 deletions(-) delete mode 100644 lib/overview/bloc/overview_bloc.dart delete mode 100644 lib/overview/bloc/overview_event.dart delete mode 100644 lib/overview/bloc/overview_state.dart diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index a724e64b..476c718b 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -40,7 +40,6 @@ class App extends StatelessWidget { required DataRepository userContentPreferencesRepository, required DataRepository remoteConfigRepository, - required DataRepository dashboardSummaryRepository, required DataRepository countriesRepository, required DataRepository languagesRepository, required DataRepository usersRepository, @@ -59,7 +58,6 @@ class App extends StatelessWidget { _userContentPreferencesRepository = userContentPreferencesRepository, _remoteConfigRepository = remoteConfigRepository, _kvStorageService = storageService, - _dashboardSummaryRepository = dashboardSummaryRepository, _countriesRepository = countriesRepository, _languagesRepository = languagesRepository, _usersRepository = usersRepository, @@ -77,7 +75,6 @@ class App extends StatelessWidget { final DataRepository _userContentPreferencesRepository; final DataRepository _remoteConfigRepository; - final DataRepository _dashboardSummaryRepository; final DataRepository _countriesRepository; final DataRepository _languagesRepository; final DataRepository _usersRepository; @@ -101,7 +98,6 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _appSettingsRepository), RepositoryProvider.value(value: _userContentPreferencesRepository), RepositoryProvider.value(value: _remoteConfigRepository), - RepositoryProvider.value(value: _dashboardSummaryRepository), RepositoryProvider.value(value: _countriesRepository), RepositoryProvider.value(value: _languagesRepository), RepositoryProvider.value(value: _usersRepository), @@ -165,8 +161,6 @@ class App extends StatelessWidget { ), BlocProvider( create: (context) => OverviewBloc( - dashboardSummaryRepository: context - .read>(), headlinesRepository: context.read>(), topicsRepository: context.read>(), sourcesRepository: context.read>(), diff --git a/lib/app_configuration/widgets/analytics_config_form.dart b/lib/app_configuration/widgets/analytics_config_form.dart index e82ae1d9..df4ecad5 100644 --- a/lib/app_configuration/widgets/analytics_config_form.dart +++ b/lib/app_configuration/widgets/analytics_config_form.dart @@ -139,7 +139,7 @@ class AnalyticsConfigForm extends StatelessWidget { final newDisabledEvents = Set.from( config.disabledEvents, ); - if (value == true) { + if (value ?? false) { newDisabledEvents.remove(event); } else { newDisabledEvents.add(event); @@ -173,8 +173,8 @@ class AnalyticsConfigForm extends StatelessWidget { Expanded( child: Slider( value: samplingRate, - min: 0.0, - max: 1.0, + min: 0, + max: 1, divisions: 20, label: '${(samplingRate * 100).toInt()}%', onChanged: (value) { diff --git a/lib/overview/bloc/overview_bloc.dart b/lib/overview/bloc/overview_bloc.dart deleted file mode 100644 index f48ff8c2..00000000 --- a/lib/overview/bloc/overview_bloc.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:core/core.dart'; -import 'package:data_repository/data_repository.dart'; -import 'package:equatable/equatable.dart'; -import 'package:stream_transform/stream_transform.dart'; - -part 'overview_event.dart'; -part 'overview_state.dart'; - -/// A BLoC to manage the state of the dashboard overview. -class OverviewBloc extends Bloc { - /// {@macro overview_bloc} - OverviewBloc({ - required DataRepository dashboardSummaryRepository, - required DataRepository headlinesRepository, - required DataRepository topicsRepository, - required DataRepository sourcesRepository, - }) : _dashboardSummaryRepository = dashboardSummaryRepository, - _headlinesRepository = headlinesRepository, - super(const OverviewState()) { - on(_onOverviewSummaryRequested); - on<_OverviewEntityUpdated>(_onOverviewEntityUpdated); - - _entityUpdatedSubscription = headlinesRepository.entityUpdated - .merge(topicsRepository.entityUpdated) - .merge(sourcesRepository.entityUpdated) - .listen((_) => add(const _OverviewEntityUpdated())); - } - - final DataRepository _dashboardSummaryRepository; - final DataRepository _headlinesRepository; - late final StreamSubscription _entityUpdatedSubscription; - - @override - Future close() { - _entityUpdatedSubscription.cancel(); - return super.close(); - } - - void _onOverviewEntityUpdated( - _OverviewEntityUpdated event, - Emitter emit, - ) { - add(OverviewSummaryRequested()); - } - - Future _onOverviewSummaryRequested( - OverviewSummaryRequested event, - Emitter emit, - ) async { - emit(state.copyWith(status: OverviewStatus.loading)); - try { - // Fetch summary and recent headlines concurrently - final [summaryResponse, recentHeadlinesResponse] = await Future.wait([ - _dashboardSummaryRepository.read(id: kDashboardSummaryId), - _headlinesRepository.readAll( - pagination: const PaginationOptions(limit: 5), - sort: const [SortOption('updatedAt', SortOrder.desc)], - filter: {'status': ContentStatus.active.name}, - ), - ]); - - final summary = summaryResponse as DashboardSummary; - final recentHeadlines = - (recentHeadlinesResponse as PaginatedResponse).items; - emit( - state.copyWith( - status: OverviewStatus.success, - summary: summary, - recentHeadlines: recentHeadlines, - ), - ); - } on HttpException catch (e) { - emit(state.copyWith(status: OverviewStatus.failure, exception: e)); - } catch (e) { - emit( - state.copyWith( - status: OverviewStatus.failure, - exception: UnknownException('An unknown error occurred: $e'), - ), - ); - } - } -} diff --git a/lib/overview/bloc/overview_event.dart b/lib/overview/bloc/overview_event.dart deleted file mode 100644 index a81664f8..00000000 --- a/lib/overview/bloc/overview_event.dart +++ /dev/null @@ -1,17 +0,0 @@ -part of 'overview_bloc.dart'; - -/// Base class for dashboard overview events. -sealed class OverviewEvent extends Equatable { - const OverviewEvent(); - - @override - List get props => []; -} - -/// Event to load the dashboard overview summary data. -final class OverviewSummaryRequested extends OverviewEvent {} - -/// Internal event triggered when a listened-to entity is updated. -final class _OverviewEntityUpdated extends OverviewEvent { - const _OverviewEntityUpdated(); -} diff --git a/lib/overview/bloc/overview_state.dart b/lib/overview/bloc/overview_state.dart deleted file mode 100644 index 526dd638..00000000 --- a/lib/overview/bloc/overview_state.dart +++ /dev/null @@ -1,48 +0,0 @@ -part of 'overview_bloc.dart'; - -/// Represents the status of the dashboard overview data loading. -enum OverviewStatus { - /// Initial state. - initial, - - /// Data is being loaded. - loading, - - /// Data has been successfully loaded. - success, - - /// An error occurred while loading data. - failure, -} - -/// The state for the [OverviewBloc]. -final class OverviewState extends Equatable { - const OverviewState({ - this.status = OverviewStatus.initial, - this.summary, - this.recentHeadlines = const [], - this.exception, - }); - - final OverviewStatus status; - final DashboardSummary? summary; - final List recentHeadlines; - final HttpException? exception; - - OverviewState copyWith({ - OverviewStatus? status, - DashboardSummary? summary, - List? recentHeadlines, - HttpException? exception, - }) { - return OverviewState( - status: status ?? this.status, - summary: summary ?? this.summary, - recentHeadlines: recentHeadlines ?? this.recentHeadlines, - exception: exception ?? this.exception, - ); - } - - @override - List get props => [status, summary, recentHeadlines, exception]; -} diff --git a/lib/overview/view/overview_page.dart b/lib/overview/view/overview_page.dart index 562d7ae3..e2f076d7 100644 --- a/lib/overview/view/overview_page.dart +++ b/lib/overview/view/overview_page.dart @@ -1,11 +1,4 @@ -import 'package:core/core.dart'; 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/overview/bloc/overview_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; -import 'package:go_router/go_router.dart'; -import 'package:ui_kit/ui_kit.dart'; /// {@template overview_page} /// The main dashboard overview page, displaying key statistics and quick actions. @@ -19,272 +12,11 @@ class OverviewPage extends StatefulWidget { } class _OverviewPageState extends State { - @override - void initState() { - super.initState(); - // Dispatch the event to load dashboard overview data when the page is initialized. - context.read().add(OverviewSummaryRequested()); - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - return Scaffold( - body: BlocBuilder( - builder: (context, state) { - if (state.status == OverviewStatus.loading || - state.status == OverviewStatus.initial) { - return LoadingStateWidget( - icon: Icons.dashboard_outlined, - headline: l10n.loadingOverview, - subheadline: l10n.loadingOverviewSubheadline, - ); - } - if (state.status == OverviewStatus.failure) { - return FailureStateWidget( - exception: state.exception!, - onRetry: () { - context.read().add(OverviewSummaryRequested()); - }, - ); - } - if (state.status == OverviewStatus.success && state.summary != null) { - final summary = state.summary!; - final recentHeadlines = state.recentHeadlines; - return ListView( - padding: const EdgeInsets.all(AppSpacing.lg), - children: [ - LayoutBuilder( - builder: (context, constraints) { - const tabletBreakpoint = 800; - final isNarrow = constraints.maxWidth < tabletBreakpoint; - - final summaryCards = [ - _SummaryCard( - icon: Icons.newspaper_outlined, - title: l10n.totalHeadlines, - value: summary.headlineCount.toString(), - ), - _SummaryCard( - icon: Icons.category_outlined, - title: l10n.totalTopics, - value: summary.topicCount.toString(), - ), - _SummaryCard( - icon: Icons.source_outlined, - title: l10n.totalSources, - value: summary.sourceCount.toString(), - ), - ]; - - if (isNarrow) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - summaryCards[0], - const SizedBox(height: AppSpacing.lg), - summaryCards[1], - const SizedBox(height: AppSpacing.lg), - summaryCards[2], - ], - ); - } - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: summaryCards[0]), - const SizedBox(width: AppSpacing.lg), - Expanded(child: summaryCards[1]), - const SizedBox(width: AppSpacing.lg), - Expanded(child: summaryCards[2]), - ], - ); - }, - ), - const SizedBox(height: AppSpacing.lg), - LayoutBuilder( - builder: (context, constraints) { - const wideBreakpoint = 1200; - final isWide = constraints.maxWidth > wideBreakpoint; - - final mainContent = [ - _RecentHeadlinesCard(headlines: recentHeadlines), - const _QuickActionsCard(), - ]; - - if (isWide) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(flex: 2, child: mainContent[0]), - const SizedBox(width: AppSpacing.lg), - Expanded(flex: 1, child: mainContent[1]), - ], - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - mainContent[0], - const SizedBox(height: AppSpacing.lg), - mainContent[1], - ], - ); - }, - ), - ], - ); - } - return const SizedBox.shrink(); - }, - ), - ); - } -} - -/// A card providing quick navigation to common administrative tasks. -class _QuickActionsCard extends StatelessWidget { - const _QuickActionsCard(); - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - final theme = Theme.of(context); - - return Card( - child: Padding( - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text(l10n.quickActions, style: theme.textTheme.titleLarge), - const SizedBox(height: AppSpacing.md), - ElevatedButton.icon( - icon: const Icon(Icons.add_circle_outline), - label: Text(l10n.createHeadlineAction), - onPressed: () => context.goNamed(Routes.createHeadlineName), - ), - const SizedBox(height: AppSpacing.sm), - OutlinedButton.icon( - icon: const Icon(Icons.create_new_folder_outlined), - label: Text(l10n.createTopic), - onPressed: () => context.goNamed(Routes.createTopicName), - ), - const SizedBox(height: AppSpacing.sm), - OutlinedButton.icon( - icon: const Icon(Icons.add_to_photos_outlined), - label: Text(l10n.createSource), - onPressed: () => context.goNamed(Routes.createSourceName), - ), - ], - ), - ), - ); - } -} - -/// A card to display a list of recently created headlines. -class _RecentHeadlinesCard extends StatelessWidget { - const _RecentHeadlinesCard({required this.headlines}); - - final List headlines; - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - final theme = Theme.of(context); - - return Card( - child: Padding( - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(l10n.recentHeadlines, style: theme.textTheme.titleLarge), - TextButton( - onPressed: () => - context.goNamed(Routes.contentManagementName), - child: Text(l10n.viewAll), - ), - ], - ), - const SizedBox(height: AppSpacing.md), - if (headlines.isEmpty) - Padding( - padding: const EdgeInsets.symmetric(vertical: AppSpacing.lg), - child: Center( - child: Text( - l10n.noRecentHeadlines, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ), - ) - else - ...headlines.map( - (headline) => ListTile( - leading: const Icon(Icons.article_outlined), - title: Text(headline.title, maxLines: 1), - subtitle: Text( - DateFormatter.formatRelativeTime( - context, - headline.createdAt, - ), - ), - onTap: () => context.goNamed( - Routes.editHeadlineName, - pathParameters: {'id': headline.id}, - ), - contentPadding: EdgeInsets.zero, - dense: true, - ), - ), - ], - ), - ), - ); - } -} - -/// A private widget to display a single summary statistic on the dashboard overview. -class _SummaryCard extends StatelessWidget { - const _SummaryCard({ - required this.icon, - required this.title, - required this.value, - }); - - final IconData icon; - final String title; - final String value; - @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Card( - child: Padding( - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(icon, size: 28, color: theme.colorScheme.primary), - const SizedBox(height: AppSpacing.sm), - Text(value, style: theme.textTheme.headlineMedium), - const SizedBox(height: AppSpacing.xs), - Text( - title, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), + // final l10n = AppLocalizationsX(context).l10n; + return const Scaffold( + body: Placeholder(), ); } } From de3636cda786ff0fc45b80db435728d4e6a493eb Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 10:17:34 +0100 Subject: [PATCH 07/24] chore: misc --- lib/app/view/app.dart | 16 ++++++++-------- lib/bootstrap.dart | 18 ------------------ 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 476c718b..736ffa56 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -18,7 +18,7 @@ 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/sources_filter/sources_filter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/topics_filter/topics_filter_bloc.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/overview/bloc/overview_bloc.dart'; +// import 'package:flutter_news_app_web_dashboard_full_source_code/overview/bloc/overview_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/router/router.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_updates_service.dart'; @@ -159,13 +159,13 @@ class App extends StatelessWidget { pendingDeletionsService: context.read(), ), ), - BlocProvider( - create: (context) => OverviewBloc( - headlinesRepository: context.read>(), - topicsRepository: context.read>(), - sourcesRepository: context.read>(), - ), - ), + // BlocProvider( + // create: (context) => OverviewBloc( + // headlinesRepository: context.read>(), + // topicsRepository: context.read>(), + // sourcesRepository: context.read>(), + // ), + // ), // The UserFilterBloc is provided here to be available for both the // UserManagementBloc and the UI components. BlocProvider(create: (_) => UserFilterBloc()), diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index f2b1507a..95a3381e 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -61,7 +61,6 @@ Future bootstrap( DataClient userContentPreferencesClient; DataClient appSettingsClient; DataClient remoteConfigClient; - DataClient dashboardSummaryClient; DataClient countriesClient; DataClient languagesClient; DataClient usersClient; @@ -106,12 +105,6 @@ Future bootstrap( initialData: remoteConfigsFixturesData, logger: Logger('DataInMemory'), ); - dashboardSummaryClient = DataInMemory( - toJson: (i) => i.toJson(), - getId: (i) => i.id, - initialData: dashboardSummaryFixturesData, - logger: Logger('DataInMemory'), - ); countriesClient = DataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, @@ -192,13 +185,6 @@ Future bootstrap( toJson: (config) => config.toJson(), logger: Logger('DataApi'), ); - dashboardSummaryClient = DataApi( - httpClient: httpClient, - modelName: 'dashboard_summary', - fromJson: DashboardSummary.fromJson, - toJson: (summary) => summary.toJson(), - logger: Logger('DataApi'), - ); countriesClient = DataApi( httpClient: httpClient, modelName: 'country', @@ -263,9 +249,6 @@ Future bootstrap( final remoteConfigRepository = DataRepository( dataClient: remoteConfigClient, ); - final dashboardSummaryRepository = DataRepository( - dataClient: dashboardSummaryClient, - ); final countriesRepository = DataRepository( dataClient: countriesClient, ); @@ -291,7 +274,6 @@ Future bootstrap( appSettingsRepository: appSettingsRepository, userContentPreferencesRepository: userContentPreferencesRepository, remoteConfigRepository: remoteConfigRepository, - dashboardSummaryRepository: dashboardSummaryRepository, countriesRepository: countriesRepository, languagesRepository: languagesRepository, usersRepository: usersRepository, From d3598e24c7c67c9f793fe0660fcbb98279728e3f Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 10:46:42 +0100 Subject: [PATCH 08/24] docs(l10n): add analytics event labels and descriptions - Added analytics event labels and descriptions for various user actions - Included events for user registration, login, content interaction, filtering, search, and subscription management - Covered events for ad interactions, theme and language changes, and review prompts - Provided labels and descriptions in both Arabic and English --- lib/l10n/arb/app_ar.arb | 304 ++++++++++++++++++++++++++++++++++++++++ lib/l10n/arb/app_en.arb | 304 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 608 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 2fe63ab8..e6013b26 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -3040,5 +3040,309 @@ "analyticsProviderMixpanel": "Mixpanel", "@analyticsProviderMixpanel": { "description": "تسمية مزود التحليلات Mixpanel" + }, + "analyticsEventUserRegisteredLabel": "تسجيل مستخدم جديد", + "@analyticsEventUserRegisteredLabel": { + "description": "تسمية لحدث تسجيل مستخدم جديد" + }, + "analyticsEventUserRegisteredDescription": "تتبع عندما يقوم مستخدم جديد بإنشاء حساب بنجاح.", + "@analyticsEventUserRegisteredDescription": { + "description": "وصف لحدث تسجيل مستخدم جديد" + }, + "analyticsEventUserLoginLabel": "تسجيل دخول المستخدم", + "@analyticsEventUserLoginLabel": { + "description": "تسمية لحدث تسجيل دخول المستخدم" + }, + "analyticsEventUserLoginDescription": "تتبع عندما يقوم المستخدم بتسجيل الدخول إلى التطبيق.", + "@analyticsEventUserLoginDescription": { + "description": "وصف لحدث تسجيل دخول المستخدم" + }, + "analyticsEventAccountLinkedLabel": "ربط الحساب", + "@analyticsEventAccountLinkedLabel": { + "description": "تسمية لحدث ربط الحساب" + }, + "analyticsEventAccountLinkedDescription": "تتبع عندما يقوم مستخدم ضيف بربط حسابه ببيانات اعتماد دائمة.", + "@analyticsEventAccountLinkedDescription": { + "description": "وصف لحدث ربط الحساب" + }, + "analyticsEventUserRoleChangedLabel": "تغيير دور المستخدم", + "@analyticsEventUserRoleChangedLabel": { + "description": "تسمية لحدث تغيير دور المستخدم" + }, + "analyticsEventUserRoleChangedDescription": "تتبع عندما يتم تحديث دور المستخدم (مثل الترقية إلى مميز).", + "@analyticsEventUserRoleChangedDescription": { + "description": "وصف لحدث تغيير دور المستخدم" + }, + "analyticsEventContentViewedLabel": "مشاهدة المحتوى", + "@analyticsEventContentViewedLabel": { + "description": "تسمية لحدث مشاهدة المحتوى" + }, + "analyticsEventContentViewedDescription": "تتبع عندما يشاهد المستخدم عنوانًا أو مقالًا.", + "@analyticsEventContentViewedDescription": { + "description": "وصف لحدث مشاهدة المحتوى" + }, + "analyticsEventContentSharedLabel": "مشاركة المحتوى", + "@analyticsEventContentSharedLabel": { + "description": "تسمية لحدث مشاركة المحتوى" + }, + "analyticsEventContentSharedDescription": "تتبع عندما يشارك المستخدم المحتوى عبر منصات خارجية.", + "@analyticsEventContentSharedDescription": { + "description": "وصف لحدث مشاركة المحتوى" + }, + "analyticsEventContentSavedLabel": "حفظ المحتوى", + "@analyticsEventContentSavedLabel": { + "description": "تسمية لحدث حفظ المحتوى" + }, + "analyticsEventContentSavedDescription": "تتبع عندما يقوم المستخدم بحفظ عنوان في المفضلة.", + "@analyticsEventContentSavedDescription": { + "description": "وصف لحدث حفظ المحتوى" + }, + "analyticsEventContentUnsavedLabel": "إلغاء حفظ المحتوى", + "@analyticsEventContentUnsavedLabel": { + "description": "تسمية لحدث إلغاء حفظ المحتوى" + }, + "analyticsEventContentUnsavedDescription": "تتبع عندما يزيل المستخدم عنوانًا من المفضلة.", + "@analyticsEventContentUnsavedDescription": { + "description": "وصف لحدث إلغاء حفظ المحتوى" + }, + "analyticsEventContentReadingTimeLabel": "وقت قراءة المحتوى", + "@analyticsEventContentReadingTimeLabel": { + "description": "تسمية لحدث وقت قراءة المحتوى" + }, + "analyticsEventContentReadingTimeDescription": "تتبع المدة التي يقضيها المستخدم في قراءة مقال.", + "@analyticsEventContentReadingTimeDescription": { + "description": "وصف لحدث وقت قراءة المحتوى" + }, + "analyticsEventReactionCreatedLabel": "إضافة رد فعل", + "@analyticsEventReactionCreatedLabel": { + "description": "تسمية لحدث إضافة رد فعل" + }, + "analyticsEventReactionCreatedDescription": "تتبع عندما يتفاعل المستخدم مع المحتوى.", + "@analyticsEventReactionCreatedDescription": { + "description": "وصف لحدث إضافة رد فعل" + }, + "analyticsEventReactionDeletedLabel": "إزالة رد فعل", + "@analyticsEventReactionDeletedLabel": { + "description": "تسمية لحدث إزالة رد فعل" + }, + "analyticsEventReactionDeletedDescription": "تتبع عندما يزيل المستخدم رد فعله.", + "@analyticsEventReactionDeletedDescription": { + "description": "وصف لحدث إزالة رد فعل" + }, + "analyticsEventCommentCreatedLabel": "نشر تعليق", + "@analyticsEventCommentCreatedLabel": { + "description": "تسمية لحدث نشر تعليق" + }, + "analyticsEventCommentCreatedDescription": "تتبع عندما يرسل المستخدم تعليقًا جديدًا.", + "@analyticsEventCommentCreatedDescription": { + "description": "وصف لحدث نشر تعليق" + }, + "analyticsEventCommentDeletedLabel": "حذف تعليق", + "@analyticsEventCommentDeletedLabel": { + "description": "تسمية لحدث حذف تعليق" + }, + "analyticsEventCommentDeletedDescription": "تتبع عندما يحذف المستخدم تعليقه الخاص.", + "@analyticsEventCommentDeletedDescription": { + "description": "وصف لحدث حذف تعليق" + }, + "analyticsEventReportSubmittedLabel": "تقديم بلاغ", + "@analyticsEventReportSubmittedLabel": { + "description": "تسمية لحدث تقديم بلاغ" + }, + "analyticsEventReportSubmittedDescription": "تتبع عندما يبلغ المستخدم عن محتوى أو مستخدمين آخرين.", + "@analyticsEventReportSubmittedDescription": { + "description": "وصف لحدث تقديم بلاغ" + }, + "analyticsEventHeadlineFilterCreatedLabel": "إنشاء مرشح عناوين", + "@analyticsEventHeadlineFilterCreatedLabel": { + "description": "تسمية لحدث إنشاء مرشح عناوين" + }, + "analyticsEventHeadlineFilterCreatedDescription": "تتبع عندما ينشئ المستخدم مرشح عناوين مخصص جديد.", + "@analyticsEventHeadlineFilterCreatedDescription": { + "description": "وصف لحدث إنشاء مرشح عناوين" + }, + "analyticsEventHeadlineFilterUpdatedLabel": "تحديث مرشح عناوين", + "@analyticsEventHeadlineFilterUpdatedLabel": { + "description": "تسمية لحدث تحديث مرشح عناوين" + }, + "analyticsEventHeadlineFilterUpdatedDescription": "تتبع عندما يعدل المستخدم مرشح عناوين موجود.", + "@analyticsEventHeadlineFilterUpdatedDescription": { + "description": "وصف لحدث تحديث مرشح عناوين" + }, + "analyticsEventHeadlineFilterUsedLabel": "استخدام مرشح عناوين", + "@analyticsEventHeadlineFilterUsedLabel": { + "description": "تسمية لحدث استخدام مرشح عناوين" + }, + "analyticsEventHeadlineFilterUsedDescription": "تتبع عندما يطبق المستخدم مرشح عناوين محفوظ.", + "@analyticsEventHeadlineFilterUsedDescription": { + "description": "وصف لحدث استخدام مرشح عناوين" + }, + "analyticsEventSourceFilterCreatedLabel": "إنشاء مرشح مصادر", + "@analyticsEventSourceFilterCreatedLabel": { + "description": "تسمية لحدث إنشاء مرشح مصادر" + }, + "analyticsEventSourceFilterCreatedDescription": "تتبع عندما ينشئ المستخدم مرشح مصادر جديد.", + "@analyticsEventSourceFilterCreatedDescription": { + "description": "وصف لحدث إنشاء مرشح مصادر" + }, + "analyticsEventSourceFilterUpdatedLabel": "تحديث مرشح مصادر", + "@analyticsEventSourceFilterUpdatedLabel": { + "description": "تسمية لحدث تحديث مرشح مصادر" + }, + "analyticsEventSourceFilterUpdatedDescription": "تتبع عندما يعدل المستخدم مرشح مصادر موجود.", + "@analyticsEventSourceFilterUpdatedDescription": { + "description": "وصف لحدث تحديث مرشح مصادر" + }, + "analyticsEventSearchPerformedLabel": "إجراء بحث", + "@analyticsEventSearchPerformedLabel": { + "description": "تسمية لحدث إجراء بحث" + }, + "analyticsEventSearchPerformedDescription": "تتبع عندما يقوم المستخدم بإجراء استعلام بحث.", + "@analyticsEventSearchPerformedDescription": { + "description": "وصف لحدث إجراء بحث" + }, + "analyticsEventAppReviewPromptRespondedLabel": "استجابة لطلب التقييم", + "@analyticsEventAppReviewPromptRespondedLabel": { + "description": "تسمية لحدث استجابة لطلب التقييم" + }, + "analyticsEventAppReviewPromptRespondedDescription": "تتبع استجابات المستخدم للموجه الداخلي 'هل تستمتع بالتطبيق؟'.", + "@analyticsEventAppReviewPromptRespondedDescription": { + "description": "وصف لحدث استجابة لطلب التقييم" + }, + "analyticsEventAppReviewStoreRequestedLabel": "طلب تقييم المتجر", + "@analyticsEventAppReviewStoreRequestedLabel": { + "description": "تسمية لحدث طلب تقييم المتجر" + }, + "analyticsEventAppReviewStoreRequestedDescription": "تتبع عندما يتم طلب مربع حوار مراجعة المتجر الأصلي لنظام التشغيل.", + "@analyticsEventAppReviewStoreRequestedDescription": { + "description": "وصف لحدث طلب تقييم المتجر" + }, + "analyticsEventLimitExceededLabel": "تجاوز الحد", + "@analyticsEventLimitExceededLabel": { + "description": "تسمية لحدث تجاوز الحد" + }, + "analyticsEventLimitExceededDescription": "تتبع عندما يصل المستخدم إلى حد الاستخدام (مثل حد العناصر المحفوظة).", + "@analyticsEventLimitExceededDescription": { + "description": "وصف لحدث تجاوز الحد" + }, + "analyticsEventLimitExceededCtaClickedLabel": "النقر على إجراء الحد", + "@analyticsEventLimitExceededCtaClickedLabel": { + "description": "تسمية لحدث النقر على إجراء الحد" + }, + "analyticsEventLimitExceededCtaClickedDescription": "تتبع النقرات على أزرار 'ترقية' أو 'ربط الحساب' في مربعات حوار الحدود.", + "@analyticsEventLimitExceededCtaClickedDescription": { + "description": "وصف لحدث النقر على إجراء الحد" + }, + "analyticsEventPaywallPresentedLabel": "ظهور جدار الدفع", + "@analyticsEventPaywallPresentedLabel": { + "description": "تسمية لحدث ظهور جدار الدفع" + }, + "analyticsEventPaywallPresentedDescription": "تتبع عندما يتم عرض شاشة جدار الدفع للمستخدم.", + "@analyticsEventPaywallPresentedDescription": { + "description": "وصف لحدث ظهور جدار الدفع" + }, + "analyticsEventSubscriptionStartedLabel": "بدء الاشتراك", + "@analyticsEventSubscriptionStartedLabel": { + "description": "تسمية لحدث بدء الاشتراك" + }, + "analyticsEventSubscriptionStartedDescription": "تتبع عندما يبدأ المستخدم اشتراكًا جديدًا بنجاح.", + "@analyticsEventSubscriptionStartedDescription": { + "description": "وصف لحدث بدء الاشتراك" + }, + "analyticsEventSubscriptionRenewedLabel": "تجديد الاشتراك", + "@analyticsEventSubscriptionRenewedLabel": { + "description": "تسمية لحدث تجديد الاشتراك" + }, + "analyticsEventSubscriptionRenewedDescription": "تتبع عندما يتم تجديد الاشتراك تلقائيًا أو يدويًا.", + "@analyticsEventSubscriptionRenewedDescription": { + "description": "وصف لحدث تجديد الاشتراك" + }, + "analyticsEventSubscriptionCancelledLabel": "إلغاء الاشتراك", + "@analyticsEventSubscriptionCancelledLabel": { + "description": "تسمية لحدث إلغاء الاشتراك" + }, + "analyticsEventSubscriptionCancelledDescription": "تتبع عندما يقوم المستخدم بإلغاء اشتراكه.", + "@analyticsEventSubscriptionCancelledDescription": { + "description": "وصف لحدث إلغاء الاشتراك" + }, + "analyticsEventSubscriptionEndedLabel": "انتهاء الاشتراك", + "@analyticsEventSubscriptionEndedLabel": { + "description": "تسمية لحدث انتهاء الاشتراك" + }, + "analyticsEventSubscriptionEndedDescription": "تتبع عندما تنتهي صلاحية الاشتراك أو يتم إنهاؤه.", + "@analyticsEventSubscriptionEndedDescription": { + "description": "وصف لحدث انتهاء الاشتراك" + }, + "analyticsEventAdImpressionLabel": "ظهور إعلان", + "@analyticsEventAdImpressionLabel": { + "description": "تسمية لحدث ظهور إعلان" + }, + "analyticsEventAdImpressionDescription": "تتبع عندما يتم عرض إعلان للمستخدم.", + "@analyticsEventAdImpressionDescription": { + "description": "وصف لحدث ظهور إعلان" + }, + "analyticsEventAdClickedLabel": "النقر على إعلان", + "@analyticsEventAdClickedLabel": { + "description": "تسمية لحدث النقر على إعلان" + }, + "analyticsEventAdClickedDescription": "تتبع عندما ينقر المستخدم على إعلان.", + "@analyticsEventAdClickedDescription": { + "description": "وصف لحدث النقر على إعلان" + }, + "analyticsEventAdLoadFailedLabel": "فشل تحميل الإعلان", + "@analyticsEventAdLoadFailedLabel": { + "description": "تسمية لحدث فشل تحميل الإعلان" + }, + "analyticsEventAdLoadFailedDescription": "تتبع الأخطاء عند محاولة تحميل الإعلانات.", + "@analyticsEventAdLoadFailedDescription": { + "description": "وصف لحدث فشل تحميل الإعلان" + }, + "analyticsEventAdRewardEarnedLabel": "كسب مكافأة الإعلان", + "@analyticsEventAdRewardEarnedLabel": { + "description": "تسمية لحدث كسب مكافأة الإعلان" + }, + "analyticsEventAdRewardEarnedDescription": "تتبع عندما يكمل المستخدم إجراء إعلان بمكافأة.", + "@analyticsEventAdRewardEarnedDescription": { + "description": "وصف لحدث كسب مكافأة الإعلان" + }, + "analyticsEventThemeChangedLabel": "تغيير السمة", + "@analyticsEventThemeChangedLabel": { + "description": "تسمية لحدث تغيير السمة" + }, + "analyticsEventThemeChangedDescription": "تتبع عندما يغير المستخدم سمة التطبيق.", + "@analyticsEventThemeChangedDescription": { + "description": "وصف لحدث تغيير السمة" + }, + "analyticsEventLanguageChangedLabel": "تغيير اللغة", + "@analyticsEventLanguageChangedLabel": { + "description": "تسمية لحدث تغيير اللغة" + }, + "analyticsEventLanguageChangedDescription": "تتبع عندما يغير المستخدم لغة التطبيق.", + "@analyticsEventLanguageChangedDescription": { + "description": "وصف لحدث تغيير اللغة" + }, + "analyticsEventFeedDensityChangedLabel": "تغيير كثافة الموجز", + "@analyticsEventFeedDensityChangedLabel": { + "description": "تسمية لحدث تغيير كثافة الموجز" + }, + "analyticsEventFeedDensityChangedDescription": "تتبع عندما يعدل المستخدم كثافة المعلومات في الموجز.", + "@analyticsEventFeedDensityChangedDescription": { + "description": "وصف لحدث تغيير كثافة الموجز" + }, + "analyticsEventBrowserChoiceChangedLabel": "تغيير تفضيل المتصفح", + "@analyticsEventBrowserChoiceChangedLabel": { + "description": "تسمية لحدث تغيير تفضيل المتصفح" + }, + "analyticsEventBrowserChoiceChangedDescription": "تتبع عندما يغير المستخدم متصفحه المفضل لفتح الروابط.", + "@analyticsEventBrowserChoiceChangedDescription": { + "description": "وصف لحدث تغيير تفضيل المتصفح" + }, + "analyticsEventSourceFilterUsedLabel": "استخدام مرشح مصادر", + "@analyticsEventSourceFilterUsedLabel": { + "description": "تسمية لحدث استخدام مرشح مصادر" + }, + "analyticsEventSourceFilterUsedDescription": "تتبع عندما يطبق المستخدم مرشح مصادر محفوظ.", + "@analyticsEventSourceFilterUsedDescription": { + "description": "وصف لحدث استخدام مرشح مصادر" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index f63e47da..b9e14336 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -3036,5 +3036,309 @@ "analyticsProviderMixpanel": "Mixpanel", "@analyticsProviderMixpanel": { "description": "Label for Mixpanel analytics provider" + }, + "analyticsEventUserRegisteredLabel": "User Registration", + "@analyticsEventUserRegisteredLabel": { + "description": "Label for the User Registration analytics event" + }, + "analyticsEventUserRegisteredDescription": "Track when a new user successfully creates an account.", + "@analyticsEventUserRegisteredDescription": { + "description": "Description for the User Registration analytics event" + }, + "analyticsEventUserLoginLabel": "User Login", + "@analyticsEventUserLoginLabel": { + "description": "Label for the User Login analytics event" + }, + "analyticsEventUserLoginDescription": "Track when a user logs in to the application.", + "@analyticsEventUserLoginDescription": { + "description": "Description for the User Login analytics event" + }, + "analyticsEventAccountLinkedLabel": "Account Linking", + "@analyticsEventAccountLinkedLabel": { + "description": "Label for the Account Linking analytics event" + }, + "analyticsEventAccountLinkedDescription": "Track when a guest user links their account to a permanent credential.", + "@analyticsEventAccountLinkedDescription": { + "description": "Description for the Account Linking analytics event" + }, + "analyticsEventUserRoleChangedLabel": "User Role Change", + "@analyticsEventUserRoleChangedLabel": { + "description": "Label for the User Role Change analytics event" + }, + "analyticsEventUserRoleChangedDescription": "Track when a user's role is updated (e.g., upgraded to Premium).", + "@analyticsEventUserRoleChangedDescription": { + "description": "Description for the User Role Change analytics event" + }, + "analyticsEventContentViewedLabel": "Content View", + "@analyticsEventContentViewedLabel": { + "description": "Label for the Content View analytics event" + }, + "analyticsEventContentViewedDescription": "Track when a user views a headline or article.", + "@analyticsEventContentViewedDescription": { + "description": "Description for the Content View analytics event" + }, + "analyticsEventContentSharedLabel": "Content Share", + "@analyticsEventContentSharedLabel": { + "description": "Label for the Content Share analytics event" + }, + "analyticsEventContentSharedDescription": "Track when a user shares content via external platforms.", + "@analyticsEventContentSharedDescription": { + "description": "Description for the Content Share analytics event" + }, + "analyticsEventContentSavedLabel": "Content Save", + "@analyticsEventContentSavedLabel": { + "description": "Label for the Content Save analytics event" + }, + "analyticsEventContentSavedDescription": "Track when a user bookmarks a headline.", + "@analyticsEventContentSavedDescription": { + "description": "Description for the Content Save analytics event" + }, + "analyticsEventContentUnsavedLabel": "Content Unsave", + "@analyticsEventContentUnsavedLabel": { + "description": "Label for the Content Unsave analytics event" + }, + "analyticsEventContentUnsavedDescription": "Track when a user removes a bookmark.", + "@analyticsEventContentUnsavedDescription": { + "description": "Description for the Content Unsave analytics event" + }, + "analyticsEventContentReadingTimeLabel": "Content Reading Time", + "@analyticsEventContentReadingTimeLabel": { + "description": "Label for the Content Reading Time analytics event" + }, + "analyticsEventContentReadingTimeDescription": "Track the duration a user spends reading an article.", + "@analyticsEventContentReadingTimeDescription": { + "description": "Description for the Content Reading Time analytics event" + }, + "analyticsEventReactionCreatedLabel": "Reaction Added", + "@analyticsEventReactionCreatedLabel": { + "description": "Label for the Reaction Added analytics event" + }, + "analyticsEventReactionCreatedDescription": "Track when a user reacts to content.", + "@analyticsEventReactionCreatedDescription": { + "description": "Description for the Reaction Added analytics event" + }, + "analyticsEventReactionDeletedLabel": "Reaction Removed", + "@analyticsEventReactionDeletedLabel": { + "description": "Label for the Reaction Removed analytics event" + }, + "analyticsEventReactionDeletedDescription": "Track when a user removes their reaction.", + "@analyticsEventReactionDeletedDescription": { + "description": "Description for the Reaction Removed analytics event" + }, + "analyticsEventCommentCreatedLabel": "Comment Posted", + "@analyticsEventCommentCreatedLabel": { + "description": "Label for the Comment Posted analytics event" + }, + "analyticsEventCommentCreatedDescription": "Track when a user submits a new comment.", + "@analyticsEventCommentCreatedDescription": { + "description": "Description for the Comment Posted analytics event" + }, + "analyticsEventCommentDeletedLabel": "Comment Deleted", + "@analyticsEventCommentDeletedLabel": { + "description": "Label for the Comment Deleted analytics event" + }, + "analyticsEventCommentDeletedDescription": "Track when a user deletes their own comment.", + "@analyticsEventCommentDeletedDescription": { + "description": "Description for the Comment Deleted analytics event" + }, + "analyticsEventReportSubmittedLabel": "Report Submitted", + "@analyticsEventReportSubmittedLabel": { + "description": "Label for the Report Submitted analytics event" + }, + "analyticsEventReportSubmittedDescription": "Track when a user reports content or other users.", + "@analyticsEventReportSubmittedDescription": { + "description": "Description for the Report Submitted analytics event" + }, + "analyticsEventHeadlineFilterCreatedLabel": "Headline Filter Creation", + "@analyticsEventHeadlineFilterCreatedLabel": { + "description": "Label for the Headline Filter Creation analytics event" + }, + "analyticsEventHeadlineFilterCreatedDescription": "Track when a user creates a new custom headline filter.", + "@analyticsEventHeadlineFilterCreatedDescription": { + "description": "Description for the Headline Filter Creation analytics event" + }, + "analyticsEventHeadlineFilterUpdatedLabel": "Headline Filter Update", + "@analyticsEventHeadlineFilterUpdatedLabel": { + "description": "Label for the Headline Filter Update analytics event" + }, + "analyticsEventHeadlineFilterUpdatedDescription": "Track when a user modifies an existing headline filter.", + "@analyticsEventHeadlineFilterUpdatedDescription": { + "description": "Description for the Headline Filter Update analytics event" + }, + "analyticsEventHeadlineFilterUsedLabel": "Headline Filter Usage", + "@analyticsEventHeadlineFilterUsedLabel": { + "description": "Label for the Headline Filter Usage analytics event" + }, + "analyticsEventHeadlineFilterUsedDescription": "Track when a user applies a saved headline filter.", + "@analyticsEventHeadlineFilterUsedDescription": { + "description": "Description for the Headline Filter Usage analytics event" + }, + "analyticsEventSourceFilterCreatedLabel": "Source Filter Creation", + "@analyticsEventSourceFilterCreatedLabel": { + "description": "Label for the Source Filter Creation analytics event" + }, + "analyticsEventSourceFilterCreatedDescription": "Track when a user creates a new source filter.", + "@analyticsEventSourceFilterCreatedDescription": { + "description": "Description for the Source Filter Creation analytics event" + }, + "analyticsEventSourceFilterUpdatedLabel": "Source Filter Update", + "@analyticsEventSourceFilterUpdatedLabel": { + "description": "Label for the Source Filter Update analytics event" + }, + "analyticsEventSourceFilterUpdatedDescription": "Track when a user modifies an existing source filter.", + "@analyticsEventSourceFilterUpdatedDescription": { + "description": "Description for the Source Filter Update analytics event" + }, + "analyticsEventSearchPerformedLabel": "Search Performed", + "@analyticsEventSearchPerformedLabel": { + "description": "Label for the Search Performed analytics event" + }, + "analyticsEventSearchPerformedDescription": "Track when a user performs a search query.", + "@analyticsEventSearchPerformedDescription": { + "description": "Description for the Search Performed analytics event" + }, + "analyticsEventAppReviewPromptRespondedLabel": "Review Prompt Response", + "@analyticsEventAppReviewPromptRespondedLabel": { + "description": "Label for the Review Prompt Response analytics event" + }, + "analyticsEventAppReviewPromptRespondedDescription": "Track user responses to the internal 'Enjoying the app?' prompt.", + "@analyticsEventAppReviewPromptRespondedDescription": { + "description": "Description for the Review Prompt Response analytics event" + }, + "analyticsEventAppReviewStoreRequestedLabel": "Store Review Request", + "@analyticsEventAppReviewStoreRequestedLabel": { + "description": "Label for the Store Review Request analytics event" + }, + "analyticsEventAppReviewStoreRequestedDescription": "Track when the native OS store review dialog is requested.", + "@analyticsEventAppReviewStoreRequestedDescription": { + "description": "Description for the Store Review Request analytics event" + }, + "analyticsEventLimitExceededLabel": "Limit Exceeded", + "@analyticsEventLimitExceededLabel": { + "description": "Label for the Limit Exceeded analytics event" + }, + "analyticsEventLimitExceededDescription": "Track when a user hits a usage limit (e.g., saved items limit).", + "@analyticsEventLimitExceededDescription": { + "description": "Description for the Limit Exceeded analytics event" + }, + "analyticsEventLimitExceededCtaClickedLabel": "Limit CTA Click", + "@analyticsEventLimitExceededCtaClickedLabel": { + "description": "Label for the Limit CTA Click analytics event" + }, + "analyticsEventLimitExceededCtaClickedDescription": "Track clicks on 'Upgrade' or 'Link Account' buttons in limit dialogs.", + "@analyticsEventLimitExceededCtaClickedDescription": { + "description": "Description for the Limit CTA Click analytics event" + }, + "analyticsEventPaywallPresentedLabel": "Paywall Impression", + "@analyticsEventPaywallPresentedLabel": { + "description": "Label for the Paywall Impression analytics event" + }, + "analyticsEventPaywallPresentedDescription": "Track when the paywall screen is shown to a user.", + "@analyticsEventPaywallPresentedDescription": { + "description": "Description for the Paywall Impression analytics event" + }, + "analyticsEventSubscriptionStartedLabel": "Subscription Start", + "@analyticsEventSubscriptionStartedLabel": { + "description": "Label for the Subscription Start analytics event" + }, + "analyticsEventSubscriptionStartedDescription": "Track when a user successfully starts a new subscription.", + "@analyticsEventSubscriptionStartedDescription": { + "description": "Description for the Subscription Start analytics event" + }, + "analyticsEventSubscriptionRenewedLabel": "Subscription Renewal", + "@analyticsEventSubscriptionRenewedLabel": { + "description": "Label for the Subscription Renewal analytics event" + }, + "analyticsEventSubscriptionRenewedDescription": "Track when a subscription is automatically or manually renewed.", + "@analyticsEventSubscriptionRenewedDescription": { + "description": "Description for the Subscription Renewal analytics event" + }, + "analyticsEventSubscriptionCancelledLabel": "Subscription Cancellation", + "@analyticsEventSubscriptionCancelledLabel": { + "description": "Label for the Subscription Cancellation analytics event" + }, + "analyticsEventSubscriptionCancelledDescription": "Track when a user cancels their subscription.", + "@analyticsEventSubscriptionCancelledDescription": { + "description": "Description for the Subscription Cancellation analytics event" + }, + "analyticsEventSubscriptionEndedLabel": "Subscription End", + "@analyticsEventSubscriptionEndedLabel": { + "description": "Label for the Subscription End analytics event" + }, + "analyticsEventSubscriptionEndedDescription": "Track when a subscription expires or is terminated.", + "@analyticsEventSubscriptionEndedDescription": { + "description": "Description for the Subscription End analytics event" + }, + "analyticsEventAdImpressionLabel": "Ad Impression", + "@analyticsEventAdImpressionLabel": { + "description": "Label for the Ad Impression analytics event" + }, + "analyticsEventAdImpressionDescription": "Track when an advertisement is displayed to the user.", + "@analyticsEventAdImpressionDescription": { + "description": "Description for the Ad Impression analytics event" + }, + "analyticsEventAdClickedLabel": "Ad Click", + "@analyticsEventAdClickedLabel": { + "description": "Label for the Ad Click analytics event" + }, + "analyticsEventAdClickedDescription": "Track when a user clicks on an advertisement.", + "@analyticsEventAdClickedDescription": { + "description": "Description for the Ad Click analytics event" + }, + "analyticsEventAdLoadFailedLabel": "Ad Load Failure", + "@analyticsEventAdLoadFailedLabel": { + "description": "Label for the Ad Load Failure analytics event" + }, + "analyticsEventAdLoadFailedDescription": "Track errors when attempting to load advertisements.", + "@analyticsEventAdLoadFailedDescription": { + "description": "Description for the Ad Load Failure analytics event" + }, + "analyticsEventAdRewardEarnedLabel": "Ad Reward Earned", + "@analyticsEventAdRewardEarnedLabel": { + "description": "Label for the Ad Reward Earned analytics event" + }, + "analyticsEventAdRewardEarnedDescription": "Track when a user completes a rewarded ad action.", + "@analyticsEventAdRewardEarnedDescription": { + "description": "Description for the Ad Reward Earned analytics event" + }, + "analyticsEventThemeChangedLabel": "Theme Change", + "@analyticsEventThemeChangedLabel": { + "description": "Label for the Theme Change analytics event" + }, + "analyticsEventThemeChangedDescription": "Track when a user changes the application theme.", + "@analyticsEventThemeChangedDescription": { + "description": "Description for the Theme Change analytics event" + }, + "analyticsEventLanguageChangedLabel": "Language Change", + "@analyticsEventLanguageChangedLabel": { + "description": "Label for the Language Change analytics event" + }, + "analyticsEventLanguageChangedDescription": "Track when a user changes the application language.", + "@analyticsEventLanguageChangedDescription": { + "description": "Description for the Language Change analytics event" + }, + "analyticsEventFeedDensityChangedLabel": "Feed Density Change", + "@analyticsEventFeedDensityChangedLabel": { + "description": "Label for the Feed Density Change analytics event" + }, + "analyticsEventFeedDensityChangedDescription": "Track when a user adjusts the information density of the feed.", + "@analyticsEventFeedDensityChangedDescription": { + "description": "Description for the Feed Density Change analytics event" + }, + "analyticsEventBrowserChoiceChangedLabel": "Browser Preference Change", + "@analyticsEventBrowserChoiceChangedLabel": { + "description": "Label for the Browser Preference Change analytics event" + }, + "analyticsEventBrowserChoiceChangedDescription": "Track when a user changes their preferred browser for opening links.", + "@analyticsEventBrowserChoiceChangedDescription": { + "description": "Description for the Browser Preference Change analytics event" + }, + "analyticsEventSourceFilterUsedLabel": "Source Filter Usage", + "@analyticsEventSourceFilterUsedLabel": { + "description": "Label for the Source Filter Usage analytics event" + }, + "analyticsEventSourceFilterUsedDescription": "Track when a user applies a saved source filter.", + "@analyticsEventSourceFilterUsedDescription": { + "description": "Description for the Source Filter Usage analytics event" } } \ No newline at end of file From c52eccd1d3137713acfc7310a0dd23ebb6e14678 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 10:47:03 +0100 Subject: [PATCH 09/24] build(l10n): sync --- lib/l10n/app_localizations.dart | 456 +++++++++++++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 268 +++++++++++++++++ lib/l10n/app_localizations_en.dart | 272 +++++++++++++++++ 3 files changed, 996 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f605650d..e387b2a9 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4393,6 +4393,462 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Mixpanel'** String get analyticsProviderMixpanel; + + /// Label for the User Registration analytics event + /// + /// In en, this message translates to: + /// **'User Registration'** + String get analyticsEventUserRegisteredLabel; + + /// Description for the User Registration analytics event + /// + /// In en, this message translates to: + /// **'Track when a new user successfully creates an account.'** + String get analyticsEventUserRegisteredDescription; + + /// Label for the User Login analytics event + /// + /// In en, this message translates to: + /// **'User Login'** + String get analyticsEventUserLoginLabel; + + /// Description for the User Login analytics event + /// + /// In en, this message translates to: + /// **'Track when a user logs in to the application.'** + String get analyticsEventUserLoginDescription; + + /// Label for the Account Linking analytics event + /// + /// In en, this message translates to: + /// **'Account Linking'** + String get analyticsEventAccountLinkedLabel; + + /// Description for the Account Linking analytics event + /// + /// In en, this message translates to: + /// **'Track when a guest user links their account to a permanent credential.'** + String get analyticsEventAccountLinkedDescription; + + /// Label for the User Role Change analytics event + /// + /// In en, this message translates to: + /// **'User Role Change'** + String get analyticsEventUserRoleChangedLabel; + + /// Description for the User Role Change analytics event + /// + /// In en, this message translates to: + /// **'Track when a user\'s role is updated (e.g., upgraded to Premium).'** + String get analyticsEventUserRoleChangedDescription; + + /// Label for the Content View analytics event + /// + /// In en, this message translates to: + /// **'Content View'** + String get analyticsEventContentViewedLabel; + + /// Description for the Content View analytics event + /// + /// In en, this message translates to: + /// **'Track when a user views a headline or article.'** + String get analyticsEventContentViewedDescription; + + /// Label for the Content Share analytics event + /// + /// In en, this message translates to: + /// **'Content Share'** + String get analyticsEventContentSharedLabel; + + /// Description for the Content Share analytics event + /// + /// In en, this message translates to: + /// **'Track when a user shares content via external platforms.'** + String get analyticsEventContentSharedDescription; + + /// Label for the Content Save analytics event + /// + /// In en, this message translates to: + /// **'Content Save'** + String get analyticsEventContentSavedLabel; + + /// Description for the Content Save analytics event + /// + /// In en, this message translates to: + /// **'Track when a user bookmarks a headline.'** + String get analyticsEventContentSavedDescription; + + /// Label for the Content Unsave analytics event + /// + /// In en, this message translates to: + /// **'Content Unsave'** + String get analyticsEventContentUnsavedLabel; + + /// Description for the Content Unsave analytics event + /// + /// In en, this message translates to: + /// **'Track when a user removes a bookmark.'** + String get analyticsEventContentUnsavedDescription; + + /// Label for the Content Reading Time analytics event + /// + /// In en, this message translates to: + /// **'Content Reading Time'** + String get analyticsEventContentReadingTimeLabel; + + /// Description for the Content Reading Time analytics event + /// + /// In en, this message translates to: + /// **'Track the duration a user spends reading an article.'** + String get analyticsEventContentReadingTimeDescription; + + /// Label for the Reaction Added analytics event + /// + /// In en, this message translates to: + /// **'Reaction Added'** + String get analyticsEventReactionCreatedLabel; + + /// Description for the Reaction Added analytics event + /// + /// In en, this message translates to: + /// **'Track when a user reacts to content.'** + String get analyticsEventReactionCreatedDescription; + + /// Label for the Reaction Removed analytics event + /// + /// In en, this message translates to: + /// **'Reaction Removed'** + String get analyticsEventReactionDeletedLabel; + + /// Description for the Reaction Removed analytics event + /// + /// In en, this message translates to: + /// **'Track when a user removes their reaction.'** + String get analyticsEventReactionDeletedDescription; + + /// Label for the Comment Posted analytics event + /// + /// In en, this message translates to: + /// **'Comment Posted'** + String get analyticsEventCommentCreatedLabel; + + /// Description for the Comment Posted analytics event + /// + /// In en, this message translates to: + /// **'Track when a user submits a new comment.'** + String get analyticsEventCommentCreatedDescription; + + /// Label for the Comment Deleted analytics event + /// + /// In en, this message translates to: + /// **'Comment Deleted'** + String get analyticsEventCommentDeletedLabel; + + /// Description for the Comment Deleted analytics event + /// + /// In en, this message translates to: + /// **'Track when a user deletes their own comment.'** + String get analyticsEventCommentDeletedDescription; + + /// Label for the Report Submitted analytics event + /// + /// In en, this message translates to: + /// **'Report Submitted'** + String get analyticsEventReportSubmittedLabel; + + /// Description for the Report Submitted analytics event + /// + /// In en, this message translates to: + /// **'Track when a user reports content or other users.'** + String get analyticsEventReportSubmittedDescription; + + /// Label for the Headline Filter Creation analytics event + /// + /// In en, this message translates to: + /// **'Headline Filter Creation'** + String get analyticsEventHeadlineFilterCreatedLabel; + + /// Description for the Headline Filter Creation analytics event + /// + /// In en, this message translates to: + /// **'Track when a user creates a new custom headline filter.'** + String get analyticsEventHeadlineFilterCreatedDescription; + + /// Label for the Headline Filter Update analytics event + /// + /// In en, this message translates to: + /// **'Headline Filter Update'** + String get analyticsEventHeadlineFilterUpdatedLabel; + + /// Description for the Headline Filter Update analytics event + /// + /// In en, this message translates to: + /// **'Track when a user modifies an existing headline filter.'** + String get analyticsEventHeadlineFilterUpdatedDescription; + + /// Label for the Headline Filter Usage analytics event + /// + /// In en, this message translates to: + /// **'Headline Filter Usage'** + String get analyticsEventHeadlineFilterUsedLabel; + + /// Description for the Headline Filter Usage analytics event + /// + /// In en, this message translates to: + /// **'Track when a user applies a saved headline filter.'** + String get analyticsEventHeadlineFilterUsedDescription; + + /// Label for the Source Filter Creation analytics event + /// + /// In en, this message translates to: + /// **'Source Filter Creation'** + String get analyticsEventSourceFilterCreatedLabel; + + /// Description for the Source Filter Creation analytics event + /// + /// In en, this message translates to: + /// **'Track when a user creates a new source filter.'** + String get analyticsEventSourceFilterCreatedDescription; + + /// Label for the Source Filter Update analytics event + /// + /// In en, this message translates to: + /// **'Source Filter Update'** + String get analyticsEventSourceFilterUpdatedLabel; + + /// Description for the Source Filter Update analytics event + /// + /// In en, this message translates to: + /// **'Track when a user modifies an existing source filter.'** + String get analyticsEventSourceFilterUpdatedDescription; + + /// Label for the Search Performed analytics event + /// + /// In en, this message translates to: + /// **'Search Performed'** + String get analyticsEventSearchPerformedLabel; + + /// Description for the Search Performed analytics event + /// + /// In en, this message translates to: + /// **'Track when a user performs a search query.'** + String get analyticsEventSearchPerformedDescription; + + /// Label for the Review Prompt Response analytics event + /// + /// In en, this message translates to: + /// **'Review Prompt Response'** + String get analyticsEventAppReviewPromptRespondedLabel; + + /// Description for the Review Prompt Response analytics event + /// + /// In en, this message translates to: + /// **'Track user responses to the internal \'Enjoying the app?\' prompt.'** + String get analyticsEventAppReviewPromptRespondedDescription; + + /// Label for the Store Review Request analytics event + /// + /// In en, this message translates to: + /// **'Store Review Request'** + String get analyticsEventAppReviewStoreRequestedLabel; + + /// Description for the Store Review Request analytics event + /// + /// In en, this message translates to: + /// **'Track when the native OS store review dialog is requested.'** + String get analyticsEventAppReviewStoreRequestedDescription; + + /// Label for the Limit Exceeded analytics event + /// + /// In en, this message translates to: + /// **'Limit Exceeded'** + String get analyticsEventLimitExceededLabel; + + /// Description for the Limit Exceeded analytics event + /// + /// In en, this message translates to: + /// **'Track when a user hits a usage limit (e.g., saved items limit).'** + String get analyticsEventLimitExceededDescription; + + /// Label for the Limit CTA Click analytics event + /// + /// In en, this message translates to: + /// **'Limit CTA Click'** + String get analyticsEventLimitExceededCtaClickedLabel; + + /// Description for the Limit CTA Click analytics event + /// + /// In en, this message translates to: + /// **'Track clicks on \'Upgrade\' or \'Link Account\' buttons in limit dialogs.'** + String get analyticsEventLimitExceededCtaClickedDescription; + + /// Label for the Paywall Impression analytics event + /// + /// In en, this message translates to: + /// **'Paywall Impression'** + String get analyticsEventPaywallPresentedLabel; + + /// Description for the Paywall Impression analytics event + /// + /// In en, this message translates to: + /// **'Track when the paywall screen is shown to a user.'** + String get analyticsEventPaywallPresentedDescription; + + /// Label for the Subscription Start analytics event + /// + /// In en, this message translates to: + /// **'Subscription Start'** + String get analyticsEventSubscriptionStartedLabel; + + /// Description for the Subscription Start analytics event + /// + /// In en, this message translates to: + /// **'Track when a user successfully starts a new subscription.'** + String get analyticsEventSubscriptionStartedDescription; + + /// Label for the Subscription Renewal analytics event + /// + /// In en, this message translates to: + /// **'Subscription Renewal'** + String get analyticsEventSubscriptionRenewedLabel; + + /// Description for the Subscription Renewal analytics event + /// + /// In en, this message translates to: + /// **'Track when a subscription is automatically or manually renewed.'** + String get analyticsEventSubscriptionRenewedDescription; + + /// Label for the Subscription Cancellation analytics event + /// + /// In en, this message translates to: + /// **'Subscription Cancellation'** + String get analyticsEventSubscriptionCancelledLabel; + + /// Description for the Subscription Cancellation analytics event + /// + /// In en, this message translates to: + /// **'Track when a user cancels their subscription.'** + String get analyticsEventSubscriptionCancelledDescription; + + /// Label for the Subscription End analytics event + /// + /// In en, this message translates to: + /// **'Subscription End'** + String get analyticsEventSubscriptionEndedLabel; + + /// Description for the Subscription End analytics event + /// + /// In en, this message translates to: + /// **'Track when a subscription expires or is terminated.'** + String get analyticsEventSubscriptionEndedDescription; + + /// Label for the Ad Impression analytics event + /// + /// In en, this message translates to: + /// **'Ad Impression'** + String get analyticsEventAdImpressionLabel; + + /// Description for the Ad Impression analytics event + /// + /// In en, this message translates to: + /// **'Track when an advertisement is displayed to the user.'** + String get analyticsEventAdImpressionDescription; + + /// Label for the Ad Click analytics event + /// + /// In en, this message translates to: + /// **'Ad Click'** + String get analyticsEventAdClickedLabel; + + /// Description for the Ad Click analytics event + /// + /// In en, this message translates to: + /// **'Track when a user clicks on an advertisement.'** + String get analyticsEventAdClickedDescription; + + /// Label for the Ad Load Failure analytics event + /// + /// In en, this message translates to: + /// **'Ad Load Failure'** + String get analyticsEventAdLoadFailedLabel; + + /// Description for the Ad Load Failure analytics event + /// + /// In en, this message translates to: + /// **'Track errors when attempting to load advertisements.'** + String get analyticsEventAdLoadFailedDescription; + + /// Label for the Ad Reward Earned analytics event + /// + /// In en, this message translates to: + /// **'Ad Reward Earned'** + String get analyticsEventAdRewardEarnedLabel; + + /// Description for the Ad Reward Earned analytics event + /// + /// In en, this message translates to: + /// **'Track when a user completes a rewarded ad action.'** + String get analyticsEventAdRewardEarnedDescription; + + /// Label for the Theme Change analytics event + /// + /// In en, this message translates to: + /// **'Theme Change'** + String get analyticsEventThemeChangedLabel; + + /// Description for the Theme Change analytics event + /// + /// In en, this message translates to: + /// **'Track when a user changes the application theme.'** + String get analyticsEventThemeChangedDescription; + + /// Label for the Language Change analytics event + /// + /// In en, this message translates to: + /// **'Language Change'** + String get analyticsEventLanguageChangedLabel; + + /// Description for the Language Change analytics event + /// + /// In en, this message translates to: + /// **'Track when a user changes the application language.'** + String get analyticsEventLanguageChangedDescription; + + /// Label for the Feed Density Change analytics event + /// + /// In en, this message translates to: + /// **'Feed Density Change'** + String get analyticsEventFeedDensityChangedLabel; + + /// Description for the Feed Density Change analytics event + /// + /// In en, this message translates to: + /// **'Track when a user adjusts the information density of the feed.'** + String get analyticsEventFeedDensityChangedDescription; + + /// Label for the Browser Preference Change analytics event + /// + /// In en, this message translates to: + /// **'Browser Preference Change'** + String get analyticsEventBrowserChoiceChangedLabel; + + /// Description for the Browser Preference Change analytics event + /// + /// In en, this message translates to: + /// **'Track when a user changes their preferred browser for opening links.'** + String get analyticsEventBrowserChoiceChangedDescription; + + /// Label for the Source Filter Usage analytics event + /// + /// In en, this message translates to: + /// **'Source Filter Usage'** + String get analyticsEventSourceFilterUsedLabel; + + /// Description for the Source Filter Usage analytics event + /// + /// In en, this message translates to: + /// **'Track when a user applies a saved source filter.'** + String get analyticsEventSourceFilterUsedDescription; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 88cd0adf..a10b849b 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -2400,4 +2400,272 @@ class AppLocalizationsAr extends AppLocalizations { @override String get analyticsProviderMixpanel => 'Mixpanel'; + + @override + String get analyticsEventUserRegisteredLabel => 'تسجيل مستخدم جديد'; + + @override + String get analyticsEventUserRegisteredDescription => + 'تتبع عندما يقوم مستخدم جديد بإنشاء حساب بنجاح.'; + + @override + String get analyticsEventUserLoginLabel => 'تسجيل دخول المستخدم'; + + @override + String get analyticsEventUserLoginDescription => + 'تتبع عندما يقوم المستخدم بتسجيل الدخول إلى التطبيق.'; + + @override + String get analyticsEventAccountLinkedLabel => 'ربط الحساب'; + + @override + String get analyticsEventAccountLinkedDescription => + 'تتبع عندما يقوم مستخدم ضيف بربط حسابه ببيانات اعتماد دائمة.'; + + @override + String get analyticsEventUserRoleChangedLabel => 'تغيير دور المستخدم'; + + @override + String get analyticsEventUserRoleChangedDescription => + 'تتبع عندما يتم تحديث دور المستخدم (مثل الترقية إلى مميز).'; + + @override + String get analyticsEventContentViewedLabel => 'مشاهدة المحتوى'; + + @override + String get analyticsEventContentViewedDescription => + 'تتبع عندما يشاهد المستخدم عنوانًا أو مقالًا.'; + + @override + String get analyticsEventContentSharedLabel => 'مشاركة المحتوى'; + + @override + String get analyticsEventContentSharedDescription => + 'تتبع عندما يشارك المستخدم المحتوى عبر منصات خارجية.'; + + @override + String get analyticsEventContentSavedLabel => 'حفظ المحتوى'; + + @override + String get analyticsEventContentSavedDescription => + 'تتبع عندما يقوم المستخدم بحفظ عنوان في المفضلة.'; + + @override + String get analyticsEventContentUnsavedLabel => 'إلغاء حفظ المحتوى'; + + @override + String get analyticsEventContentUnsavedDescription => + 'تتبع عندما يزيل المستخدم عنوانًا من المفضلة.'; + + @override + String get analyticsEventContentReadingTimeLabel => 'وقت قراءة المحتوى'; + + @override + String get analyticsEventContentReadingTimeDescription => + 'تتبع المدة التي يقضيها المستخدم في قراءة مقال.'; + + @override + String get analyticsEventReactionCreatedLabel => 'إضافة رد فعل'; + + @override + String get analyticsEventReactionCreatedDescription => + 'تتبع عندما يتفاعل المستخدم مع المحتوى.'; + + @override + String get analyticsEventReactionDeletedLabel => 'إزالة رد فعل'; + + @override + String get analyticsEventReactionDeletedDescription => + 'تتبع عندما يزيل المستخدم رد فعله.'; + + @override + String get analyticsEventCommentCreatedLabel => 'نشر تعليق'; + + @override + String get analyticsEventCommentCreatedDescription => + 'تتبع عندما يرسل المستخدم تعليقًا جديدًا.'; + + @override + String get analyticsEventCommentDeletedLabel => 'حذف تعليق'; + + @override + String get analyticsEventCommentDeletedDescription => + 'تتبع عندما يحذف المستخدم تعليقه الخاص.'; + + @override + String get analyticsEventReportSubmittedLabel => 'تقديم بلاغ'; + + @override + String get analyticsEventReportSubmittedDescription => + 'تتبع عندما يبلغ المستخدم عن محتوى أو مستخدمين آخرين.'; + + @override + String get analyticsEventHeadlineFilterCreatedLabel => 'إنشاء مرشح عناوين'; + + @override + String get analyticsEventHeadlineFilterCreatedDescription => + 'تتبع عندما ينشئ المستخدم مرشح عناوين مخصص جديد.'; + + @override + String get analyticsEventHeadlineFilterUpdatedLabel => 'تحديث مرشح عناوين'; + + @override + String get analyticsEventHeadlineFilterUpdatedDescription => + 'تتبع عندما يعدل المستخدم مرشح عناوين موجود.'; + + @override + String get analyticsEventHeadlineFilterUsedLabel => 'استخدام مرشح عناوين'; + + @override + String get analyticsEventHeadlineFilterUsedDescription => + 'تتبع عندما يطبق المستخدم مرشح عناوين محفوظ.'; + + @override + String get analyticsEventSourceFilterCreatedLabel => 'إنشاء مرشح مصادر'; + + @override + String get analyticsEventSourceFilterCreatedDescription => + 'تتبع عندما ينشئ المستخدم مرشح مصادر جديد.'; + + @override + String get analyticsEventSourceFilterUpdatedLabel => 'تحديث مرشح مصادر'; + + @override + String get analyticsEventSourceFilterUpdatedDescription => + 'تتبع عندما يعدل المستخدم مرشح مصادر موجود.'; + + @override + String get analyticsEventSearchPerformedLabel => 'إجراء بحث'; + + @override + String get analyticsEventSearchPerformedDescription => + 'تتبع عندما يقوم المستخدم بإجراء استعلام بحث.'; + + @override + String get analyticsEventAppReviewPromptRespondedLabel => + 'استجابة لطلب التقييم'; + + @override + String get analyticsEventAppReviewPromptRespondedDescription => + 'تتبع استجابات المستخدم للموجه الداخلي \'هل تستمتع بالتطبيق؟\'.'; + + @override + String get analyticsEventAppReviewStoreRequestedLabel => 'طلب تقييم المتجر'; + + @override + String get analyticsEventAppReviewStoreRequestedDescription => + 'تتبع عندما يتم طلب مربع حوار مراجعة المتجر الأصلي لنظام التشغيل.'; + + @override + String get analyticsEventLimitExceededLabel => 'تجاوز الحد'; + + @override + String get analyticsEventLimitExceededDescription => + 'تتبع عندما يصل المستخدم إلى حد الاستخدام (مثل حد العناصر المحفوظة).'; + + @override + String get analyticsEventLimitExceededCtaClickedLabel => + 'النقر على إجراء الحد'; + + @override + String get analyticsEventLimitExceededCtaClickedDescription => + 'تتبع النقرات على أزرار \'ترقية\' أو \'ربط الحساب\' في مربعات حوار الحدود.'; + + @override + String get analyticsEventPaywallPresentedLabel => 'ظهور جدار الدفع'; + + @override + String get analyticsEventPaywallPresentedDescription => + 'تتبع عندما يتم عرض شاشة جدار الدفع للمستخدم.'; + + @override + String get analyticsEventSubscriptionStartedLabel => 'بدء الاشتراك'; + + @override + String get analyticsEventSubscriptionStartedDescription => + 'تتبع عندما يبدأ المستخدم اشتراكًا جديدًا بنجاح.'; + + @override + String get analyticsEventSubscriptionRenewedLabel => 'تجديد الاشتراك'; + + @override + String get analyticsEventSubscriptionRenewedDescription => + 'تتبع عندما يتم تجديد الاشتراك تلقائيًا أو يدويًا.'; + + @override + String get analyticsEventSubscriptionCancelledLabel => 'إلغاء الاشتراك'; + + @override + String get analyticsEventSubscriptionCancelledDescription => + 'تتبع عندما يقوم المستخدم بإلغاء اشتراكه.'; + + @override + String get analyticsEventSubscriptionEndedLabel => 'انتهاء الاشتراك'; + + @override + String get analyticsEventSubscriptionEndedDescription => + 'تتبع عندما تنتهي صلاحية الاشتراك أو يتم إنهاؤه.'; + + @override + String get analyticsEventAdImpressionLabel => 'ظهور إعلان'; + + @override + String get analyticsEventAdImpressionDescription => + 'تتبع عندما يتم عرض إعلان للمستخدم.'; + + @override + String get analyticsEventAdClickedLabel => 'النقر على إعلان'; + + @override + String get analyticsEventAdClickedDescription => + 'تتبع عندما ينقر المستخدم على إعلان.'; + + @override + String get analyticsEventAdLoadFailedLabel => 'فشل تحميل الإعلان'; + + @override + String get analyticsEventAdLoadFailedDescription => + 'تتبع الأخطاء عند محاولة تحميل الإعلانات.'; + + @override + String get analyticsEventAdRewardEarnedLabel => 'كسب مكافأة الإعلان'; + + @override + String get analyticsEventAdRewardEarnedDescription => + 'تتبع عندما يكمل المستخدم إجراء إعلان بمكافأة.'; + + @override + String get analyticsEventThemeChangedLabel => 'تغيير السمة'; + + @override + String get analyticsEventThemeChangedDescription => + 'تتبع عندما يغير المستخدم سمة التطبيق.'; + + @override + String get analyticsEventLanguageChangedLabel => 'تغيير اللغة'; + + @override + String get analyticsEventLanguageChangedDescription => + 'تتبع عندما يغير المستخدم لغة التطبيق.'; + + @override + String get analyticsEventFeedDensityChangedLabel => 'تغيير كثافة الموجز'; + + @override + String get analyticsEventFeedDensityChangedDescription => + 'تتبع عندما يعدل المستخدم كثافة المعلومات في الموجز.'; + + @override + String get analyticsEventBrowserChoiceChangedLabel => 'تغيير تفضيل المتصفح'; + + @override + String get analyticsEventBrowserChoiceChangedDescription => + 'تتبع عندما يغير المستخدم متصفحه المفضل لفتح الروابط.'; + + @override + String get analyticsEventSourceFilterUsedLabel => 'استخدام مرشح مصادر'; + + @override + String get analyticsEventSourceFilterUsedDescription => + 'تتبع عندما يطبق المستخدم مرشح مصادر محفوظ.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 39801969..1c598751 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2406,4 +2406,276 @@ class AppLocalizationsEn extends AppLocalizations { @override String get analyticsProviderMixpanel => 'Mixpanel'; + + @override + String get analyticsEventUserRegisteredLabel => 'User Registration'; + + @override + String get analyticsEventUserRegisteredDescription => + 'Track when a new user successfully creates an account.'; + + @override + String get analyticsEventUserLoginLabel => 'User Login'; + + @override + String get analyticsEventUserLoginDescription => + 'Track when a user logs in to the application.'; + + @override + String get analyticsEventAccountLinkedLabel => 'Account Linking'; + + @override + String get analyticsEventAccountLinkedDescription => + 'Track when a guest user links their account to a permanent credential.'; + + @override + String get analyticsEventUserRoleChangedLabel => 'User Role Change'; + + @override + String get analyticsEventUserRoleChangedDescription => + 'Track when a user\'s role is updated (e.g., upgraded to Premium).'; + + @override + String get analyticsEventContentViewedLabel => 'Content View'; + + @override + String get analyticsEventContentViewedDescription => + 'Track when a user views a headline or article.'; + + @override + String get analyticsEventContentSharedLabel => 'Content Share'; + + @override + String get analyticsEventContentSharedDescription => + 'Track when a user shares content via external platforms.'; + + @override + String get analyticsEventContentSavedLabel => 'Content Save'; + + @override + String get analyticsEventContentSavedDescription => + 'Track when a user bookmarks a headline.'; + + @override + String get analyticsEventContentUnsavedLabel => 'Content Unsave'; + + @override + String get analyticsEventContentUnsavedDescription => + 'Track when a user removes a bookmark.'; + + @override + String get analyticsEventContentReadingTimeLabel => 'Content Reading Time'; + + @override + String get analyticsEventContentReadingTimeDescription => + 'Track the duration a user spends reading an article.'; + + @override + String get analyticsEventReactionCreatedLabel => 'Reaction Added'; + + @override + String get analyticsEventReactionCreatedDescription => + 'Track when a user reacts to content.'; + + @override + String get analyticsEventReactionDeletedLabel => 'Reaction Removed'; + + @override + String get analyticsEventReactionDeletedDescription => + 'Track when a user removes their reaction.'; + + @override + String get analyticsEventCommentCreatedLabel => 'Comment Posted'; + + @override + String get analyticsEventCommentCreatedDescription => + 'Track when a user submits a new comment.'; + + @override + String get analyticsEventCommentDeletedLabel => 'Comment Deleted'; + + @override + String get analyticsEventCommentDeletedDescription => + 'Track when a user deletes their own comment.'; + + @override + String get analyticsEventReportSubmittedLabel => 'Report Submitted'; + + @override + String get analyticsEventReportSubmittedDescription => + 'Track when a user reports content or other users.'; + + @override + String get analyticsEventHeadlineFilterCreatedLabel => + 'Headline Filter Creation'; + + @override + String get analyticsEventHeadlineFilterCreatedDescription => + 'Track when a user creates a new custom headline filter.'; + + @override + String get analyticsEventHeadlineFilterUpdatedLabel => + 'Headline Filter Update'; + + @override + String get analyticsEventHeadlineFilterUpdatedDescription => + 'Track when a user modifies an existing headline filter.'; + + @override + String get analyticsEventHeadlineFilterUsedLabel => 'Headline Filter Usage'; + + @override + String get analyticsEventHeadlineFilterUsedDescription => + 'Track when a user applies a saved headline filter.'; + + @override + String get analyticsEventSourceFilterCreatedLabel => 'Source Filter Creation'; + + @override + String get analyticsEventSourceFilterCreatedDescription => + 'Track when a user creates a new source filter.'; + + @override + String get analyticsEventSourceFilterUpdatedLabel => 'Source Filter Update'; + + @override + String get analyticsEventSourceFilterUpdatedDescription => + 'Track when a user modifies an existing source filter.'; + + @override + String get analyticsEventSearchPerformedLabel => 'Search Performed'; + + @override + String get analyticsEventSearchPerformedDescription => + 'Track when a user performs a search query.'; + + @override + String get analyticsEventAppReviewPromptRespondedLabel => + 'Review Prompt Response'; + + @override + String get analyticsEventAppReviewPromptRespondedDescription => + 'Track user responses to the internal \'Enjoying the app?\' prompt.'; + + @override + String get analyticsEventAppReviewStoreRequestedLabel => + 'Store Review Request'; + + @override + String get analyticsEventAppReviewStoreRequestedDescription => + 'Track when the native OS store review dialog is requested.'; + + @override + String get analyticsEventLimitExceededLabel => 'Limit Exceeded'; + + @override + String get analyticsEventLimitExceededDescription => + 'Track when a user hits a usage limit (e.g., saved items limit).'; + + @override + String get analyticsEventLimitExceededCtaClickedLabel => 'Limit CTA Click'; + + @override + String get analyticsEventLimitExceededCtaClickedDescription => + 'Track clicks on \'Upgrade\' or \'Link Account\' buttons in limit dialogs.'; + + @override + String get analyticsEventPaywallPresentedLabel => 'Paywall Impression'; + + @override + String get analyticsEventPaywallPresentedDescription => + 'Track when the paywall screen is shown to a user.'; + + @override + String get analyticsEventSubscriptionStartedLabel => 'Subscription Start'; + + @override + String get analyticsEventSubscriptionStartedDescription => + 'Track when a user successfully starts a new subscription.'; + + @override + String get analyticsEventSubscriptionRenewedLabel => 'Subscription Renewal'; + + @override + String get analyticsEventSubscriptionRenewedDescription => + 'Track when a subscription is automatically or manually renewed.'; + + @override + String get analyticsEventSubscriptionCancelledLabel => + 'Subscription Cancellation'; + + @override + String get analyticsEventSubscriptionCancelledDescription => + 'Track when a user cancels their subscription.'; + + @override + String get analyticsEventSubscriptionEndedLabel => 'Subscription End'; + + @override + String get analyticsEventSubscriptionEndedDescription => + 'Track when a subscription expires or is terminated.'; + + @override + String get analyticsEventAdImpressionLabel => 'Ad Impression'; + + @override + String get analyticsEventAdImpressionDescription => + 'Track when an advertisement is displayed to the user.'; + + @override + String get analyticsEventAdClickedLabel => 'Ad Click'; + + @override + String get analyticsEventAdClickedDescription => + 'Track when a user clicks on an advertisement.'; + + @override + String get analyticsEventAdLoadFailedLabel => 'Ad Load Failure'; + + @override + String get analyticsEventAdLoadFailedDescription => + 'Track errors when attempting to load advertisements.'; + + @override + String get analyticsEventAdRewardEarnedLabel => 'Ad Reward Earned'; + + @override + String get analyticsEventAdRewardEarnedDescription => + 'Track when a user completes a rewarded ad action.'; + + @override + String get analyticsEventThemeChangedLabel => 'Theme Change'; + + @override + String get analyticsEventThemeChangedDescription => + 'Track when a user changes the application theme.'; + + @override + String get analyticsEventLanguageChangedLabel => 'Language Change'; + + @override + String get analyticsEventLanguageChangedDescription => + 'Track when a user changes the application language.'; + + @override + String get analyticsEventFeedDensityChangedLabel => 'Feed Density Change'; + + @override + String get analyticsEventFeedDensityChangedDescription => + 'Track when a user adjusts the information density of the feed.'; + + @override + String get analyticsEventBrowserChoiceChangedLabel => + 'Browser Preference Change'; + + @override + String get analyticsEventBrowserChoiceChangedDescription => + 'Track when a user changes their preferred browser for opening links.'; + + @override + String get analyticsEventSourceFilterUsedLabel => 'Source Filter Usage'; + + @override + String get analyticsEventSourceFilterUsedDescription => + 'Track when a user applies a saved source filter.'; } From 66c7226fcef1752436d940c4d34ad7efaf759bc4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 10:47:40 +0100 Subject: [PATCH 10/24] feat(app_configuration): add analytics configuration to features tab - Add Analytics config form to FeaturesConfigurationTab - Implement ExpansionTile for analytics settings - Include analytics description and icon - Ensure proper spacing and styling for new configuration section --- .../view/tabs/features_configuration_tab.dart | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/lib/app_configuration/view/tabs/features_configuration_tab.dart b/lib/app_configuration/view/tabs/features_configuration_tab.dart index 84a1592e..c0ec3754 100644 --- a/lib/app_configuration/view/tabs/features_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -222,6 +222,49 @@ class _FeaturesConfigurationTabState extends State { ), const SizedBox(height: AppSpacing.lg), + // Analytics + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 2; + return ExpansionTile( + leading: Icon( + Icons.analytics_outlined, + color: Theme.of(context).colorScheme.onSurface.withOpacity( + 0.7, + ), + ), + key: ValueKey('analyticsTile_$expandedIndex'), + title: Text(l10n.analyticsTab), + subtitle: Text( + l10n.analyticsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), + ), + onExpansionChanged: (bool isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.xxl, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnalyticsConfigForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + // Feed ValueListenableBuilder( valueListenable: _expandedTileIndex, From 6e7034c38099a1fffdd005e5a01e12368a5327a7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 10:47:55 +0100 Subject: [PATCH 11/24] feat(analytics): enhance event configuration with labels and descriptions - Add labels and descriptions to analytics events in the configuration form - Implement `_getEventLabel` and `_getEventDescription` methods - Update UI to display event labels and descriptions - Improve code readability and type safety --- .../widgets/analytics_config_form.dart | 178 +++++++++++++++++- 1 file changed, 174 insertions(+), 4 deletions(-) diff --git a/lib/app_configuration/widgets/analytics_config_form.dart b/lib/app_configuration/widgets/analytics_config_form.dart index df4ecad5..c4089535 100644 --- a/lib/app_configuration/widgets/analytics_config_form.dart +++ b/lib/app_configuration/widgets/analytics_config_form.dart @@ -133,13 +133,17 @@ class AnalyticsConfigForm extends StatelessWidget { return Column( children: [ CheckboxListTile( - title: Text(event.name), + title: Text(_getEventLabel(context, event)), + subtitle: Text( + _getEventDescription(context, event), + style: Theme.of(context).textTheme.bodySmall, + ), value: isEnabled, onChanged: (value) { final newDisabledEvents = Set.from( config.disabledEvents, ); - if (value ?? false) { + if (value == true) { newDisabledEvents.remove(event); } else { newDisabledEvents.add(event); @@ -173,8 +177,8 @@ class AnalyticsConfigForm extends StatelessWidget { Expanded( child: Slider( value: samplingRate, - min: 0, - max: 1, + min: 0.0, + max: 1.0, divisions: 20, label: '${(samplingRate * 100).toInt()}%', onChanged: (value) { @@ -223,4 +227,170 @@ class AnalyticsConfigForm extends StatelessWidget { return 'Demo'; } } + + String _getEventLabel(BuildContext context, AnalyticsEvent event) { + final l10n = AppLocalizationsX(context).l10n; + switch (event) { + case AnalyticsEvent.userRegistered: + return l10n.analyticsEventUserRegisteredLabel; + case AnalyticsEvent.userLogin: + return l10n.analyticsEventUserLoginLabel; + case AnalyticsEvent.accountLinked: + return l10n.analyticsEventAccountLinkedLabel; + case AnalyticsEvent.userRoleChanged: + return l10n.analyticsEventUserRoleChangedLabel; + case AnalyticsEvent.contentViewed: + return l10n.analyticsEventContentViewedLabel; + case AnalyticsEvent.contentShared: + return l10n.analyticsEventContentSharedLabel; + case AnalyticsEvent.contentSaved: + return l10n.analyticsEventContentSavedLabel; + case AnalyticsEvent.contentUnsaved: + return l10n.analyticsEventContentUnsavedLabel; + case AnalyticsEvent.contentReadingTime: + return l10n.analyticsEventContentReadingTimeLabel; + case AnalyticsEvent.reactionCreated: + return l10n.analyticsEventReactionCreatedLabel; + case AnalyticsEvent.reactionDeleted: + return l10n.analyticsEventReactionDeletedLabel; + case AnalyticsEvent.commentCreated: + return l10n.analyticsEventCommentCreatedLabel; + case AnalyticsEvent.commentDeleted: + return l10n.analyticsEventCommentDeletedLabel; + case AnalyticsEvent.reportSubmitted: + return l10n.analyticsEventReportSubmittedLabel; + case AnalyticsEvent.headlineFilterCreated: + return l10n.analyticsEventHeadlineFilterCreatedLabel; + case AnalyticsEvent.headlineFilterUpdated: + return l10n.analyticsEventHeadlineFilterUpdatedLabel; + case AnalyticsEvent.headlineFilterUsed: + return l10n.analyticsEventHeadlineFilterUsedLabel; + case AnalyticsEvent.sourceFilterCreated: + return l10n.analyticsEventSourceFilterCreatedLabel; + case AnalyticsEvent.sourceFilterUpdated: + return l10n.analyticsEventSourceFilterUpdatedLabel; + case AnalyticsEvent.searchPerformed: + return l10n.analyticsEventSearchPerformedLabel; + case AnalyticsEvent.appReviewPromptResponded: + return l10n.analyticsEventAppReviewPromptRespondedLabel; + case AnalyticsEvent.appReviewStoreRequested: + return l10n.analyticsEventAppReviewStoreRequestedLabel; + case AnalyticsEvent.limitExceeded: + return l10n.analyticsEventLimitExceededLabel; + case AnalyticsEvent.limitExceededCtaClicked: + return l10n.analyticsEventLimitExceededCtaClickedLabel; + case AnalyticsEvent.paywallPresented: + return l10n.analyticsEventPaywallPresentedLabel; + case AnalyticsEvent.subscriptionStarted: + return l10n.analyticsEventSubscriptionStartedLabel; + case AnalyticsEvent.subscriptionRenewed: + return l10n.analyticsEventSubscriptionRenewedLabel; + case AnalyticsEvent.subscriptionCancelled: + return l10n.analyticsEventSubscriptionCancelledLabel; + case AnalyticsEvent.subscriptionEnded: + return l10n.analyticsEventSubscriptionEndedLabel; + case AnalyticsEvent.adImpression: + return l10n.analyticsEventAdImpressionLabel; + case AnalyticsEvent.adClicked: + return l10n.analyticsEventAdClickedLabel; + case AnalyticsEvent.adLoadFailed: + return l10n.analyticsEventAdLoadFailedLabel; + case AnalyticsEvent.adRewardEarned: + return l10n.analyticsEventAdRewardEarnedLabel; + case AnalyticsEvent.themeChanged: + return l10n.analyticsEventThemeChangedLabel; + case AnalyticsEvent.languageChanged: + return l10n.analyticsEventLanguageChangedLabel; + case AnalyticsEvent.feedDensityChanged: + return l10n.analyticsEventFeedDensityChangedLabel; + case AnalyticsEvent.browserChoiceChanged: + return l10n.analyticsEventBrowserChoiceChangedLabel; + case AnalyticsEvent.sourceFilterUsed: + // TODO: Handle this case. + throw UnimplementedError(); + } + } + + String _getEventDescription(BuildContext context, AnalyticsEvent event) { + final l10n = AppLocalizationsX(context).l10n; + switch (event) { + case AnalyticsEvent.userRegistered: + return l10n.analyticsEventUserRegisteredDescription; + case AnalyticsEvent.userLogin: + return l10n.analyticsEventUserLoginDescription; + case AnalyticsEvent.accountLinked: + return l10n.analyticsEventAccountLinkedDescription; + case AnalyticsEvent.userRoleChanged: + return l10n.analyticsEventUserRoleChangedDescription; + case AnalyticsEvent.contentViewed: + return l10n.analyticsEventContentViewedDescription; + case AnalyticsEvent.contentShared: + return l10n.analyticsEventContentSharedDescription; + case AnalyticsEvent.contentSaved: + return l10n.analyticsEventContentSavedDescription; + case AnalyticsEvent.contentUnsaved: + return l10n.analyticsEventContentUnsavedDescription; + case AnalyticsEvent.contentReadingTime: + return l10n.analyticsEventContentReadingTimeDescription; + case AnalyticsEvent.reactionCreated: + return l10n.analyticsEventReactionCreatedDescription; + case AnalyticsEvent.reactionDeleted: + return l10n.analyticsEventReactionDeletedDescription; + case AnalyticsEvent.commentCreated: + return l10n.analyticsEventCommentCreatedDescription; + case AnalyticsEvent.commentDeleted: + return l10n.analyticsEventCommentDeletedDescription; + case AnalyticsEvent.reportSubmitted: + return l10n.analyticsEventReportSubmittedDescription; + case AnalyticsEvent.headlineFilterCreated: + return l10n.analyticsEventHeadlineFilterCreatedDescription; + case AnalyticsEvent.headlineFilterUpdated: + return l10n.analyticsEventHeadlineFilterUpdatedDescription; + case AnalyticsEvent.headlineFilterUsed: + return l10n.analyticsEventHeadlineFilterUsedDescription; + case AnalyticsEvent.sourceFilterCreated: + return l10n.analyticsEventSourceFilterCreatedDescription; + case AnalyticsEvent.sourceFilterUpdated: + return l10n.analyticsEventSourceFilterUpdatedDescription; + case AnalyticsEvent.searchPerformed: + return l10n.analyticsEventSearchPerformedDescription; + case AnalyticsEvent.appReviewPromptResponded: + return l10n.analyticsEventAppReviewPromptRespondedDescription; + case AnalyticsEvent.appReviewStoreRequested: + return l10n.analyticsEventAppReviewStoreRequestedDescription; + case AnalyticsEvent.limitExceeded: + return l10n.analyticsEventLimitExceededDescription; + case AnalyticsEvent.limitExceededCtaClicked: + return l10n.analyticsEventLimitExceededCtaClickedDescription; + case AnalyticsEvent.paywallPresented: + return l10n.analyticsEventPaywallPresentedDescription; + case AnalyticsEvent.subscriptionStarted: + return l10n.analyticsEventSubscriptionStartedDescription; + case AnalyticsEvent.subscriptionRenewed: + return l10n.analyticsEventSubscriptionRenewedDescription; + case AnalyticsEvent.subscriptionCancelled: + return l10n.analyticsEventSubscriptionCancelledDescription; + case AnalyticsEvent.subscriptionEnded: + return l10n.analyticsEventSubscriptionEndedDescription; + case AnalyticsEvent.adImpression: + return l10n.analyticsEventAdImpressionDescription; + case AnalyticsEvent.adClicked: + return l10n.analyticsEventAdClickedDescription; + case AnalyticsEvent.adLoadFailed: + return l10n.analyticsEventAdLoadFailedDescription; + case AnalyticsEvent.adRewardEarned: + return l10n.analyticsEventAdRewardEarnedDescription; + case AnalyticsEvent.themeChanged: + return l10n.analyticsEventThemeChangedDescription; + case AnalyticsEvent.languageChanged: + return l10n.analyticsEventLanguageChangedDescription; + case AnalyticsEvent.feedDensityChanged: + return l10n.analyticsEventFeedDensityChangedDescription; + case AnalyticsEvent.browserChoiceChanged: + return l10n.analyticsEventBrowserChoiceChangedDescription; + case AnalyticsEvent.sourceFilterUsed: + // TODO: Handle this case. + throw UnimplementedError(); + } + } } From a4714de601672fd5d96adeded4174ad81e2d5e6a Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 10:49:26 +0100 Subject: [PATCH 12/24] fix(analytics): handle null values and update event descriptions - Update null safety for event enabling toggle - Remove unnecessary decimals in slider definition - Add missing case description for source filter event --- .../widgets/analytics_config_form.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/app_configuration/widgets/analytics_config_form.dart b/lib/app_configuration/widgets/analytics_config_form.dart index c4089535..97dcc9e4 100644 --- a/lib/app_configuration/widgets/analytics_config_form.dart +++ b/lib/app_configuration/widgets/analytics_config_form.dart @@ -143,7 +143,7 @@ class AnalyticsConfigForm extends StatelessWidget { final newDisabledEvents = Set.from( config.disabledEvents, ); - if (value == true) { + if (value ?? false) { newDisabledEvents.remove(event); } else { newDisabledEvents.add(event); @@ -177,8 +177,8 @@ class AnalyticsConfigForm extends StatelessWidget { Expanded( child: Slider( value: samplingRate, - min: 0.0, - max: 1.0, + min: 0, + max: 1, divisions: 20, label: '${(samplingRate * 100).toInt()}%', onChanged: (value) { @@ -306,8 +306,7 @@ class AnalyticsConfigForm extends StatelessWidget { case AnalyticsEvent.browserChoiceChanged: return l10n.analyticsEventBrowserChoiceChangedLabel; case AnalyticsEvent.sourceFilterUsed: - // TODO: Handle this case. - throw UnimplementedError(); + return l10n.analyticsEventSourceFilterUsedLabel; } } @@ -389,8 +388,7 @@ class AnalyticsConfigForm extends StatelessWidget { case AnalyticsEvent.browserChoiceChanged: return l10n.analyticsEventBrowserChoiceChangedDescription; case AnalyticsEvent.sourceFilterUsed: - // TODO: Handle this case. - throw UnimplementedError(); + return l10n.analyticsEventSourceFilterUsedDescription; } } } From 2311f4413fe161aa80106c1ee795ccbfb0a168c9 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 10:59:44 +0100 Subject: [PATCH 13/24] refactor(features-tab): remove redundnt analytics configuration - Remove the entire ExpansionTile block related to analytics configuration - Remove the unused SizedBox --- .../view/tabs/features_configuration_tab.dart | 45 +------------------ 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/lib/app_configuration/view/tabs/features_configuration_tab.dart b/lib/app_configuration/view/tabs/features_configuration_tab.dart index c0ec3754..7cf83824 100644 --- a/lib/app_configuration/view/tabs/features_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -221,50 +221,7 @@ class _FeaturesConfigurationTabState extends State { }, ), const SizedBox(height: AppSpacing.lg), - - // Analytics - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 2; - return ExpansionTile( - leading: Icon( - Icons.analytics_outlined, - color: Theme.of(context).colorScheme.onSurface.withOpacity( - 0.7, - ), - ), - key: ValueKey('analyticsTile_$expandedIndex'), - title: Text(l10n.analyticsTab), - subtitle: Text( - l10n.analyticsDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - ), - onExpansionChanged: (bool isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; - }, - initiallyExpanded: expandedIndex == tileIndex, - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.xxl, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - AnalyticsConfigForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), - ], - ); - }, - ), - const SizedBox(height: AppSpacing.lg), - + // Feed ValueListenableBuilder( valueListenable: _expandedTileIndex, From 35ce71800e94814fd1b0d91ff8943372708c4d40 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 11:09:21 +0100 Subject: [PATCH 14/24] feat(app_configuration): add feed config form widget - Implement FeedConfigForm widget for configuring feed settings - Add functionality to change feed item click behavior - Implement expandable list for feed decorators configuration - Integrate localization for all labels and descriptions --- .../widgets/feed_config_form.dart | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 lib/app_configuration/widgets/feed_config_form.dart diff --git a/lib/app_configuration/widgets/feed_config_form.dart b/lib/app_configuration/widgets/feed_config_form.dart new file mode 100644 index 00000000..49e5e9aa --- /dev/null +++ b/lib/app_configuration/widgets/feed_config_form.dart @@ -0,0 +1,153 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_decorator_form.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/feed_decorator_type_l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/feed_item_click_behavior_l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template feed_config_form} +/// A form widget for configuring feed settings. +/// {@endtemplate} +class FeedConfigForm extends StatelessWidget { + /// {@macro feed_config_form} + const FeedConfigForm({ + 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; + + String _getDecoratorDescription( + BuildContext context, + FeedDecoratorType type, + ) { + final l10n = AppLocalizationsX(context).l10n; + switch (type) { + case FeedDecoratorType.linkAccount: + return l10n.feedDecoratorLinkAccountDescription; + case FeedDecoratorType.upgrade: + return l10n.feedDecoratorUpgradeDescription; + case FeedDecoratorType.rateApp: + return l10n.feedDecoratorRateAppDescription; + case FeedDecoratorType.enableNotifications: + return l10n.feedDecoratorEnableNotificationsDescription; + case FeedDecoratorType.suggestedTopics: + return l10n.feedDecoratorSuggestedTopicsDescription; + case FeedDecoratorType.suggestedSources: + return l10n.feedDecoratorSuggestedSourcesDescription; + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ExpansionTile( + title: Text(l10n.feedItemClickBehaviorTitle), + subtitle: Text( + l10n.feedItemClickBehaviorDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), + ), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: SegmentedButton( + segments: FeedItemClickBehavior.values + .where( + (b) => b != FeedItemClickBehavior.defaultBehavior, + ) + .map( + (behavior) => ButtonSegment( + value: behavior, + label: Text(behavior.l10n(context)), + ), + ) + .toList(), + selected: { + remoteConfig.features.feed.itemClickBehavior, + }, + onSelectionChanged: (newSelection) { + onConfigChanged( + remoteConfig.copyWith( + features: remoteConfig.features.copyWith( + feed: remoteConfig.features.feed.copyWith( + itemClickBehavior: newSelection.first, + ), + ), + ), + ); + }, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.lg), + ExpansionTile( + title: Text(l10n.feedDecoratorsTitle), + subtitle: Text( + l10n.feedDecoratorsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), + ), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final decoratorType in FeedDecoratorType.values) + Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.md), + child: ExpansionTile( + title: Text(decoratorType.l10n(context)), + subtitle: Text( + _getDecoratorDescription(context, decoratorType), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), + ), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.xl, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + FeedDecoratorForm( + decoratorType: decoratorType, + remoteConfig: remoteConfig, + onConfigChanged: onConfigChanged, + ), + ], + ), + ), + ], + ), + ], + ); + } +} From d1e615b80e12f19f1fdf640444df52bfa495493c Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 11:09:50 +0100 Subject: [PATCH 15/24] refactor(app_configuration): simplify features configuration tab - Remove unused _getDecoratorDescription method - Replace individual feed-related widgets with FeedConfigForm - Conditionally render ad-related forms based on remoteConfig features - Remove direct manipulation of feed item click behavior --- .../view/tabs/features_configuration_tab.dart | 156 +++--------------- 1 file changed, 21 insertions(+), 135 deletions(-) diff --git a/lib/app_configuration/view/tabs/features_configuration_tab.dart b/lib/app_configuration/view/tabs/features_configuration_tab.dart index 7cf83824..b79d2a51 100644 --- a/lib/app_configuration/view/tabs/features_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -5,6 +5,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuratio import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/analytics_config_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/community_config_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_ad_settings_form.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_config_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_decorator_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/navigation_ad_settings_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/push_notification_settings_form.dart'; @@ -50,27 +51,6 @@ class _FeaturesConfigurationTabState extends State { super.dispose(); } - String _getDecoratorDescription( - BuildContext context, - FeedDecoratorType type, - ) { - final l10n = AppLocalizationsX(context).l10n; - switch (type) { - case FeedDecoratorType.linkAccount: - return l10n.feedDecoratorLinkAccountDescription; - case FeedDecoratorType.upgrade: - return l10n.feedDecoratorUpgradeDescription; - case FeedDecoratorType.rateApp: - return l10n.feedDecoratorRateAppDescription; - case FeedDecoratorType.enableNotifications: - return l10n.feedDecoratorEnableNotificationsDescription; - case FeedDecoratorType.suggestedTopics: - return l10n.feedDecoratorSuggestedTopicsDescription; - case FeedDecoratorType.suggestedSources: - return l10n.feedDecoratorSuggestedSourcesDescription; - } - } - @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; @@ -115,21 +95,23 @@ class _FeaturesConfigurationTabState extends State { remoteConfig: widget.remoteConfig, onConfigChanged: widget.onConfigChanged, ), - const SizedBox(height: AppSpacing.lg), - AdPlatformConfigForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), - const SizedBox(height: AppSpacing.lg), - FeedAdSettingsForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), - const SizedBox(height: AppSpacing.lg), - NavigationAdSettingsForm( - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), + if (widget.remoteConfig.features.ads.enabled) ...[ + const SizedBox(height: AppSpacing.lg), + AdPlatformConfigForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + const SizedBox(height: AppSpacing.lg), + FeedAdSettingsForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + const SizedBox(height: AppSpacing.lg), + NavigationAdSettingsForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + ), + ], ], ); }, @@ -255,105 +237,9 @@ class _FeaturesConfigurationTabState extends State { ), expandedCrossAxisAlignment: CrossAxisAlignment.start, children: [ - ExpansionTile( - title: Text(l10n.feedItemClickBehaviorTitle), - subtitle: Text( - l10n.feedItemClickBehaviorDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - ), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - Align( - alignment: AlignmentDirectional.centerStart, - child: SegmentedButton( - segments: FeedItemClickBehavior.values - .where( - (b) => b != FeedItemClickBehavior.defaultBehavior, - ) - .map( - (behavior) => - ButtonSegment( - value: behavior, - label: Text(behavior.l10n(context)), - ), - ) - .toList(), - selected: { - widget.remoteConfig.features.feed.itemClickBehavior, - }, - onSelectionChanged: (newSelection) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - features: widget.remoteConfig.features.copyWith( - feed: widget.remoteConfig.features.feed - .copyWith( - itemClickBehavior: newSelection.first, - ), - ), - ), - ); - }, - ), - ), - ], - ), - const SizedBox(height: AppSpacing.lg), - ExpansionTile( - title: Text(l10n.feedDecoratorsTitle), - subtitle: Text( - l10n.feedDecoratorsDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - ), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (final decoratorType in FeedDecoratorType.values) - Padding( - padding: const EdgeInsets.only(bottom: AppSpacing.md), - child: ExpansionTile( - title: Text(decoratorType.l10n(context)), - subtitle: Text( - _getDecoratorDescription(context, decoratorType), - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), - ), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.xl, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - FeedDecoratorForm( - decoratorType: decoratorType, - remoteConfig: widget.remoteConfig, - onConfigChanged: widget.onConfigChanged, - ), - ], - ), - ), - ], + FeedConfigForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, ), ], ); From a616919ac8711979722d1cbd2c74ae7b64c1585b Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 11:10:01 +0100 Subject: [PATCH 16/24] feat(analytics): conditionally render config sections based on analytics enablement - Wrap provider and events sections in a conditional check for analyticsConfig.enabled - Ensure these sections are only rendered if analytics is enabled --- .../widgets/analytics_config_form.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/app_configuration/widgets/analytics_config_form.dart b/lib/app_configuration/widgets/analytics_config_form.dart index 97dcc9e4..789deb1a 100644 --- a/lib/app_configuration/widgets/analytics_config_form.dart +++ b/lib/app_configuration/widgets/analytics_config_form.dart @@ -44,10 +44,12 @@ class AnalyticsConfigForm extends StatelessWidget { ); }, ), - const SizedBox(height: AppSpacing.lg), - _buildProviderSection(context, l10n, analyticsConfig), - const SizedBox(height: AppSpacing.lg), - _buildEventsSection(context, l10n, analyticsConfig), + if (analyticsConfig.enabled) ...[ + const SizedBox(height: AppSpacing.lg), + _buildProviderSection(context, l10n, analyticsConfig), + const SizedBox(height: AppSpacing.lg), + _buildEventsSection(context, l10n, analyticsConfig), + ], ], ); } From d1272154be829c2bfd2912b66a2ae31ced8d1efc Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 11:10:12 +0100 Subject: [PATCH 17/24] feat(push-notification): conditionally render settings sections based on push configuration - Wrap primary provider and delivery types sections in a conditional check for pushConfig.enabled - Ensure these sections are only displayed when push notifications are enabled --- .../widgets/push_notification_settings_form.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/app_configuration/widgets/push_notification_settings_form.dart b/lib/app_configuration/widgets/push_notification_settings_form.dart index c35089ac..b3d6eb70 100644 --- a/lib/app_configuration/widgets/push_notification_settings_form.dart +++ b/lib/app_configuration/widgets/push_notification_settings_form.dart @@ -46,10 +46,12 @@ class PushNotificationSettingsForm extends StatelessWidget { ); }, ), - const SizedBox(height: AppSpacing.lg), - _buildPrimaryProviderSection(context, l10n, pushConfig), - const SizedBox(height: AppSpacing.lg), - _buildDeliveryTypesSection(context, l10n, pushConfig), + if (pushConfig.enabled) ...[ + const SizedBox(height: AppSpacing.lg), + _buildPrimaryProviderSection(context, l10n, pushConfig), + const SizedBox(height: AppSpacing.lg), + _buildDeliveryTypesSection(context, l10n, pushConfig), + ], ], ); } From 7711cfe0865e96d0b78d36f6a7eed6efdfcaac34 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 11:12:49 +0100 Subject: [PATCH 18/24] style: format --- .../view/tabs/features_configuration_tab.dart | 2 +- .../widgets/analytics_config_form.dart | 180 +++++++++--------- .../widgets/feed_config_form.dart | 24 +-- 3 files changed, 102 insertions(+), 104 deletions(-) diff --git a/lib/app_configuration/view/tabs/features_configuration_tab.dart b/lib/app_configuration/view/tabs/features_configuration_tab.dart index b79d2a51..509836cb 100644 --- a/lib/app_configuration/view/tabs/features_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -203,7 +203,7 @@ class _FeaturesConfigurationTabState extends State { }, ), const SizedBox(height: AppSpacing.lg), - + // Feed ValueListenableBuilder( valueListenable: _expandedTileIndex, diff --git a/lib/app_configuration/widgets/analytics_config_form.dart b/lib/app_configuration/widgets/analytics_config_form.dart index 789deb1a..96879cd4 100644 --- a/lib/app_configuration/widgets/analytics_config_form.dart +++ b/lib/app_configuration/widgets/analytics_config_form.dart @@ -77,16 +77,15 @@ class AnalyticsConfigForm extends StatelessWidget { Align( alignment: AlignmentDirectional.centerStart, child: SegmentedButton( - segments: - AnalyticsProvider.values - .where((provider) => provider != AnalyticsProvider.demo) - .map((provider) { - return ButtonSegment( - value: provider, - label: Text(_getProviderLabel(l10n, provider)), - ); - }) - .toList(), + segments: AnalyticsProvider.values + .where((provider) => provider != AnalyticsProvider.demo) + .map((provider) { + return ButtonSegment( + value: provider, + label: Text(_getProviderLabel(l10n, provider)), + ); + }) + .toList(), selected: {config.activeProvider}, onSelectionChanged: (newSelection) { onConfigChanged( @@ -127,92 +126,91 @@ class AnalyticsConfigForm extends StatelessWidget { children: [ // We use a Column here instead of ListView because it's inside a scrollable parent Column( - children: - AnalyticsEvent.values.map((event) { - final isEnabled = !config.disabledEvents.contains(event); - final samplingRate = config.eventSamplingRates[event] ?? 1.0; + children: AnalyticsEvent.values.map((event) { + final isEnabled = !config.disabledEvents.contains(event); + final samplingRate = config.eventSamplingRates[event] ?? 1.0; - return Column( - children: [ - CheckboxListTile( - title: Text(_getEventLabel(context, event)), - subtitle: Text( - _getEventDescription(context, event), - style: Theme.of(context).textTheme.bodySmall, - ), - value: isEnabled, - onChanged: (value) { - final newDisabledEvents = Set.from( - config.disabledEvents, - ); - if (value ?? false) { - newDisabledEvents.remove(event); - } else { - newDisabledEvents.add(event); - } - onConfigChanged( - remoteConfig.copyWith( - features: remoteConfig.features.copyWith( - analytics: config.copyWith( - disabledEvents: newDisabledEvents, - ), - ), + return Column( + children: [ + CheckboxListTile( + title: Text(_getEventLabel(context, event)), + subtitle: Text( + _getEventDescription(context, event), + style: Theme.of(context).textTheme.bodySmall, + ), + value: isEnabled, + onChanged: (value) { + final newDisabledEvents = Set.from( + config.disabledEvents, + ); + if (value ?? false) { + newDisabledEvents.remove(event); + } else { + newDisabledEvents.add(event); + } + onConfigChanged( + remoteConfig.copyWith( + features: remoteConfig.features.copyWith( + analytics: config.copyWith( + disabledEvents: newDisabledEvents, ), - ); - }, + ), + ), + ); + }, + ), + if (isEnabled) + Padding( + padding: const EdgeInsetsDirectional.only( + start: AppSpacing.xxl, + end: AppSpacing.md, + bottom: AppSpacing.sm, ), - if (isEnabled) - Padding( - padding: const EdgeInsetsDirectional.only( - start: AppSpacing.xxl, - end: AppSpacing.md, - bottom: AppSpacing.sm, + child: Row( + children: [ + Text( + l10n.samplingRateLabel( + (samplingRate * 100).toInt(), + ), + style: Theme.of(context).textTheme.bodySmall, ), - child: Row( - children: [ - Text( - l10n.samplingRateLabel( - (samplingRate * 100).toInt(), - ), - style: Theme.of(context).textTheme.bodySmall, - ), - Expanded( - child: Slider( - value: samplingRate, - min: 0, - max: 1, - divisions: 20, - label: '${(samplingRate * 100).toInt()}%', - onChanged: (value) { - final newSamplingRates = - Map.from( - config.eventSamplingRates, - ); - // If value is 1.0, we can remove it from the map to save space/default - if (value == 1.0) { - newSamplingRates.remove(event); - } else { - newSamplingRates[event] = value; - } - onConfigChanged( - remoteConfig.copyWith( - features: remoteConfig.features.copyWith( - analytics: config.copyWith( - eventSamplingRates: newSamplingRates, - ), - ), - ), + Expanded( + child: Slider( + value: samplingRate, + min: 0, + max: 1, + divisions: 20, + label: '${(samplingRate * 100).toInt()}%', + onChanged: (value) { + final newSamplingRates = + Map.from( + config.eventSamplingRates, ); - }, - ), - ), - ], + // If value is 1.0, we can remove it from the map to save space/default + if (value == 1.0) { + newSamplingRates.remove(event); + } else { + newSamplingRates[event] = value; + } + onConfigChanged( + remoteConfig.copyWith( + features: remoteConfig.features.copyWith( + analytics: config.copyWith( + eventSamplingRates: newSamplingRates, + ), + ), + ), + ); + }, + ), ), - ), - const Divider(height: 1), - ], - ); - }).toList(), + ], + ), + ), + const Divider(height: 1), + ], + ); + }).toList(), ), ], ); @@ -308,7 +306,7 @@ class AnalyticsConfigForm extends StatelessWidget { case AnalyticsEvent.browserChoiceChanged: return l10n.analyticsEventBrowserChoiceChangedLabel; case AnalyticsEvent.sourceFilterUsed: - return l10n.analyticsEventSourceFilterUsedLabel; + return l10n.analyticsEventSourceFilterUsedLabel; } } diff --git a/lib/app_configuration/widgets/feed_config_form.dart b/lib/app_configuration/widgets/feed_config_form.dart index 49e5e9aa..5133278a 100644 --- a/lib/app_configuration/widgets/feed_config_form.dart +++ b/lib/app_configuration/widgets/feed_config_form.dart @@ -55,10 +55,10 @@ class FeedConfigForm extends StatelessWidget { subtitle: Text( l10n.feedItemClickBehaviorDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), childrenPadding: const EdgeInsetsDirectional.only( start: AppSpacing.lg, @@ -105,10 +105,10 @@ class FeedConfigForm extends StatelessWidget { subtitle: Text( l10n.feedDecoratorsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), childrenPadding: const EdgeInsetsDirectional.only( start: AppSpacing.lg, @@ -125,10 +125,10 @@ class FeedConfigForm extends StatelessWidget { subtitle: Text( _getDecoratorDescription(context, decoratorType), style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), childrenPadding: const EdgeInsetsDirectional.only( start: AppSpacing.xl, From 4f7d5f3ea1908e2f9b96ab6afd1d64ed08d990c8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 11:25:29 +0100 Subject: [PATCH 19/24] refactor(README): streamline remote control & app configuration details - Consolidate App Monetization & Remote Control sections into Real-Time App Configuration & Remote Control - Combine Dynamic Application Control and Centralized Monetization Engine into a single, cohesive section - Add Analytics & Data Insights subsection to highlight new capabilities - Improve readability and conciseness of the content --- README.md | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 2d93376f..9f12ed7d 100644 --- a/README.md +++ b/README.md @@ -59,24 +59,16 @@ Directly manage all user-generated content from a centralized command center. Re
-⚙️ App Monetization & Remote Control - -### 💸 Centralized Monetization Engine -Take direct control of your mobile app's revenue strategy. This integrated system allows you to manage your ad inventory and fine-tune display rules without ever touching the mobile app's code. -- **Flexible Ad Provider Strategy:** Instantly switch the mobile app's primary ad source between industry-standard networks like Google AdMob for maximum control. -- **Granular, Role-Based Rules:** Define precisely how and when ads are shown based on user subscription tiers (e.g., guest, standard, premium), optimizing both revenue and user experience. -> **Your Advantage:** Deploy a powerful, backend-driven monetization strategy. A/B test ad providers and adjust revenue models on the fly to respond to market demands in real-time. - ---- - -### 🚀 Real-Time Application Management -Dynamically control the mobile app's behavior and operational state directly from the dashboard, eliminating the need for constant app store updates. -- **Critical State Management:** Instantly activate a maintenance mode or enforce a mandatory app update for your users to handle operational issues or critical releases gracefully. -- **Dynamic In-App Content:** Remotely manage the visibility and behavior of in-feed promotional prompts and user engagement elements. -- **Tier-Based Feature Gating:** Define and enforce feature limits based on user roles, such as setting the maximum number of followed topics or saved headlines for different subscription levels. -- **Full Community Feature Control:** Remotely enable or disable the entire user engagement system (reactions, comments), the content reporting feature, and the in-app review funnel. Fine-tune engagement modes and configure rules for when and how users are prompted for feedback. -- **Global Notification Control:** Remotely enable or disable the entire push notification system, switch between providers (e.g., Firebase, OneSignal), and toggle specific delivery types like breaking news or daily digests. -> **Your Advantage:** Gain unparalleled agility to manage your live application. Ensure service stability, drive user actions, and configure business rules instantly, all from a centralized control panel. +⚙️ Real-Time App Configuration & Remote Control + +### 🚀 Dynamic Application Control +Dynamically control the mobile app's behavior, features, and operational state directly from the dashboard, eliminating the need for constant app store updates. +- **App Monetization:** Take direct control of your mobile app's revenue. Remotely manage ad providers like Google AdMob and define granular, role-based display rules to optimize revenue and user experience. +- **Feature Toggling:** Instantly enable or disable major features. Globally toggle the entire push notification system, the community engagement suite (reactions, comments), the content reporting feature, and the in-app review funnel. +- **Operational Control:** Ensure stability and manage releases gracefully. Activate a maintenance mode or enforce a mandatory app update for all users directly from the dashboard. +- **Content & Access Control:** Define and enforce feature limits based on user subscription tiers, such as setting the maximum number of saved articles. Remotely manage the visibility and behavior of in-feed promotional prompts. +- **Analytics & Data Insights:** Gain critical insights by remotely configuring the analytics system. Enable or disable the entire system, switch between providers like Firebase or Mixpanel, and fine-tune event tracking and sampling rates to understand user behavior without deploying new app versions. +> **Your Advantage:** Gain unparalleled agility to manage your live application. Ensure service stability, drive user actions, configure business rules, and adjust your data strategy instantly, all from a single, centralized control panel.
From 0e1aa407e0e49b6e5fee4645fe3666b706c5f4ce Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 11:33:33 +0100 Subject: [PATCH 20/24] refactor(analytics): improve event info retrieval - Combine event label and description into a single function - Simplify checkbox list tile creation with destructuring - Update function signature to use AppLocalizations directly - Return tuple with label and description for each event --- .../widgets/analytics_config_form.dart | 280 ++++++++++-------- 1 file changed, 156 insertions(+), 124 deletions(-) diff --git a/lib/app_configuration/widgets/analytics_config_form.dart b/lib/app_configuration/widgets/analytics_config_form.dart index 96879cd4..c4bd6a0a 100644 --- a/lib/app_configuration/widgets/analytics_config_form.dart +++ b/lib/app_configuration/widgets/analytics_config_form.dart @@ -129,13 +129,14 @@ class AnalyticsConfigForm extends StatelessWidget { children: AnalyticsEvent.values.map((event) { final isEnabled = !config.disabledEvents.contains(event); final samplingRate = config.eventSamplingRates[event] ?? 1.0; + final (label, description) = _getEventInfo(l10n, event); return Column( children: [ CheckboxListTile( - title: Text(_getEventLabel(context, event)), + title: Text(label), subtitle: Text( - _getEventDescription(context, event), + description, style: Theme.of(context).textTheme.bodySmall, ), value: isEnabled, @@ -228,167 +229,198 @@ class AnalyticsConfigForm extends StatelessWidget { } } - String _getEventLabel(BuildContext context, AnalyticsEvent event) { - final l10n = AppLocalizationsX(context).l10n; - switch (event) { - case AnalyticsEvent.userRegistered: - return l10n.analyticsEventUserRegisteredLabel; - case AnalyticsEvent.userLogin: - return l10n.analyticsEventUserLoginLabel; - case AnalyticsEvent.accountLinked: - return l10n.analyticsEventAccountLinkedLabel; - case AnalyticsEvent.userRoleChanged: - return l10n.analyticsEventUserRoleChangedLabel; - case AnalyticsEvent.contentViewed: - return l10n.analyticsEventContentViewedLabel; - case AnalyticsEvent.contentShared: - return l10n.analyticsEventContentSharedLabel; - case AnalyticsEvent.contentSaved: - return l10n.analyticsEventContentSavedLabel; - case AnalyticsEvent.contentUnsaved: - return l10n.analyticsEventContentUnsavedLabel; - case AnalyticsEvent.contentReadingTime: - return l10n.analyticsEventContentReadingTimeLabel; - case AnalyticsEvent.reactionCreated: - return l10n.analyticsEventReactionCreatedLabel; - case AnalyticsEvent.reactionDeleted: - return l10n.analyticsEventReactionDeletedLabel; - case AnalyticsEvent.commentCreated: - return l10n.analyticsEventCommentCreatedLabel; - case AnalyticsEvent.commentDeleted: - return l10n.analyticsEventCommentDeletedLabel; - case AnalyticsEvent.reportSubmitted: - return l10n.analyticsEventReportSubmittedLabel; - case AnalyticsEvent.headlineFilterCreated: - return l10n.analyticsEventHeadlineFilterCreatedLabel; - case AnalyticsEvent.headlineFilterUpdated: - return l10n.analyticsEventHeadlineFilterUpdatedLabel; - case AnalyticsEvent.headlineFilterUsed: - return l10n.analyticsEventHeadlineFilterUsedLabel; - case AnalyticsEvent.sourceFilterCreated: - return l10n.analyticsEventSourceFilterCreatedLabel; - case AnalyticsEvent.sourceFilterUpdated: - return l10n.analyticsEventSourceFilterUpdatedLabel; - case AnalyticsEvent.searchPerformed: - return l10n.analyticsEventSearchPerformedLabel; - case AnalyticsEvent.appReviewPromptResponded: - return l10n.analyticsEventAppReviewPromptRespondedLabel; - case AnalyticsEvent.appReviewStoreRequested: - return l10n.analyticsEventAppReviewStoreRequestedLabel; - case AnalyticsEvent.limitExceeded: - return l10n.analyticsEventLimitExceededLabel; - case AnalyticsEvent.limitExceededCtaClicked: - return l10n.analyticsEventLimitExceededCtaClickedLabel; - case AnalyticsEvent.paywallPresented: - return l10n.analyticsEventPaywallPresentedLabel; - case AnalyticsEvent.subscriptionStarted: - return l10n.analyticsEventSubscriptionStartedLabel; - case AnalyticsEvent.subscriptionRenewed: - return l10n.analyticsEventSubscriptionRenewedLabel; - case AnalyticsEvent.subscriptionCancelled: - return l10n.analyticsEventSubscriptionCancelledLabel; - case AnalyticsEvent.subscriptionEnded: - return l10n.analyticsEventSubscriptionEndedLabel; - case AnalyticsEvent.adImpression: - return l10n.analyticsEventAdImpressionLabel; - case AnalyticsEvent.adClicked: - return l10n.analyticsEventAdClickedLabel; - case AnalyticsEvent.adLoadFailed: - return l10n.analyticsEventAdLoadFailedLabel; - case AnalyticsEvent.adRewardEarned: - return l10n.analyticsEventAdRewardEarnedLabel; - case AnalyticsEvent.themeChanged: - return l10n.analyticsEventThemeChangedLabel; - case AnalyticsEvent.languageChanged: - return l10n.analyticsEventLanguageChangedLabel; - case AnalyticsEvent.feedDensityChanged: - return l10n.analyticsEventFeedDensityChangedLabel; - case AnalyticsEvent.browserChoiceChanged: - return l10n.analyticsEventBrowserChoiceChangedLabel; - case AnalyticsEvent.sourceFilterUsed: - return l10n.analyticsEventSourceFilterUsedLabel; - } - } - - String _getEventDescription(BuildContext context, AnalyticsEvent event) { - final l10n = AppLocalizationsX(context).l10n; + (String, String) _getEventInfo(AppLocalizations l10n, AnalyticsEvent event) { switch (event) { case AnalyticsEvent.userRegistered: - return l10n.analyticsEventUserRegisteredDescription; + return ( + l10n.analyticsEventUserRegisteredLabel, + l10n.analyticsEventUserRegisteredDescription + ); case AnalyticsEvent.userLogin: - return l10n.analyticsEventUserLoginDescription; + return ( + l10n.analyticsEventUserLoginLabel, + l10n.analyticsEventUserLoginDescription + ); case AnalyticsEvent.accountLinked: - return l10n.analyticsEventAccountLinkedDescription; + return ( + l10n.analyticsEventAccountLinkedLabel, + l10n.analyticsEventAccountLinkedDescription + ); case AnalyticsEvent.userRoleChanged: - return l10n.analyticsEventUserRoleChangedDescription; + return ( + l10n.analyticsEventUserRoleChangedLabel, + l10n.analyticsEventUserRoleChangedDescription + ); case AnalyticsEvent.contentViewed: - return l10n.analyticsEventContentViewedDescription; + return ( + l10n.analyticsEventContentViewedLabel, + l10n.analyticsEventContentViewedDescription + ); case AnalyticsEvent.contentShared: - return l10n.analyticsEventContentSharedDescription; + return ( + l10n.analyticsEventContentSharedLabel, + l10n.analyticsEventContentSharedDescription + ); case AnalyticsEvent.contentSaved: - return l10n.analyticsEventContentSavedDescription; + return ( + l10n.analyticsEventContentSavedLabel, + l10n.analyticsEventContentSavedDescription + ); case AnalyticsEvent.contentUnsaved: - return l10n.analyticsEventContentUnsavedDescription; + return ( + l10n.analyticsEventContentUnsavedLabel, + l10n.analyticsEventContentUnsavedDescription + ); case AnalyticsEvent.contentReadingTime: - return l10n.analyticsEventContentReadingTimeDescription; + return ( + l10n.analyticsEventContentReadingTimeLabel, + l10n.analyticsEventContentReadingTimeDescription + ); case AnalyticsEvent.reactionCreated: - return l10n.analyticsEventReactionCreatedDescription; + return ( + l10n.analyticsEventReactionCreatedLabel, + l10n.analyticsEventReactionCreatedDescription + ); case AnalyticsEvent.reactionDeleted: - return l10n.analyticsEventReactionDeletedDescription; + return ( + l10n.analyticsEventReactionDeletedLabel, + l10n.analyticsEventReactionDeletedDescription + ); case AnalyticsEvent.commentCreated: - return l10n.analyticsEventCommentCreatedDescription; + return ( + l10n.analyticsEventCommentCreatedLabel, + l10n.analyticsEventCommentCreatedDescription + ); case AnalyticsEvent.commentDeleted: - return l10n.analyticsEventCommentDeletedDescription; + return ( + l10n.analyticsEventCommentDeletedLabel, + l10n.analyticsEventCommentDeletedDescription + ); case AnalyticsEvent.reportSubmitted: - return l10n.analyticsEventReportSubmittedDescription; + return ( + l10n.analyticsEventReportSubmittedLabel, + l10n.analyticsEventReportSubmittedDescription + ); case AnalyticsEvent.headlineFilterCreated: - return l10n.analyticsEventHeadlineFilterCreatedDescription; + return ( + l10n.analyticsEventHeadlineFilterCreatedLabel, + l10n.analyticsEventHeadlineFilterCreatedDescription + ); case AnalyticsEvent.headlineFilterUpdated: - return l10n.analyticsEventHeadlineFilterUpdatedDescription; + return ( + l10n.analyticsEventHeadlineFilterUpdatedLabel, + l10n.analyticsEventHeadlineFilterUpdatedDescription + ); case AnalyticsEvent.headlineFilterUsed: - return l10n.analyticsEventHeadlineFilterUsedDescription; + return ( + l10n.analyticsEventHeadlineFilterUsedLabel, + l10n.analyticsEventHeadlineFilterUsedDescription + ); case AnalyticsEvent.sourceFilterCreated: - return l10n.analyticsEventSourceFilterCreatedDescription; + return ( + l10n.analyticsEventSourceFilterCreatedLabel, + l10n.analyticsEventSourceFilterCreatedDescription + ); case AnalyticsEvent.sourceFilterUpdated: - return l10n.analyticsEventSourceFilterUpdatedDescription; + return ( + l10n.analyticsEventSourceFilterUpdatedLabel, + l10n.analyticsEventSourceFilterUpdatedDescription + ); case AnalyticsEvent.searchPerformed: - return l10n.analyticsEventSearchPerformedDescription; + return ( + l10n.analyticsEventSearchPerformedLabel, + l10n.analyticsEventSearchPerformedDescription + ); case AnalyticsEvent.appReviewPromptResponded: - return l10n.analyticsEventAppReviewPromptRespondedDescription; + return ( + l10n.analyticsEventAppReviewPromptRespondedLabel, + l10n.analyticsEventAppReviewPromptRespondedDescription + ); case AnalyticsEvent.appReviewStoreRequested: - return l10n.analyticsEventAppReviewStoreRequestedDescription; + return ( + l10n.analyticsEventAppReviewStoreRequestedLabel, + l10n.analyticsEventAppReviewStoreRequestedDescription + ); case AnalyticsEvent.limitExceeded: - return l10n.analyticsEventLimitExceededDescription; + return ( + l10n.analyticsEventLimitExceededLabel, + l10n.analyticsEventLimitExceededDescription + ); case AnalyticsEvent.limitExceededCtaClicked: - return l10n.analyticsEventLimitExceededCtaClickedDescription; + return ( + l10n.analyticsEventLimitExceededCtaClickedLabel, + l10n.analyticsEventLimitExceededCtaClickedDescription + ); case AnalyticsEvent.paywallPresented: - return l10n.analyticsEventPaywallPresentedDescription; + return ( + l10n.analyticsEventPaywallPresentedLabel, + l10n.analyticsEventPaywallPresentedDescription + ); case AnalyticsEvent.subscriptionStarted: - return l10n.analyticsEventSubscriptionStartedDescription; + return ( + l10n.analyticsEventSubscriptionStartedLabel, + l10n.analyticsEventSubscriptionStartedDescription + ); case AnalyticsEvent.subscriptionRenewed: - return l10n.analyticsEventSubscriptionRenewedDescription; + return ( + l10n.analyticsEventSubscriptionRenewedLabel, + l10n.analyticsEventSubscriptionRenewedDescription + ); case AnalyticsEvent.subscriptionCancelled: - return l10n.analyticsEventSubscriptionCancelledDescription; + return ( + l10n.analyticsEventSubscriptionCancelledLabel, + l10n.analyticsEventSubscriptionCancelledDescription + ); case AnalyticsEvent.subscriptionEnded: - return l10n.analyticsEventSubscriptionEndedDescription; + return ( + l10n.analyticsEventSubscriptionEndedLabel, + l10n.analyticsEventSubscriptionEndedDescription + ); case AnalyticsEvent.adImpression: - return l10n.analyticsEventAdImpressionDescription; + return ( + l10n.analyticsEventAdImpressionLabel, + l10n.analyticsEventAdImpressionDescription + ); case AnalyticsEvent.adClicked: - return l10n.analyticsEventAdClickedDescription; + return ( + l10n.analyticsEventAdClickedLabel, + l10n.analyticsEventAdClickedDescription + ); case AnalyticsEvent.adLoadFailed: - return l10n.analyticsEventAdLoadFailedDescription; + return ( + l10n.analyticsEventAdLoadFailedLabel, + l10n.analyticsEventAdLoadFailedDescription + ); case AnalyticsEvent.adRewardEarned: - return l10n.analyticsEventAdRewardEarnedDescription; + return ( + l10n.analyticsEventAdRewardEarnedLabel, + l10n.analyticsEventAdRewardEarnedDescription + ); case AnalyticsEvent.themeChanged: - return l10n.analyticsEventThemeChangedDescription; + return ( + l10n.analyticsEventThemeChangedLabel, + l10n.analyticsEventThemeChangedDescription + ); case AnalyticsEvent.languageChanged: - return l10n.analyticsEventLanguageChangedDescription; + return ( + l10n.analyticsEventLanguageChangedLabel, + l10n.analyticsEventLanguageChangedDescription + ); case AnalyticsEvent.feedDensityChanged: - return l10n.analyticsEventFeedDensityChangedDescription; + return ( + l10n.analyticsEventFeedDensityChangedLabel, + l10n.analyticsEventFeedDensityChangedDescription + ); case AnalyticsEvent.browserChoiceChanged: - return l10n.analyticsEventBrowserChoiceChangedDescription; + return ( + l10n.analyticsEventBrowserChoiceChangedLabel, + l10n.analyticsEventBrowserChoiceChangedDescription + ); case AnalyticsEvent.sourceFilterUsed: - return l10n.analyticsEventSourceFilterUsedDescription; + return ( + l10n.analyticsEventSourceFilterUsedLabel, + l10n.analyticsEventSourceFilterUsedDescription + ); } } } From b3a86d2670f3cf751325a4a7251640e8a2b41946 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 11:34:17 +0100 Subject: [PATCH 21/24] refactor(app_configuration): use enum for ExpansionTile state management - Replace int-based indexing with _FeatureTile enum for tracking expanded tile - Update ValueNotifier type from int? to _FeatureTile? - Modify ExpansionTile builders to use enum values instead of indices - Improve readability and maintainability of features configuration tab --- .../view/tabs/features_configuration_tab.dart | 140 +++++++++--------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/lib/app_configuration/view/tabs/features_configuration_tab.dart b/lib/app_configuration/view/tabs/features_configuration_tab.dart index 509836cb..e6620581 100644 --- a/lib/app_configuration/view/tabs/features_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -6,14 +6,13 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuratio import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/community_config_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_ad_settings_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_config_form.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_decorator_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/navigation_ad_settings_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/push_notification_settings_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/feed_decorator_type_l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/feed_item_click_behavior_l10n.dart'; import 'package:ui_kit/ui_kit.dart'; +enum _FeatureTile { advertisements, pushNotifications, analytics, feed, community } + /// {@template features_configuration_tab} /// A widget representing the "Features" tab in the App Configuration page. /// @@ -43,11 +42,12 @@ class _FeaturesConfigurationTabState extends State { /// Notifier for the index of the currently expanded top-level ExpansionTile. /// /// A value of `null` means no tile is expanded. - final ValueNotifier _expandedTileIndex = ValueNotifier(null); + final ValueNotifier<_FeatureTile?> _expandedTile = + ValueNotifier<_FeatureTile?>(null); @override void dispose() { - _expandedTileIndex.dispose(); + _expandedTile.dispose(); super.dispose(); } @@ -59,31 +59,31 @@ class _FeaturesConfigurationTabState extends State { padding: const EdgeInsets.all(AppSpacing.lg), children: [ // Advertisements - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 0; + ValueListenableBuilder<_FeatureTile?>( + valueListenable: _expandedTile, + builder: (context, expandedTile, child) { + const tile = _FeatureTile.advertisements; return ExpansionTile( leading: Icon( Icons.paid_outlined, color: Theme.of(context).colorScheme.onSurface.withOpacity( - 0.7, - ), + 0.7, + ), ), - key: ValueKey('advertisementsTile_$expandedIndex'), + key: ValueKey('advertisementsTile_$expandedTile'), title: Text(l10n.advertisementsTab), subtitle: Text( l10n.advertisementsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), onExpansionChanged: (bool isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; + _expandedTile.value = isExpanded ? tile : null; }, - initiallyExpanded: expandedIndex == tileIndex, + initiallyExpanded: expandedTile == tile, childrenPadding: const EdgeInsetsDirectional.only( start: AppSpacing.xxl, top: AppSpacing.md, @@ -119,31 +119,31 @@ class _FeaturesConfigurationTabState extends State { const SizedBox(height: AppSpacing.lg), // Push Notifications - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 1; + ValueListenableBuilder<_FeatureTile?>( + valueListenable: _expandedTile, + builder: (context, expandedTile, child) { + const tile = _FeatureTile.pushNotifications; return ExpansionTile( leading: Icon( Icons.notifications_active_outlined, color: Theme.of(context).colorScheme.onSurface.withOpacity( - 0.7, - ), + 0.7, + ), ), - key: ValueKey('pushNotificationsTile_$expandedIndex'), + key: ValueKey('pushNotificationsTile_$expandedTile'), title: Text(l10n.notificationsTab), subtitle: Text( l10n.notificationsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), onExpansionChanged: (bool isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; + _expandedTile.value = isExpanded ? tile : null; }, - initiallyExpanded: expandedIndex == tileIndex, + initiallyExpanded: expandedTile == tile, childrenPadding: const EdgeInsetsDirectional.only( start: AppSpacing.xxl, top: AppSpacing.md, @@ -162,31 +162,31 @@ class _FeaturesConfigurationTabState extends State { const SizedBox(height: AppSpacing.lg), // Analytics - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 2; + ValueListenableBuilder<_FeatureTile?>( + valueListenable: _expandedTile, + builder: (context, expandedTile, child) { + const tile = _FeatureTile.analytics; return ExpansionTile( leading: Icon( Icons.analytics_outlined, color: Theme.of(context).colorScheme.onSurface.withOpacity( - 0.7, - ), + 0.7, + ), ), - key: ValueKey('analyticsTile_$expandedIndex'), + key: ValueKey('analyticsTile_$expandedTile'), title: Text(l10n.analyticsTab), subtitle: Text( l10n.analyticsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), onExpansionChanged: (bool isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; + _expandedTile.value = isExpanded ? tile : null; }, - initiallyExpanded: expandedIndex == tileIndex, + initiallyExpanded: expandedTile == tile, childrenPadding: const EdgeInsetsDirectional.only( start: AppSpacing.xxl, top: AppSpacing.md, @@ -205,31 +205,31 @@ class _FeaturesConfigurationTabState extends State { const SizedBox(height: AppSpacing.lg), // Feed - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 3; + ValueListenableBuilder<_FeatureTile?>( + valueListenable: _expandedTile, + builder: (context, expandedTile, child) { + const tile = _FeatureTile.feed; return ExpansionTile( leading: Icon( Icons.dynamic_feed_outlined, color: Theme.of(context).colorScheme.onSurface.withOpacity( - 0.7, - ), + 0.7, + ), ), - key: ValueKey('feedTile_$expandedIndex'), + key: ValueKey('feedTile_$expandedTile'), title: Text(l10n.feedTab), subtitle: Text( l10n.feedDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), onExpansionChanged: (bool isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; + _expandedTile.value = isExpanded ? tile : null; }, - initiallyExpanded: expandedIndex == tileIndex, + initiallyExpanded: expandedTile == tile, childrenPadding: const EdgeInsetsDirectional.only( start: AppSpacing.xxl, top: AppSpacing.md, @@ -248,31 +248,31 @@ class _FeaturesConfigurationTabState extends State { const SizedBox(height: AppSpacing.lg), // Community & Engagement - ValueListenableBuilder( - valueListenable: _expandedTileIndex, - builder: (context, expandedIndex, child) { - const tileIndex = 4; + ValueListenableBuilder<_FeatureTile?>( + valueListenable: _expandedTile, + builder: (context, expandedTile, child) { + const tile = _FeatureTile.community; return ExpansionTile( leading: Icon( Icons.groups_outlined, color: Theme.of(context).colorScheme.onSurface.withOpacity( - 0.7, - ), + 0.7, + ), ), - key: ValueKey('communityTile_$expandedIndex'), + key: ValueKey('communityTile_$expandedTile'), title: Text(l10n.communityAndEngagementTitle), subtitle: Text( l10n.communityAndEngagementDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), onExpansionChanged: (bool isExpanded) { - _expandedTileIndex.value = isExpanded ? tileIndex : null; + _expandedTile.value = isExpanded ? tile : null; }, - initiallyExpanded: expandedIndex == tileIndex, + initiallyExpanded: expandedTile == tile, childrenPadding: const EdgeInsetsDirectional.only( start: AppSpacing.xxl, top: AppSpacing.md, From e0707c3347dc040aa0c031db5b4cb2f5d864029d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 11:34:34 +0100 Subject: [PATCH 22/24] style: format --- .../view/tabs/features_configuration_tab.dart | 68 +++++++++-------- .../widgets/analytics_config_form.dart | 76 +++++++++---------- 2 files changed, 75 insertions(+), 69 deletions(-) diff --git a/lib/app_configuration/view/tabs/features_configuration_tab.dart b/lib/app_configuration/view/tabs/features_configuration_tab.dart index e6620581..8453bf96 100644 --- a/lib/app_configuration/view/tabs/features_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/features_configuration_tab.dart @@ -11,7 +11,13 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuratio import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:ui_kit/ui_kit.dart'; -enum _FeatureTile { advertisements, pushNotifications, analytics, feed, community } +enum _FeatureTile { + advertisements, + pushNotifications, + analytics, + feed, + community, +} /// {@template features_configuration_tab} /// A widget representing the "Features" tab in the App Configuration page. @@ -67,18 +73,18 @@ class _FeaturesConfigurationTabState extends State { leading: Icon( Icons.paid_outlined, color: Theme.of(context).colorScheme.onSurface.withOpacity( - 0.7, - ), + 0.7, + ), ), key: ValueKey('advertisementsTile_$expandedTile'), title: Text(l10n.advertisementsTab), subtitle: Text( l10n.advertisementsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), onExpansionChanged: (bool isExpanded) { _expandedTile.value = isExpanded ? tile : null; @@ -127,18 +133,18 @@ class _FeaturesConfigurationTabState extends State { leading: Icon( Icons.notifications_active_outlined, color: Theme.of(context).colorScheme.onSurface.withOpacity( - 0.7, - ), + 0.7, + ), ), key: ValueKey('pushNotificationsTile_$expandedTile'), title: Text(l10n.notificationsTab), subtitle: Text( l10n.notificationsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), onExpansionChanged: (bool isExpanded) { _expandedTile.value = isExpanded ? tile : null; @@ -170,18 +176,18 @@ class _FeaturesConfigurationTabState extends State { leading: Icon( Icons.analytics_outlined, color: Theme.of(context).colorScheme.onSurface.withOpacity( - 0.7, - ), + 0.7, + ), ), key: ValueKey('analyticsTile_$expandedTile'), title: Text(l10n.analyticsTab), subtitle: Text( l10n.analyticsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), onExpansionChanged: (bool isExpanded) { _expandedTile.value = isExpanded ? tile : null; @@ -213,18 +219,18 @@ class _FeaturesConfigurationTabState extends State { leading: Icon( Icons.dynamic_feed_outlined, color: Theme.of(context).colorScheme.onSurface.withOpacity( - 0.7, - ), + 0.7, + ), ), key: ValueKey('feedTile_$expandedTile'), title: Text(l10n.feedTab), subtitle: Text( l10n.feedDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), onExpansionChanged: (bool isExpanded) { _expandedTile.value = isExpanded ? tile : null; @@ -256,18 +262,18 @@ class _FeaturesConfigurationTabState extends State { leading: Icon( Icons.groups_outlined, color: Theme.of(context).colorScheme.onSurface.withOpacity( - 0.7, - ), + 0.7, + ), ), key: ValueKey('communityTile_$expandedTile'), title: Text(l10n.communityAndEngagementTitle), subtitle: Text( l10n.communityAndEngagementDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), onExpansionChanged: (bool isExpanded) { _expandedTile.value = isExpanded ? tile : null; diff --git a/lib/app_configuration/widgets/analytics_config_form.dart b/lib/app_configuration/widgets/analytics_config_form.dart index c4bd6a0a..e4cfea02 100644 --- a/lib/app_configuration/widgets/analytics_config_form.dart +++ b/lib/app_configuration/widgets/analytics_config_form.dart @@ -234,192 +234,192 @@ class AnalyticsConfigForm extends StatelessWidget { case AnalyticsEvent.userRegistered: return ( l10n.analyticsEventUserRegisteredLabel, - l10n.analyticsEventUserRegisteredDescription + l10n.analyticsEventUserRegisteredDescription, ); case AnalyticsEvent.userLogin: return ( l10n.analyticsEventUserLoginLabel, - l10n.analyticsEventUserLoginDescription + l10n.analyticsEventUserLoginDescription, ); case AnalyticsEvent.accountLinked: return ( l10n.analyticsEventAccountLinkedLabel, - l10n.analyticsEventAccountLinkedDescription + l10n.analyticsEventAccountLinkedDescription, ); case AnalyticsEvent.userRoleChanged: return ( l10n.analyticsEventUserRoleChangedLabel, - l10n.analyticsEventUserRoleChangedDescription + l10n.analyticsEventUserRoleChangedDescription, ); case AnalyticsEvent.contentViewed: return ( l10n.analyticsEventContentViewedLabel, - l10n.analyticsEventContentViewedDescription + l10n.analyticsEventContentViewedDescription, ); case AnalyticsEvent.contentShared: return ( l10n.analyticsEventContentSharedLabel, - l10n.analyticsEventContentSharedDescription + l10n.analyticsEventContentSharedDescription, ); case AnalyticsEvent.contentSaved: return ( l10n.analyticsEventContentSavedLabel, - l10n.analyticsEventContentSavedDescription + l10n.analyticsEventContentSavedDescription, ); case AnalyticsEvent.contentUnsaved: return ( l10n.analyticsEventContentUnsavedLabel, - l10n.analyticsEventContentUnsavedDescription + l10n.analyticsEventContentUnsavedDescription, ); case AnalyticsEvent.contentReadingTime: return ( l10n.analyticsEventContentReadingTimeLabel, - l10n.analyticsEventContentReadingTimeDescription + l10n.analyticsEventContentReadingTimeDescription, ); case AnalyticsEvent.reactionCreated: return ( l10n.analyticsEventReactionCreatedLabel, - l10n.analyticsEventReactionCreatedDescription + l10n.analyticsEventReactionCreatedDescription, ); case AnalyticsEvent.reactionDeleted: return ( l10n.analyticsEventReactionDeletedLabel, - l10n.analyticsEventReactionDeletedDescription + l10n.analyticsEventReactionDeletedDescription, ); case AnalyticsEvent.commentCreated: return ( l10n.analyticsEventCommentCreatedLabel, - l10n.analyticsEventCommentCreatedDescription + l10n.analyticsEventCommentCreatedDescription, ); case AnalyticsEvent.commentDeleted: return ( l10n.analyticsEventCommentDeletedLabel, - l10n.analyticsEventCommentDeletedDescription + l10n.analyticsEventCommentDeletedDescription, ); case AnalyticsEvent.reportSubmitted: return ( l10n.analyticsEventReportSubmittedLabel, - l10n.analyticsEventReportSubmittedDescription + l10n.analyticsEventReportSubmittedDescription, ); case AnalyticsEvent.headlineFilterCreated: return ( l10n.analyticsEventHeadlineFilterCreatedLabel, - l10n.analyticsEventHeadlineFilterCreatedDescription + l10n.analyticsEventHeadlineFilterCreatedDescription, ); case AnalyticsEvent.headlineFilterUpdated: return ( l10n.analyticsEventHeadlineFilterUpdatedLabel, - l10n.analyticsEventHeadlineFilterUpdatedDescription + l10n.analyticsEventHeadlineFilterUpdatedDescription, ); case AnalyticsEvent.headlineFilterUsed: return ( l10n.analyticsEventHeadlineFilterUsedLabel, - l10n.analyticsEventHeadlineFilterUsedDescription + l10n.analyticsEventHeadlineFilterUsedDescription, ); case AnalyticsEvent.sourceFilterCreated: return ( l10n.analyticsEventSourceFilterCreatedLabel, - l10n.analyticsEventSourceFilterCreatedDescription + l10n.analyticsEventSourceFilterCreatedDescription, ); case AnalyticsEvent.sourceFilterUpdated: return ( l10n.analyticsEventSourceFilterUpdatedLabel, - l10n.analyticsEventSourceFilterUpdatedDescription + l10n.analyticsEventSourceFilterUpdatedDescription, ); case AnalyticsEvent.searchPerformed: return ( l10n.analyticsEventSearchPerformedLabel, - l10n.analyticsEventSearchPerformedDescription + l10n.analyticsEventSearchPerformedDescription, ); case AnalyticsEvent.appReviewPromptResponded: return ( l10n.analyticsEventAppReviewPromptRespondedLabel, - l10n.analyticsEventAppReviewPromptRespondedDescription + l10n.analyticsEventAppReviewPromptRespondedDescription, ); case AnalyticsEvent.appReviewStoreRequested: return ( l10n.analyticsEventAppReviewStoreRequestedLabel, - l10n.analyticsEventAppReviewStoreRequestedDescription + l10n.analyticsEventAppReviewStoreRequestedDescription, ); case AnalyticsEvent.limitExceeded: return ( l10n.analyticsEventLimitExceededLabel, - l10n.analyticsEventLimitExceededDescription + l10n.analyticsEventLimitExceededDescription, ); case AnalyticsEvent.limitExceededCtaClicked: return ( l10n.analyticsEventLimitExceededCtaClickedLabel, - l10n.analyticsEventLimitExceededCtaClickedDescription + l10n.analyticsEventLimitExceededCtaClickedDescription, ); case AnalyticsEvent.paywallPresented: return ( l10n.analyticsEventPaywallPresentedLabel, - l10n.analyticsEventPaywallPresentedDescription + l10n.analyticsEventPaywallPresentedDescription, ); case AnalyticsEvent.subscriptionStarted: return ( l10n.analyticsEventSubscriptionStartedLabel, - l10n.analyticsEventSubscriptionStartedDescription + l10n.analyticsEventSubscriptionStartedDescription, ); case AnalyticsEvent.subscriptionRenewed: return ( l10n.analyticsEventSubscriptionRenewedLabel, - l10n.analyticsEventSubscriptionRenewedDescription + l10n.analyticsEventSubscriptionRenewedDescription, ); case AnalyticsEvent.subscriptionCancelled: return ( l10n.analyticsEventSubscriptionCancelledLabel, - l10n.analyticsEventSubscriptionCancelledDescription + l10n.analyticsEventSubscriptionCancelledDescription, ); case AnalyticsEvent.subscriptionEnded: return ( l10n.analyticsEventSubscriptionEndedLabel, - l10n.analyticsEventSubscriptionEndedDescription + l10n.analyticsEventSubscriptionEndedDescription, ); case AnalyticsEvent.adImpression: return ( l10n.analyticsEventAdImpressionLabel, - l10n.analyticsEventAdImpressionDescription + l10n.analyticsEventAdImpressionDescription, ); case AnalyticsEvent.adClicked: return ( l10n.analyticsEventAdClickedLabel, - l10n.analyticsEventAdClickedDescription + l10n.analyticsEventAdClickedDescription, ); case AnalyticsEvent.adLoadFailed: return ( l10n.analyticsEventAdLoadFailedLabel, - l10n.analyticsEventAdLoadFailedDescription + l10n.analyticsEventAdLoadFailedDescription, ); case AnalyticsEvent.adRewardEarned: return ( l10n.analyticsEventAdRewardEarnedLabel, - l10n.analyticsEventAdRewardEarnedDescription + l10n.analyticsEventAdRewardEarnedDescription, ); case AnalyticsEvent.themeChanged: return ( l10n.analyticsEventThemeChangedLabel, - l10n.analyticsEventThemeChangedDescription + l10n.analyticsEventThemeChangedDescription, ); case AnalyticsEvent.languageChanged: return ( l10n.analyticsEventLanguageChangedLabel, - l10n.analyticsEventLanguageChangedDescription + l10n.analyticsEventLanguageChangedDescription, ); case AnalyticsEvent.feedDensityChanged: return ( l10n.analyticsEventFeedDensityChangedLabel, - l10n.analyticsEventFeedDensityChangedDescription + l10n.analyticsEventFeedDensityChangedDescription, ); case AnalyticsEvent.browserChoiceChanged: return ( l10n.analyticsEventBrowserChoiceChangedLabel, - l10n.analyticsEventBrowserChoiceChangedDescription + l10n.analyticsEventBrowserChoiceChangedDescription, ); case AnalyticsEvent.sourceFilterUsed: return ( l10n.analyticsEventSourceFilterUsedLabel, - l10n.analyticsEventSourceFilterUsedDescription + l10n.analyticsEventSourceFilterUsedDescription, ); } } From 4c62e71aa45146e5a23f1f7487a0036274d6e42d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 11:41:33 +0100 Subject: [PATCH 23/24] build: upgrade pinput package from 5.0.1 to 6.0.1 - Update pinput dependency in pubspec.yaml - This change ensures compatibility with the latest Flutter version --- pubspec.lock | 12 ++---------- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index c32fe080..85a22687 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -440,10 +440,10 @@ packages: dependency: "direct main" description: name: pinput - sha256: c41f42ee301505ae2375ec32871c985d3717bf8aee845620465b286e0140aad2 + sha256: "692e1c29703abefad6c502e97b4d946d506384397438ea242afadbfe48354819" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "6.0.1" platform: dependency: transitive description: @@ -602,14 +602,6 @@ packages: url: "https://github.com/flutter-news-app-full-source-code/ui-kit.git" source: git version: "1.0.1" - universal_platform: - dependency: transitive - description: - name: universal_platform - sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" - url: "https://pub.dev" - source: hosted - version: "1.1.0" uuid: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 4567ee0e..1b2e22c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,7 +73,7 @@ dependencies: url: https://github.com/flutter-news-app-full-source-code/kv-storage-shared-preferences.git ref: v1.0.1 logging: ^1.3.0 - pinput: ^5.0.1 + pinput: ^6.0.1 rxdart: ^0.28.0 stream_transform: ^2.1.1 timeago: ^3.7.1 From 69ef8abcf49609c2d80f27c1e58fdd7a2f9c616b Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 20 Dec 2025 11:47:50 +0100 Subject: [PATCH 24/24] chore(dependencies): revert pinput version to 6.0.0 - Change pinput version from 6.0.1 to 6.0.0 in pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 1b2e22c5..ec526fd4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,7 +73,7 @@ dependencies: url: https://github.com/flutter-news-app-full-source-code/kv-storage-shared-preferences.git ref: v1.0.1 logging: ^1.3.0 - pinput: ^6.0.1 + pinput: ^6.0.0 rxdart: ^0.28.0 stream_transform: ^2.1.1 timeago: ^3.7.1