From 497cae11b4dc62ac7a91ae10a6b7b136b1510c2d Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 15:53:36 +0100 Subject: [PATCH 01/59] build(deps): update core and http-client dependencies - Update core dependency to latest version with ref 064c4387b3f7df835565c41c918dc2d80dd2f49a - Update http-client dependency to version v1.1.0 with ref e3540bcd27de93f96f4bce79cc20ff55dfe3b2bf - Remove TODO comment in pubspec.yaml --- pubspec.lock | 10 +++++----- pubspec.yaml | 7 +++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index fa8c6a9f..e0782b92 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,8 +89,8 @@ packages: dependency: "direct main" description: path: "." - ref: e7c808c9d459233196e2eac3137a9c87d3976af3 - resolved-ref: e7c808c9d459233196e2eac3137a9c87d3976af3 + ref: "064c4387b3f7df835565c41c918dc2d80dd2f49a" + resolved-ref: "064c4387b3f7df835565c41c918dc2d80dd2f49a" url: "https://github.com/flutter-news-app-full-source-code/core.git" source: git version: "1.3.1" @@ -293,11 +293,11 @@ packages: dependency: "direct main" description: path: "." - ref: "v1.0.1" - resolved-ref: "22a1531a279769ec472f698e9c727cd2c29a81b9" + ref: "v1.1.0" + resolved-ref: e3540bcd27de93f96f4bce79cc20ff55dfe3b2bf url: "https://github.com/flutter-news-app-full-source-code/http-client.git" source: git - version: "1.0.1" + version: "1.1.0" http_parser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7905a815..222f4faa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -91,8 +91,11 @@ flutter: generate: true dependency_overrides: - # TODO: Remove this override before upcoming released. core: git: url: https://github.com/flutter-news-app-full-source-code/core.git - ref: e7c808c9d459233196e2eac3137a9c87d3976af3 \ No newline at end of file + ref: 064c4387b3f7df835565c41c918dc2d80dd2f49a + http_client: + git: + url: https://github.com/flutter-news-app-full-source-code/http-client.git + ref: v1.1.0 \ No newline at end of file From d49a08675ab37d4c7b0478c5edbed8585091426e Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 16:30:25 +0100 Subject: [PATCH 02/59] feat(l10n): add localization for new filter limit configurations --- lib/l10n/arb/app_en.arb | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 31b4f90a..e37b03e6 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1797,5 +1797,57 @@ "subscriptionPremium": "Premium", "@subscriptionPremium": { "description": "Subscription status for a premium user" + }, + "savedHeadlineFilterLimitsTitle": "Saved Headline Filter Limits", + "@savedHeadlineFilterLimitsTitle": { + "description": "Title for the Saved Headline Filter Limits section" + }, + "savedHeadlineFilterLimitsDescription": "Set limits on the number of saved headline filters for each user tier, including total, pinned, and notification subscriptions.", + "@savedHeadlineFilterLimitsDescription": { + "description": "Description for the Saved Headline Filter Limits section" + }, + "savedSourceFilterLimitsTitle": "Saved Source Filter Limits", + "@savedSourceFilterLimitsTitle": { + "description": "Title for the Saved Source Filter Limits section" + }, + "savedSourceFilterLimitsDescription": "Set limits on the number of saved source filters for each user tier, including total and pinned.", + "@savedSourceFilterLimitsDescription": { + "description": "Description for the Saved Source Filter Limits section" + }, + "totalLimitLabel": "Total Limit", + "@totalLimitLabel": { + "description": "Label for the total limit of a filter type" + }, + "totalLimitDescription": "The total number of filters of this type a user can create.", + "@totalLimitDescription": { + "description": "Description for the total limit of a filter type" + }, + "pinnedLimitLabel": "Pinned Limit", + "@pinnedLimitLabel": { + "description": "Label for the pinned limit of a filter type" + }, + "pinnedLimitDescription": "The maximum number of filters of this type that can be pinned.", + "@pinnedLimitDescription": { + "description": "Description for the pinned limit of a filter type" + }, + "notificationSubscriptionLimitLabel": "Notification Subscription Limit", + "@notificationSubscriptionLimitLabel": { + "description": "Label for the notification subscription limit of a filter type" + }, + "notificationSubscriptionLimitDescription": "The maximum number of filters a user can subscribe to for this notification type.", + "@notificationSubscriptionLimitDescription": { + "description": "Description for the notification subscription limit of a filter type" + }, + "pushNotificationSubscriptionDeliveryTypeBreakingOnly": "Breaking News", + "@pushNotificationSubscriptionDeliveryTypeBreakingOnly": { + "description": "Label for the 'breaking only' push notification delivery type" + }, + "pushNotificationSubscriptionDeliveryTypeDailyDigest": "Daily Digest", + "@pushNotificationSubscriptionDeliveryTypeDailyDigest": { + "description": "Label for the 'daily digest' push notification delivery type" + }, + "pushNotificationSubscriptionDeliveryTypeWeeklyRoundup": "Weekly Roundup", + "@pushNotificationSubscriptionDeliveryTypeWeeklyRoundup": { + "description": "Label for the 'weekly roundup' push notification delivery type" } } \ No newline at end of file From 286e8c2cb0f16086eac4df3774cc4540502c2e21 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 16:33:07 +0100 Subject: [PATCH 03/59] feat(l10n): add Arabic translations for filter limits and notification settings - Add Arabic translations for saved headline filter limits - Include translations for saved source filter limits - Add descriptions for total, pinned, and notification subscription limits - Translate push notification subscription delivery types --- lib/l10n/arb/app_ar.arb | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index c43b11b7..a5b14018 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1802,4 +1802,57 @@ "@subscriptionPremium": { "description": "حالة الاشتراك لمستخدم مميز" } +, + "savedHeadlineFilterLimitsTitle": "حدود مرشحات العناوين المحفوظة", + "@savedHeadlineFilterLimitsTitle": { + "description": "عنوان قسم حدود مرشحات العناوين المحفوظة" + }, + "savedHeadlineFilterLimitsDescription": "تعيين حدود على عدد مرشحات العناوين المحفوظة لكل فئة من فئات المستخدمين، بما في ذلك الإجمالي والمثبتة واشتراكات الإشعارات.", + "@savedHeadlineFilterLimitsDescription": { + "description": "وصف قسم حدود مرشحات العناوين المحفوظة" + }, + "savedSourceFilterLimitsTitle": "حدود مرشحات المصادر المحفوظة", + "@savedSourceFilterLimitsTitle": { + "description": "عنوان قسم حدود مرشحات المصادر المحفوظة" + }, + "savedSourceFilterLimitsDescription": "تعيين حدود على عدد مرشحات المصادر المحفوظة لكل فئة من فئات المستخدمين، بما في ذلك الإجمالي والمثبتة.", + "@savedSourceFilterLimitsDescription": { + "description": "وصف قسم حدود مرشحات المصادر المحفوظة" + }, + "totalLimitLabel": "الحد الإجمالي", + "@totalLimitLabel": { + "description": "تسمية للحد الإجمالي لنوع مرشح" + }, + "totalLimitDescription": "العدد الإجمالي للمرشحات من هذا النوع التي يمكن للمستخدم إنشاؤها.", + "@totalLimitDescription": { + "description": "وصف للحد الإجمالي لنوع مرشح" + }, + "pinnedLimitLabel": "الحد المثبت", + "@pinnedLimitLabel": { + "description": "تسمية للحد المثبت لنوع مرشح" + }, + "pinnedLimitDescription": "الحد الأقصى لعدد المرشحات من هذا النوع التي يمكن تثبيتها.", + "@pinnedLimitDescription": { + "description": "وصف للحد المثبت لنوع مرشح" + }, + "notificationSubscriptionLimitLabel": "حد اشتراك الإشعارات", + "@notificationSubscriptionLimitLabel": { + "description": "تسمية لحد اشتراك الإشعارات لنوع مرشح" + }, + "notificationSubscriptionLimitDescription": "الحد الأقصى لعدد المرشحات التي يمكن للمستخدم الاشتراك فيها لهذا النوع من الإشعارات.", + "@notificationSubscriptionLimitDescription": { + "description": "وصف لحد اشتراك الإشعارات لنوع مرشح" + }, + "pushNotificationSubscriptionDeliveryTypeBreakingOnly": "الأخبار العاجلة", + "@pushNotificationSubscriptionDeliveryTypeBreakingOnly": { + "description": "تسمية لنوع توصيل إشعارات 'الأخبار العاجلة فقط'" + }, + "pushNotificationSubscriptionDeliveryTypeDailyDigest": "الملخص اليومي", + "@pushNotificationSubscriptionDeliveryTypeDailyDigest": { + "description": "تسمية لنوع توصيل إشعارات 'الملخص اليومي'" + }, + "pushNotificationSubscriptionDeliveryTypeWeeklyRoundup": "حصاد الأسبوع", + "@pushNotificationSubscriptionDeliveryTypeWeeklyRoundup": { + "description": "تسمية لنوع توصيل إشعارات 'حصاد الأسبوع'" + } } \ No newline at end of file From 1d794a6d90d79112c43b9386eab3145de3c456d5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 16:37:13 +0100 Subject: [PATCH 04/59] feat(app_config): create reusable saved filter limits form Creates a new reusable `SavedFilterLimitsForm` widget to manage the complex configuration for both headline and source saved filter limits. This stateful widget uses a `TabBar` to separate configurations by user role (Guest, Standard, Premium) and dynamically builds form fields for total, pinned, and notification subscription limits based on the filter type. It manages its own controllers and uses a robust callback system to propagate changes to the parent, ensuring a clean separation of concerns and high reusability. --- .../widgets/saved_filter_limits_form.dart | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 lib/app_configuration/widgets/saved_filter_limits_form.dart diff --git a/lib/app_configuration/widgets/saved_filter_limits_form.dart b/lib/app_configuration/widgets/saved_filter_limits_form.dart new file mode 100644 index 00000000..c91503e1 --- /dev/null +++ b/lib/app_configuration/widgets/saved_filter_limits_form.dart @@ -0,0 +1,269 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_config_form_fields.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/push_notification_subscription_delivery_type_l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// Defines the type of filter being configured. +enum SavedFilterType { headline, source } + +/// {@template saved_filter_limits_form} +/// A reusable form widget for configuring limits for saved filters. +/// +/// This widget is designed to handle the configuration for both headline and +/// source filters by using the [filterType] parameter. It displays a tabbed +/// interface for different user roles and provides fields for setting total, +/// pinned, and (if applicable) notification subscription limits. +/// {@endtemplate} +class SavedFilterLimitsForm extends StatefulWidget { + /// {@macro saved_filter_limits_form} + const SavedFilterLimitsForm({ + required this.remoteConfig, + required this.onConfigChanged, + required this.filterType, + super.key, + }); + + /// The current [RemoteConfig] object. + final RemoteConfig remoteConfig; + + /// Callback to notify parent of changes to the [RemoteConfig]. + final ValueChanged onConfigChanged; + + /// The type of filter to configure (headline or source). + final SavedFilterType filterType; + + @override + State createState() => _SavedFilterLimitsFormState(); +} + +class _SavedFilterLimitsFormState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + // A nested map to hold controllers: Role -> Field -> Controller + final Map> _controllers = {}; + + @override + void initState() { + super.initState(); + _tabController = TabController( + length: AppUserRole.values.length, + vsync: this, + ); + _initializeControllers(); + } + + @override + void didUpdateWidget(covariant SavedFilterLimitsForm oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.remoteConfig.userPreferenceConfig != + oldWidget.remoteConfig.userPreferenceConfig) { + _updateControllerValues(); + } + } + + /// Initializes all TextEditingControllers based on the initial config. + void _initializeControllers() { + for (final role in AppUserRole.values) { + _controllers[role] = {}; + final limits = _getLimitsForRole(role); + + _controllers[role]!['total'] = _createController(limits.total.toString()); + _controllers[role]!['pinned'] = _createController( + limits.pinned.toString(), + ); + + if (widget.filterType == SavedFilterType.headline) { + for (final type in PushNotificationSubscriptionDeliveryType.values) { + final value = limits.notificationSubscriptions?[type] ?? 0; + _controllers[role]![type.name] = _createController(value.toString()); + } + } + } + } + + /// Creates a single TextEditingController with appropriate selection. + TextEditingController _createController(String text) { + return TextEditingController(text: text) + ..selection = TextSelection.collapsed(offset: text.length); + } + + /// Updates controller values if the remote config has changed. + void _updateControllerValues() { + for (final role in AppUserRole.values) { + final limits = _getLimitsForRole(role); + _updateControllerText(_controllers[role]!['total']!, limits.total); + _updateControllerText(_controllers[role]!['pinned']!, limits.pinned); + + if (widget.filterType == SavedFilterType.headline) { + for (final type in PushNotificationSubscriptionDeliveryType.values) { + final value = limits.notificationSubscriptions?[type] ?? 0; + _updateControllerText(_controllers[role]![type.name]!, value); + } + } + } + } + + /// Safely updates a controller's text and selection. + void _updateControllerText(TextEditingController controller, int value) { + final text = value.toString(); + if (controller.text != text) { + controller + ..text = text + ..selection = TextSelection.collapsed(offset: text.length); + } + } + + @override + void dispose() { + _tabController.dispose(); + for (final roleControllers in _controllers.values) { + for (final controller in roleControllers.values) { + controller.dispose(); + } + } + super.dispose(); + } + + /// Retrieves the correct [SavedFilterLimits] for a given role. + SavedFilterLimits _getLimitsForRole(AppUserRole role) { + final config = widget.remoteConfig.userPreferenceConfig; + final limitsMap = widget.filterType == SavedFilterType.headline + ? config.savedHeadlineFiltersLimit + : config.savedSourceFiltersLimit; + return limitsMap[role]!; + } + + /// Updates the remote config when a value changes. + void _onValueChanged(AppUserRole role, String field, int value) { + final config = widget.remoteConfig.userPreferenceConfig; + final isHeadline = widget.filterType == SavedFilterType.headline; + + // Create a mutable copy of the role-to-limits map. + final newLimitsMap = Map.from( + isHeadline + ? config.savedHeadlineFiltersLimit + : config.savedSourceFiltersLimit, + ); + + // Get the current limits for the role and create a modified copy. + final currentLimits = newLimitsMap[role]!; + final SavedFilterLimits newLimits; + + if (field == 'total') { + newLimits = currentLimits.copyWith(total: value); + } else if (field == 'pinned') { + newLimits = currentLimits.copyWith(pinned: value); + } else { + // This must be a notification subscription change. + final deliveryType = PushNotificationSubscriptionDeliveryType.values + .byName(field); + final newSubscriptions = + Map.from( + currentLimits.notificationSubscriptions ?? {}, + ); + newSubscriptions[deliveryType] = value; + newLimits = currentLimits.copyWith( + notificationSubscriptions: newSubscriptions, + ); + } + + // Update the map with the new limits for the role. + newLimitsMap[role] = newLimits; + + // Create the updated UserPreferenceConfig. + final newUserPreferenceConfig = isHeadline + ? config.copyWith(savedHeadlineFiltersLimit: newLimitsMap) + : config.copyWith(savedSourceFiltersLimit: newLimitsMap); + + // Notify the parent widget. + widget.onConfigChanged( + widget.remoteConfig.copyWith( + userPreferenceConfig: newUserPreferenceConfig, + ), + ); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + final isHeadlineFilter = widget.filterType == SavedFilterType.headline; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: SizedBox( + height: kTextTabBarHeight, + child: TabBar( + controller: _tabController, + tabAlignment: TabAlignment.start, + isScrollable: true, + tabs: AppUserRole.values + .map((role) => Tab(text: role.l10n(context))) + .toList(), + ), + ), + ), + const SizedBox(height: AppSpacing.lg), + // TabBarView to display role-specific fields + SizedBox( + // Adjust height based on whether notification fields are shown. + height: isHeadlineFilter ? 400 : 250, + child: TabBarView( + controller: _tabController, + children: AppUserRole.values.map((role) { + final limits = _getLimitsForRole(role); + return SingleChildScrollView( + child: Column( + children: [ + AppConfigIntField( + label: l10n.totalLimitLabel, + description: l10n.totalLimitDescription, + value: limits.total, + onChanged: (value) => + _onValueChanged(role, 'total', value), + controller: _controllers[role]!['total'], + ), + AppConfigIntField( + label: l10n.pinnedLimitLabel, + description: l10n.pinnedLimitDescription, + value: limits.pinned, + onChanged: (value) => + _onValueChanged(role, 'pinned', value), + controller: _controllers[role]!['pinned'], + ), + if (isHeadlineFilter) + ..._buildNotificationFields(l10n, role, limits), + ], + ), + ); + }).toList(), + ), + ), + ], + ); + } + + /// Builds the list of input fields for notification subscription limits. + List _buildNotificationFields( + AppLocalizations l10n, + AppUserRole role, + SavedFilterLimits limits, + ) { + return PushNotificationSubscriptionDeliveryType.values.map((type) { + final value = limits.notificationSubscriptions?[type] ?? 0; + return AppConfigIntField( + label: type.l10n(context), + description: l10n.notificationSubscriptionLimitDescription, + value: value, + onChanged: (newValue) => _onValueChanged(role, type.name, newValue), + controller: _controllers[role]![type.name], + ); + }).toList(); + } +} From bc848ee9495c24a2188e801ff4a1fc367e25d347 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 16:39:03 +0100 Subject: [PATCH 05/59] refactor(app_config): update user preference form for new data model Refactors the `UserPreferenceLimitsForm` to align with the new map-based structure of `UserPreferenceConfig`. - Updates helper methods (`_getFollowedItemsLimit`, `_getSavedHeadlinesLimit`, `_updateFollowedItemsLimit`, `_updateSavedHeadlinesLimit`) to read from and write to `Map` instead of using individual properties and switch statements. - This change resolves all related compile-time errors regarding undefined getters and parameters. - Updates all relevant doc comments to accurately describe the new map-based logic, improving maintainability. --- .../widgets/user_preference_limits_form.dart | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/lib/app_configuration/widgets/user_preference_limits_form.dart b/lib/app_configuration/widgets/user_preference_limits_form.dart index 6bfca60d..03be00d0 100644 --- a/lib/app_configuration/widgets/user_preference_limits_form.dart +++ b/lib/app_configuration/widgets/user_preference_limits_form.dart @@ -274,55 +274,53 @@ class _UserPreferenceLimitsFormState extends State } } + /// Retrieves the followed items limit for a given [AppUserRole] from the map. int _getFollowedItemsLimit(UserPreferenceConfig config, AppUserRole role) { - switch (role) { - case AppUserRole.guestUser: - return config.guestFollowedItemsLimit; - case AppUserRole.standardUser: - return config.authenticatedFollowedItemsLimit; - case AppUserRole.premiumUser: - return config.premiumFollowedItemsLimit; - } + // The '!' is safe as the model guarantees a value for every role. + return config.followedItemsLimit[role]!; } + /// Retrieves the saved headlines limit for a given [AppUserRole] from the map. int _getSavedHeadlinesLimit(UserPreferenceConfig config, AppUserRole role) { - switch (role) { - case AppUserRole.guestUser: - return config.guestSavedHeadlinesLimit; - case AppUserRole.standardUser: - return config.authenticatedSavedHeadlinesLimit; - case AppUserRole.premiumUser: - return config.premiumSavedHeadlinesLimit; - } + // The '!' is safe as the model guarantees a value for every role. + return config.savedHeadlinesLimit[role]!; } + /// Creates an updated [UserPreferenceConfig] with a new followed items limit + /// for a specific [AppUserRole]. + /// + /// This method creates a mutable copy of the existing map, updates the value + /// for the specified role, and then returns a new `UserPreferenceConfig` + /// with the updated map. UserPreferenceConfig _updateFollowedItemsLimit( UserPreferenceConfig config, int value, AppUserRole role, ) { - switch (role) { - case AppUserRole.guestUser: - return config.copyWith(guestFollowedItemsLimit: value); - case AppUserRole.standardUser: - return config.copyWith(authenticatedFollowedItemsLimit: value); - case AppUserRole.premiumUser: - return config.copyWith(premiumFollowedItemsLimit: value); - } + // Create a mutable copy of the map to avoid modifying the original state. + final newLimits = Map.from(config.followedItemsLimit); + // Update the value for the specified role. + newLimits[role] = value; + // Return a new config object with the updated map. + return config.copyWith(followedItemsLimit: newLimits); } + /// Creates an updated [UserPreferenceConfig] with a new saved headlines limit + /// for a specific [AppUserRole]. + /// + /// This method creates a mutable copy of the existing map, updates the value + /// for the specified role, and then returns a new `UserPreferenceConfig` + /// with the updated map. UserPreferenceConfig _updateSavedHeadlinesLimit( UserPreferenceConfig config, int value, AppUserRole role, ) { - switch (role) { - case AppUserRole.guestUser: - return config.copyWith(guestSavedHeadlinesLimit: value); - case AppUserRole.standardUser: - return config.copyWith(authenticatedSavedHeadlinesLimit: value); - case AppUserRole.premiumUser: - return config.copyWith(premiumSavedHeadlinesLimit: value); - } + // Create a mutable copy of the map to avoid modifying the original state. + final newLimits = Map.from(config.savedHeadlinesLimit); + // Update the value for the specified role. + newLimits[role] = value; + // Return a new config object with the updated map. + return config.copyWith(savedHeadlinesLimit: newLimits); } } From f73ea63872811e57b3dd8e04981a5712d98e07f3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 16:40:53 +0100 Subject: [PATCH 06/59] refactor(app_config): repurpose saved_feed_filters_limit_form Repurposes `SavedFeedFiltersLimitForm` to act as a container for two new `SavedFilterLimitsForm` instances, one for headline filters and one for source filters. - Transforms `SavedFeedFiltersLimitForm` from a StatefulWidget with direct form logic into a StatelessWidget that composes the new reusable `SavedFilterLimitsForm` widgets. - Removes all previous internal state management, TextEditingControllers, and helper methods related to the old flat `UserPreferenceConfig` structure. - Integrates two `ExpansionTile` widgets within `SavedFeedFiltersLimitForm` to logically separate the headline and source filter limit configurations, enhancing UI organization. - Passes down the `remoteConfig` and `onConfigChanged` callbacks to the child `SavedFilterLimitsForm` instances. - Updates the widget's documentation to reflect its new role as a container. --- .../saved_feed_filters_limit_form.dart | 225 +++++++----------- 1 file changed, 84 insertions(+), 141 deletions(-) diff --git a/lib/app_configuration/widgets/saved_feed_filters_limit_form.dart b/lib/app_configuration/widgets/saved_feed_filters_limit_form.dart index d0bc2534..78658e85 100644 --- a/lib/app_configuration/widgets/saved_feed_filters_limit_form.dart +++ b/lib/app_configuration/widgets/saved_feed_filters_limit_form.dart @@ -1,16 +1,16 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_config_form_fields.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; import 'package:ui_kit/ui_kit.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/saved_filter_limits_form.dart'; /// {@template saved_feed_filters_limit_form} -/// A form for configuring saved feed filter limits within the -/// [RemoteConfig]. +/// A container widget for configuring both saved headline and source filter +/// limits within the [RemoteConfig]. /// -/// This form provides fields to set the maximum number of saved filters -/// for guest, authenticated, and premium users. +/// This widget composes two [SavedFilterLimitsForm] instances, one for +/// headline filters and one for source filters, providing a unified interface +/// for managing these configurations. /// {@endtemplate} class SavedFeedFiltersLimitForm extends StatefulWidget { /// {@macro saved_feed_filters_limit_form} @@ -27,157 +27,100 @@ class SavedFeedFiltersLimitForm extends StatefulWidget { final ValueChanged onConfigChanged; @override - State createState() => - _SavedFeedFiltersLimitFormState(); + State createState() => _SavedFeedFiltersLimitFormState(); } class _SavedFeedFiltersLimitFormState extends State - with SingleTickerProviderStateMixin { - late TabController _tabController; - late final Map _controllers; - - @override - void initState() { - super.initState(); - _tabController = TabController( - length: AppUserRole.values.length, - vsync: this, - ); - _initializeControllers(); - } - - @override - void didUpdateWidget(covariant SavedFeedFiltersLimitForm oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.remoteConfig.userPreferenceConfig != - oldWidget.remoteConfig.userPreferenceConfig) { - _updateControllerValues(); - } - } - - void _initializeControllers() { - _controllers = { - for (final role in AppUserRole.values) - role: () { - final limitText = _getSavedFiltersLimit( - widget.remoteConfig.userPreferenceConfig, - role, - ).toString(); - return TextEditingController(text: limitText) - ..selection = TextSelection.collapsed(offset: limitText.length); - }(), - }; - } - - void _updateControllerValues() { - for (final role in AppUserRole.values) { - final newLimit = _getSavedFiltersLimit( - widget.remoteConfig.userPreferenceConfig, - role, - ).toString(); - final controller = _controllers[role]; - if (controller != null && controller.text != newLimit) { - controller - ..text = newLimit - ..selection = TextSelection.collapsed( - offset: newLimit.length, - ); - } - } - } - - @override - void dispose() { - _tabController.dispose(); - for (final controller in _controllers.values) { - controller.dispose(); - } - super.dispose(); - } + with TickerProviderStateMixin { + /// Notifier for the index of the currently expanded top-level ExpansionTile. + /// + /// A value of `null` means no tile is expanded. + final ValueNotifier _expandedTileIndex = ValueNotifier(null); @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; return Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Align( - alignment: AlignmentDirectional.centerStart, - child: SizedBox( - height: kTextTabBarHeight, - child: TabBar( - controller: _tabController, - tabAlignment: TabAlignment.start, - isScrollable: true, - tabs: AppUserRole.values - .map((role) => Tab(text: role.l10n(context))) - .toList(), - ), - ), + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 0; + return ExpansionTile( + key: ValueKey('savedHeadlineFilterLimitsTile_$expandedIndex'), + title: Text(l10n.savedHeadlineFilterLimitsTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + children: [ + Text( + l10n.savedHeadlineFilterLimitsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.7), + ), + ), + const SizedBox(height: AppSpacing.lg), + SavedFilterLimitsForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + filterType: SavedFilterType.headline, + ), + ], + ); + }, ), const SizedBox(height: AppSpacing.lg), - SizedBox( - height: 120, - child: TabBarView( - controller: _tabController, - children: AppUserRole.values.map((role) { - final config = widget.remoteConfig.userPreferenceConfig; - return AppConfigIntField( - label: l10n.savedFeedFiltersLimitLabel, - description: l10n.savedFeedFiltersLimitLabel, - value: _getSavedFiltersLimit(config, role), - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - userPreferenceConfig: _updateSavedFiltersLimit( - config, - value, - role, + ValueListenableBuilder( + valueListenable: _expandedTileIndex, + builder: (context, expandedIndex, child) { + const tileIndex = 1; + return ExpansionTile( + key: ValueKey('savedSourceFilterLimitsTile_$expandedIndex'), + title: Text(l10n.savedSourceFilterLimitsTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + onExpansionChanged: (isExpanded) { + _expandedTileIndex.value = isExpanded ? tileIndex : null; + }, + initiallyExpanded: expandedIndex == tileIndex, + children: [ + Text( + l10n.savedSourceFilterLimitsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.7), ), - ), - ); - }, - controller: _controllers[role], - ); - }).toList(), - ), + ), + const SizedBox(height: AppSpacing.lg), + SavedFilterLimitsForm( + remoteConfig: widget.remoteConfig, + onConfigChanged: widget.onConfigChanged, + filterType: SavedFilterType.source, + ), + ], + ); + }, ), ], ); } - - /// Retrieves the saved filters limit for a given [AppUserRole]. - /// - /// This helper method abstracts the logic for accessing the correct limit - /// from the [UserPreferenceConfig] based on the provided [role]. - int _getSavedFiltersLimit(UserPreferenceConfig config, AppUserRole role) { - switch (role) { - case AppUserRole.guestUser: - return config.guestSavedFiltersLimit; - case AppUserRole.standardUser: - return config.authenticatedSavedFiltersLimit; - case AppUserRole.premiumUser: - return config.premiumSavedFiltersLimit; - } - } - - /// Updates the saved filters limit for a given [AppUserRole]. - /// - /// This helper method abstracts the logic for updating the correct limit - /// within the [UserPreferenceConfig] based on the provided [role] and [value]. - UserPreferenceConfig _updateSavedFiltersLimit( - UserPreferenceConfig config, - int value, - AppUserRole role, - ) { - switch (role) { - case AppUserRole.guestUser: - return config.copyWith(guestSavedFiltersLimit: value); - case AppUserRole.standardUser: - return config.copyWith(authenticatedSavedFiltersLimit: value); - case AppUserRole.premiumUser: - return config.copyWith(premiumSavedFiltersLimit: value); - } +} } } From f851774813f1e06ea4a05f22c0d3e22a8f2c6b95 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 16:41:51 +0100 Subject: [PATCH 07/59] refactor(app_config): update feed tab to use new filter limit container Updates `FeedConfigurationTab` to correctly integrate the refactored `SavedFeedFiltersLimitForm`. - Replaces the outdated `savedFeedFiltersLimitLabel` and `savedFeedFiltersLimitDescription` localization keys with the new, more generic `savedFeedFilterLimitsTitle` and `savedFeedFilterLimitsDescription`. - This ensures the main `ExpansionTile` on the feed tab has the correct title and description, reflecting that it is now a container for both headline and source filter limit configurations. --- lib/app_configuration/view/tabs/feed_configuration_tab.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app_configuration/view/tabs/feed_configuration_tab.dart b/lib/app_configuration/view/tabs/feed_configuration_tab.dart index 3f6acc66..f783e332 100644 --- a/lib/app_configuration/view/tabs/feed_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/feed_configuration_tab.dart @@ -93,7 +93,7 @@ class _FeedConfigurationTabState extends State { const tileIndex = 1; return ExpansionTile( key: ValueKey('savedFeedFilterLimitsTile_$expandedIndex'), - title: Text(l10n.savedFeedFiltersLimitLabel), + title: Text(l10n.savedFeedFilterLimitsTitle), childrenPadding: const EdgeInsetsDirectional.only( start: AppSpacing.lg, top: AppSpacing.md, From d631b4b27149f57ce67ac64c3a81cf683de2bcdb Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 16:44:30 +0100 Subject: [PATCH 08/59] build(l10n): generation --- lib/l10n/app_localizations.dart | 78 ++++++++++++++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 47 ++++++++++++++++++ lib/l10n/app_localizations_en.dart | 48 ++++++++++++++++++ 3 files changed, 173 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ddae4859..3f4997dd 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2671,6 +2671,84 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Premium'** String get subscriptionPremium; + + /// Title for the Saved Headline Filter Limits section + /// + /// In en, this message translates to: + /// **'Saved Headline Filter Limits'** + String get savedHeadlineFilterLimitsTitle; + + /// Description for the Saved Headline Filter Limits section + /// + /// In en, this message translates to: + /// **'Set limits on the number of saved headline filters for each user tier, including total, pinned, and notification subscriptions.'** + String get savedHeadlineFilterLimitsDescription; + + /// Title for the Saved Source Filter Limits section + /// + /// In en, this message translates to: + /// **'Saved Source Filter Limits'** + String get savedSourceFilterLimitsTitle; + + /// Description for the Saved Source Filter Limits section + /// + /// In en, this message translates to: + /// **'Set limits on the number of saved source filters for each user tier, including total and pinned.'** + String get savedSourceFilterLimitsDescription; + + /// Label for the total limit of a filter type + /// + /// In en, this message translates to: + /// **'Total Limit'** + String get totalLimitLabel; + + /// Description for the total limit of a filter type + /// + /// In en, this message translates to: + /// **'The total number of filters of this type a user can create.'** + String get totalLimitDescription; + + /// Label for the pinned limit of a filter type + /// + /// In en, this message translates to: + /// **'Pinned Limit'** + String get pinnedLimitLabel; + + /// Description for the pinned limit of a filter type + /// + /// In en, this message translates to: + /// **'The maximum number of filters of this type that can be pinned.'** + String get pinnedLimitDescription; + + /// Label for the notification subscription limit of a filter type + /// + /// In en, this message translates to: + /// **'Notification Subscription Limit'** + String get notificationSubscriptionLimitLabel; + + /// Description for the notification subscription limit of a filter type + /// + /// In en, this message translates to: + /// **'The maximum number of filters a user can subscribe to for this notification type.'** + String get notificationSubscriptionLimitDescription; + + /// Label for the 'breaking only' push notification delivery type + /// + /// In en, this message translates to: + /// **'Breaking News'** + String get pushNotificationSubscriptionDeliveryTypeBreakingOnly; + + /// Label for the 'daily digest' push notification delivery type + /// + /// In en, this message translates to: + /// **'Daily Digest'** + String get pushNotificationSubscriptionDeliveryTypeDailyDigest; + + /// Label for the 'weekly roundup' push notification delivery type + /// + /// In en, this message translates to: + /// **'Weekly Roundup'** + String get pushNotificationSubscriptionDeliveryTypeWeeklyRoundup; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 83a758c5..2a1cde82 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1421,4 +1421,51 @@ class AppLocalizationsAr extends AppLocalizations { @override String get subscriptionPremium => 'مميز'; + + @override + String get savedHeadlineFilterLimitsTitle => 'حدود مرشحات العناوين المحفوظة'; + + @override + String get savedHeadlineFilterLimitsDescription => + 'تعيين حدود على عدد مرشحات العناوين المحفوظة لكل فئة من فئات المستخدمين، بما في ذلك الإجمالي والمثبتة واشتراكات الإشعارات.'; + + @override + String get savedSourceFilterLimitsTitle => 'حدود مرشحات المصادر المحفوظة'; + + @override + String get savedSourceFilterLimitsDescription => + 'تعيين حدود على عدد مرشحات المصادر المحفوظة لكل فئة من فئات المستخدمين، بما في ذلك الإجمالي والمثبتة.'; + + @override + String get totalLimitLabel => 'الحد الإجمالي'; + + @override + String get totalLimitDescription => + 'العدد الإجمالي للمرشحات من هذا النوع التي يمكن للمستخدم إنشاؤها.'; + + @override + String get pinnedLimitLabel => 'الحد المثبت'; + + @override + String get pinnedLimitDescription => + 'الحد الأقصى لعدد المرشحات من هذا النوع التي يمكن تثبيتها.'; + + @override + String get notificationSubscriptionLimitLabel => 'حد اشتراك الإشعارات'; + + @override + String get notificationSubscriptionLimitDescription => + 'الحد الأقصى لعدد المرشحات التي يمكن للمستخدم الاشتراك فيها لهذا النوع من الإشعارات.'; + + @override + String get pushNotificationSubscriptionDeliveryTypeBreakingOnly => + 'الأخبار العاجلة'; + + @override + String get pushNotificationSubscriptionDeliveryTypeDailyDigest => + 'الملخص اليومي'; + + @override + String get pushNotificationSubscriptionDeliveryTypeWeeklyRoundup => + 'حصاد الأسبوع'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2aceca0c..71c5fcf3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1423,4 +1423,52 @@ class AppLocalizationsEn extends AppLocalizations { @override String get subscriptionPremium => 'Premium'; + + @override + String get savedHeadlineFilterLimitsTitle => 'Saved Headline Filter Limits'; + + @override + String get savedHeadlineFilterLimitsDescription => + 'Set limits on the number of saved headline filters for each user tier, including total, pinned, and notification subscriptions.'; + + @override + String get savedSourceFilterLimitsTitle => 'Saved Source Filter Limits'; + + @override + String get savedSourceFilterLimitsDescription => + 'Set limits on the number of saved source filters for each user tier, including total and pinned.'; + + @override + String get totalLimitLabel => 'Total Limit'; + + @override + String get totalLimitDescription => + 'The total number of filters of this type a user can create.'; + + @override + String get pinnedLimitLabel => 'Pinned Limit'; + + @override + String get pinnedLimitDescription => + 'The maximum number of filters of this type that can be pinned.'; + + @override + String get notificationSubscriptionLimitLabel => + 'Notification Subscription Limit'; + + @override + String get notificationSubscriptionLimitDescription => + 'The maximum number of filters a user can subscribe to for this notification type.'; + + @override + String get pushNotificationSubscriptionDeliveryTypeBreakingOnly => + 'Breaking News'; + + @override + String get pushNotificationSubscriptionDeliveryTypeDailyDigest => + 'Daily Digest'; + + @override + String get pushNotificationSubscriptionDeliveryTypeWeeklyRoundup => + 'Weekly Roundup'; } From 1b2e012d6ab5a4c060ac24288da4a6173af36974 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 16:59:35 +0100 Subject: [PATCH 09/59] feat(l10n): add localization extension for PushNotificationSubscriptionDeliveryType Creates a new extension file to provide localized string representations for the `PushNotificationSubscriptionDeliveryType` enum. This resolves the compile-time error where the `.l10n()` method was undefined. --- .../saved_feed_filters_limit_form.dart | 5 ++--- .../widgets/saved_filter_limits_form.dart | 1 + ...ation_subscription_delivery_type_l10n.dart | 21 +++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 lib/shared/extensions/push_notification_subscription_delivery_type_l10n.dart diff --git a/lib/app_configuration/widgets/saved_feed_filters_limit_form.dart b/lib/app_configuration/widgets/saved_feed_filters_limit_form.dart index 78658e85..30ba7769 100644 --- a/lib/app_configuration/widgets/saved_feed_filters_limit_form.dart +++ b/lib/app_configuration/widgets/saved_feed_filters_limit_form.dart @@ -1,8 +1,8 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/saved_filter_limits_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:ui_kit/ui_kit.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/saved_filter_limits_form.dart'; /// {@template saved_feed_filters_limit_form} /// A container widget for configuring both saved headline and source filter @@ -122,5 +122,4 @@ class _SavedFeedFiltersLimitFormState extends State ); } } - } -} + diff --git a/lib/app_configuration/widgets/saved_filter_limits_form.dart b/lib/app_configuration/widgets/saved_filter_limits_form.dart index c91503e1..b5467566 100644 --- a/lib/app_configuration/widgets/saved_filter_limits_form.dart +++ b/lib/app_configuration/widgets/saved_filter_limits_form.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_config_form_fields.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/push_notification_subscription_delivery_type_l10n.dart'; diff --git a/lib/shared/extensions/push_notification_subscription_delivery_type_l10n.dart b/lib/shared/extensions/push_notification_subscription_delivery_type_l10n.dart new file mode 100644 index 00000000..fbab6840 --- /dev/null +++ b/lib/shared/extensions/push_notification_subscription_delivery_type_l10n.dart @@ -0,0 +1,21 @@ +import 'package:core/core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +/// Provides a localized string representation for +/// [PushNotificationSubscriptionDeliveryType]. +extension PushNotificationSubscriptionDeliveryTypeL10n + on PushNotificationSubscriptionDeliveryType { + /// Returns the localized string for the enum value. + String l10n(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + switch (this) { + case PushNotificationSubscriptionDeliveryType.breakingOnly: + return l10n.pushNotificationSubscriptionDeliveryTypeBreakingOnly; + case PushNotificationSubscriptionDeliveryType.dailyDigest: + return l10n.pushNotificationSubscriptionDeliveryTypeDailyDigest; + case PushNotificationSubscriptionDeliveryType.weeklyRoundup: + return l10n.pushNotificationSubscriptionDeliveryTypeWeeklyRoundup; + } + } +} From 8fb0d39ee824f101493f06ae2839fed2315a878d Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 17:08:06 +0100 Subject: [PATCH 10/59] refactor(app_config): rename saved filter limit widgets for clarity Refactors the naming of the saved filter limit widgets to improve clarity and better reflect their distinct roles, addressing confusion while upholding the DRY principle. - Renames `saved_feed_filters_limit_form.dart` to `saved_filter_limits_section.dart`. - Renames the class `SavedFeedFiltersLimitForm` to `SavedFilterLimitsSection`. - This change clarifies that this widget's role is a high-level UI **section** or container, not a form itself. - Updates `feed_configuration_tab.dart` to import and use the newly named `SavedFilterLimitsSection`. The name of the reusable `SavedFilterLimitsForm` is kept, as its purpose as a specific **form** is now unambiguous in contrast to the container **section**. --- .../view/tabs/feed_configuration_tab.dart | 4 ++-- ...orm.dart => saved_filter_limits_section.dart} | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) rename lib/app_configuration/widgets/{saved_feed_filters_limit_form.dart => saved_filter_limits_section.dart} (92%) diff --git a/lib/app_configuration/view/tabs/feed_configuration_tab.dart b/lib/app_configuration/view/tabs/feed_configuration_tab.dart index f783e332..35e8c186 100644 --- a/lib/app_configuration/view/tabs/feed_configuration_tab.dart +++ b/lib/app_configuration/view/tabs/feed_configuration_tab.dart @@ -1,7 +1,7 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_decorator_form.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/saved_feed_filters_limit_form.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/saved_filter_limits_section.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/user_preference_limits_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/feed_decorator_type_l10n.dart'; @@ -114,7 +114,7 @@ class _FeedConfigurationTabState extends State { ), ), const SizedBox(height: AppSpacing.lg), - SavedFeedFiltersLimitForm( + SavedFilterLimitsSection( remoteConfig: widget.remoteConfig, onConfigChanged: widget.onConfigChanged, ), diff --git a/lib/app_configuration/widgets/saved_feed_filters_limit_form.dart b/lib/app_configuration/widgets/saved_filter_limits_section.dart similarity index 92% rename from lib/app_configuration/widgets/saved_feed_filters_limit_form.dart rename to lib/app_configuration/widgets/saved_filter_limits_section.dart index 30ba7769..ea0cd1cf 100644 --- a/lib/app_configuration/widgets/saved_feed_filters_limit_form.dart +++ b/lib/app_configuration/widgets/saved_filter_limits_section.dart @@ -1,10 +1,10 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/saved_filter_limits_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:ui_kit/ui_kit.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/saved_filter_limits_form.dart'; -/// {@template saved_feed_filters_limit_form} +/// {@template saved_filter_limits_section} /// A container widget for configuring both saved headline and source filter /// limits within the [RemoteConfig]. /// @@ -12,9 +12,9 @@ import 'package:ui_kit/ui_kit.dart'; /// headline filters and one for source filters, providing a unified interface /// for managing these configurations. /// {@endtemplate} -class SavedFeedFiltersLimitForm extends StatefulWidget { - /// {@macro saved_feed_filters_limit_form} - const SavedFeedFiltersLimitForm({ +class SavedFilterLimitsSection extends StatefulWidget { + /// {@macro saved_filter_limits_section} + const SavedFilterLimitsSection({ required this.remoteConfig, required this.onConfigChanged, super.key, @@ -27,11 +27,11 @@ class SavedFeedFiltersLimitForm extends StatefulWidget { final ValueChanged onConfigChanged; @override - State createState() => _SavedFeedFiltersLimitFormState(); + State createState() => _SavedFilterLimitsSectionState(); } -class _SavedFeedFiltersLimitFormState extends State - with TickerProviderStateMixin { +class _SavedFilterLimitsSectionState extends State + with SingleTickerProviderStateMixin { /// Notifier for the index of the currently expanded top-level ExpansionTile. /// /// A value of `null` means no tile is expanded. From 0e42c68e40b5b1777587352c9ffa5e82eb6331eb Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 17:44:41 +0100 Subject: [PATCH 11/59] feat(create_headline): add isBreaking changed event Adds the `CreateHeadlineIsBreakingChanged` event to the `CreateHeadlineBloc` to handle changes to the breaking news status. --- .../bloc/create_headline/create_headline_event.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/content_management/bloc/create_headline/create_headline_event.dart b/lib/content_management/bloc/create_headline/create_headline_event.dart index dfe8af99..ddd08832 100644 --- a/lib/content_management/bloc/create_headline/create_headline_event.dart +++ b/lib/content_management/bloc/create_headline/create_headline_event.dart @@ -73,3 +73,12 @@ final class CreateHeadlineSavedAsDraft extends CreateHeadlineEvent { final class CreateHeadlinePublished extends CreateHeadlineEvent { const CreateHeadlinePublished(); } + +/// Event for when the headline's breaking news status is changed. +final class CreateHeadlineIsBreakingChanged extends CreateHeadlineEvent { + const CreateHeadlineIsBreakingChanged(this.isBreaking); + + final bool isBreaking; + @override + List get props => [isBreaking]; +} From bce27807d8cadf5ad16032e996672e17470df2c4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 17:47:14 +0100 Subject: [PATCH 12/59] feat(create_headline): add isBreaking to state and form validation Adds the `isBreaking` property to `CreateHeadlineState` with a default value of `false`. Updates the `copyWith` method and `props` list to include `isBreaking`. Modifies the `isFormValid` getter to ensure that a headline marked as breaking news (`isBreaking: true`) cannot be saved as a draft, enforcing that breaking news must be published. --- .../bloc/create_headline/create_headline_state.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/content_management/bloc/create_headline/create_headline_state.dart b/lib/content_management/bloc/create_headline/create_headline_state.dart index fccbd476..97c06d0c 100644 --- a/lib/content_management/bloc/create_headline/create_headline_state.dart +++ b/lib/content_management/bloc/create_headline/create_headline_state.dart @@ -30,6 +30,7 @@ final class CreateHeadlineState extends Equatable { this.topic, this.eventCountry, this.exception, + this.isBreaking = false, this.createdHeadline, }); @@ -42,6 +43,7 @@ final class CreateHeadlineState extends Equatable { final Topic? topic; final Country? eventCountry; final HttpException? exception; + final bool isBreaking; final Headline? createdHeadline; /// Returns true if the form is valid and can be submitted. @@ -52,7 +54,8 @@ final class CreateHeadlineState extends Equatable { imageUrl.isNotEmpty && source != null && topic != null && - eventCountry != null; + eventCountry != null && + !isBreaking; // If breaking, it must be published, not drafted. CreateHeadlineState copyWith({ CreateHeadlineStatus? status, @@ -63,6 +66,7 @@ final class CreateHeadlineState extends Equatable { ValueGetter? source, ValueGetter? topic, ValueGetter? eventCountry, + bool? isBreaking, HttpException? exception, Headline? createdHeadline, }) { @@ -75,6 +79,7 @@ final class CreateHeadlineState extends Equatable { source: source != null ? source() : this.source, topic: topic != null ? topic() : this.topic, eventCountry: eventCountry != null ? eventCountry() : this.eventCountry, + isBreaking: isBreaking ?? this.isBreaking, exception: exception, createdHeadline: createdHeadline ?? this.createdHeadline, ); @@ -90,6 +95,7 @@ final class CreateHeadlineState extends Equatable { source, topic, eventCountry, + isBreaking, exception, createdHeadline, ]; From 43165f0a408653bd95b483fa808d595941575ca9 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 17:48:48 +0100 Subject: [PATCH 13/59] feat(create_headline): handle isBreaking event and include in headline creation Updates `CreateHeadlineBloc` to manage the `isBreaking` status. - Adds a handler for the `CreateHeadlineIsBreakingChanged` event to update the state. - Modifies the `_onSavedAsDraft` and `_onPublished` methods to include the `isBreaking` value from the state when creating the new `Headline` object. --- .../bloc/create_headline/create_headline_bloc.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/content_management/bloc/create_headline/create_headline_bloc.dart b/lib/content_management/bloc/create_headline/create_headline_bloc.dart index 50fc11ce..c912cfa5 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -23,6 +23,7 @@ class CreateHeadlineBloc on(_onSourceChanged); on(_onTopicChanged); on(_onCountryChanged); + on(_onIsBreakingChanged); on(_onSavedAsDraft); on(_onPublished); } @@ -80,6 +81,13 @@ class CreateHeadlineBloc emit(state.copyWith(eventCountry: () => event.country)); } + void _onIsBreakingChanged( + CreateHeadlineIsBreakingChanged event, + Emitter emit, + ) { + emit(state.copyWith(isBreaking: event.isBreaking)); + } + /// Handles saving the headline as a draft. Future _onSavedAsDraft( CreateHeadlineSavedAsDraft event, @@ -100,6 +108,7 @@ class CreateHeadlineBloc createdAt: now, updatedAt: now, status: ContentStatus.draft, + isBreaking: state.isBreaking, ); await _headlinesRepository.create(item: newHeadline); @@ -141,6 +150,7 @@ class CreateHeadlineBloc createdAt: now, updatedAt: now, status: ContentStatus.active, + isBreaking: state.isBreaking, ); await _headlinesRepository.create(item: newHeadline); From a02460659bbf59a2519fae74baea8e5237ece997 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 17:50:36 +0100 Subject: [PATCH 14/59] feat(create_headline): add isBreaking toggle and confirmation dialog Implements the UI and logic for the "Mark as Breaking News" feature on the `CreateHeadlinePage`. - Adds a `Switch` widget to the form, bound to the `isBreaking` property in the `CreateHeadlineState`. - Includes descriptive text below the switch to inform the user of its impact. - Refactors the save logic into a `_handleSave` method to improve clarity and handle the new confirmation flow. - Implements a mandatory confirmation dialog that appears only when an admin attempts to publish a new headline with the `isBreaking` flag enabled. - Prevents saving a breaking news article as a draft by showing an informational dialog, enforcing the rule that breaking news must be published. - Updates the save button's enabled state to correctly reflect form validity for both draft and publish actions. --- .../view/create_headline_page.dart | 128 +++++++++++++++--- 1 file changed, 111 insertions(+), 17 deletions(-) diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index ced81804..42a9b5d8 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -105,23 +105,8 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { return IconButton( icon: const Icon(Icons.save), tooltip: l10n.saveChanges, - onPressed: state.isFormValid - ? () async { - final selectedStatus = await _showSaveOptionsDialog( - context, - ); - if (selectedStatus == ContentStatus.active && - context.mounted) { - context.read().add( - const CreateHeadlinePublished(), - ); - } else if (selectedStatus == ContentStatus.draft && - context.mounted) { - context.read().add( - const CreateHeadlineSavedAsDraft(), - ); - } - } + onPressed: _isSaveButtonEnabled(state) + ? () => _handleSave(context, state, l10n) : null, ); }, @@ -206,6 +191,25 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { .add(CreateHeadlineImageUrlChanged(value)), ), const SizedBox(height: AppSpacing.lg), + Row( + children: [ + Expanded( + child: Text( + l10n.isBreakingNewsLabel, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + Switch( + value: state.isBreaking, + onChanged: (value) => context + .read() + .add(CreateHeadlineIsBreakingChanged(value)), + ), + ], + ), + Text(l10n.isBreakingNewsDescription), + const SizedBox(height: AppSpacing.lg), + // Existing SearchableSelectionInput widgets SearchableSelectionInput( label: l10n.sourceName, selectedItems: state.source != null @@ -305,4 +309,94 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { ), ); } + + /// Determines if the save button should be enabled. + /// + /// The button is enabled if the form is valid for drafting, or if all + /// fields are filled for publishing (regardless of breaking news status). + bool _isSaveButtonEnabled(CreateHeadlineState state) { + final allFieldsFilled = + state.title.isNotEmpty && + state.excerpt.isNotEmpty && + state.url.isNotEmpty && + state.imageUrl.isNotEmpty && + state.source != null && + state.topic != null && + state.eventCountry != null; + + return allFieldsFilled; + } + + /// Handles the save logic, including showing save options and the + /// confirmation dialog for breaking news. + Future _handleSave( + BuildContext context, + CreateHeadlineState state, + AppLocalizations l10n, + ) async { + final selectedStatus = await _showSaveOptionsDialog(context); + + // If the user cancels the dialog, do nothing. + if (selectedStatus == null) return; + + // If the user tries to save as draft but it's breaking news, show an error. + if (selectedStatus == ContentStatus.draft && state.isBreaking) { + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.invalidFormTitle), + content: Text(l10n.cannotDraftBreakingNews), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.ok), + ), + ], + ), + ); + return; + } + + // If publishing as breaking news, show an extra confirmation. + if (selectedStatus == ContentStatus.active && state.isBreaking) { + final confirmBreaking = await _showBreakingNewsConfirmationDialog( + context, + l10n, + ); + if (confirmBreaking != true) return; // If not confirmed, do nothing. + } + + // Dispatch the appropriate event based on user's choice. + if (selectedStatus == ContentStatus.active) { + context.read().add(const CreateHeadlinePublished()); + } else if (selectedStatus == ContentStatus.draft) { + context.read().add( + const CreateHeadlineSavedAsDraft(), + ); + } + } + + /// Shows a confirmation dialog specifically for publishing breaking news. + Future _showBreakingNewsConfirmationDialog( + BuildContext context, + AppLocalizations l10n, + ) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.confirmBreakingNewsTitle), + content: Text(l10n.confirmBreakingNewsMessage), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.cancelButton), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l10n.confirmPublishButton), + ), + ], + ), + ); + } } From d2d05348376954400da2d86700f17e2df1514c95 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 17:51:16 +0100 Subject: [PATCH 15/59] feat(edit_headline): add isBreaking changed event Adds the `EditHeadlineIsBreakingChanged` event to the `EditHeadlineBloc` to handle changes to the breaking news status during an edit operation. --- .../bloc/edit_headline/edit_headline_event.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/content_management/bloc/edit_headline/edit_headline_event.dart b/lib/content_management/bloc/edit_headline/edit_headline_event.dart index 273d6edf..fa9b1a15 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_event.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_event.dart @@ -78,3 +78,12 @@ final class EditHeadlineSavedAsDraft extends EditHeadlineEvent { final class EditHeadlinePublished extends EditHeadlineEvent { const EditHeadlinePublished(); } + +/// Event for when the headline's breaking news status is changed. +final class EditHeadlineIsBreakingChanged extends EditHeadlineEvent { + const EditHeadlineIsBreakingChanged(this.isBreaking); + + final bool isBreaking; + @override + List get props => [isBreaking]; +} From 8d49993fcd84c87f584203c980efb8f70d110963 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 17:52:28 +0100 Subject: [PATCH 16/59] feat(edit_headline): add isBreaking to state Adds the `isBreaking` property to `EditHeadlineState` with a default value of `false`. Updates the `copyWith` method and `props` list to include `isBreaking`. The `isFormValid` getter remains unchanged, as `isBreaking` does not affect form validity during editing. --- .../bloc/edit_headline/edit_headline_state.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/content_management/bloc/edit_headline/edit_headline_state.dart b/lib/content_management/bloc/edit_headline/edit_headline_state.dart index 743953b4..87667319 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_state.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_state.dart @@ -31,6 +31,7 @@ final class EditHeadlineState extends Equatable { this.topic, this.eventCountry, this.exception, + this.isBreaking = false, this.updatedHeadline, }); @@ -44,6 +45,7 @@ final class EditHeadlineState extends Equatable { final Topic? topic; final Country? eventCountry; final HttpException? exception; + final bool isBreaking; final Headline? updatedHeadline; /// Returns true if the form is valid and can be submitted. @@ -57,6 +59,9 @@ final class EditHeadlineState extends Equatable { topic != null && eventCountry != null; + // isBreaking is not part of form validity for editing, as it doesn't + // trigger new notifications on update. + EditHeadlineState copyWith({ EditHeadlineStatus? status, String? headlineId, @@ -67,6 +72,7 @@ final class EditHeadlineState extends Equatable { ValueGetter? source, ValueGetter? topic, ValueGetter? eventCountry, + bool? isBreaking, HttpException? exception, Headline? updatedHeadline, }) { @@ -80,6 +86,7 @@ final class EditHeadlineState extends Equatable { source: source != null ? source() : this.source, topic: topic != null ? topic() : this.topic, eventCountry: eventCountry != null ? eventCountry() : this.eventCountry, + isBreaking: isBreaking ?? this.isBreaking, exception: exception, updatedHeadline: updatedHeadline ?? this.updatedHeadline, ); @@ -96,6 +103,7 @@ final class EditHeadlineState extends Equatable { source, topic, eventCountry, + isBreaking, exception, updatedHeadline, ]; From d3ab226961f86b2eef1075415c78a576ff59c1ca Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 17:53:48 +0100 Subject: [PATCH 17/59] feat(edit_headline): handle isBreaking event and include in headline update Updates `EditHeadlineBloc` to manage the `isBreaking` status for existing headlines. - Initializes the `isBreaking` state from the loaded headline data in `_onEditHeadlineLoaded`. - Adds a handler for the `EditHeadlineIsBreakingChanged` event to update the state. - Modifies the `_onSavedAsDraft` and `_onPublished` methods to include the `isBreaking` value from the state when updating the `Headline` object. --- .../bloc/edit_headline/edit_headline_bloc.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart index d57dba7b..7b8e93b5 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart @@ -28,6 +28,7 @@ class EditHeadlineBloc extends Bloc { on(_onSourceChanged); on(_onTopicChanged); on(_onCountryChanged); + on(_onIsBreakingChanged); on(_onSavedAsDraft); on(_onPublished); @@ -52,6 +53,7 @@ class EditHeadlineBloc extends Bloc { source: () => headline.source, topic: () => headline.topic, eventCountry: () => headline.eventCountry, + isBreaking: headline.isBreaking, ), ); } on HttpException catch (e) { @@ -142,6 +144,16 @@ class EditHeadlineBloc extends Bloc { ); } + void _onIsBreakingChanged( + EditHeadlineIsBreakingChanged event, + Emitter emit, + ) { + emit( + state.copyWith( + isBreaking: event.isBreaking, status: EditHeadlineStatus.initial), + ); + } + /// Handles saving the headline as a draft. Future _onSavedAsDraft( EditHeadlineSavedAsDraft event, @@ -160,6 +172,7 @@ class EditHeadlineBloc extends Bloc { source: state.source, topic: state.topic, eventCountry: state.eventCountry, + isBreaking: state.isBreaking, status: ContentStatus.draft, updatedAt: DateTime.now(), ); @@ -204,6 +217,7 @@ class EditHeadlineBloc extends Bloc { source: state.source, topic: state.topic, eventCountry: state.eventCountry, + isBreaking: state.isBreaking, status: ContentStatus.active, updatedAt: DateTime.now(), ); From 513c770105ebab08c997fdfd4f74a2e2ba3bb0ff Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 17:58:49 +0100 Subject: [PATCH 18/59] feat(edit_headline): add isBreaking toggle to edit headline page Adds the UI elements and logic for the "Mark as Breaking News" feature on the `EditHeadlinePage`. - Introduces a `Switch` widget to the form, bound to the `isBreaking` property in the `EditHeadlineState`. - Includes descriptive text below the switch to inform the user that changing the `isBreaking` status during editing will *not* trigger new push notifications. - Adds a comment to the `listener` block to clarify why `isBreaking` is not handled there. --- .../view/edit_headline_page.dart | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/content_management/view/edit_headline_page.dart b/lib/content_management/view/edit_headline_page.dart index 06aee388..611ca305 100644 --- a/lib/content_management/view/edit_headline_page.dart +++ b/lib/content_management/view/edit_headline_page.dart @@ -147,6 +147,7 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { _excerptController.text = state.excerpt; _urlController.text = state.url; _imageUrlController.text = state.imageUrl; + // No need to update a controller for `isBreaking` as it's a Switch. } }, builder: (context, state) { @@ -221,6 +222,25 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { .add(EditHeadlineImageUrlChanged(value)), ), const SizedBox(height: AppSpacing.lg), + Row( + children: [ + Expanded( + child: Text( + l10n.isBreakingNewsLabel, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + Switch( + value: state.isBreaking, + onChanged: (value) => context + .read() + .add(EditHeadlineIsBreakingChanged(value)), + ), + ], + ), + Text(l10n.isBreakingNewsDescriptionEdit), + const SizedBox(height: AppSpacing.lg), + // Existing SearchableSelectionInput widgets SearchableSelectionInput( label: l10n.sourceName, selectedItems: state.source != null From f881f5326578d1fe83d1fcc270f199c0e39c1de8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 18:00:35 +0100 Subject: [PATCH 19/59] feat(headlines): display breaking news icon in data table Updates the `HeadlinesPage` data table to provide a clear visual indicator for breaking news. - A lightning bolt icon (`Icons.flash_on`) is now displayed next to the title of any headline where `isBreaking` is `true`. - The title `Text` widget is wrapped in an `Expanded` widget within a `Row` to ensure proper layout and prevent overflow issues. --- .../view/headlines_page.dart | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/content_management/view/headlines_page.dart b/lib/content_management/view/headlines_page.dart index c9b90eef..5604b361 100644 --- a/lib/content_management/view/headlines_page.dart +++ b/lib/content_management/view/headlines_page.dart @@ -221,10 +221,25 @@ class _HeadlinesDataSource extends DataTableSource { }, cells: [ DataCell( - Text( - headline.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, + Row( + children: [ + if (headline.isBreaking) + Padding( + padding: const EdgeInsets.only(right: AppSpacing.sm), + child: Icon( + Icons.flash_on, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + ), + Expanded( + child: Text( + headline.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ), if (!isMobile) // Conditionally show Source Name From 10b9ad8627a07e270e4cd66dbce8b4c4e54a3101 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 18:04:39 +0100 Subject: [PATCH 20/59] feat(l10n): add Arabic and English translations for breaking news features - Add Arabic translations for breaking news labels, descriptions, and dialog messages - Add English translations for breaking news labels, descriptions, and dialog messages - Include translations for new features such as marking news as breaking, publishing confirmation, and error messages --- lib/l10n/arb/app_ar.arb | 33 +++++++++++++++++++++++++++++++++ lib/l10n/arb/app_en.arb | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index a5b14018..f8e06fdb 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1855,4 +1855,37 @@ "@pushNotificationSubscriptionDeliveryTypeWeeklyRoundup": { "description": "تسمية لنوع توصيل إشعارات 'حصاد الأسبوع'" } +, + "isBreakingNewsLabel": "وضع علامة 'خبر عاجل'", + "@isBreakingNewsLabel": { + "description": "تسمية مفتاح وضع علامة 'خبر عاجل' على العنوان" + }, + "isBreakingNewsDescription": "سيؤدي تمكين هذا إلى إرسال إشعار فوري لجميع المستخدمين المشتركين عند النشر.", + "@isBreakingNewsDescription": { + "description": "وصف مفتاح 'خبر عاجل' في صفحة الإنشاء" + }, + "isBreakingNewsDescriptionEdit": "تغيير هذه الحالة أثناء التعديل لن يؤدي إلى إرسال إشعار فوري جديد، حيث يتم إرسال الإشعارات فقط عند الإنشاء الأولي.", + "@isBreakingNewsDescriptionEdit": { + "description": "وصف مفتاح 'خبر عاجل' في صفحة التعديل" + }, + "confirmBreakingNewsTitle": "تأكيد نشر خبر عاجل", + "@confirmBreakingNewsTitle": { + "description": "عنوان مربع حوار التأكيد عند نشر خبر عاجل" + }, + "confirmBreakingNewsMessage": "هل أنت متأكد أنك تريد نشر هذا كخبر عاجل؟ سيؤدي هذا الإجراء إلى إرسال إشعار فوري لجميع المستخدمين المشتركين.", + "@confirmBreakingNewsMessage": { + "description": "رسالة مربع حوار التأكيد عند نشر خبر عاجل" + }, + "confirmPublishButton": "تأكيد ونشر", + "@confirmPublishButton": { + "description": "نص زر التأكيد والنشر لخبر عاجل" + }, + "cannotDraftBreakingNews": "لا يمكن حفظ الأخبار العاجلة كمسودة. يرجى نشرها أو تعطيل مفتاح 'خبر عاجل'.", + "@cannotDraftBreakingNews": { + "description": "رسالة خطأ تظهر عندما يحاول المستخدم حفظ مقال إخباري عاجل كمسودة." + }, + "ok": "موافق", + "@ok": { + "description": "نص زر 'موافق' الشائع." + } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index e37b03e6..c1b8ac00 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1850,4 +1850,37 @@ "@pushNotificationSubscriptionDeliveryTypeWeeklyRoundup": { "description": "Label for the 'weekly roundup' push notification delivery type" } +, + "isBreakingNewsLabel": "Mark as Breaking News", + "@isBreakingNewsLabel": { + "description": "Label for the switch to mark a headline as breaking news" + }, + "isBreakingNewsDescription": "Enabling this will send an immediate push notification to all subscribed users upon publication.", + "@isBreakingNewsDescription": { + "description": "Description for the breaking news switch on the create page" + }, + "isBreakingNewsDescriptionEdit": "Changing this status during an edit will NOT trigger a new push notification, as notifications are only sent on initial creation.", + "@isBreakingNewsDescriptionEdit": { + "description": "Description for the breaking news switch on the edit page" + }, + "confirmBreakingNewsTitle": "Confirm Breaking News Publication", + "@confirmBreakingNewsTitle": { + "description": "Title for the confirmation dialog when publishing breaking news" + }, + "confirmBreakingNewsMessage": "Are you sure you want to publish this as breaking news? This action will send an immediate push notification to all subscribed users.", + "@confirmBreakingNewsMessage": { + "description": "Message for the confirmation dialog when publishing breaking news" + }, + "confirmPublishButton": "Confirm & Publish", + "@confirmPublishButton": { + "description": "Text for the button to confirm publishing breaking news" + }, + "cannotDraftBreakingNews": "Breaking news cannot be saved as a draft. Please publish it or disable the 'Breaking News' toggle.", + "@cannotDraftBreakingNews": { + "description": "Error message shown when a user tries to save a breaking news article as a draft." + }, + "ok": "OK", + "@ok": { + "description": "A common 'OK' button text." + } } \ No newline at end of file From 39bfded791bf653e4bc07fdcecd7e125ab5ae322 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 18:05:00 +0100 Subject: [PATCH 21/59] build(l10n): generation --- lib/l10n/app_localizations.dart | 48 ++++++++++++++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 28 +++++++++++++++++ lib/l10n/app_localizations_en.dart | 28 +++++++++++++++++ 3 files changed, 104 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 3f4997dd..d317c213 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2749,6 +2749,54 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Weekly Roundup'** String get pushNotificationSubscriptionDeliveryTypeWeeklyRoundup; + + /// Label for the switch to mark a headline as breaking news + /// + /// In en, this message translates to: + /// **'Mark as Breaking News'** + String get isBreakingNewsLabel; + + /// Description for the breaking news switch on the create page + /// + /// In en, this message translates to: + /// **'Enabling this will send an immediate push notification to all subscribed users upon publication.'** + String get isBreakingNewsDescription; + + /// Description for the breaking news switch on the edit page + /// + /// In en, this message translates to: + /// **'Changing this status during an edit will NOT trigger a new push notification, as notifications are only sent on initial creation.'** + String get isBreakingNewsDescriptionEdit; + + /// Title for the confirmation dialog when publishing breaking news + /// + /// In en, this message translates to: + /// **'Confirm Breaking News Publication'** + String get confirmBreakingNewsTitle; + + /// Message for the confirmation dialog when publishing breaking news + /// + /// In en, this message translates to: + /// **'Are you sure you want to publish this as breaking news? This action will send an immediate push notification to all subscribed users.'** + String get confirmBreakingNewsMessage; + + /// Text for the button to confirm publishing breaking news + /// + /// In en, this message translates to: + /// **'Confirm & Publish'** + String get confirmPublishButton; + + /// Error message shown when a user tries to save a breaking news article as a draft. + /// + /// In en, this message translates to: + /// **'Breaking news cannot be saved as a draft. Please publish it or disable the \'Breaking News\' toggle.'** + String get cannotDraftBreakingNews; + + /// A common 'OK' button text. + /// + /// In en, this message translates to: + /// **'OK'** + String get ok; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 2a1cde82..de1b8d6e 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1468,4 +1468,32 @@ class AppLocalizationsAr extends AppLocalizations { @override String get pushNotificationSubscriptionDeliveryTypeWeeklyRoundup => 'حصاد الأسبوع'; + + @override + String get isBreakingNewsLabel => 'وضع علامة \'خبر عاجل\''; + + @override + String get isBreakingNewsDescription => + 'سيؤدي تمكين هذا إلى إرسال إشعار فوري لجميع المستخدمين المشتركين عند النشر.'; + + @override + String get isBreakingNewsDescriptionEdit => + 'تغيير هذه الحالة أثناء التعديل لن يؤدي إلى إرسال إشعار فوري جديد، حيث يتم إرسال الإشعارات فقط عند الإنشاء الأولي.'; + + @override + String get confirmBreakingNewsTitle => 'تأكيد نشر خبر عاجل'; + + @override + String get confirmBreakingNewsMessage => + 'هل أنت متأكد أنك تريد نشر هذا كخبر عاجل؟ سيؤدي هذا الإجراء إلى إرسال إشعار فوري لجميع المستخدمين المشتركين.'; + + @override + String get confirmPublishButton => 'تأكيد ونشر'; + + @override + String get cannotDraftBreakingNews => + 'لا يمكن حفظ الأخبار العاجلة كمسودة. يرجى نشرها أو تعطيل مفتاح \'خبر عاجل\'.'; + + @override + String get ok => 'موافق'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 71c5fcf3..58bbd408 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1471,4 +1471,32 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pushNotificationSubscriptionDeliveryTypeWeeklyRoundup => 'Weekly Roundup'; + + @override + String get isBreakingNewsLabel => 'Mark as Breaking News'; + + @override + String get isBreakingNewsDescription => + 'Enabling this will send an immediate push notification to all subscribed users upon publication.'; + + @override + String get isBreakingNewsDescriptionEdit => + 'Changing this status during an edit will NOT trigger a new push notification, as notifications are only sent on initial creation.'; + + @override + String get confirmBreakingNewsTitle => 'Confirm Breaking News Publication'; + + @override + String get confirmBreakingNewsMessage => + 'Are you sure you want to publish this as breaking news? This action will send an immediate push notification to all subscribed users.'; + + @override + String get confirmPublishButton => 'Confirm & Publish'; + + @override + String get cannotDraftBreakingNews => + 'Breaking news cannot be saved as a draft. Please publish it or disable the \'Breaking News\' toggle.'; + + @override + String get ok => 'OK'; } From fae10f98019fe59f48efbb353a4226252e1f2725 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 18:19:40 +0100 Subject: [PATCH 22/59] fix(content_management): check context.mounted before popping routes - Add context.mounted check before popping routes in dialog actions - Add import for AppLocalizations - Improve null safety by checking context.mounted in other places --- .../widgets/saved_filter_limits_section.dart | 2 +- .../view/create_headline_page.dart | 30 +++++++++++++++---- lib/content_management/view/topics_page.dart | 1 - 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/app_configuration/widgets/saved_filter_limits_section.dart b/lib/app_configuration/widgets/saved_filter_limits_section.dart index ea0cd1cf..7e763f55 100644 --- a/lib/app_configuration/widgets/saved_filter_limits_section.dart +++ b/lib/app_configuration/widgets/saved_filter_limits_section.dart @@ -1,8 +1,8 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/saved_filter_limits_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:ui_kit/ui_kit.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/saved_filter_limits_form.dart'; /// {@template saved_filter_limits_section} /// A container widget for configuring both saved headline and source filter diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index 42a9b5d8..0cdd7f39 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -3,6 +3,7 @@ import 'package:data_repository/data_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/create_headline/create_headline_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; import 'package:go_router/go_router.dart'; @@ -70,11 +71,17 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { content: Text(l10n.saveHeadlineMessage), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(ContentStatus.draft), + onPressed: () { + if (!context.mounted) return; + Navigator.of(context).pop(ContentStatus.draft); + }, child: Text(l10n.saveAsDraft), ), TextButton( - onPressed: () => Navigator.of(context).pop(ContentStatus.active), + onPressed: () { + if (!context.mounted) return; + Navigator.of(context).pop(ContentStatus.active); + }, child: Text(l10n.publish), ), ], @@ -341,6 +348,7 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { // If the user tries to save as draft but it's breaking news, show an error. if (selectedStatus == ContentStatus.draft && state.isBreaking) { + if (!context.mounted) return; await showDialog( context: context, builder: (context) => AlertDialog( @@ -348,7 +356,10 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { content: Text(l10n.cannotDraftBreakingNews), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () { + if (!context.mounted) return; + Navigator.of(context).pop(); + }, child: Text(l10n.ok), ), ], @@ -359,6 +370,7 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { // If publishing as breaking news, show an extra confirmation. if (selectedStatus == ContentStatus.active && state.isBreaking) { + if (!context.mounted) return; final confirmBreaking = await _showBreakingNewsConfirmationDialog( context, l10n, @@ -368,8 +380,10 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { // Dispatch the appropriate event based on user's choice. if (selectedStatus == ContentStatus.active) { + if (!context.mounted) return; context.read().add(const CreateHeadlinePublished()); } else if (selectedStatus == ContentStatus.draft) { + if (!context.mounted) return; context.read().add( const CreateHeadlineSavedAsDraft(), ); @@ -388,11 +402,17 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { content: Text(l10n.confirmBreakingNewsMessage), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(false), + onPressed: () { + if (!context.mounted) return; + Navigator.of(context).pop(false); + }, child: Text(l10n.cancelButton), ), TextButton( - onPressed: () => Navigator.of(context).pop(true), + onPressed: () { + if (!context.mounted) return; + Navigator.of(context).pop(true); + }, child: Text(l10n.confirmPublishButton), ), ], diff --git a/lib/content_management/view/topics_page.dart b/lib/content_management/view/topics_page.dart index e7152456..fc12d199 100644 --- a/lib/content_management/view/topics_page.dart +++ b/lib/content_management/view/topics_page.dart @@ -219,7 +219,6 @@ class _TopicsDataSource extends DataTableSource { ), DataCell( Text( - // TODO(fulleni): Make date format configurable by admin. DateFormat('dd-MM-yyyy').format(topic.updatedAt.toLocal()), ), ), From 26a257c5aa3f470a5eb4fe2d633de164dc43041a Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 18:30:26 +0100 Subject: [PATCH 23/59] refactor(headlines): improve breaking news icon alignment and RTL support Refactors the `HeadlinesPage` data table to enhance the display of the "breaking news" icon. - Replaces the `Row` layout with a `Stack` to overlay the icon. This ensures that all headline titles are vertically aligned at their starting point, whether the icon is present or not, creating a cleaner look. - Uses `EdgeInsetsDirectional` for padding to provide correct spacing in both LTR and RTL layouts, fixing the previous alignment issue in RTL mode. --- .../view/headlines_page.dart | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/content_management/view/headlines_page.dart b/lib/content_management/view/headlines_page.dart index 5604b361..f4ba6b18 100644 --- a/lib/content_management/view/headlines_page.dart +++ b/lib/content_management/view/headlines_page.dart @@ -221,24 +221,31 @@ class _HeadlinesDataSource extends DataTableSource { }, cells: [ DataCell( - Row( - children: [ - if (headline.isBreaking) - Padding( - padding: const EdgeInsets.only(right: AppSpacing.sm), - child: Icon( - Icons.flash_on, - size: 18, - color: Theme.of(context).colorScheme.primary, - ), + Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + // Add padding to the text to make space for the icon only when + // the headline is breaking news. This ensures all titles align + // vertically regardless of the icon's presence. + Padding( + padding: EdgeInsetsDirectional.only( + start: headline.isBreaking + ? AppSpacing.xl + AppSpacing.xs + : 0, ), - Expanded( child: Text( headline.title, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), + // Conditionally display the icon at the start of the cell. + if (headline.isBreaking) + Icon( + Icons.flash_on, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), ], ), ), From 3948b149647e917c3a4b2082cf49781aec8face8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 18:44:23 +0100 Subject: [PATCH 24/59] feat(headlines_filter): add isBreaking to filter state Adds a nullable `bool? isBreaking` property to `HeadlinesFilterState` to support filtering by breaking news status. - `null` (default) includes all headlines. - `true` includes only breaking news. - `false` includes only non-breaking news. Updates the constructor, `copyWith`, and `props` to accommodate the new property. --- .../bloc/headlines_filter/headlines_filter_state.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/content_management/bloc/headlines_filter/headlines_filter_state.dart b/lib/content_management/bloc/headlines_filter/headlines_filter_state.dart index 7017a7ab..577a9794 100644 --- a/lib/content_management/bloc/headlines_filter/headlines_filter_state.dart +++ b/lib/content_management/bloc/headlines_filter/headlines_filter_state.dart @@ -15,6 +15,7 @@ class HeadlinesFilterState extends Equatable { this.selectedSourceIds = const [], this.selectedTopicIds = const [], this.selectedCountryIds = const [], + this.isBreaking, }); /// The current text in the search query field. @@ -32,6 +33,10 @@ class HeadlinesFilterState extends Equatable { /// The list of country IDs to be included in the filter. final List selectedCountryIds; + /// The breaking news status to filter by. + /// `null` = all, `true` = breaking only, `false` = non-breaking only. + final bool? isBreaking; + /// Creates a copy of this state with the given fields replaced with the /// new values. HeadlinesFilterState copyWith({ @@ -40,6 +45,7 @@ class HeadlinesFilterState extends Equatable { List? selectedSourceIds, List? selectedTopicIds, List? selectedCountryIds, + bool? isBreaking, }) { return HeadlinesFilterState( searchQuery: searchQuery ?? this.searchQuery, @@ -47,15 +53,17 @@ class HeadlinesFilterState extends Equatable { selectedSourceIds: selectedSourceIds ?? this.selectedSourceIds, selectedTopicIds: selectedTopicIds ?? this.selectedTopicIds, selectedCountryIds: selectedCountryIds ?? this.selectedCountryIds, + isBreaking: isBreaking ?? this.isBreaking, ); } @override - List get props => [ + List get props => [ searchQuery, selectedStatus, selectedSourceIds, selectedTopicIds, selectedCountryIds, + isBreaking, ]; } From 970c84b8830cab51d665b717c9ad0339b0015e56 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 18:45:35 +0100 Subject: [PATCH 25/59] feat(headlines_filter): add breaking news filter event Updates `headlines_filter_event.dart` to support filtering by breaking news status. - Adds a new `HeadlinesBreakingNewsFilterChanged` event to manage the three-state filter (`true`, `false`, `null`). - Updates the `HeadlinesFilterApplied` event to carry the `isBreaking` state when filters are applied. --- .../headlines_filter/headlines_filter_event.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/content_management/bloc/headlines_filter/headlines_filter_event.dart b/lib/content_management/bloc/headlines_filter/headlines_filter_event.dart index d4299379..6efa1e8d 100644 --- a/lib/content_management/bloc/headlines_filter/headlines_filter_event.dart +++ b/lib/content_management/bloc/headlines_filter/headlines_filter_event.dart @@ -57,6 +57,16 @@ final class HeadlinesCountryFilterChanged extends HeadlinesFilterEvent { List get props => [countryIds]; } +/// Event to notify the BLoC that the breaking news filter has changed. +final class HeadlinesBreakingNewsFilterChanged extends HeadlinesFilterEvent { + const HeadlinesBreakingNewsFilterChanged(this.isBreaking); + + final bool? isBreaking; + + @override + List get props => [isBreaking]; +} + /// Event to request applying all current filters. final class HeadlinesFilterApplied extends HeadlinesFilterEvent { const HeadlinesFilterApplied({ @@ -65,6 +75,7 @@ final class HeadlinesFilterApplied extends HeadlinesFilterEvent { required this.selectedSourceIds, required this.selectedTopicIds, required this.selectedCountryIds, + required this.isBreaking, }); final String searchQuery; @@ -72,6 +83,7 @@ final class HeadlinesFilterApplied extends HeadlinesFilterEvent { final List selectedSourceIds; final List selectedTopicIds; final List selectedCountryIds; + final bool? isBreaking; @override List get props => [ @@ -80,6 +92,7 @@ final class HeadlinesFilterApplied extends HeadlinesFilterEvent { selectedSourceIds, selectedTopicIds, selectedCountryIds, + isBreaking, ]; } From 44cd47937ee89c7c53aa47fd3f37fe30fb0223a6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 18:46:56 +0100 Subject: [PATCH 26/59] feat(headlines_filter): handle breaking news filter state Updates `HeadlinesFilterBloc` to manage the `isBreaking` filter state. - Adds a handler for the `HeadlinesBreakingNewsFilterChanged` event to update the state with `true`, `false`, or `null`. - Updates the `_onHeadlinesFilterApplied` handler to persist the `isBreaking` state when filters are applied. - The existing `_onHeadlinesFilterReset` handler correctly resets `isBreaking` to `null` by re-emitting the initial state. --- .../headlines_filter/headlines_filter_bloc.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart b/lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart index e0a14b54..c6a9f6bf 100644 --- a/lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart +++ b/lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart @@ -21,6 +21,9 @@ class HeadlinesFilterBloc on(_onHeadlinesSourceFilterChanged); on(_onHeadlinesTopicFilterChanged); on(_onHeadlinesCountryFilterChanged); + on( + _onHeadlinesBreakingNewsFilterChanged, + ); on(_onHeadlinesFilterApplied); on(_onHeadlinesFilterReset); } @@ -73,6 +76,17 @@ class HeadlinesFilterBloc emit(state.copyWith(selectedCountryIds: event.countryIds)); } + /// Handles changes to the breaking news filter. + /// + /// This updates the `isBreaking` status for the filter, which can be + /// `true` (breaking only), `false` (non-breaking only), or `null` (all). + void _onHeadlinesBreakingNewsFilterChanged( + HeadlinesBreakingNewsFilterChanged event, + Emitter emit, + ) { + emit(state.copyWith(isBreaking: event.isBreaking)); + } + /// Handles the application of all current filter settings. /// /// This event is dispatched when the user explicitly confirms the filters @@ -89,6 +103,7 @@ class HeadlinesFilterBloc selectedSourceIds: event.selectedSourceIds, selectedTopicIds: event.selectedTopicIds, selectedCountryIds: event.selectedCountryIds, + isBreaking: event.isBreaking, ), ); } From b0f6be15ed8e2abd5232ac82928886cbb845142c Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 18:48:45 +0100 Subject: [PATCH 27/59] feat(content_management): update headlines filter map with isBreaking Modifies the `buildHeadlinesFilterMap` method in `ContentManagementBloc` to include the new `isBreaking` filter. - If `state.isBreaking` is not null, it adds an `'isBreaking'` entry to the filter map with the corresponding boolean value. - This allows the backend query to filter headlines based on their breaking news status. --- lib/content_management/bloc/content_management_bloc.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/content_management/bloc/content_management_bloc.dart b/lib/content_management/bloc/content_management_bloc.dart index 704e948d..87f6bb5c 100644 --- a/lib/content_management/bloc/content_management_bloc.dart +++ b/lib/content_management/bloc/content_management_bloc.dart @@ -153,6 +153,10 @@ class ContentManagementBloc filter['eventCountry.id'] = {r'$in': state.selectedCountryIds}; } + if (state.isBreaking != null) { + filter['isBreaking'] = state.isBreaking; + } + return filter; } From 872fbe5fefa34b2705ea9d452b3732d3b0da579b Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 18:51:45 +0100 Subject: [PATCH 28/59] feat(filter_dialog): add isBreaking to dialog state Updates `FilterDialogState` to include a nullable `bool? isBreaking` property. This allows the filter dialog to temporarily manage the state of the new breaking news filter while the dialog is open. The constructor, `copyWith`, and `props` are updated accordingly. --- .../widgets/filter_dialog/bloc/filter_dialog_state.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_state.dart b/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_state.dart index da727489..85ff8881 100644 --- a/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_state.dart +++ b/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_state.dart @@ -30,6 +30,7 @@ final class FilterDialogState extends Equatable { this.selectedSourceIds = const [], this.selectedTopicIds = const [], this.selectedCountryIds = const [], + this.isBreaking, this.selectedSourceTypes = const [], this.selectedLanguageCodes = const [], this.selectedHeadquartersCountryIds = const [], @@ -66,6 +67,10 @@ final class FilterDialogState extends Equatable { /// The list of country IDs to be included in the filter for headlines. final List selectedCountryIds; + /// The breaking news status to filter by for headlines. + /// `null` = all, `true` = breaking only, `false` = non-breaking only. + final bool? isBreaking; + /// The list of source types to be included in the filter for sources. final List selectedSourceTypes; @@ -98,6 +103,7 @@ final class FilterDialogState extends Equatable { List? selectedSourceIds, List? selectedTopicIds, List? selectedCountryIds, + bool? isBreaking, List? selectedSourceTypes, List? selectedLanguageCodes, List? selectedHeadquartersCountryIds, @@ -116,6 +122,7 @@ final class FilterDialogState extends Equatable { selectedSourceIds: selectedSourceIds ?? this.selectedSourceIds, selectedTopicIds: selectedTopicIds ?? this.selectedTopicIds, selectedCountryIds: selectedCountryIds ?? this.selectedCountryIds, + isBreaking: isBreaking ?? this.isBreaking, selectedSourceTypes: selectedSourceTypes ?? this.selectedSourceTypes, selectedLanguageCodes: selectedLanguageCodes ?? this.selectedLanguageCodes, @@ -139,6 +146,7 @@ final class FilterDialogState extends Equatable { selectedSourceIds, selectedTopicIds, selectedCountryIds, + isBreaking, selectedSourceTypes, selectedLanguageCodes, selectedHeadquartersCountryIds, From 0bdced611b0270b5e4bf9dc321606f7c0ce21b08 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 18:52:57 +0100 Subject: [PATCH 29/59] feat(filter_dialog): add breaking news changed event Adds the `FilterDialogBreakingNewsChanged` event to the `FilterDialogBloc`. This event allows the UI to update the temporary `isBreaking` filter status (`true`, `false`, or `null`) while the dialog is open. --- .../filter_dialog/bloc/filter_dialog_event.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_event.dart b/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_event.dart index f6ecb976..b3e15305 100644 --- a/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_event.dart +++ b/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_event.dart @@ -86,6 +86,18 @@ final class FilterDialogHeadlinesCountryIdsChanged extends FilterDialogEvent { List get props => [countryIds]; } +/// Event to update the temporary breaking news filter for headlines. +final class FilterDialogBreakingNewsChanged extends FilterDialogEvent { + const FilterDialogBreakingNewsChanged(this.isBreaking); + + /// The new breaking news status: `true` for breaking only, `false` for + /// non-breaking only, and `null` for all. + final bool? isBreaking; + + @override + List get props => [isBreaking]; +} + /// Event to update the temporary selected source types for sources. final class FilterDialogSourceTypesChanged extends FilterDialogEvent { const FilterDialogSourceTypesChanged(this.sourceTypes); From 4b422b665f755db3c2e085874d9a6540b88da716 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 18:54:08 +0100 Subject: [PATCH 30/59] feat(filter_dialog): handle breaking news dialog state Updates `FilterDialogBloc` to manage the temporary state of the `isBreaking` filter. - Adds a handler for the `FilterDialogBreakingNewsChanged` event to update the dialog's state. - Updates the `_onFilterDialogInitialized` handler to correctly initialize the `isBreaking` state from the main `HeadlinesFilterBloc` when the dialog is opened for headlines. - The existing `_onFilterDialogReset` handler correctly resets `isBreaking` to `null`. --- .../filter_dialog/bloc/filter_dialog_bloc.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_bloc.dart b/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_bloc.dart index a1b1e94b..5499054f 100644 --- a/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_bloc.dart +++ b/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_bloc.dart @@ -58,6 +58,9 @@ class FilterDialogBloc extends Bloc { on( _onFilterDialogHeadlinesTopicIdsChanged, ); + on( + _onFilterDialogBreakingNewsChanged, + ); on( _onFilterDialogHeadlinesCountryIdsChanged, ); @@ -94,6 +97,7 @@ class FilterDialogBloc extends Bloc { selectedSourceIds: headlinesState.selectedSourceIds, selectedTopicIds: headlinesState.selectedTopicIds, selectedCountryIds: headlinesState.selectedCountryIds, + isBreaking: headlinesState.isBreaking, ), ); } @@ -219,6 +223,14 @@ class FilterDialogBloc extends Bloc { emit(state.copyWith(selectedCountryIds: event.countryIds)); } + /// Updates the temporary breaking news filter for headlines. + void _onFilterDialogBreakingNewsChanged( + FilterDialogBreakingNewsChanged event, + Emitter emit, + ) { + emit(state.copyWith(isBreaking: event.isBreaking)); + } + /// Updates the temporary selected source types for sources. void _onFilterDialogSourceTypesChanged( FilterDialogSourceTypesChanged event, From 9f2f4f3408a8eaf9b8a0ffa196ac3c98872d5c79 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 19:07:47 +0100 Subject: [PATCH 31/59] feat(filter_dialog): add breaking news filter UI Implements the UI for the "Breaking News" filter within the `FilterDialog`. - Adds a new section with `ChoiceChip` widgets for "All", "Breaking Only", and "Non-Breaking Only" when filtering headlines. - Binds the chip selection to the `isBreaking` property in the `FilterDialogState`. - Dispatches `FilterDialogBreakingNewsChanged` events when a chip is selected. - Updates `_dispatchFilterApplied` to pass the `isBreaking` state to the `HeadlinesFilterApplied` event, ensuring the filter is correctly applied. --- .../widgets/filter_dialog/filter_dialog.dart | 171 +++++++++++------- 1 file changed, 107 insertions(+), 64 deletions(-) diff --git a/lib/content_management/widgets/filter_dialog/filter_dialog.dart b/lib/content_management/widgets/filter_dialog/filter_dialog.dart index d2a2de27..5d157b46 100644 --- a/lib/content_management/widgets/filter_dialog/filter_dialog.dart +++ b/lib/content_management/widgets/filter_dialog/filter_dialog.dart @@ -118,22 +118,21 @@ class _FilterDialogState extends State { onPressed: () { // Dispatch reset event context.read().add( - const FilterDialogReset(), - ); - // After reset, get the new state and apply filters - final resetState = context - .read() - .state - .copyWith( - searchQuery: '', - selectedStatus: ContentStatus.active, - selectedSourceIds: [], - selectedTopicIds: [], - selectedCountryIds: [], - selectedSourceTypes: [], - selectedLanguageCodes: [], - selectedHeadquartersCountryIds: [], + const FilterDialogReset(), ); + // After reset, get the new state and apply filters + final resetState = + context.read().state.copyWith( + searchQuery: '', + selectedStatus: ContentStatus.active, + selectedSourceIds: [], + selectedTopicIds: [], + selectedCountryIds: [], + isBreaking: null, + selectedSourceTypes: [], + selectedLanguageCodes: [], + selectedHeadquartersCountryIds: [], + ); _dispatchFilterApplied(resetState); Navigator.of(context).pop(); }, @@ -164,8 +163,8 @@ class _FilterDialogState extends State { ), onChanged: (query) { context.read().add( - FilterDialogSearchQueryChanged(query), - ); + FilterDialogSearchQueryChanged(query), + ); }, ), const SizedBox(height: AppSpacing.lg), @@ -175,7 +174,6 @@ class _FilterDialogState extends State { ), const SizedBox(height: AppSpacing.sm), _buildStatusFilterChips(l10n, theme, filterDialogState), - const SizedBox(height: AppSpacing.lg), _buildAdditionalFilters(l10n, filterDialogState), ], ), @@ -225,8 +223,8 @@ class _FilterDialogState extends State { onSelected: (isSelected) { if (isSelected) { context.read().add( - FilterDialogStatusChanged(status), - ); + FilterDialogStatusChanged(status), + ); } }, selectedColor: theme.colorScheme.primaryContainer, @@ -250,6 +248,50 @@ class _FilterDialogState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: AppSpacing.lg), + Text( + l10n.breakingNewsFilterTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: AppSpacing.sm), + Wrap( + spacing: AppSpacing.sm, + children: [ + ChoiceChip( + label: Text(l10n.breakingNewsFilterAll), + selected: filterDialogState.isBreaking == null, + onSelected: (isSelected) { + if (isSelected) { + context.read().add( + const FilterDialogBreakingNewsChanged(null), + ); + } + }, + ), + ChoiceChip( + label: Text(l10n.breakingNewsFilterBreakingOnly), + selected: filterDialogState.isBreaking == true, + onSelected: (isSelected) { + if (isSelected) { + context.read().add( + const FilterDialogBreakingNewsChanged(true), + ); + } + }, + ), + ChoiceChip( + label: Text(l10n.breakingNewsFilterNonBreakingOnly), + selected: filterDialogState.isBreaking == false, + onSelected: (isSelected) { + if (isSelected) { + context.read().add( + const FilterDialogBreakingNewsChanged(false), + ); + } + }, + ), + ], + ), const SizedBox(height: AppSpacing.lg), SearchableSelectionInput( label: l10n.sources, @@ -295,10 +337,10 @@ class _FilterDialogState extends State { itemToString: (item) => item.name, onChanged: (items) { context.read().add( - FilterDialogHeadlinesSourceIdsChanged( - items?.map((e) => e.id).toList() ?? [], - ), - ); + FilterDialogHeadlinesSourceIdsChanged( + items?.map((e) => e.id).toList() ?? [], + ), + ); }, repository: widget.sourcesRepository, filterBuilder: (searchTerm) => { @@ -334,10 +376,10 @@ class _FilterDialogState extends State { itemToString: (item) => item.name, onChanged: (items) { context.read().add( - FilterDialogHeadlinesTopicIdsChanged( - items?.map((e) => e.id).toList() ?? [], - ), - ); + FilterDialogHeadlinesTopicIdsChanged( + items?.map((e) => e.id).toList() ?? [], + ), + ); }, repository: widget.topicsRepository, filterBuilder: (searchTerm) => { @@ -373,10 +415,10 @@ class _FilterDialogState extends State { itemToString: (item) => item.name, onChanged: (items) { context.read().add( - FilterDialogHeadlinesCountryIdsChanged( - items?.map((e) => e.id).toList() ?? [], - ), - ); + FilterDialogHeadlinesCountryIdsChanged( + items?.map((e) => e.id).toList() ?? [], + ), + ); }, repository: widget.countriesRepository, filterBuilder: (searchTerm) => { @@ -405,8 +447,8 @@ class _FilterDialogState extends State { itemToString: (item) => item.localizedName(l10n), onChanged: (items) { context.read().add( - FilterDialogSourceTypesChanged(items ?? []), - ); + FilterDialogSourceTypesChanged(items ?? []), + ); }, staticItems: SourceType.values, ), @@ -435,10 +477,10 @@ class _FilterDialogState extends State { itemToString: (item) => item.name, onChanged: (items) { context.read().add( - FilterDialogLanguageCodesChanged( - items?.map((e) => e.code).toList() ?? [], - ), - ); + FilterDialogLanguageCodesChanged( + items?.map((e) => e.code).toList() ?? [], + ), + ); }, repository: widget.languagesRepository, filterBuilder: (searchTerm) => { @@ -474,10 +516,10 @@ class _FilterDialogState extends State { itemToString: (item) => item.name, onChanged: (items) { context.read().add( - FilterDialogHeadquartersCountryIdsChanged( - items?.map((e) => e.id).toList() ?? [], - ), - ); + FilterDialogHeadquartersCountryIdsChanged( + items?.map((e) => e.id).toList() ?? [], + ), + ); }, repository: widget.countriesRepository, filterBuilder: (searchTerm) => { @@ -498,32 +540,33 @@ class _FilterDialogState extends State { switch (widget.activeTab) { case ContentManagementTab.headlines: context.read().add( - HeadlinesFilterApplied( - searchQuery: filterDialogState.searchQuery, - selectedStatus: filterDialogState.selectedStatus, - selectedSourceIds: filterDialogState.selectedSourceIds, - selectedTopicIds: filterDialogState.selectedTopicIds, - selectedCountryIds: filterDialogState.selectedCountryIds, - ), - ); + HeadlinesFilterApplied( + searchQuery: filterDialogState.searchQuery, + selectedStatus: filterDialogState.selectedStatus, + selectedSourceIds: filterDialogState.selectedSourceIds, + selectedTopicIds: filterDialogState.selectedTopicIds, + selectedCountryIds: filterDialogState.selectedCountryIds, + isBreaking: filterDialogState.isBreaking, + ), + ); case ContentManagementTab.topics: context.read().add( - TopicsFilterApplied( - searchQuery: filterDialogState.searchQuery, - selectedStatus: filterDialogState.selectedStatus, - ), - ); + TopicsFilterApplied( + searchQuery: filterDialogState.searchQuery, + selectedStatus: filterDialogState.selectedStatus, + ), + ); case ContentManagementTab.sources: context.read().add( - SourcesFilterApplied( - searchQuery: filterDialogState.searchQuery, - selectedStatus: filterDialogState.selectedStatus, - selectedSourceTypes: filterDialogState.selectedSourceTypes, - selectedLanguageCodes: filterDialogState.selectedLanguageCodes, - selectedHeadquartersCountryIds: - filterDialogState.selectedHeadquartersCountryIds, - ), - ); + SourcesFilterApplied( + searchQuery: filterDialogState.searchQuery, + selectedStatus: filterDialogState.selectedStatus, + selectedSourceTypes: filterDialogState.selectedSourceTypes, + selectedLanguageCodes: filterDialogState.selectedLanguageCodes, + selectedHeadquartersCountryIds: + filterDialogState.selectedHeadquartersCountryIds, + ), + ); } } } From 2b61987610b0bf18d5b547b44289ab170dd86830 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 19:10:48 +0100 Subject: [PATCH 32/59] feat(l10n): add breaking news filter translations - Add Arabic and English translations for breaking news filter - Include titles and labels for all, breaking only, and non-breaking only options --- lib/l10n/arb/app_ar.arb | 16 ++++++++++++++++ lib/l10n/arb/app_en.arb | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index f8e06fdb..a184e5c4 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1887,5 +1887,21 @@ "ok": "موافق", "@ok": { "description": "نص زر 'موافق' الشائع." + }, + "breakingNewsFilterTitle": "تصفية الأخبار العاجلة", + "@breakingNewsFilterTitle": { + "description": "عنوان قسم تصفية الأخبار العاجلة في مربع حوار التصفية." + }, + "breakingNewsFilterAll": "الكل", + "@breakingNewsFilterAll": { + "description": "تسمية شريحة الاختيار 'الكل' في مرشح الأخبار العاجلة." + }, + "breakingNewsFilterBreakingOnly": "العاجلة فقط", + "@breakingNewsFilterBreakingOnly": { + "description": "تسمية شريحة الاختيار 'العاجلة فقط' في مرشح الأخبار العاجلة." + }, + "breakingNewsFilterNonBreakingOnly": "غير العاجلة فقط", + "@breakingNewsFilterNonBreakingOnly": { + "description": "تسمية شريحة الاختيار 'غير العاجلة فقط' في مرشح الأخبار العاجلة." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index c1b8ac00..4d494579 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1882,5 +1882,21 @@ "ok": "OK", "@ok": { "description": "A common 'OK' button text." + }, + "breakingNewsFilterTitle": "Breaking News", + "@breakingNewsFilterTitle": { + "description": "Title for the breaking news filter section in the filter dialog." + }, + "breakingNewsFilterAll": "All", + "@breakingNewsFilterAll": { + "description": "Label for the 'All' choice chip in the breaking news filter." + }, + "breakingNewsFilterBreakingOnly": "Breaking Only", + "@breakingNewsFilterBreakingOnly": { + "description": "Label for the 'Breaking Only' choice chip in the breaking news filter." + }, + "breakingNewsFilterNonBreakingOnly": "Non-Breaking Only", + "@breakingNewsFilterNonBreakingOnly": { + "description": "Label for the 'Non-Breaking Only' choice chip in the breaking news filter." } } \ No newline at end of file From e6ed452df5234b02c04e5ea0304fcb8faac97368 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 19:11:47 +0100 Subject: [PATCH 33/59] build(l10n): generation --- .../widgets/filter_dialog/filter_dialog.dart | 2 +- lib/l10n/app_localizations.dart | 24 +++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 12 ++++++++++ lib/l10n/app_localizations_en.dart | 12 ++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/content_management/widgets/filter_dialog/filter_dialog.dart b/lib/content_management/widgets/filter_dialog/filter_dialog.dart index 5d157b46..cbacc49e 100644 --- a/lib/content_management/widgets/filter_dialog/filter_dialog.dart +++ b/lib/content_management/widgets/filter_dialog/filter_dialog.dart @@ -270,7 +270,7 @@ class _FilterDialogState extends State { ), ChoiceChip( label: Text(l10n.breakingNewsFilterBreakingOnly), - selected: filterDialogState.isBreaking == true, + selected: filterDialogState.isBreaking ?? false, onSelected: (isSelected) { if (isSelected) { context.read().add( diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d317c213..9efdb013 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2797,6 +2797,30 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'OK'** String get ok; + + /// Title for the breaking news filter section in the filter dialog. + /// + /// In en, this message translates to: + /// **'Breaking News'** + String get breakingNewsFilterTitle; + + /// Label for the 'All' choice chip in the breaking news filter. + /// + /// In en, this message translates to: + /// **'All'** + String get breakingNewsFilterAll; + + /// Label for the 'Breaking Only' choice chip in the breaking news filter. + /// + /// In en, this message translates to: + /// **'Breaking Only'** + String get breakingNewsFilterBreakingOnly; + + /// Label for the 'Non-Breaking Only' choice chip in the breaking news filter. + /// + /// In en, this message translates to: + /// **'Non-Breaking Only'** + String get breakingNewsFilterNonBreakingOnly; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index de1b8d6e..79754c08 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1496,4 +1496,16 @@ class AppLocalizationsAr extends AppLocalizations { @override String get ok => 'موافق'; + + @override + String get breakingNewsFilterTitle => 'تصفية الأخبار العاجلة'; + + @override + String get breakingNewsFilterAll => 'الكل'; + + @override + String get breakingNewsFilterBreakingOnly => 'العاجلة فقط'; + + @override + String get breakingNewsFilterNonBreakingOnly => 'غير العاجلة فقط'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 58bbd408..da6349e9 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1499,4 +1499,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get ok => 'OK'; + + @override + String get breakingNewsFilterTitle => 'Breaking News'; + + @override + String get breakingNewsFilterAll => 'All'; + + @override + String get breakingNewsFilterBreakingOnly => 'Breaking Only'; + + @override + String get breakingNewsFilterNonBreakingOnly => 'Non-Breaking Only'; } From 27fe07bb9b5c04c3f600a6157d64eaa8afb4e706 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 19:12:04 +0100 Subject: [PATCH 34/59] style: format --- .../widgets/saved_filter_limits_section.dart | 22 ++- .../edit_headline/edit_headline_bloc.dart | 4 +- .../widgets/filter_dialog/filter_dialog.dart | 140 +++++++++--------- 3 files changed, 84 insertions(+), 82 deletions(-) diff --git a/lib/app_configuration/widgets/saved_filter_limits_section.dart b/lib/app_configuration/widgets/saved_filter_limits_section.dart index 7e763f55..7c86bf83 100644 --- a/lib/app_configuration/widgets/saved_filter_limits_section.dart +++ b/lib/app_configuration/widgets/saved_filter_limits_section.dart @@ -27,7 +27,8 @@ class SavedFilterLimitsSection extends StatefulWidget { final ValueChanged onConfigChanged; @override - State createState() => _SavedFilterLimitsSectionState(); + State createState() => + _SavedFilterLimitsSectionState(); } class _SavedFilterLimitsSectionState extends State @@ -64,11 +65,10 @@ class _SavedFilterLimitsSectionState extends State Text( l10n.savedHeadlineFilterLimitsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), SavedFilterLimitsForm( @@ -102,11 +102,10 @@ class _SavedFilterLimitsSectionState extends State Text( l10n.savedSourceFilterLimitsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), SavedFilterLimitsForm( @@ -122,4 +121,3 @@ class _SavedFilterLimitsSectionState extends State ); } } - diff --git a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart index 7b8e93b5..e89da9e8 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart @@ -150,7 +150,9 @@ class EditHeadlineBloc extends Bloc { ) { emit( state.copyWith( - isBreaking: event.isBreaking, status: EditHeadlineStatus.initial), + isBreaking: event.isBreaking, + status: EditHeadlineStatus.initial, + ), ); } diff --git a/lib/content_management/widgets/filter_dialog/filter_dialog.dart b/lib/content_management/widgets/filter_dialog/filter_dialog.dart index cbacc49e..96db6cd5 100644 --- a/lib/content_management/widgets/filter_dialog/filter_dialog.dart +++ b/lib/content_management/widgets/filter_dialog/filter_dialog.dart @@ -118,21 +118,23 @@ class _FilterDialogState extends State { onPressed: () { // Dispatch reset event context.read().add( - const FilterDialogReset(), - ); + const FilterDialogReset(), + ); // After reset, get the new state and apply filters - final resetState = - context.read().state.copyWith( - searchQuery: '', - selectedStatus: ContentStatus.active, - selectedSourceIds: [], - selectedTopicIds: [], - selectedCountryIds: [], - isBreaking: null, - selectedSourceTypes: [], - selectedLanguageCodes: [], - selectedHeadquartersCountryIds: [], - ); + final resetState = context + .read() + .state + .copyWith( + searchQuery: '', + selectedStatus: ContentStatus.active, + selectedSourceIds: [], + selectedTopicIds: [], + selectedCountryIds: [], + isBreaking: null, + selectedSourceTypes: [], + selectedLanguageCodes: [], + selectedHeadquartersCountryIds: [], + ); _dispatchFilterApplied(resetState); Navigator.of(context).pop(); }, @@ -163,8 +165,8 @@ class _FilterDialogState extends State { ), onChanged: (query) { context.read().add( - FilterDialogSearchQueryChanged(query), - ); + FilterDialogSearchQueryChanged(query), + ); }, ), const SizedBox(height: AppSpacing.lg), @@ -223,8 +225,8 @@ class _FilterDialogState extends State { onSelected: (isSelected) { if (isSelected) { context.read().add( - FilterDialogStatusChanged(status), - ); + FilterDialogStatusChanged(status), + ); } }, selectedColor: theme.colorScheme.primaryContainer, @@ -263,8 +265,8 @@ class _FilterDialogState extends State { onSelected: (isSelected) { if (isSelected) { context.read().add( - const FilterDialogBreakingNewsChanged(null), - ); + const FilterDialogBreakingNewsChanged(null), + ); } }, ), @@ -274,8 +276,8 @@ class _FilterDialogState extends State { onSelected: (isSelected) { if (isSelected) { context.read().add( - const FilterDialogBreakingNewsChanged(true), - ); + const FilterDialogBreakingNewsChanged(true), + ); } }, ), @@ -285,8 +287,8 @@ class _FilterDialogState extends State { onSelected: (isSelected) { if (isSelected) { context.read().add( - const FilterDialogBreakingNewsChanged(false), - ); + const FilterDialogBreakingNewsChanged(false), + ); } }, ), @@ -337,10 +339,10 @@ class _FilterDialogState extends State { itemToString: (item) => item.name, onChanged: (items) { context.read().add( - FilterDialogHeadlinesSourceIdsChanged( - items?.map((e) => e.id).toList() ?? [], - ), - ); + FilterDialogHeadlinesSourceIdsChanged( + items?.map((e) => e.id).toList() ?? [], + ), + ); }, repository: widget.sourcesRepository, filterBuilder: (searchTerm) => { @@ -376,10 +378,10 @@ class _FilterDialogState extends State { itemToString: (item) => item.name, onChanged: (items) { context.read().add( - FilterDialogHeadlinesTopicIdsChanged( - items?.map((e) => e.id).toList() ?? [], - ), - ); + FilterDialogHeadlinesTopicIdsChanged( + items?.map((e) => e.id).toList() ?? [], + ), + ); }, repository: widget.topicsRepository, filterBuilder: (searchTerm) => { @@ -415,10 +417,10 @@ class _FilterDialogState extends State { itemToString: (item) => item.name, onChanged: (items) { context.read().add( - FilterDialogHeadlinesCountryIdsChanged( - items?.map((e) => e.id).toList() ?? [], - ), - ); + FilterDialogHeadlinesCountryIdsChanged( + items?.map((e) => e.id).toList() ?? [], + ), + ); }, repository: widget.countriesRepository, filterBuilder: (searchTerm) => { @@ -447,8 +449,8 @@ class _FilterDialogState extends State { itemToString: (item) => item.localizedName(l10n), onChanged: (items) { context.read().add( - FilterDialogSourceTypesChanged(items ?? []), - ); + FilterDialogSourceTypesChanged(items ?? []), + ); }, staticItems: SourceType.values, ), @@ -477,10 +479,10 @@ class _FilterDialogState extends State { itemToString: (item) => item.name, onChanged: (items) { context.read().add( - FilterDialogLanguageCodesChanged( - items?.map((e) => e.code).toList() ?? [], - ), - ); + FilterDialogLanguageCodesChanged( + items?.map((e) => e.code).toList() ?? [], + ), + ); }, repository: widget.languagesRepository, filterBuilder: (searchTerm) => { @@ -516,10 +518,10 @@ class _FilterDialogState extends State { itemToString: (item) => item.name, onChanged: (items) { context.read().add( - FilterDialogHeadquartersCountryIdsChanged( - items?.map((e) => e.id).toList() ?? [], - ), - ); + FilterDialogHeadquartersCountryIdsChanged( + items?.map((e) => e.id).toList() ?? [], + ), + ); }, repository: widget.countriesRepository, filterBuilder: (searchTerm) => { @@ -540,33 +542,33 @@ class _FilterDialogState extends State { switch (widget.activeTab) { case ContentManagementTab.headlines: context.read().add( - HeadlinesFilterApplied( - searchQuery: filterDialogState.searchQuery, - selectedStatus: filterDialogState.selectedStatus, - selectedSourceIds: filterDialogState.selectedSourceIds, - selectedTopicIds: filterDialogState.selectedTopicIds, - selectedCountryIds: filterDialogState.selectedCountryIds, - isBreaking: filterDialogState.isBreaking, - ), - ); + HeadlinesFilterApplied( + searchQuery: filterDialogState.searchQuery, + selectedStatus: filterDialogState.selectedStatus, + selectedSourceIds: filterDialogState.selectedSourceIds, + selectedTopicIds: filterDialogState.selectedTopicIds, + selectedCountryIds: filterDialogState.selectedCountryIds, + isBreaking: filterDialogState.isBreaking, + ), + ); case ContentManagementTab.topics: context.read().add( - TopicsFilterApplied( - searchQuery: filterDialogState.searchQuery, - selectedStatus: filterDialogState.selectedStatus, - ), - ); + TopicsFilterApplied( + searchQuery: filterDialogState.searchQuery, + selectedStatus: filterDialogState.selectedStatus, + ), + ); case ContentManagementTab.sources: context.read().add( - SourcesFilterApplied( - searchQuery: filterDialogState.searchQuery, - selectedStatus: filterDialogState.selectedStatus, - selectedSourceTypes: filterDialogState.selectedSourceTypes, - selectedLanguageCodes: filterDialogState.selectedLanguageCodes, - selectedHeadquartersCountryIds: - filterDialogState.selectedHeadquartersCountryIds, - ), - ); + SourcesFilterApplied( + searchQuery: filterDialogState.searchQuery, + selectedStatus: filterDialogState.selectedStatus, + selectedSourceTypes: filterDialogState.selectedSourceTypes, + selectedLanguageCodes: filterDialogState.selectedLanguageCodes, + selectedHeadquartersCountryIds: + filterDialogState.selectedHeadquartersCountryIds, + ), + ); } } } From 58fbef942fa1aa9c826e0b2434c0a01ea71c6b8b Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 20:09:32 +0100 Subject: [PATCH 35/59] feat(headlines_filter): introduce BreakingNewsFilterStatus enum Creates a new `BreakingNewsFilterStatus` enum to explicitly manage the state of the breaking news filter. This replaces the ambiguous nullable boolean (`bool?`) with a clear, type-safe set of values: `all`, `breakingOnly`, and `nonBreakingOnly`. This is the first step in refactoring the filter logic for improved clarity and robustness. --- .../breaking_news_filter_status.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 lib/content_management/bloc/headlines_filter/breaking_news_filter_status.dart diff --git a/lib/content_management/bloc/headlines_filter/breaking_news_filter_status.dart b/lib/content_management/bloc/headlines_filter/breaking_news_filter_status.dart new file mode 100644 index 00000000..bf4689d3 --- /dev/null +++ b/lib/content_management/bloc/headlines_filter/breaking_news_filter_status.dart @@ -0,0 +1,15 @@ +/// Defines the possible states for the breaking news filter. +/// +/// This enum provides a clear, type-safe way to represent the three +/// distinct filtering options for breaking news, avoiding the ambiguity +/// of using a nullable boolean. +enum BreakingNewsFilterStatus { + /// Show all headlines, regardless of their breaking status. + all, + + /// Show only headlines marked as breaking news. + breakingOnly, + + /// Show only headlines that are not marked as breaking news. + nonBreakingOnly, +} From aaeab8303adf884b598161900f6ef08ad7508cbe Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 12 Nov 2025 20:12:58 +0100 Subject: [PATCH 36/59] Revert "feat(headlines_filter): introduce BreakingNewsFilterStatus enum" This reverts commit 58fbef942fa1aa9c826e0b2434c0a01ea71c6b8b. --- .../breaking_news_filter_status.dart | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 lib/content_management/bloc/headlines_filter/breaking_news_filter_status.dart diff --git a/lib/content_management/bloc/headlines_filter/breaking_news_filter_status.dart b/lib/content_management/bloc/headlines_filter/breaking_news_filter_status.dart deleted file mode 100644 index bf4689d3..00000000 --- a/lib/content_management/bloc/headlines_filter/breaking_news_filter_status.dart +++ /dev/null @@ -1,15 +0,0 @@ -/// Defines the possible states for the breaking news filter. -/// -/// This enum provides a clear, type-safe way to represent the three -/// distinct filtering options for breaking news, avoiding the ambiguity -/// of using a nullable boolean. -enum BreakingNewsFilterStatus { - /// Show all headlines, regardless of their breaking status. - all, - - /// Show only headlines marked as breaking news. - breakingOnly, - - /// Show only headlines that are not marked as breaking news. - nonBreakingOnly, -} From 580ec077d63c52d0de794ca17371eb2c1c702596 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 06:42:49 +0100 Subject: [PATCH 37/59] feat(content): introduce BreakingNewsFilterStatus enum Introduces a new enum `BreakingNewsFilterStatus` to replace the nullable boolean for managing the breaking news filter state. This improves type safety and code clarity. --- .../models/breaking_news_filter_status.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 lib/content_management/models/breaking_news_filter_status.dart diff --git a/lib/content_management/models/breaking_news_filter_status.dart b/lib/content_management/models/breaking_news_filter_status.dart new file mode 100644 index 00000000..f818ead8 --- /dev/null +++ b/lib/content_management/models/breaking_news_filter_status.dart @@ -0,0 +1,11 @@ +/// Defines the status of the breaking news filter. +enum BreakingNewsFilterStatus { + /// Show all headlines, regardless of their breaking news status. + all, + + /// Show only headlines marked as breaking news. + breakingOnly, + + /// Show only headlines not marked as breaking news. + nonBreakingOnly, +} From ec19832a80e71c989749e0a8751d6038021dda34 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 06:44:07 +0100 Subject: [PATCH 38/59] refactor(content): update HeadlinesFilterState to use enum Refactors `HeadlinesFilterState` to replace the `bool? isBreaking` property with the new `BreakingNewsFilterStatus` enum. The default filter state is now explicitly `BreakingNewsFilterStatus.all`. --- .../bloc/headlines_filter/headlines_filter_state.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/content_management/bloc/headlines_filter/headlines_filter_state.dart b/lib/content_management/bloc/headlines_filter/headlines_filter_state.dart index 577a9794..dcbb9e5e 100644 --- a/lib/content_management/bloc/headlines_filter/headlines_filter_state.dart +++ b/lib/content_management/bloc/headlines_filter/headlines_filter_state.dart @@ -15,7 +15,7 @@ class HeadlinesFilterState extends Equatable { this.selectedSourceIds = const [], this.selectedTopicIds = const [], this.selectedCountryIds = const [], - this.isBreaking, + this.isBreaking = BreakingNewsFilterStatus.all, }); /// The current text in the search query field. @@ -35,7 +35,7 @@ class HeadlinesFilterState extends Equatable { /// The breaking news status to filter by. /// `null` = all, `true` = breaking only, `false` = non-breaking only. - final bool? isBreaking; + final BreakingNewsFilterStatus isBreaking; /// Creates a copy of this state with the given fields replaced with the /// new values. @@ -45,7 +45,7 @@ class HeadlinesFilterState extends Equatable { List? selectedSourceIds, List? selectedTopicIds, List? selectedCountryIds, - bool? isBreaking, + BreakingNewsFilterStatus? isBreaking, }) { return HeadlinesFilterState( searchQuery: searchQuery ?? this.searchQuery, From 980558f2581d3c4713d432b4c717c790947d6707 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 06:44:52 +0100 Subject: [PATCH 39/59] refactor(content): update HeadlinesFilterEvent to use enum Refactors `HeadlinesBreakingNewsFilterChanged` and `HeadlinesFilterApplied` events to use the `BreakingNewsFilterStatus` enum, ensuring type safety for filter-related events. --- .../bloc/headlines_filter/headlines_filter_event.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/content_management/bloc/headlines_filter/headlines_filter_event.dart b/lib/content_management/bloc/headlines_filter/headlines_filter_event.dart index 6efa1e8d..abd400b1 100644 --- a/lib/content_management/bloc/headlines_filter/headlines_filter_event.dart +++ b/lib/content_management/bloc/headlines_filter/headlines_filter_event.dart @@ -1,5 +1,6 @@ part of 'headlines_filter_bloc.dart'; +/// Base class for all events related to the [HeadlinesFilterBloc]. sealed class HeadlinesFilterEvent extends Equatable { const HeadlinesFilterEvent(); @@ -61,7 +62,7 @@ final class HeadlinesCountryFilterChanged extends HeadlinesFilterEvent { final class HeadlinesBreakingNewsFilterChanged extends HeadlinesFilterEvent { const HeadlinesBreakingNewsFilterChanged(this.isBreaking); - final bool? isBreaking; + final BreakingNewsFilterStatus isBreaking; @override List get props => [isBreaking]; @@ -83,7 +84,7 @@ final class HeadlinesFilterApplied extends HeadlinesFilterEvent { final List selectedSourceIds; final List selectedTopicIds; final List selectedCountryIds; - final bool? isBreaking; + final BreakingNewsFilterStatus isBreaking; @override List get props => [ From c6f22181f07c690dc9eecdeba33dd9008a3d5221 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 06:48:41 +0100 Subject: [PATCH 40/59] refactor(content): update FilterDialogEvent to use enum Refactors `FilterDialogBreakingNewsChanged` event to use the `BreakingNewsFilterStatus` enum instead of a nullable boolean, ensuring type safety. --- .../widgets/filter_dialog/bloc/filter_dialog_state.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_state.dart b/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_state.dart index 85ff8881..3373615e 100644 --- a/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_state.dart +++ b/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_state.dart @@ -30,7 +30,7 @@ final class FilterDialogState extends Equatable { this.selectedSourceIds = const [], this.selectedTopicIds = const [], this.selectedCountryIds = const [], - this.isBreaking, + this.isBreaking = BreakingNewsFilterStatus.all, this.selectedSourceTypes = const [], this.selectedLanguageCodes = const [], this.selectedHeadquartersCountryIds = const [], @@ -69,7 +69,7 @@ final class FilterDialogState extends Equatable { /// The breaking news status to filter by for headlines. /// `null` = all, `true` = breaking only, `false` = non-breaking only. - final bool? isBreaking; + final BreakingNewsFilterStatus isBreaking; /// The list of source types to be included in the filter for sources. final List selectedSourceTypes; @@ -103,7 +103,7 @@ final class FilterDialogState extends Equatable { List? selectedSourceIds, List? selectedTopicIds, List? selectedCountryIds, - bool? isBreaking, + BreakingNewsFilterStatus? isBreaking, List? selectedSourceTypes, List? selectedLanguageCodes, List? selectedHeadquartersCountryIds, From 5f4acf145351e32a3ecb72a84d095fea4b950b80 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 06:48:50 +0100 Subject: [PATCH 41/59] refactor(content_management): improve FilterDialogBreakingNewsChanged event - Replace bool? with BreakingNewsFilterStatus for better type safety - Simplify props list by removing nullable types --- .../widgets/filter_dialog/bloc/filter_dialog_event.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_event.dart b/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_event.dart index b3e15305..eac566e5 100644 --- a/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_event.dart +++ b/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_event.dart @@ -90,12 +90,10 @@ final class FilterDialogHeadlinesCountryIdsChanged extends FilterDialogEvent { final class FilterDialogBreakingNewsChanged extends FilterDialogEvent { const FilterDialogBreakingNewsChanged(this.isBreaking); - /// The new breaking news status: `true` for breaking only, `false` for - /// non-breaking only, and `null` for all. - final bool? isBreaking; + final BreakingNewsFilterStatus isBreaking; @override - List get props => [isBreaking]; + List get props => [isBreaking]; } /// Event to update the temporary selected source types for sources. From a85d44b8e3f9bdb1ee36d475ce2d0a84657c62e8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 06:52:39 +0100 Subject: [PATCH 42/59] fix(content_management): improve breaking news filter chip selection - Replace boolean values with BreakingNewsFilterStatus enum - Update default selection to BreakingNewsFilterStatus.all - Enhance readability of breaking news filter chip options - Refactor filter update logic for better maintainability --- .../widgets/filter_dialog/filter_dialog.dart | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/content_management/widgets/filter_dialog/filter_dialog.dart b/lib/content_management/widgets/filter_dialog/filter_dialog.dart index 96db6cd5..b0a37b85 100644 --- a/lib/content_management/widgets/filter_dialog/filter_dialog.dart +++ b/lib/content_management/widgets/filter_dialog/filter_dialog.dart @@ -6,6 +6,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/headlines_filter/headlines_filter_bloc.dart'; 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/content_management/models/breaking_news_filter_status.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/widgets/filter_dialog/bloc/filter_dialog_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/l10n/l10n.dart'; @@ -130,7 +131,7 @@ class _FilterDialogState extends State { selectedSourceIds: [], selectedTopicIds: [], selectedCountryIds: [], - isBreaking: null, + isBreaking: BreakingNewsFilterStatus.all, selectedSourceTypes: [], selectedLanguageCodes: [], selectedHeadquartersCountryIds: [], @@ -260,34 +261,47 @@ class _FilterDialogState extends State { spacing: AppSpacing.sm, children: [ ChoiceChip( - label: Text(l10n.breakingNewsFilterAll), - selected: filterDialogState.isBreaking == null, + label: Text(l10n.breakingNewsFilterAll), // 'All' option + selected: filterDialogState.isBreaking == + BreakingNewsFilterStatus.all, onSelected: (isSelected) { if (isSelected) { context.read().add( - const FilterDialogBreakingNewsChanged(null), + const FilterDialogBreakingNewsChanged( + BreakingNewsFilterStatus.all, + ), ); } }, ), ChoiceChip( - label: Text(l10n.breakingNewsFilterBreakingOnly), - selected: filterDialogState.isBreaking ?? false, + label: Text( + l10n.breakingNewsFilterBreakingOnly, + ), // 'Breaking Only' + selected: filterDialogState.isBreaking == + BreakingNewsFilterStatus.breakingOnly, onSelected: (isSelected) { if (isSelected) { context.read().add( - const FilterDialogBreakingNewsChanged(true), + const FilterDialogBreakingNewsChanged( + BreakingNewsFilterStatus.breakingOnly, + ), ); } }, ), ChoiceChip( - label: Text(l10n.breakingNewsFilterNonBreakingOnly), - selected: filterDialogState.isBreaking == false, + label: Text( + l10n.breakingNewsFilterNonBreakingOnly, + ), // 'Non-Breaking' + selected: filterDialogState.isBreaking == + BreakingNewsFilterStatus.nonBreakingOnly, onSelected: (isSelected) { if (isSelected) { context.read().add( - const FilterDialogBreakingNewsChanged(false), + const FilterDialogBreakingNewsChanged( + BreakingNewsFilterStatus.nonBreakingOnly, + ), ); } }, From 243c695cde8e0d1abc59ed3eef4789e43140bacb Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 06:52:50 +0100 Subject: [PATCH 43/59] style(content_management): update filter dialog code formatting - Add import statement for BreakingNewsFilterStatus model - Improve code readability by formatting emit statement in _onBreakingNewsChanged method --- .../widgets/filter_dialog/bloc/filter_dialog_bloc.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_bloc.dart b/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_bloc.dart index 5499054f..036f6ab7 100644 --- a/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_bloc.dart +++ b/lib/content_management/widgets/filter_dialog/bloc/filter_dialog_bloc.dart @@ -9,6 +9,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/headlines_filter/headlines_filter_bloc.dart'; 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/content_management/models/breaking_news_filter_status.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/widgets/filter_dialog/filter_dialog.dart' show FilterDialog; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/constants/constants.dart'; @@ -228,7 +229,9 @@ class FilterDialogBloc extends Bloc { FilterDialogBreakingNewsChanged event, Emitter emit, ) { - emit(state.copyWith(isBreaking: event.isBreaking)); + emit( + state.copyWith(isBreaking: event.isBreaking), + ); } /// Updates the temporary selected source types for sources. From 4a5783974af982851a9d928604e03a35c23aeb2a Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 06:53:03 +0100 Subject: [PATCH 44/59] refactor(content_management): update headlines filter breaking news status - Replace direct boolean values with BreakingNewsFilterStatus enum - Improve code readability and maintainability --- .../bloc/headlines_filter/headlines_filter_bloc.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart b/lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart index c6a9f6bf..528673c2 100644 --- a/lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart +++ b/lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart @@ -1,6 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:core/core.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/models/breaking_news_filter_status.dart'; part 'headlines_filter_event.dart'; part 'headlines_filter_state.dart'; @@ -78,8 +79,8 @@ class HeadlinesFilterBloc /// Handles changes to the breaking news filter. /// - /// This updates the `isBreaking` status for the filter, which can be - /// `true` (breaking only), `false` (non-breaking only), or `null` (all). + /// This updates the `isBreaking` status for the filter using the + /// [BreakingNewsFilterStatus] enum. void _onHeadlinesBreakingNewsFilterChanged( HeadlinesBreakingNewsFilterChanged event, Emitter emit, From 07b452709e1bd1545cc1e742fd14bc37640ab76c Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 06:53:35 +0100 Subject: [PATCH 45/59] feat(content_management): enhance breaking news filtering with enum support - Introduce BreakingNewsFilterStatus enum to handle breaking news filter options - Update ContentManagementBloc to use the new enum for more granular filtering - Add support for 'breaking only', 'non-breaking only', and 'all' options --- .../bloc/content_management_bloc.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/content_management/bloc/content_management_bloc.dart b/lib/content_management/bloc/content_management_bloc.dart index 87f6bb5c..a312c5c4 100644 --- a/lib/content_management/bloc/content_management_bloc.dart +++ b/lib/content_management/bloc/content_management_bloc.dart @@ -7,6 +7,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/headlines_filter/headlines_filter_bloc.dart'; 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/content_management/models/breaking_news_filter_status.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/constants/app_constants.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -153,8 +154,15 @@ class ContentManagementBloc filter['eventCountry.id'] = {r'$in': state.selectedCountryIds}; } - if (state.isBreaking != null) { - filter['isBreaking'] = state.isBreaking; + // Handle the breaking news filter based on the enum status. + switch (state.isBreaking) { + case BreakingNewsFilterStatus.breakingOnly: + filter['isBreaking'] = true; + case BreakingNewsFilterStatus.nonBreakingOnly: + filter['isBreaking'] = false; + case BreakingNewsFilterStatus.all: + // For 'all', we don't add the 'isBreaking' key to the filter. + break; } return filter; From 7b55c37bb1d7e8a31d406acccca80e983832498c Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 06:53:42 +0100 Subject: [PATCH 46/59] fix(content_management): trigger headlines load when breaking news toggle changes - Add condition to check for changes in isBreaking state - Ensure headlines are reloaded when the breaking news toggle is switched --- lib/content_management/view/content_management_page.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/content_management/view/content_management_page.dart b/lib/content_management/view/content_management_page.dart index fecb7144..7f5c7ed2 100644 --- a/lib/content_management/view/content_management_page.dart +++ b/lib/content_management/view/content_management_page.dart @@ -78,7 +78,8 @@ class _ContentManagementPageState extends State !const DeepCollectionEquality().equals( previous.selectedCountryIds, current.selectedCountryIds, - ), + ) || + previous.isBreaking != current.isBreaking, listener: (context, state) { context.read().add( LoadHeadlinesRequested( From a4416050844320a410e7847280da9175651e5c6a Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 07:00:24 +0100 Subject: [PATCH 47/59] refactor(filter_dialog): improve breaking news filter UI and accessibility - Replace individual ChoiceChip widgets with a dynamic list - Add localization support for breaking news filter options - Implement theming for selected and unselected chips - Improve code maintainability and reduce duplication --- .../widgets/filter_dialog/filter_dialog.dart | 82 ++++++++----------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/lib/content_management/widgets/filter_dialog/filter_dialog.dart b/lib/content_management/widgets/filter_dialog/filter_dialog.dart index b0a37b85..1d5f33cc 100644 --- a/lib/content_management/widgets/filter_dialog/filter_dialog.dart +++ b/lib/content_management/widgets/filter_dialog/filter_dialog.dart @@ -260,52 +260,27 @@ class _FilterDialogState extends State { Wrap( spacing: AppSpacing.sm, children: [ - ChoiceChip( - label: Text(l10n.breakingNewsFilterAll), // 'All' option - selected: filterDialogState.isBreaking == - BreakingNewsFilterStatus.all, - onSelected: (isSelected) { - if (isSelected) { - context.read().add( - const FilterDialogBreakingNewsChanged( - BreakingNewsFilterStatus.all, - ), - ); - } - }, - ), - ChoiceChip( - label: Text( - l10n.breakingNewsFilterBreakingOnly, - ), // 'Breaking Only' - selected: filterDialogState.isBreaking == - BreakingNewsFilterStatus.breakingOnly, - onSelected: (isSelected) { - if (isSelected) { - context.read().add( - const FilterDialogBreakingNewsChanged( - BreakingNewsFilterStatus.breakingOnly, - ), - ); - } - }, - ), - ChoiceChip( - label: Text( - l10n.breakingNewsFilterNonBreakingOnly, - ), // 'Non-Breaking' - selected: filterDialogState.isBreaking == - BreakingNewsFilterStatus.nonBreakingOnly, - onSelected: (isSelected) { - if (isSelected) { - context.read().add( - const FilterDialogBreakingNewsChanged( - BreakingNewsFilterStatus.nonBreakingOnly, - ), - ); - } - }, - ), + ...BreakingNewsFilterStatus.values.map((status) { + return ChoiceChip( + label: Text(_getBreakingNewsStatusL10n(status, l10n)), + selected: filterDialogState.isBreaking == status, + onSelected: (isSelected) { + if (isSelected) { + context.read().add( + FilterDialogBreakingNewsChanged(status), + ); + } + }, + selectedColor: Theme.of( + context, + ).colorScheme.primaryContainer, + labelStyle: TextStyle( + color: filterDialogState.isBreaking == status + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.onSurface, + ), + ); + }), ], ), const SizedBox(height: AppSpacing.lg), @@ -551,6 +526,21 @@ class _FilterDialogState extends State { } } + /// Returns the localized string for a given [BreakingNewsFilterStatus]. + String _getBreakingNewsStatusL10n( + BreakingNewsFilterStatus status, + AppLocalizations l10n, + ) { + switch (status) { + case BreakingNewsFilterStatus.all: + return l10n.breakingNewsFilterAll; + case BreakingNewsFilterStatus.breakingOnly: + return l10n.breakingNewsFilterBreakingOnly; + case BreakingNewsFilterStatus.nonBreakingOnly: + return l10n.breakingNewsFilterNonBreakingOnly; + } + } + /// Dispatches the filter applied event to the appropriate BLoC. void _dispatchFilterApplied(FilterDialogState filterDialogState) { switch (widget.activeTab) { From 351a1825dd2f08530d3ea961ce7521f3582f43d1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 07:02:45 +0100 Subject: [PATCH 48/59] fix(content): prevent text auto-selection in filter dialog Fixes an issue where the search text in the filter dialog would become automatically selected after typing. This was caused by unnecessarily updating the `TextEditingController`'s text on every state change. The fix ensures the controller is only updated if its text differs from the BLoC state, preserving the user's cursor position and selection. --- .../widgets/filter_dialog/filter_dialog.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/content_management/widgets/filter_dialog/filter_dialog.dart b/lib/content_management/widgets/filter_dialog/filter_dialog.dart index 1d5f33cc..d340802b 100644 --- a/lib/content_management/widgets/filter_dialog/filter_dialog.dart +++ b/lib/content_management/widgets/filter_dialog/filter_dialog.dart @@ -104,7 +104,11 @@ class _FilterDialogState extends State { // so we can directly use BlocBuilder here. return BlocBuilder( builder: (context, filterDialogState) { - _searchController.text = filterDialogState.searchQuery; + // Synchronize the controller with the state, but only if they differ. + // This prevents the cursor from jumping and text from being re-selected. + if (_searchController.text != filterDialogState.searchQuery) { + _searchController.text = filterDialogState.searchQuery; + } return Scaffold( appBar: AppBar( title: Text(_getDialogTitle(l10n)), From 595893b9523135d51ac2f9721831d55d68ca09a0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 11:06:05 +0100 Subject: [PATCH 49/59] feat(l10n): add push notification settings translations - Add Arabic and English translations for push notification settings - Include labels, descriptions, and titles for various notification-related UI elements - Cover translations for Firebase and OneSignal providers --- lib/l10n/arb/app_ar.arb | 44 ++++++++++++++++++++++++++++++++++++++++ lib/l10n/arb/app_en.arb | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index a184e5c4..a6d4a18a 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1903,5 +1903,49 @@ "breakingNewsFilterNonBreakingOnly": "غير العاجلة فقط", "@breakingNewsFilterNonBreakingOnly": { "description": "تسمية شريحة الاختيار 'غير العاجلة فقط' في مرشح الأخبار العاجلة." + }, + "notificationsTab": "الإشعارات", + "@notificationsTab": { + "description": "عنوان تبويب إعدادات الإشعارات" + }, + "pushNotificationSettingsTitle": "إعدادات الإشعارات الفورية", + "@pushNotificationSettingsTitle": { + "description": "عنوان قسم إعدادات الإشعارات الفورية" + }, + "pushNotificationSettingsDescription": "إدارة الإعدادات العامة لنظام الإشعارات الفورية، بما في ذلك المزود الأساسي وأنواع الإشعارات النشطة.", + "@pushNotificationSettingsDescription": { + "description": "وصف قسم إعدادات الإشعارات الفورية" + }, + "pushNotificationSystemStatusTitle": "نظام الإشعارات الفورية", + "@pushNotificationSystemStatusTitle": { + "description": "عنوان قسم حالة نظام الإشعارات الفورية" + }, + "pushNotificationSystemStatusDescription": "مفتاح عام لتمكين أو تعطيل نظام الإشعارات الفورية بأكمله.", + "@pushNotificationSystemStatusDescription": { + "description": "وصف قسم حالة نظام الإشعارات الفورية" + }, + "pushNotificationPrimaryProviderTitle": "المزود الأساسي", + "@pushNotificationPrimaryProviderTitle": { + "description": "عنوان قسم المزود الأساسي للإشعارات الفورية" + }, + "pushNotificationPrimaryProviderDescription": "اختر مزود الخدمة الأساسي لإرسال الإشعارات الفورية.", + "@pushNotificationPrimaryProviderDescription": { + "description": "وصف قسم المزود الأساسي للإشعارات الفورية" + }, + "pushNotificationDeliveryTypesTitle": "أنواع التسليم", + "@pushNotificationDeliveryTypesTitle": { + "description": "عنوان قسم أنواع تسليم الإشعارات الفورية" + }, + "pushNotificationDeliveryTypesDescription": "تمكين أو تعطيل أنواع معينة من الإشعارات الفورية.", + "@pushNotificationDeliveryTypesDescription": { + "description": "وصف قسم أنواع تسليم الإشعارات الفورية" + }, + "pushNotificationProviderFirebase": "Firebase", + "@pushNotificationProviderFirebase": { + "description": "تسمية مزود الإشعارات الفورية Firebase" + }, + "pushNotificationProviderOneSignal": "OneSignal", + "@pushNotificationProviderOneSignal": { + "description": "تسمية مزود الإشعارات الفورية OneSignal" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 4d494579..a1134081 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1899,4 +1899,49 @@ "@breakingNewsFilterNonBreakingOnly": { "description": "Label for the 'Non-Breaking Only' choice chip in the breaking news filter." } +, + "notificationsTab": "Notifications", + "@notificationsTab": { + "description": "Tab title for Notifications settings" + }, + "pushNotificationSettingsTitle": "Push Notification Settings", + "@pushNotificationSettingsTitle": { + "description": "Title for the Push Notification Settings section" + }, + "pushNotificationSettingsDescription": "Manage global settings for the push notification system, including the primary provider and which notification types are active.", + "@pushNotificationSettingsDescription": { + "description": "Description for the Push Notification Settings section" + }, + "pushNotificationSystemStatusTitle": "Push Notification System", + "@pushNotificationSystemStatusTitle": { + "description": "Title for the Push Notification System status section" + }, + "pushNotificationSystemStatusDescription": "A global switch to enable or disable the entire push notification system.", + "@pushNotificationSystemStatusDescription": { + "description": "Description for the Push Notification System status section" + }, + "pushNotificationPrimaryProviderTitle": "Primary Provider", + "@pushNotificationPrimaryProviderTitle": { + "description": "Title for the Push Notification Primary Provider section" + }, + "pushNotificationPrimaryProviderDescription": "Select the primary service provider for sending push notifications.", + "@pushNotificationPrimaryProviderDescription": { + "description": "Description for the Push Notification Primary Provider section" + }, + "pushNotificationDeliveryTypesTitle": "Delivery Types", + "@pushNotificationDeliveryTypesTitle": { + "description": "Title for the Push Notification Delivery Types section" + }, + "pushNotificationDeliveryTypesDescription": "Globally enable or disable specific types of push notifications.", + "@pushNotificationDeliveryTypesDescription": { + "description": "Description for the Push Notification Delivery Types section" + }, + "pushNotificationProviderFirebase": "Firebase", + "@pushNotificationProviderFirebase": { + "description": "Label for the Firebase push notification provider" + }, + "pushNotificationProviderOneSignal": "OneSignal", + "@pushNotificationProviderOneSignal": { + "description": "Label for the OneSignal push notification provider" + } } \ No newline at end of file From 7a332b6ce55aba7dc6a793d7698ba8a8798885b4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 11:06:19 +0100 Subject: [PATCH 50/59] build(l10n): generate --- lib/l10n/app_localizations.dart | 66 ++++++++++++++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 37 +++++++++++++++++ lib/l10n/app_localizations_en.dart | 37 +++++++++++++++++ 3 files changed, 140 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 9efdb013..efc43408 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2821,6 +2821,72 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Non-Breaking Only'** String get breakingNewsFilterNonBreakingOnly; + + /// Tab title for Notifications settings + /// + /// In en, this message translates to: + /// **'Notifications'** + String get notificationsTab; + + /// Title for the Push Notification Settings section + /// + /// In en, this message translates to: + /// **'Push Notification Settings'** + String get pushNotificationSettingsTitle; + + /// Description for the Push Notification Settings section + /// + /// In en, this message translates to: + /// **'Manage global settings for the push notification system, including the primary provider and which notification types are active.'** + String get pushNotificationSettingsDescription; + + /// Title for the Push Notification System status section + /// + /// In en, this message translates to: + /// **'Push Notification System'** + String get pushNotificationSystemStatusTitle; + + /// Description for the Push Notification System status section + /// + /// In en, this message translates to: + /// **'A global switch to enable or disable the entire push notification system.'** + String get pushNotificationSystemStatusDescription; + + /// Title for the Push Notification Primary Provider section + /// + /// In en, this message translates to: + /// **'Primary Provider'** + String get pushNotificationPrimaryProviderTitle; + + /// Description for the Push Notification Primary Provider section + /// + /// In en, this message translates to: + /// **'Select the primary service provider for sending push notifications.'** + String get pushNotificationPrimaryProviderDescription; + + /// Title for the Push Notification Delivery Types section + /// + /// In en, this message translates to: + /// **'Delivery Types'** + String get pushNotificationDeliveryTypesTitle; + + /// Description for the Push Notification Delivery Types section + /// + /// In en, this message translates to: + /// **'Globally enable or disable specific types of push notifications.'** + String get pushNotificationDeliveryTypesDescription; + + /// Label for the Firebase push notification provider + /// + /// In en, this message translates to: + /// **'Firebase'** + String get pushNotificationProviderFirebase; + + /// Label for the OneSignal push notification provider + /// + /// In en, this message translates to: + /// **'OneSignal'** + String get pushNotificationProviderOneSignal; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 79754c08..5ffa4918 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1508,4 +1508,41 @@ class AppLocalizationsAr extends AppLocalizations { @override String get breakingNewsFilterNonBreakingOnly => 'غير العاجلة فقط'; + + @override + String get notificationsTab => 'الإشعارات'; + + @override + String get pushNotificationSettingsTitle => 'إعدادات الإشعارات الفورية'; + + @override + String get pushNotificationSettingsDescription => + 'إدارة الإعدادات العامة لنظام الإشعارات الفورية، بما في ذلك المزود الأساسي وأنواع الإشعارات النشطة.'; + + @override + String get pushNotificationSystemStatusTitle => 'نظام الإشعارات الفورية'; + + @override + String get pushNotificationSystemStatusDescription => + 'مفتاح عام لتمكين أو تعطيل نظام الإشعارات الفورية بأكمله.'; + + @override + String get pushNotificationPrimaryProviderTitle => 'المزود الأساسي'; + + @override + String get pushNotificationPrimaryProviderDescription => + 'اختر مزود الخدمة الأساسي لإرسال الإشعارات الفورية.'; + + @override + String get pushNotificationDeliveryTypesTitle => 'أنواع التسليم'; + + @override + String get pushNotificationDeliveryTypesDescription => + 'تمكين أو تعطيل أنواع معينة من الإشعارات الفورية.'; + + @override + String get pushNotificationProviderFirebase => 'Firebase'; + + @override + String get pushNotificationProviderOneSignal => 'OneSignal'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index da6349e9..9de6006d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1511,4 +1511,41 @@ class AppLocalizationsEn extends AppLocalizations { @override String get breakingNewsFilterNonBreakingOnly => 'Non-Breaking Only'; + + @override + String get notificationsTab => 'Notifications'; + + @override + String get pushNotificationSettingsTitle => 'Push Notification Settings'; + + @override + String get pushNotificationSettingsDescription => + 'Manage global settings for the push notification system, including the primary provider and which notification types are active.'; + + @override + String get pushNotificationSystemStatusTitle => 'Push Notification System'; + + @override + String get pushNotificationSystemStatusDescription => + 'A global switch to enable or disable the entire push notification system.'; + + @override + String get pushNotificationPrimaryProviderTitle => 'Primary Provider'; + + @override + String get pushNotificationPrimaryProviderDescription => + 'Select the primary service provider for sending push notifications.'; + + @override + String get pushNotificationDeliveryTypesTitle => 'Delivery Types'; + + @override + String get pushNotificationDeliveryTypesDescription => + 'Globally enable or disable specific types of push notifications.'; + + @override + String get pushNotificationProviderFirebase => 'Firebase'; + + @override + String get pushNotificationProviderOneSignal => 'OneSignal'; } From 192ea6a01874d05c837f173ccc5f470036daa228 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 11:06:35 +0100 Subject: [PATCH 51/59] feat(app_configuration): add push notification settings form - Implement PushNotificationSettingsForm widget for configuring push notification settings - Add system status, primary provider, and delivery types configuration sections - Use ExpansionTile for collapsible sections - Implement SwitchListTile and SegmentedButton for user interactions - Localize all strings using AppLocalizations --- .../push_notification_settings_form.dart | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 lib/app_configuration/widgets/push_notification_settings_form.dart diff --git a/lib/app_configuration/widgets/push_notification_settings_form.dart b/lib/app_configuration/widgets/push_notification_settings_form.dart new file mode 100644 index 00000000..3b9cb260 --- /dev/null +++ b/lib/app_configuration/widgets/push_notification_settings_form.dart @@ -0,0 +1,188 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/push_notification_provider_l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/push_notification_subscription_delivery_type_l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +/// {@template push_notification_settings_form} +/// A form widget for configuring push notification settings. +/// {@endtemplate} +class PushNotificationSettingsForm extends StatelessWidget { + /// {@macro push_notification_settings_form} + const PushNotificationSettingsForm({ + 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 pushConfig = remoteConfig.pushNotificationConfig; + + return SingleChildScrollView( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ExpansionTile( + initiallyExpanded: true, + title: Text(l10n.pushNotificationSettingsTitle), + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.pushNotificationSettingsDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), + ), + const SizedBox(height: AppSpacing.lg), + _buildSystemStatusSection(context, l10n, pushConfig), + const SizedBox(height: AppSpacing.lg), + _buildPrimaryProviderSection(context, l10n, pushConfig), + const SizedBox(height: AppSpacing.lg), + _buildDeliveryTypesSection(context, l10n, pushConfig), + ], + ), + ], + ), + ); + } + + Widget _buildSystemStatusSection( + BuildContext context, + AppLocalizations l10n, + PushNotificationConfig pushConfig, + ) { + return ExpansionTile( + title: Text(l10n.pushNotificationSystemStatusTitle), + childrenPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.md, + ), + children: [ + SwitchListTile( + title: Text(l10n.enabledLabel), + subtitle: Text(l10n.pushNotificationSystemStatusDescription), + value: pushConfig.enabled, + onChanged: (value) { + onConfigChanged( + remoteConfig.copyWith( + pushNotificationConfig: pushConfig.copyWith(enabled: value), + ), + ); + }, + ), + ], + ); + } + + Widget _buildPrimaryProviderSection( + BuildContext context, + AppLocalizations l10n, + PushNotificationConfig pushConfig, + ) { + return ExpansionTile( + title: Text(l10n.pushNotificationPrimaryProviderTitle), + childrenPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.pushNotificationPrimaryProviderDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + const SizedBox(height: AppSpacing.lg), + SegmentedButton( + segments: PushNotificationProvider.values + .map( + (provider) => ButtonSegment( + value: provider, + label: Text(provider.l10n(context)), + ), + ) + .toList(), + selected: {pushConfig.primaryProvider}, + onSelectionChanged: (newSelection) { + onConfigChanged( + remoteConfig.copyWith( + pushNotificationConfig: pushConfig.copyWith( + primaryProvider: newSelection.first, + ), + ), + ); + }, + ), + ], + ); + } + + Widget _buildDeliveryTypesSection( + BuildContext context, + AppLocalizations l10n, + PushNotificationConfig pushConfig, + ) { + return ExpansionTile( + title: Text(l10n.pushNotificationDeliveryTypesTitle), + childrenPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.md, + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.pushNotificationDeliveryTypesDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + const SizedBox(height: AppSpacing.lg), + Column( + children: PushNotificationSubscriptionDeliveryType.values + .map( + (type) => SwitchListTile( + title: Text(type.l10n(context)), + value: pushConfig.deliveryConfigs[type] ?? false, + onChanged: (value) { + final newDeliveryConfigs = + Map< + PushNotificationSubscriptionDeliveryType, + bool + >.from( + pushConfig.deliveryConfigs, + ); + newDeliveryConfigs[type] = value; + onConfigChanged( + remoteConfig.copyWith( + pushNotificationConfig: pushConfig.copyWith( + deliveryConfigs: newDeliveryConfigs, + ), + ), + ); + }, + ), + ) + .toList(), + ), + ], + ); + } +} From b32a5c3c5324a44fc8991a95281585cc69f4bd4b Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 11:06:50 +0100 Subject: [PATCH 52/59] feat(app_configuration): add push notification settings form - Import PushNotificationSettingsForm widget - Update TabController length to 4 - Add new tab for notifications configuration - Implement PushNotificationSettingsForm in AppConfigurationPage --- .../view/app_configuration_page.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index 20281e82..ff746207 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuratio import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/view/tabs/advertisements_configuration_tab.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/view/tabs/feed_configuration_tab.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/view/tabs/general_configuration_tab.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/push_notification_settings_form.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/about_icon.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -30,7 +31,7 @@ class _AppConfigurationPageState extends State @override void initState() { super.initState(); - _tabController = TabController(length: 3, vsync: this); + _tabController = TabController(length: 4, vsync: this); context.read().add(const AppConfigurationLoaded()); } @@ -68,6 +69,7 @@ class _AppConfigurationPageState extends State Tab(text: l10n.generalTab), Tab(text: l10n.feedTab), Tab(text: l10n.advertisementsTab), + Tab(text: l10n.notificationsTab), ], ), ), @@ -156,6 +158,14 @@ class _AppConfigurationPageState extends State ); }, ), + PushNotificationSettingsForm( + remoteConfig: remoteConfig, + onConfigChanged: (newConfig) { + context.read().add( + AppConfigurationFieldChanged(remoteConfig: newConfig), + ); + }, + ), ], ); } From 7958d2f5128b5083bb69a4a39a691070efd287c2 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 11:11:19 +0100 Subject: [PATCH 53/59] feat(l10n): add localization extension for PushNotificationProvider - Create new extension file for PushNotificationProvider enum - Implement l10n method to return localized string representations - Add cases for firebase and oneSignal push notification providers --- .../push_notification_settings_form.dart | 1 + lib/shared/extensions/extensions.dart | 2 ++ .../push_notification_provider_l10n.dart | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 lib/shared/extensions/push_notification_provider_l10n.dart diff --git a/lib/app_configuration/widgets/push_notification_settings_form.dart b/lib/app_configuration/widgets/push_notification_settings_form.dart index 3b9cb260..26564f36 100644 --- a/lib/app_configuration/widgets/push_notification_settings_form.dart +++ b/lib/app_configuration/widgets/push_notification_settings_form.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/push_notification_provider_l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/push_notification_subscription_delivery_type_l10n.dart'; diff --git a/lib/shared/extensions/extensions.dart b/lib/shared/extensions/extensions.dart index 9987aa14..bac9865f 100644 --- a/lib/shared/extensions/extensions.dart +++ b/lib/shared/extensions/extensions.dart @@ -6,5 +6,7 @@ export 'content_status_l10n.dart'; export 'dashboard_user_role_l10n.dart'; export 'feed_decorator_type_l10n.dart'; export 'in_article_ad_slot_type_l10n.dart'; +export 'push_notification_provider_l10n.dart'; +export 'push_notification_subscription_delivery_type_l10n.dart'; export 'source_type_l10n.dart'; export 'string_truncate.dart'; diff --git a/lib/shared/extensions/push_notification_provider_l10n.dart b/lib/shared/extensions/push_notification_provider_l10n.dart new file mode 100644 index 00000000..a5203c49 --- /dev/null +++ b/lib/shared/extensions/push_notification_provider_l10n.dart @@ -0,0 +1,18 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +/// Extension to localize the [PushNotificationProvider] enum. +extension PushNotificationProviderL10n on PushNotificationProvider { + /// Returns the localized string representation of the + /// [PushNotificationProvider]. + String l10n(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + switch (this) { + case PushNotificationProvider.firebase: + return l10n.pushNotificationProviderFirebase; + case PushNotificationProvider.oneSignal: + return l10n.pushNotificationProviderOneSignal; + } + } +} From 69f3e30efbe3bca89fbf7ecd05e79f5beac87ec7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 11:20:21 +0100 Subject: [PATCH 54/59] refactor(app_configuration): simplify push notification settings form - Remove nested ExpansionTile for system status section - Replace with a single SwitchListTile at the top level - Adjust padding and layout for primary provider and delivery types sections - Remove unnecessary containers and adjust alignments --- .../push_notification_settings_form.dart | 119 +++++++----------- 1 file changed, 43 insertions(+), 76 deletions(-) diff --git a/lib/app_configuration/widgets/push_notification_settings_form.dart b/lib/app_configuration/widgets/push_notification_settings_form.dart index 26564f36..f011fa95 100644 --- a/lib/app_configuration/widgets/push_notification_settings_form.dart +++ b/lib/app_configuration/widgets/push_notification_settings_form.dart @@ -33,65 +33,27 @@ class PushNotificationSettingsForm extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ExpansionTile( - initiallyExpanded: true, - title: Text(l10n.pushNotificationSettingsTitle), - childrenPadding: const EdgeInsetsDirectional.only( - start: AppSpacing.lg, - top: AppSpacing.md, - bottom: AppSpacing.md, - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.pushNotificationSettingsDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.7), + SwitchListTile( + title: Text(l10n.pushNotificationSystemStatusTitle), + subtitle: Text(l10n.pushNotificationSystemStatusDescription), + value: pushConfig.enabled, + onChanged: (value) { + onConfigChanged( + remoteConfig.copyWith( + pushNotificationConfig: pushConfig.copyWith(enabled: value), ), - ), - const SizedBox(height: AppSpacing.lg), - _buildSystemStatusSection(context, l10n, pushConfig), - const SizedBox(height: AppSpacing.lg), - _buildPrimaryProviderSection(context, l10n, pushConfig), - const SizedBox(height: AppSpacing.lg), - _buildDeliveryTypesSection(context, l10n, pushConfig), - ], + ); + }, ), + const SizedBox(height: AppSpacing.lg), + _buildPrimaryProviderSection(context, l10n, pushConfig), + const SizedBox(height: AppSpacing.lg), + _buildDeliveryTypesSection(context, l10n, pushConfig), ], ), ); } - Widget _buildSystemStatusSection( - BuildContext context, - AppLocalizations l10n, - PushNotificationConfig pushConfig, - ) { - return ExpansionTile( - title: Text(l10n.pushNotificationSystemStatusTitle), - childrenPadding: const EdgeInsets.symmetric( - horizontal: AppSpacing.lg, - vertical: AppSpacing.md, - ), - children: [ - SwitchListTile( - title: Text(l10n.enabledLabel), - subtitle: Text(l10n.pushNotificationSystemStatusDescription), - value: pushConfig.enabled, - onChanged: (value) { - onConfigChanged( - remoteConfig.copyWith( - pushNotificationConfig: pushConfig.copyWith(enabled: value), - ), - ); - }, - ), - ], - ); - } - Widget _buildPrimaryProviderSection( BuildContext context, AppLocalizations l10n, @@ -99,9 +61,10 @@ class PushNotificationSettingsForm extends StatelessWidget { ) { return ExpansionTile( title: Text(l10n.pushNotificationPrimaryProviderTitle), - childrenPadding: const EdgeInsets.symmetric( - horizontal: AppSpacing.lg, - vertical: AppSpacing.md, + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, ), expandedCrossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -112,25 +75,28 @@ class PushNotificationSettingsForm extends StatelessWidget { ), ), const SizedBox(height: AppSpacing.lg), - SegmentedButton( - segments: PushNotificationProvider.values - .map( - (provider) => ButtonSegment( - value: provider, - label: Text(provider.l10n(context)), + Align( + alignment: AlignmentDirectional.centerStart, + child: SegmentedButton( + segments: PushNotificationProvider.values + .map( + (provider) => ButtonSegment( + value: provider, + label: Text(provider.l10n(context)), + ), + ) + .toList(), + selected: {pushConfig.primaryProvider}, + onSelectionChanged: (newSelection) { + onConfigChanged( + remoteConfig.copyWith( + pushNotificationConfig: pushConfig.copyWith( + primaryProvider: newSelection.first, + ), ), - ) - .toList(), - selected: {pushConfig.primaryProvider}, - onSelectionChanged: (newSelection) { - onConfigChanged( - remoteConfig.copyWith( - pushNotificationConfig: pushConfig.copyWith( - primaryProvider: newSelection.first, - ), - ), - ); - }, + ); + }, + ), ), ], ); @@ -143,9 +109,10 @@ class PushNotificationSettingsForm extends StatelessWidget { ) { return ExpansionTile( title: Text(l10n.pushNotificationDeliveryTypesTitle), - childrenPadding: const EdgeInsets.symmetric( - horizontal: AppSpacing.lg, - vertical: AppSpacing.md, + childrenPadding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + top: AppSpacing.md, + bottom: AppSpacing.md, ), expandedCrossAxisAlignment: CrossAxisAlignment.start, children: [ From 18c8ef818c09498c2b27deadb73de6e31b7b4fe7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 11:24:06 +0100 Subject: [PATCH 55/59] fix(l10n): update push notification settings descriptions and titles - Update Arabic and English descriptions for push notification system status - Change title from "Push Notification System" to "Enable Notifications" - Clarify that the switch disables/enables all push notifications --- lib/l10n/arb/app_ar.arb | 4 ++-- lib/l10n/arb/app_en.arb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index a6d4a18a..882fb2ae 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1916,11 +1916,11 @@ "@pushNotificationSettingsDescription": { "description": "وصف قسم إعدادات الإشعارات الفورية" }, - "pushNotificationSystemStatusTitle": "نظام الإشعارات الفورية", + "pushNotificationSystemStatusTitle": "تفعيل الإشعارات", "@pushNotificationSystemStatusTitle": { "description": "عنوان قسم حالة نظام الإشعارات الفورية" }, - "pushNotificationSystemStatusDescription": "مفتاح عام لتمكين أو تعطيل نظام الإشعارات الفورية بأكمله.", + "pushNotificationSystemStatusDescription": "مفتاح عام لتمكين أو تعطيل جميع الإشعارات الفورية.", "@pushNotificationSystemStatusDescription": { "description": "وصف قسم حالة نظام الإشعارات الفورية" }, diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a1134081..e2d33e06 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1912,11 +1912,11 @@ "@pushNotificationSettingsDescription": { "description": "Description for the Push Notification Settings section" }, - "pushNotificationSystemStatusTitle": "Push Notification System", + "pushNotificationSystemStatusTitle": "Enable Notifications", "@pushNotificationSystemStatusTitle": { "description": "Title for the Push Notification System status section" }, - "pushNotificationSystemStatusDescription": "A global switch to enable or disable the entire push notification system.", + "pushNotificationSystemStatusDescription": "A global switch to enable or disable all push notifications.", "@pushNotificationSystemStatusDescription": { "description": "Description for the Push Notification System status section" }, From c5e2b5af709b6463c5e478e11acdac4dbf35ead1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 11:24:30 +0100 Subject: [PATCH 56/59] build(l10n): generate --- lib/l10n/app_localizations.dart | 4 ++-- lib/l10n/app_localizations_ar.dart | 4 ++-- lib/l10n/app_localizations_en.dart | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index efc43408..f6c261ba 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2843,13 +2843,13 @@ abstract class AppLocalizations { /// Title for the Push Notification System status section /// /// In en, this message translates to: - /// **'Push Notification System'** + /// **'Enable Notifications'** String get pushNotificationSystemStatusTitle; /// Description for the Push Notification System status section /// /// In en, this message translates to: - /// **'A global switch to enable or disable the entire push notification system.'** + /// **'A global switch to enable or disable all push notifications.'** String get pushNotificationSystemStatusDescription; /// Title for the Push Notification Primary Provider section diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 5ffa4918..fdeba6c0 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1520,11 +1520,11 @@ class AppLocalizationsAr extends AppLocalizations { 'إدارة الإعدادات العامة لنظام الإشعارات الفورية، بما في ذلك المزود الأساسي وأنواع الإشعارات النشطة.'; @override - String get pushNotificationSystemStatusTitle => 'نظام الإشعارات الفورية'; + String get pushNotificationSystemStatusTitle => 'تفعيل الإشعارات'; @override String get pushNotificationSystemStatusDescription => - 'مفتاح عام لتمكين أو تعطيل نظام الإشعارات الفورية بأكمله.'; + 'مفتاح عام لتمكين أو تعطيل جميع الإشعارات الفورية.'; @override String get pushNotificationPrimaryProviderTitle => 'المزود الأساسي'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9de6006d..592ae4dd 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1523,11 +1523,11 @@ class AppLocalizationsEn extends AppLocalizations { 'Manage global settings for the push notification system, including the primary provider and which notification types are active.'; @override - String get pushNotificationSystemStatusTitle => 'Push Notification System'; + String get pushNotificationSystemStatusTitle => 'Enable Notifications'; @override String get pushNotificationSystemStatusDescription => - 'A global switch to enable or disable the entire push notification system.'; + 'A global switch to enable or disable all push notifications.'; @override String get pushNotificationPrimaryProviderTitle => 'Primary Provider'; From 807c67133ee49299c042b7237144d03639f7607f Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 11:37:00 +0100 Subject: [PATCH 57/59] feat(l10n): add push notification provider configuration notes - Add new localization keys for push notification provider configuration information - Include titles and messages for backend configuration notes in both Arabic and English --- lib/l10n/app_localizations.dart | 12 ++++++++++++ lib/l10n/app_localizations_ar.dart | 8 ++++++++ lib/l10n/app_localizations_en.dart | 8 ++++++++ lib/l10n/arb/app_ar.arb | 8 ++++++++ lib/l10n/arb/app_en.arb | 8 ++++++++ 5 files changed, 44 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f6c261ba..2af9097d 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2887,6 +2887,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'OneSignal'** String get pushNotificationProviderOneSignal; + + /// Title for the informational note about backend configuration for push notification providers. + /// + /// In en, this message translates to: + /// **'Backend Configuration Note'** + String get pushNotificationProviderConfigInfoTitle; + + /// Informational message about configuring the selected push notification provider in the backend. + /// + /// In en, this message translates to: + /// **'Please ensure the selected provider is correctly configured in your backend\'s environment file. Refer to the server setup documentation for details on the required variables.'** + String get pushNotificationProviderConfigInfoBody; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index fdeba6c0..1d1573ea 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1545,4 +1545,12 @@ class AppLocalizationsAr extends AppLocalizations { @override String get pushNotificationProviderOneSignal => 'OneSignal'; + + @override + String get pushNotificationProviderConfigInfoTitle => + 'ملاحظة حول تكوين الواجهة الخلفية'; + + @override + String get pushNotificationProviderConfigInfoBody => + 'يرجى التأكد من أن المزود المحدد قد تم إعداده بشكل صحيح في ملف البيئة الخاص بالواجهة الخلفية. للحصول على تفاصيل حول المتغيرات المطلوبة، يرجى الرجوع إلى وثائق إعداد الخادم.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 592ae4dd..2a8ba84e 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1548,4 +1548,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pushNotificationProviderOneSignal => 'OneSignal'; + + @override + String get pushNotificationProviderConfigInfoTitle => + 'Backend Configuration Note'; + + @override + String get pushNotificationProviderConfigInfoBody => + 'Please ensure the selected provider is correctly configured in your backend\'s environment file. Refer to the server setup documentation for details on the required variables.'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 882fb2ae..9f9996c1 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1947,5 +1947,13 @@ "pushNotificationProviderOneSignal": "OneSignal", "@pushNotificationProviderOneSignal": { "description": "تسمية مزود الإشعارات الفورية OneSignal" + }, + "pushNotificationProviderConfigInfoTitle": "ملاحظة حول تكوين الواجهة الخلفية", + "@pushNotificationProviderConfigInfoTitle": { + "description": "عنوان الملاحظة المعلوماتية حول إعداد الواجهة الخلفية لمزودي خدمة الإشعارات." + }, + "pushNotificationProviderConfigInfoBody": "يرجى التأكد من أن المزود المحدد قد تم إعداده بشكل صحيح في ملف البيئة الخاص بالواجهة الخلفية. للحصول على تفاصيل حول المتغيرات المطلوبة، يرجى الرجوع إلى وثائق إعداد الخادم.", + "@pushNotificationProviderConfigInfoBody": { + "description": "رسالة معلوماتية حول تكوين مزود الإشعارات المختار في الواجهة الخلفية." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index e2d33e06..1f219c23 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1943,5 +1943,13 @@ "pushNotificationProviderOneSignal": "OneSignal", "@pushNotificationProviderOneSignal": { "description": "Label for the OneSignal push notification provider" + }, + "pushNotificationProviderConfigInfoTitle": "Backend Configuration Note", + "@pushNotificationProviderConfigInfoTitle": { + "description": "Title for the informational note about backend configuration for push notification providers." + }, + "pushNotificationProviderConfigInfoBody": "Please ensure the selected provider is correctly configured in your backend's environment file. Refer to the server setup documentation for details on the required variables.", + "@pushNotificationProviderConfigInfoBody": { + "description": "Informational message about configuring the selected push notification provider in the backend." } } \ No newline at end of file From 971cc444c676eba91fd11aa09b6f2c71f8b55010 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 11:43:50 +0100 Subject: [PATCH 58/59] refactor(push-notification): update provider configuration descriptions - Remove separate configuration info entries for push notification providers - Integrate configuration instructions directly into the primary provider description - Update descriptions in both English and Arabic localization files --- lib/l10n/app_localizations.dart | 14 +------------- lib/l10n/app_localizations_ar.dart | 10 +--------- lib/l10n/app_localizations_en.dart | 10 +--------- lib/l10n/arb/app_ar.arb | 10 +--------- lib/l10n/arb/app_en.arb | 18 ++++-------------- 5 files changed, 8 insertions(+), 54 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2af9097d..e13c8776 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2861,7 +2861,7 @@ abstract class AppLocalizations { /// Description for the Push Notification Primary Provider section /// /// In en, this message translates to: - /// **'Select the primary service provider for sending push notifications.'** + /// **'Select the primary service provider. Ensure the chosen provider is correctly configured in your backend\'s .env file as per the documentation.'** String get pushNotificationPrimaryProviderDescription; /// Title for the Push Notification Delivery Types section @@ -2887,18 +2887,6 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'OneSignal'** String get pushNotificationProviderOneSignal; - - /// Title for the informational note about backend configuration for push notification providers. - /// - /// In en, this message translates to: - /// **'Backend Configuration Note'** - String get pushNotificationProviderConfigInfoTitle; - - /// Informational message about configuring the selected push notification provider in the backend. - /// - /// In en, this message translates to: - /// **'Please ensure the selected provider is correctly configured in your backend\'s environment file. Refer to the server setup documentation for details on the required variables.'** - String get pushNotificationProviderConfigInfoBody; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 1d1573ea..bb30b7ce 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1531,7 +1531,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get pushNotificationPrimaryProviderDescription => - 'اختر مزود الخدمة الأساسي لإرسال الإشعارات الفورية.'; + 'اختر مزود الخدمة الأساسي. تأكد من أن المزود المختار قد تم إعداده بشكل صحيح في ملف .env الخاص بالواجهة الخلفية.'; @override String get pushNotificationDeliveryTypesTitle => 'أنواع التسليم'; @@ -1545,12 +1545,4 @@ class AppLocalizationsAr extends AppLocalizations { @override String get pushNotificationProviderOneSignal => 'OneSignal'; - - @override - String get pushNotificationProviderConfigInfoTitle => - 'ملاحظة حول تكوين الواجهة الخلفية'; - - @override - String get pushNotificationProviderConfigInfoBody => - 'يرجى التأكد من أن المزود المحدد قد تم إعداده بشكل صحيح في ملف البيئة الخاص بالواجهة الخلفية. للحصول على تفاصيل حول المتغيرات المطلوبة، يرجى الرجوع إلى وثائق إعداد الخادم.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2a8ba84e..b4066573 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1534,7 +1534,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pushNotificationPrimaryProviderDescription => - 'Select the primary service provider for sending push notifications.'; + 'Select the primary service provider. Ensure the chosen provider is correctly configured in your backend\'s .env file as per the documentation.'; @override String get pushNotificationDeliveryTypesTitle => 'Delivery Types'; @@ -1548,12 +1548,4 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pushNotificationProviderOneSignal => 'OneSignal'; - - @override - String get pushNotificationProviderConfigInfoTitle => - 'Backend Configuration Note'; - - @override - String get pushNotificationProviderConfigInfoBody => - 'Please ensure the selected provider is correctly configured in your backend\'s environment file. Refer to the server setup documentation for details on the required variables.'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 9f9996c1..be004178 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1928,7 +1928,7 @@ "@pushNotificationPrimaryProviderTitle": { "description": "عنوان قسم المزود الأساسي للإشعارات الفورية" }, - "pushNotificationPrimaryProviderDescription": "اختر مزود الخدمة الأساسي لإرسال الإشعارات الفورية.", + "pushNotificationPrimaryProviderDescription": "اختر مزود الخدمة الأساسي. تأكد من أن المزود المختار قد تم إعداده بشكل صحيح في ملف .env الخاص بالواجهة الخلفية.", "@pushNotificationPrimaryProviderDescription": { "description": "وصف قسم المزود الأساسي للإشعارات الفورية" }, @@ -1947,13 +1947,5 @@ "pushNotificationProviderOneSignal": "OneSignal", "@pushNotificationProviderOneSignal": { "description": "تسمية مزود الإشعارات الفورية OneSignal" - }, - "pushNotificationProviderConfigInfoTitle": "ملاحظة حول تكوين الواجهة الخلفية", - "@pushNotificationProviderConfigInfoTitle": { - "description": "عنوان الملاحظة المعلوماتية حول إعداد الواجهة الخلفية لمزودي خدمة الإشعارات." - }, - "pushNotificationProviderConfigInfoBody": "يرجى التأكد من أن المزود المحدد قد تم إعداده بشكل صحيح في ملف البيئة الخاص بالواجهة الخلفية. للحصول على تفاصيل حول المتغيرات المطلوبة، يرجى الرجوع إلى وثائق إعداد الخادم.", - "@pushNotificationProviderConfigInfoBody": { - "description": "رسالة معلوماتية حول تكوين مزود الإشعارات المختار في الواجهة الخلفية." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 1f219c23..643aef4b 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1798,7 +1798,7 @@ "@subscriptionPremium": { "description": "Subscription status for a premium user" }, - "savedHeadlineFilterLimitsTitle": "Saved Headline Filter Limits", + "savedHeadlineFilterLimitsTitle": "Saved Headline Filter Limits", "@savedHeadlineFilterLimitsTitle": { "description": "Title for the Saved Headline Filter Limits section" }, @@ -1849,8 +1849,7 @@ "pushNotificationSubscriptionDeliveryTypeWeeklyRoundup": "Weekly Roundup", "@pushNotificationSubscriptionDeliveryTypeWeeklyRoundup": { "description": "Label for the 'weekly roundup' push notification delivery type" - } -, + }, "isBreakingNewsLabel": "Mark as Breaking News", "@isBreakingNewsLabel": { "description": "Label for the switch to mark a headline as breaking news" @@ -1898,8 +1897,7 @@ "breakingNewsFilterNonBreakingOnly": "Non-Breaking Only", "@breakingNewsFilterNonBreakingOnly": { "description": "Label for the 'Non-Breaking Only' choice chip in the breaking news filter." - } -, + }, "notificationsTab": "Notifications", "@notificationsTab": { "description": "Tab title for Notifications settings" @@ -1924,7 +1922,7 @@ "@pushNotificationPrimaryProviderTitle": { "description": "Title for the Push Notification Primary Provider section" }, - "pushNotificationPrimaryProviderDescription": "Select the primary service provider for sending push notifications.", + "pushNotificationPrimaryProviderDescription": "Select the primary service provider. Ensure the chosen provider is correctly configured in your backend's .env file as per the documentation.", "@pushNotificationPrimaryProviderDescription": { "description": "Description for the Push Notification Primary Provider section" }, @@ -1943,13 +1941,5 @@ "pushNotificationProviderOneSignal": "OneSignal", "@pushNotificationProviderOneSignal": { "description": "Label for the OneSignal push notification provider" - }, - "pushNotificationProviderConfigInfoTitle": "Backend Configuration Note", - "@pushNotificationProviderConfigInfoTitle": { - "description": "Title for the informational note about backend configuration for push notification providers." - }, - "pushNotificationProviderConfigInfoBody": "Please ensure the selected provider is correctly configured in your backend's environment file. Refer to the server setup documentation for details on the required variables.", - "@pushNotificationProviderConfigInfoBody": { - "description": "Informational message about configuring the selected push notification provider in the backend." } } \ No newline at end of file From 0dc1be23b202fae8666bad53527218c56bb545b5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 13 Nov 2025 11:50:04 +0100 Subject: [PATCH 59/59] docs(README): add Global Notification Control feature description - Added a new bullet point under the 'Dynamically control the mobile app's behavior and operational state directly from your back-end' section - Described the capabilities of remotely managing the notification system, switching providers, and toggling delivery types - This update provides clearer information about the Global Notification Control feature --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 11b6fd6d..2335e7b0 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ Dynamically control the mobile app's behavior and operational state directly fro - **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. +- **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.